Websocket: Unterschied zwischen den Versionen

Aus FHEMWiki
K (Forenlink über Wiki-Vorlage; Typo)
K (Noch einen Screenshot damit man sich die Tibber-Websocket ein wenig vorstellen kann.)
 
(4 dazwischenliegende Versionen desselben Benutzers werden nicht angezeigt)
Zeile 8: Zeile 8:
#* Wenn Daten von der Websocket empfangen werden, werden diese an eine Funktion übergeben. Diese macht man mit der directReadFn bekannt: <code>$hash->{directReadFn}</code>
#* Wenn Daten von der Websocket empfangen werden, werden diese an eine Funktion übergeben. Diese macht man mit der directReadFn bekannt: <code>$hash->{directReadFn}</code>
# Starten der Kommunikation: <code>DevIo_OpenDev(...);</code>
# Starten der Kommunikation: <code>DevIo_OpenDev(...);</code>
# Ist die Verbindung aufgebaut, kann man Daten mit der Funktion <code>DevIo_SimpleWrite(...)</code> senden. Die Beispiele zu "Tibber" und "Owntone" senden jeweils Daten beim Aufbau der Verbindung als ASCII String.


== Beispiele ==
== Beispiele ==


=== Tibber Live-Messdaten auslesen ===
=== Tibber Live-Messdaten auslesen ===
Folgendes Beispiel liest Strommesswerte des Anbieters Tibber aus. Mit  
[[Datei:Screenshot von Tibber-Websocket und weiteren Tibber-Devices.png|ohne|mini|300x300px|Screenshot vom Tibber Websocket und weitere Tibber-Devices]]
:<code>set Tibber.ws start</code>  
Folgendes Beispiel liest Strommesswerte des Anbieters Tibber aus. Mit
:<code>set Tibber.ws start</code>
wird die Verbindung aufgebaut und mit  
wird die Verbindung aufgebaut und mit  
:<code>set Tibber.ws stop</code>  
:<code>set Tibber.ws stop</code>
wieder gestoppt. Siehe auch das zugehörige Forenthema  {{Link2Forum|Topic=130407|Message=1298863|LinkText=ab diesem Beitrag}}.
wieder gestoppt. Siehe auch das zugehörige Forenthema  {{Link2Forum|Topic=130407|Message=1298863|LinkText=ab diesem Beitrag}}.
<syntaxhighlight lang="perl" line="1">
<syntaxhighlight lang="perl" line="1">
Zeile 84: Zeile 86:
});;\
});;\
readingsBulkUpdate($hash, "websocketData", "");;\
readingsBulkUpdate($hash, "websocketData", "");;\
\
\
return POSIX::strftime("%H:%M:%S",localtime(time()));;\
return POSIX::strftime("%H:%M:%S",localtime(time()));;\
},\
},\
Zeile 102: Zeile 104:
## timer callback function, called after a few seconds to initiate a reconnect\
## timer callback function, called after a few seconds to initiate a reconnect\
my $timerFunction = sub() {\
my $timerFunction = sub() {\
my ($hash) = @_;;\
my ($arg) = @_;;\
my $hash = $defs{$name};;\
my $devState = DevIo_IsOpen($hash);;\
my $devState = DevIo_IsOpen($hash);;\
\
\
Zeile 108: Zeile 111:
readingsSingleUpdate($hash, "cmd", "connect", 1) if (!defined($devState));;\
readingsSingleUpdate($hash, "cmd", "connect", 1) if (!defined($devState));;\
};;\
};;\
my $hash = $defs{$name};;\
RemoveInternalTimer($name.$reading.'Timer');;\
RemoveInternalTimer($hash, $timerFunction);;\
\
\
# wait a random time before reconnect (exponential backoff TBD):\
# wait a random time before reconnect (exponential backoff TBD):\
my $rwait = int(rand(200)) + 30;;\
my $rwait = int(rand(200)) + 30;;\
InternalTimer(gettimeofday() + $rwait, $timerFunction, $hash);;\
InternalTimer(gettimeofday() + $rwait, $timerFunction, $name.$reading.'Timer');;\
\
#set cmd to a new value, informs user and allows to retrigger when timer expires\
my $hash = $defs{$name};;\
readingsBulkUpdate($hash, "cmd", "reconnect attempt in $rwait seconds");;\
readingsBulkUpdate($hash, "cmd", "reconnect attempt in $rwait seconds");;\
\
return POSIX::strftime("%H:%M:%S",localtime(time()));;\
},\
onTimeout:websocketData:.* {\
#re-establish websocket connection if no data received in the past ten minutes\
#but only if our reading "cmd" was not set to the value "disconnect"\
\
#timeout in seconds when the connection is considered dead\
my $timeoutTime = 600;;\
\
# function to execute when timeout expired\
# defining the function here in the userReading, allows us to insert variables directly\
my $timerFunction = sub() {\
my ($arg) = @_;;\
my $hash = $defs{$name};;\
my $rCmd = ReadingsVal($name, "cmd", "???");;\
my $age  = ReadingsAge($name, "websocketData", 0);;\
\
Log(3, "$name: onTimeoutTimer triggered >>$arg<<");;\
\
#do not do anything further if disconnect is on purpose\
if ( $rCmd eq "disconnect" ) {\
Log(3, "$name: cmd was set to disconnect");;\
return;;\
}\
\
# for whatever reason, we triggered to soon (80%)\
if ( $age < $timeoutTime*0.8 ) {\
Log(3, "$name: websocketData is not outdated");;\
return;;\
}\
\
DevIo_CloseDev($hash);;\
Log(3, "$name: onTimeoutTimer closed DevIo...");;\
\
readingsSingleUpdate($hash, "cmd", "connect", 1);;\
Log(3, "$name: onTimeoutTimer set cmd to value 'connect'");;\
};;\
\
#remove/cancel previous timers, because we got fresh data and countdown starts again\
RemoveInternalTimer($name.$reading.'Timer');;\
\
#set timer to expire and execute function defined above, give special arg as identifier\
InternalTimer(gettimeofday() + $timeoutTime, $timerFunction, $name.$reading.'Timer');;\
\
\
return POSIX::strftime("%H:%M:%S",localtime(time()));;\
return POSIX::strftime("%H:%M:%S",localtime(time()));;\
Zeile 139: Zeile 188:
$json .= '"extensions":{}'.", ";;\
$json .= '"extensions":{}'.", ";;\
$json .= '"query":"subscription { liveMeasurement( homeId: \"'.$homeId.'\" ) ';;\
$json .= '"query":"subscription { liveMeasurement( homeId: \"'.$homeId.'\" ) ';;\
#$json .= '{ timestamp power accumulatedConsumption accumulatedCost currency minPower averagePower maxPower signalStrength }}"';;\
$json .= '{ timestamp power lastMeterConsumption accumulatedConsumption accumulatedProduction ';;\
$json .= '{ timestamp power lastMeterConsumption accumulatedConsumption accumulatedProduction ';;\
$json .= 'accumulatedProductionLastHour accumulatedCost accumulatedReward currency minPower averagePower maxPower ';;\
$json .= 'accumulatedProductionLastHour accumulatedCost accumulatedReward currency minPower averagePower maxPower ';;\
Zeile 148: Zeile 198:
Log(3, "$name:$reading: sending JSON: >>>$json<<<");;\
Log(3, "$name:$reading: sending JSON: >>>$json<<<");;\
DevIo_SimpleWrite($hash, $json, 2);;\
DevIo_SimpleWrite($hash, $json, 2);;\
\
\
return POSIX::strftime("%H:%M:%S",localtime(time()));;\
return POSIX::strftime("%H:%M:%S",localtime(time()));;\
},\
},\
Zeile 167: Zeile 217:
</syntaxhighlight>
</syntaxhighlight>


=== Owntone (ehemals ForkedDaapd) ===
===Owntone (ehemals ForkedDaapd) ===
Der Musikserver Owntone kann mit einer Websocket Informationen bereitstellen. Diese kann man ebenfalls mit einem einfachen Device auswerten (siehe auch {{Link2Forum|Topic=135838|Message=1294236|Linktext=diesen Forenbeitrag}}):
[[Datei:Screenshot von dem OwnTone Device und der Websocket.png|ohne|mini|300x300px]]
Der Musikserver Owntone kann mit einer Websocket Informationen bereitstellen. Diese kann man ebenfalls mit einem einfachen Device auswerten (siehe auch {{Link2Forum|Topic=135838|Message=1294236|Linktext=diesen Forenbeitrag}} und das zugehörige Owntone-Device im {{Link2Forum|Topic=135666.0}} ):
<syntaxhighlight lang="perl" line="1">
<syntaxhighlight lang="perl" line="1">
defmod WS dummy
defmod WS dummy
Zeile 175: Zeile 226:
attr WS devStateIcon opened:general_ok@green:stop disconnected:rc_STOP@red:start
attr WS devStateIcon opened:general_ok@green:stop disconnected:rc_STOP@red:start
attr WS eventMap /wert connect:start/wert disconnect:stop/
attr WS eventMap /wert connect:start/wert disconnect:stop/
attr WS group Musik
attr WS icon hue_filled_plug
attr WS icon hue_filled_plug
attr WS readingList wert
attr WS readingList wert
attr WS room Musik
attr WS setList wert
attr WS setList wert
attr WS userReadings connect:wert:.connect {\
attr WS userReadings connect:wert:.connect {\
Zeile 186: Zeile 235:
\
\
$hash->{DeviceName} = AttrVal($name, "websocketURL", "ws:echo.websocket.org:443");;\
$hash->{DeviceName} = AttrVal($name, "websocketURL", "ws:echo.websocket.org:443");;\
#$hash->{nextOpenDelay} = 10;;\
\
\
# special headers needed for Owntone\
# special headers needed for Owntone\
Zeile 212: Zeile 260:
readingsEndUpdate($hash, 1);;\
readingsEndUpdate($hash, 1);;\
};;\
};;\
\
#my $timerFunction = sub() {\
# my ($hash) = @_;;\
# my $devState = DevIo_IsOpen($hash);;\
# readingsSingleUpdate($hash, "wert", "connect", 1) if (!defined($devState));;\
#};;\
#RemoveInternalTimer($hash, $timerFunction);;\
#InternalTimer(gettimeofday() + 10, $timerFunction, $hash);;\
\
\
# open DevIo websocket\
# open DevIo websocket\
Zeile 229: Zeile 269:
DevIo_SimpleWrite($hash, '{"notify":["update","database","player","options","outputs","volume","queue","spotify","lastfm","pairing"]}', 2);;\
DevIo_SimpleWrite($hash, '{"notify":["update","database","player","options","outputs","volume","queue","spotify","lastfm","pairing"]}', 2);;\
});;\
});;\
\
\
return POSIX::strftime("%H:%M:%S",localtime(time()));;\
return POSIX::strftime("%H:%M:%S",localtime(time()));;\
},\
},\
Zeile 244: Zeile 284:
return if ($myState ne "disconnected");;\
return if ($myState ne "disconnected");;\
\
\
# timer callback function, called after a few seconds to initiate a reconnect\
my $timerFunction = sub() {\
my $timerFunction = sub() {\
my ($hash) = @_;;\
my ($arg) = @_;;\
my $hash = $defs{$name};;\
my $devState = DevIo_IsOpen($hash);;\
my $devState = DevIo_IsOpen($hash);;\
readingsSingleUpdate($hash, "wert", "connect", 1) if (!defined($devState));;\
readingsSingleUpdate($hash, "wert", "connect", 1) if (!defined($devState));;\
};;\
};;\
my $hash = $defs{$name};;\
\
RemoveInternalTimer($hash, $timerFunction);;\
RemoveInternalTimer($name.$reading.'Timer');;\
InternalTimer(gettimeofday() + 10, $timerFunction, $hash);;\
InternalTimer(gettimeofday() + 10, $timerFunction, $name.$reading.'Timer');;\
\
\
return POSIX::strftime("%H:%M:%S",localtime(time()));;\
return POSIX::strftime("%H:%M:%S",localtime(time()));;\

Aktuelle Version vom 20. Januar 2024, 12:33 Uhr

FHEM kann mit Websockets kommunizieren indem DevIo genutzt wird (Ankündigung des Supports in diesem Forenbeitrag). Bisher werden Websockets nur mit Perl-Befehlen angesprochen.

Schritte:

  1. Setzen der Parameter:
    • Setzen des Kommunikationsendpunktes: $hash->{DeviceName} = "wss:echo.websocket.org:443/pfad"; Wobei die Portnummer zwingend hinzugefügt werden muss, wenn ein Pfad spezifiziert wird.
    • Optional: Setzen von speziellen Headerangaben: $hash->{header}{'Sec-WebSocket-Protocol'} = 'graphql-transport-ws';
  2. Setzen einer CallBack Funktion:
    • Wenn Daten von der Websocket empfangen werden, werden diese an eine Funktion übergeben. Diese macht man mit der directReadFn bekannt: $hash->{directReadFn}
  3. Starten der Kommunikation: DevIo_OpenDev(...);
  4. Ist die Verbindung aufgebaut, kann man Daten mit der Funktion DevIo_SimpleWrite(...) senden. Die Beispiele zu "Tibber" und "Owntone" senden jeweils Daten beim Aufbau der Verbindung als ASCII String.

Beispiele

Tibber Live-Messdaten auslesen

Screenshot vom Tibber Websocket und weitere Tibber-Devices

Folgendes Beispiel liest Strommesswerte des Anbieters Tibber aus. Mit

set Tibber.ws start

wird die Verbindung aufgebaut und mit

set Tibber.ws stop

wieder gestoppt. Siehe auch das zugehörige Forenthema ab diesem Beitrag.

defmod Tibber.ws dummy
attr Tibber.ws userattr websocketURL homeId token myId minInterval
attr Tibber.ws alias Tibber Websocket
attr Tibber.ws event-on-change-reading .*
attr Tibber.ws eventMap /cmd connect:start/cmd disconnect:stop/
attr Tibber.ws homeId 96a14971-525a-4420-aae9-e5aedaa129ff
attr Tibber.ws icon hue_filled_plug
attr Tibber.ws minInterval 30
attr Tibber.ws myId TorxgewindeID
attr Tibber.ws readingList cmd
attr Tibber.ws setList cmd
attr Tibber.ws stateFormat payload_data_liveMeasurement_accumulatedCost payload_data_liveMeasurement_currency (payload_data_liveMeasurement_power W, Import: payload_data_liveMeasurement_accumulatedConsumption kWh, Export: payload_data_liveMeasurement_accumulatedProduction kWh)
attr Tibber.ws token 5K4MVS-OjfWhK_4yrjOlFe1F6kJXPVf7eQYggo8ebAE
attr Tibber.ws userReadings connect:cmd:.connect {\
	my $hash = $defs{$name};;\
	my $devState = DevIo_IsOpen($hash);;\
	return "Device already open" if (defined($devState));;\
	\
	# establish connection to websocket\
	# format must also include portnumber if a path is to be specified\
	$hash->{DeviceName} = AttrVal($name, "websocketURL", "wss:echo.websocket.org:443");;\
	\
	# special headers needed for Tibber, see also Developer Tools in Browser\
	$hash->{header}{'Sec-WebSocket-Protocol'} = 'graphql-transport-ws';;\
	$hash->{header}{'Host'} = 'websocket-api.tibber.com';;\
	$hash->{header}{'Origin'} = 'https://developer.tibber.com';;\
	\
	# callback function when "select()" signals data for us\
	# websocket Ping/Pongs are treated in DevIo but still call this function\
	$hash->{directReadFn} = sub () {\
		my $hash = $defs{$name};;\
		\
		# we can read without closing the DevIo, because select() signalled data\
		my $buf = DevIo_SimpleRead($hash);;\
		\
		# if read fails, close device\
		if(!defined($buf)) {\
			DevIo_CloseDev($hash);;\
			$buf = "not_connected";;\
		}\
		\
		#Log(3, "$name:$reading: websocket data: >>>$buf<<<");;\
		\
		# only update our reading if buffer is not empty and if last update is older than minInterval\
		if ($buf ne "") {\
			my $websocketDataAge = ReadingsAge($name, "websocketData", 3600);;\
			my $minInterval = AttrVal($name, "minInterval", 0);;\
			my $isNext = ($buf =~ /.*id.*type.*next.*payload.*data.*liveMeasurement.*/s);;\
			\
			readingsBeginUpdate($hash);;\
			readingsBulkUpdate($hash, "websocketData", "$buf") if ($isNext && $websocketDataAge > $minInterval);;\
			readingsBulkUpdate($hash, "websocketData", "$buf") if (!$isNext);;\
			readingsEndUpdate($hash, 1);;\
		}\
	};;\
	\
	# open DevIo websocket\
	DevIo_OpenDev($hash, 0, undef, sub(){\
		my ($hash, $error) = @_;;\
		return "$error" if ($error);;\
		\
		my $token = AttrVal($name, "token", "???");;\
		\
		DevIo_SimpleWrite($hash, '{"type":"connection_init","payload":{"token":"'.$token.'"}}', 2);;\
	});;\
	readingsBulkUpdate($hash, "websocketData", "");;\
		\
	return POSIX::strftime("%H:%M:%S",localtime(time()));;\
},\
disconnect:cmd:.disconnect {\
	my $hash = $defs{$name};;\
	RemoveInternalTimer($hash);;\
	DevIo_SimpleRead($hash);;\
	DevIo_CloseDev($hash);;\
	\
	return POSIX::strftime("%H:%M:%S",localtime(time()));;\
},\
onDisconnect {\
	my $myState = ReadingsVal($name, "state", "???");;\
	my $myData = ReadingsVal($name, "websocketData", "???");;\
	return if ($myState ne "disconnected" and $myData ne "not_connected");;\
	\
	## timer callback function, called after a few seconds to initiate a reconnect\
	my $timerFunction = sub() {\
		my ($arg) = @_;;\
		my $hash = $defs{$name};;\
		my $devState = DevIo_IsOpen($hash);;\
		\
		# only re-connect if device is not connected\
		readingsSingleUpdate($hash, "cmd", "connect", 1) if (!defined($devState));;\
	};;\
	RemoveInternalTimer($name.$reading.'Timer');;\
	\
	# wait a random time before reconnect (exponential backoff TBD):\
	my $rwait = int(rand(200)) + 30;;\
	InternalTimer(gettimeofday() + $rwait, $timerFunction, $name.$reading.'Timer');;\
	\
	#set cmd to a new value, informs user and allows to retrigger when timer expires\
	my $hash = $defs{$name};;\
	readingsBulkUpdate($hash, "cmd", "reconnect attempt in $rwait seconds");;\
	\
	return POSIX::strftime("%H:%M:%S",localtime(time()));;\
},\
onTimeout:websocketData:.* {\
	#re-establish websocket connection if no data received in the past ten minutes\
	#but only if our reading "cmd" was not set to the value "disconnect"\
	\
	#timeout in seconds when the connection is considered dead\
	my $timeoutTime = 600;;\
	\
	# function to execute when timeout expired\
	# defining the function here in the userReading, allows us to insert variables directly\
	my $timerFunction = sub() {\
		my ($arg) = @_;;\
		my $hash = $defs{$name};;\
		my $rCmd = ReadingsVal($name, "cmd", "???");;\
		my $age  = ReadingsAge($name, "websocketData", 0);;\
		\
		Log(3, "$name: onTimeoutTimer triggered >>$arg<<");;\
		\
		#do not do anything further if disconnect is on purpose\
		if ( $rCmd eq "disconnect" ) {\
			Log(3, "$name: cmd was set to disconnect");;\
			return;;\
		}\
		\
		# for whatever reason, we triggered to soon (80%)\
		if ( $age < $timeoutTime*0.8 ) {\
			Log(3, "$name: websocketData is not outdated");;\
			return;;\
		}\
		\
		DevIo_CloseDev($hash);;\
		Log(3, "$name: onTimeoutTimer closed DevIo...");;\
		\
		readingsSingleUpdate($hash, "cmd", "connect", 1);;\
		Log(3, "$name: onTimeoutTimer set cmd to value 'connect'");;\
	};;\
	\
	#remove/cancel previous timers, because we got fresh data and countdown starts again\
	RemoveInternalTimer($name.$reading.'Timer');;\
	\
	#set timer to expire and execute function defined above, give special arg as identifier\
	InternalTimer(gettimeofday() + $timeoutTime, $timerFunction, $name.$reading.'Timer');;\
	\
	return POSIX::strftime("%H:%M:%S",localtime(time()));;\
},\
onConnectionAck:websocketData:.*connection_ack.* {\
	#websocketData contains the string "connection_ack"\
	Log(3, "$name:$reading: got connection ack");;\
	\
	# do not proceed if connection is lost\
	my $hash = $defs{$name};;\
	my $devState = DevIo_IsOpen($hash);;\
	return "Device not open" if (!defined($devState));;\
	\
	readingsBulkUpdate($hash, "cmd", "got connection ack");;\
	\
	my $homeId = AttrVal($name, "homeId", "???");;\
	my $myId = AttrVal($name, "myId", "???");;\
	\
	# build the query, do it in pieces, the comma at the end caused perl errors\
	# so we put it together in this not very elegant way\
	my $json = '{ "id":"'. $myId .'", "type":"subscribe"'.", ";;\
	$json .= '"payload":{';;\
	$json .= '"variables":{}'.", ";;\
	$json .= '"extensions":{}'.", ";;\
	$json .= '"query":"subscription { liveMeasurement( homeId: \"'.$homeId.'\" ) ';;\
	#$json .= '{ timestamp power accumulatedConsumption accumulatedCost currency minPower averagePower maxPower signalStrength }}"';;\
	$json .= '{ timestamp power lastMeterConsumption accumulatedConsumption accumulatedProduction ';;\
	$json .= 'accumulatedProductionLastHour accumulatedCost accumulatedReward currency minPower averagePower maxPower ';;\
	$json .= 'powerProduction powerReactive powerProductionReactive minPowerProduction maxPowerProduction lastMeterProduction ';;\
	$json .= 'powerFactor voltagePhase1 voltagePhase2 voltagePhase3 signalStrength }}"';;\
	$json .= '}}';;\
	\
	#send the string via websocket as ASCII\
	Log(3, "$name:$reading: sending JSON: >>>$json<<<");;\
	DevIo_SimpleWrite($hash, $json, 2);;\
		\
	return POSIX::strftime("%H:%M:%S",localtime(time()));;\
},\
onNextLiveMeasurement:websocketData:.*next.*payload.*data.*liveMeasurement.* {\
	#websocketData contains next-live-measurement-data\
	my $val = ReadingsVal($name, "websocketData", "{}");;\
	my %res = %{json2nameValue($val, undef, undef, "payload_data_liveMeasurement.*")};;\
	\
	my $ret = "got values for:\n";;\
	foreach my $k (sort keys %res) {\
		$ret .= "$k\n";;\
		readingsBulkUpdate($hash, makeReadingName($k), $res{$k});;\
	}\
	return $ret;;\
}
attr Tibber.ws webCmd start:stop
attr Tibber.ws websocketURL wss:websocket-api.tibber.com:443/v1-beta/gql/subscriptions

Owntone (ehemals ForkedDaapd)

Screenshot von dem OwnTone Device und der Websocket.png

Der Musikserver Owntone kann mit einer Websocket Informationen bereitstellen. Diese kann man ebenfalls mit einem einfachen Device auswerten (siehe auch Beitrag und das zugehörige Owntone-Device im Thema ):

defmod WS dummy
attr WS userattr websocketURL
attr WS alias Owntone Websocket
attr WS devStateIcon opened:general_ok@green:stop disconnected:rc_STOP@red:start
attr WS eventMap /wert connect:start/wert disconnect:stop/
attr WS icon hue_filled_plug
attr WS readingList wert
attr WS setList wert
attr WS userReadings connect:wert:.connect {\
	my $hash = $defs{$name};;\
	my $devState = DevIo_IsOpen($hash);;\
	return "Device already open" if (defined($devState));;\
	\
	$hash->{DeviceName} = AttrVal($name, "websocketURL", "ws:echo.websocket.org:443");;\
	\
	# special headers needed for Owntone\
	# https://owntone.github.io/owntone-server/json-api/#push-notifications\
	$hash->{header}{'Sec-WebSocket-Protocol'} = 'notify';;\
	$hash->{header}{'Host'} = 'localhost:3688';;\
	$hash->{header}{'Origin'} = 'http://localhost:3688';;\
	\
	# callback function when "select" signals data for us\
	# websocket Ping/Pongs are treated in DevIo but still call this function\
	$hash->{directReadFn} = sub () {\
		my $hash = $defs{$name};;\
		readingsBeginUpdate($hash);;\
		\
		# we can read without closing the DevIo, because select signalled data\
		my $buf = DevIo_SimpleRead($hash);;\
		\
		if(!defined($buf)) {\
			DevIo_CloseDev($hash);;\
			$buf = "not connected";;\
		}\
		\
		# only update our reading if buffer is not empty\
		readingsBulkUpdate($hash, "websocketData", "$buf") if ($buf ne "");;\
		readingsEndUpdate($hash, 1);;\
	};;\
	\
	# open DevIo websocket\
	DevIo_OpenDev($hash, 0, undef, sub(){\
		my ($hash, $error) = @_;;\
		return "$error" if ($error);;\
		\
		#immediately send Owntone what we would like to be notified for (here we selected everything)\
		DevIo_SimpleWrite($hash, '{"notify":["update","database","player","options","outputs","volume","queue","spotify","lastfm","pairing"]}', 2);;\
	});;\
	\
	return POSIX::strftime("%H:%M:%S",localtime(time()));;\
},\
disconnect:wert:.disconnect {\
	my $hash = $defs{$name};;\
	RemoveInternalTimer($hash);;\
	DevIo_SimpleRead($hash);;\
	DevIo_CloseDev($hash);;\
	\
	return POSIX::strftime("%H:%M:%S",localtime(time()));;\
},\
onDisconnect {\
	my $myState = ReadingsVal($name, "state", "???");;\
	return if ($myState ne "disconnected");;\
	\
	# timer callback function, called after a few seconds to initiate a reconnect\
	my $timerFunction = sub() {\
		my ($arg) = @_;;\
		my $hash = $defs{$name};;\
		my $devState = DevIo_IsOpen($hash);;\
		readingsSingleUpdate($hash, "wert", "connect", 1) if (!defined($devState));;\
	};;\
	\
	RemoveInternalTimer($name.$reading.'Timer');;\
	InternalTimer(gettimeofday() + 10, $timerFunction, $name.$reading.'Timer');;\
	\
	return POSIX::strftime("%H:%M:%S",localtime(time()));;\
},\
onPlayer:websocketData:.*player.* {\
	fhem("set Owntone.device reread");;\
	return POSIX::strftime("%H:%M:%S",localtime(time()));;\
},\
onOutputs:websocketData:.*outputs.* {\
	fhem("get Owntone.device outputs");;\
	return POSIX::strftime("%H:%M:%S",localtime(time()));;\
},\
onVolume:websocketData:.*volume.* {\
	fhem("get Owntone.device volume");;\
	return POSIX::strftime("%H:%M:%S",localtime(time()));;\
},\
onQueue:websocketData:.*queue.* {\
	fhem("get Owntone.device queue");;\
	return POSIX::strftime("%H:%M:%S",localtime(time()));;\
}
attr WS websocketURL ws:192.168.123.123:3688