Tibber Pulse (API): Unterschied zwischen den Versionen

Aus TippvomTibb
Zur Navigation springen Zur Suche springen
Zeile 47: Zeile 47:
  
 
Der Aufbau ist recht simpel und das Doc in knapp einer Stunde durchprobiert. So dass ich dann schnell an dem Punkt angelangt bin, dass ich ein Programm (PHP, Python oder JavaScript) brauche welches die fuer mich interessanten Daten in meine MySQL-DB uebertraegt. Ob ich zuerst anzeige und dann speichere, also sozusagen ueber die Datenbank puffere, oder umgekehrt muss ich noch festlegen.
 
Der Aufbau ist recht simpel und das Doc in knapp einer Stunde durchprobiert. So dass ich dann schnell an dem Punkt angelangt bin, dass ich ein Programm (PHP, Python oder JavaScript) brauche welches die fuer mich interessanten Daten in meine MySQL-DB uebertraegt. Ob ich zuerst anzeige und dann speichere, also sozusagen ueber die Datenbank puffere, oder umgekehrt muss ich noch festlegen.
 +
 +
===ChatGPT zum Subprotokoll===
 +
 +
Das Subprotokoll "graphql-transport-ws" ist ein WebSocket-Subprotokoll, das speziell für die Kommunikation zwischen Client und Server in GraphQL-Anwendungen entwickelt wurde. Es basiert auf dem WebSocket-Protokoll, das eine bidirektionale, persistente Verbindung zwischen einem Client und einem Server ermöglicht.
 +
 +
Im Kontext von GraphQL wird "graphql-transport-ws" verwendet, um GraphQL-Abfragen, Mutationen und Abonnements über WebSocket-Verbindungen zu übertragen. Im Gegensatz zum traditionellen HTTP-Protokoll, das eine separate Anfrage-Antwort-Struktur hat, ermöglicht WebSocket eine kontinuierliche Kommunikation zwischen Client und Server, was besonders nützlich ist, wenn Echtzeit-Aktualisierungen benötigt werden, wie bei Abonnements in GraphQL.
 +
 +
Das Subprotokoll "graphql-transport-ws" definiert spezifische Nachrichtenformate und Handshake-Verfahren, die für die GraphQL-Kommunikation über WebSockets erforderlich sind. Durch die Verwendung dieses Subprotokolls können Entwickler GraphQL-Anwendungen erstellen, die von den Echtzeitfähigkeiten von WebSocket profitieren, ohne auf die Vorteile und Flexibilität von GraphQL verzichten zu müssen.
  
 
==Mehrfacher Hinweis Subprotokolwechsel Dezember 2022==
 
==Mehrfacher Hinweis Subprotokolwechsel Dezember 2022==

Version vom 16. März 2024, 08:10 Uhr

Allgemeines

Da ich aktuell vom Energieversorger einen zweiten Hausanschluss (40kW :-() gelegt bekommen habe und ich für die Wallboxen zur Foerderung einen 100% Oekostrom haben musste, habe ich kurzerhand einen Vertrag mit Tibber mal zum Testen abgeschlossen.

Installation

Pulse IR und Bridge

Tibber Pulse IR und Bridge

Eigentlich verlief die Installation problemlos. Fuer mich war allerdings der Assistent in der Tibber-App zu kindisch. Das ist eher was für Apple-User. Ich haette mir an der ein oder anderen Stelle mehr Hardcore-Infos gewuenscht. Da der mehrstufige Prozess (WLAN-Bridge-Pulse-Zaehler) doch einige Solperfalle enthaelt ist es fuer mich eher frustrierend jedesmal bei einem Fehler zurueck auf Los geschickt zu werden. Das geht definitiv besser. Ist aber wieder ein Beispiel, dass die Informatiker-Jobs heute nur noch durch 5-jaehrige Schimpansen (ungelernte ;-)) besetzt werden.

Bei mir gab es letztendlich nur einen gravierenden Fehler. Von den mitgelieferten Akkus des Pulse war einer defekt. Bei mir sind das zwei AA-Zellen auf LiBasis! Ein Nachladen mit einem Lithium-1.5V-Lader hat keinen Erfolg gebracht. Erst der Austausch gegen 2 neue Zellen war erfolgreich.

App

TibberAppIcon

Die App find ich ziemlich verquer. Sobald Rechnungsanschrift und Installationsorte unterschiedlich sind, geht das Chaos los. Auch der Support tut sich dann schwer. Ich traue mich gar nicht so richtig weitere Vertraege abzuschlieszen. Alles ueber die App machen zu wollen/muessen ist ... Das gehoert fuer mich ins WebUserPortal.

Was echt nervt ist, dass sich gefühlt jeden Monat das LookandFeel aendert. Schon die dritte oder vierte Designaenderung des Homescreens (Zuhause).

API

Da die App eher suboptimal ist, habe ich recht zuegig damit angefangen mir die notwendigen Infos ueber die API auf eine Infoseite im Intranet zu holen. Ein Integration in FHEM steht noch aus.

Erste Schritte

Uebersicht [1]

Zusammenfassung: Die Plattform wird über GraphQL verfügbar gemacht, eine Technologie, die von Facebook konzipiert und entwickelt wurde.

Was ist GraphQL?

The GraphQL query language is basically about selecting fields on objects.

Was dies bedeutet kommt weiter unten. Weiter heiszt es auf der Tibber Seite.

   GraphQL ist eine Abfragesprache für Ihre(? unsere) API und eine serverseitige Laufzeitumgebung zum Ausführen von Abfragen mithilfe eines Typsystems, das Sie für Ihre Daten definieren. GraphQL ist nicht an eine bestimmte Datenbank oder Speicher-Engine gebunden und wird stattdessen durch Ihren vorhandenen Code und Ihre Daten unterstützt.

Mit der Uebersetzung von Google hadere ich noch. Der Satz kommt vermutlich ursrpruenglich von Facebook und wurde von Tibber 1:1 uebernommen.

Der naechste Satz gibt schon mehr Aufschluss:

Tibber hat sich für GraphQL als API entschieden, weil es unseren Integratoren und uns selbst Flexibilitaet bietet. Die Moeglichkeit, die gewuenschten Daten genau zu definieren, macht die Nutzung wesentlich einfacher als herkoemmliche REST-basierte APIs. Es ist auch sehr schoen, dass die serverseitige Implementierung von GraphQL sehr gut mit unserer Microservice-Architektur zusammenspielt :-)

Zum Einstieg nutze ich den Api Explorer im TibberDev. Nachdem man sich dort einen Personal Token angelegt hat, kann's direkt losgehen. Der GraphiQL hat auch eine Autovervollstaendigung. Sehr praktisch.

TibberDevAccount

Die Root-Types sind

  • query: Query
  • mutation: RootMutation
  • subscription: RootSubscription
  • fragment feht irgendwie in der Doc

Der Aufbau ist recht simpel und das Doc in knapp einer Stunde durchprobiert. So dass ich dann schnell an dem Punkt angelangt bin, dass ich ein Programm (PHP, Python oder JavaScript) brauche welches die fuer mich interessanten Daten in meine MySQL-DB uebertraegt. Ob ich zuerst anzeige und dann speichere, also sozusagen ueber die Datenbank puffere, oder umgekehrt muss ich noch festlegen.

ChatGPT zum Subprotokoll

Das Subprotokoll "graphql-transport-ws" ist ein WebSocket-Subprotokoll, das speziell für die Kommunikation zwischen Client und Server in GraphQL-Anwendungen entwickelt wurde. Es basiert auf dem WebSocket-Protokoll, das eine bidirektionale, persistente Verbindung zwischen einem Client und einem Server ermöglicht.

Im Kontext von GraphQL wird "graphql-transport-ws" verwendet, um GraphQL-Abfragen, Mutationen und Abonnements über WebSocket-Verbindungen zu übertragen. Im Gegensatz zum traditionellen HTTP-Protokoll, das eine separate Anfrage-Antwort-Struktur hat, ermöglicht WebSocket eine kontinuierliche Kommunikation zwischen Client und Server, was besonders nützlich ist, wenn Echtzeit-Aktualisierungen benötigt werden, wie bei Abonnements in GraphQL.

Das Subprotokoll "graphql-transport-ws" definiert spezifische Nachrichtenformate und Handshake-Verfahren, die für die GraphQL-Kommunikation über WebSockets erforderlich sind. Durch die Verwendung dieses Subprotokolls können Entwickler GraphQL-Anwendungen erstellen, die von den Echtzeitfähigkeiten von WebSocket profitieren, ohne auf die Vorteile und Flexibilität von GraphQL verzichten zu müssen.

Mehrfacher Hinweis Subprotokolwechsel Dezember 2022

Die TibberAPI unterstuetzte zunaechst GraphQL-Websocket-Subscriptions mithilfe des Unterprotokolls graphql-ws. Diese Bibliothek wird/wurde jedoch archiviert und nicht mehr gepflegt. Daher wurde am 31. Maerz 2022 die Unterstützung für das Unterprotokoll graphql-transport-ws hinzugefuegt.
Die Unterstuetzung für das alte Protokoll wird im Dezember 2022 entfernt. Gleichzeitig aendert sich auch die URL für die Verbindung zum Websocket-Server. Zuvor konnte die Verbindung unter wss://api.tibber.com/v1-beta/gql/subscriptions hergestellt werden. Nach der Aenderung muss die URL ueber GraphQL abgefragt werden (siehe Beispiel unten) und der Client muss das Unterprotokoll graphql-transport-ws unterstuetzen.
Frage:
{
  viewer {
    websocketSubscriptionUrl
  }
}

Antwort:
{
  "data": {
    "viewer": {
      "websocketSubscriptionUrl": "wss://websocket-api.tibber.com/v1-beta/gql/subscriptions"
    }
  }
}


Anfragenbegrenzung

Zum Schutz der API gilt eine Ratenbegrenzung von 100 Anfragen in 5 Minuten pro IP-Adresse. Beachten Sie, dass die Preise einmal täglich am Nachmittag berechnet werden (für Norwegen und Schweden sind sie zunächst vorläufig und werden später möglicherweise mit geringfügigen Änderungen nach Bestätigung der Wechselkurse endgültig festgelegt). Sie können „priceInfo.today“ und „priceInfo.tomorrow“ verwenden, um sie im Voraus abzurufen, anstatt nur „priceInfo.current“ für die aktuelle Stunde zu verwenden.

In medias res

Zum Starten empfiehlt die Tibber-API-Doc, die fundamentalen Konzepte von GraphQL, die Kommunikation mit der API und den Explorer.

Um die nachfolgenden Beispiele schnell zu verstehen, hier der Link zur API-Referenz

Bei der GraphQL-Abfragesprache geht es im Wesentlichen um die Auswahl von Feldern in Objekten.

Objekt Typen

Die Begriffsdifferenzierung Objekt und Object Types ist mir derzeit noch nicht klar.

Bei 'Home' wird von einem Objekttyp gesprochen. Klar ist man hat Objekte, die mit dem Schluesselwort type eingeleitet und dann beschrieben werden.

Einige Felder sind Skalare (Boolesche Werte, Zeichenfolgen, Ganzzahlen, Gleitkommazahlen), waehrend andere Objekttypen sind (Adresse, LegalEntity). Eine GraphQL-Abfrage muss vollstaendig auf Skalarebene sein. D.h. in einer Abfrage duerfen die abgefragten Felder keine Objekte (keine weiteren Objekttypen) sein, sondern hierarchisch heruntergebrochen auf weitere Inhalte bis es Skalare sind.
This query is valid:

{
    homes{
        id
    }
} 

This is not:

{
    homes{
        id
        owner   <- hier ist der Fehler. Dies ist kein Skalar!
    }
} 

The owner field is an object type and you would need to specify which scalar fields you would like returned:

{
    homes{
        id
        owner{
            name
        }
    }
}

query Datenabfrage (read)

query->viewer

Zuerst mit homes die Zaehlerplatz-iD holen und dan mit home (id: "<ID>") nur die data dazu holen.

Hello World;-) {

 viewer {
   login
   name
   userId
   accountType
   home (id: "1cdd8b6d-956c-447f-9b95-9a57801eaf4c") {
     id
     features {
       realTimeConsumptionEnabled
     }
     subscriptions {
       id
     }
     currentSubscription {
       id
     }
     meteringPointData {
       consumptionEan
       gridCompany
       gridAreaCode
       priceAreaCode
       productionEan
       energyTaxType
       vatType
       estimatedAnnualConsumption
     }
     owner {
       id
     }
     mainFuseSize
   }
   websocketSubscriptionUrl
 }

}


Interessant fand ich z.B.

         "priceAreaCode": "Amprion",
         "estimatedAnnualConsumption": 500
         "mainFuseSize": null

und was da noch alles an Daten einzutragen ist.

size: Int
The size of the home in square meters


mutation Datenveraenderung (write)

Mutationen gibt es nur 3, wobei die erste bei mir entfaellt.

sendMeterReading(input: MeterReadingInput!): MeterReadingResponse!
Send meter reading for home (only available for Norwegian users) 

updateHome(input: UpdateHomeInput!): Home!
Update home information

sendPushNotification(input: PushNotificationInput!): PushNotificationResponse!
Send notification to Tibber app on registered devices


subscription

Subscription hat nur 2 Felder, wobei dauerhaft nur das esrte interessant ist.

liveMeasurement(homeId: ID!): LiveMeasurement
Subscribe to real-time measurement stream from Pulse or Watty device
testMeasurement(homeId: ID!): LiveMeasurement
Subscribe to test stream

subscription{

 liveMeasurement(homeId: "<homeID>"){
   timestamp
   power
   lastMeterConsumption
   accumulatedConsumption
   accumulatedProduction
   accumulatedConsumptionLastHour
   accumulatedProductionLastHour
   accumulatedCost
   accumulatedReward
   currency
   minPower
   averagePower
   maxPower
   powerProduction
   powerReactive
   powerProductionReactive
   minPowerProduction
   maxPowerProduction
   lastMeterProduction
   powerFactor
   voltagePhase1
   voltagePhase2
   voltagePhase3
   currentL1
   currentL2
   currentL3
   signalStrength
 }

}

fragment

?

Dazu habe ich bisher keine weiteren Infos gefunden.

Programmierung

Query/Mutation

Die Abfragen und Aenderungen zu programmieren sind schnell erledigt.

Der Einstieg auf der Konsole gelingt mit curl recht gut.

Subscription

Die Subscriptions sind schon ein wenig anspruchsvoller zu programmieren als Query und Mutation (QuM). Ist QuM eine simple http-Anfrage mit einen Antwort im JSON-Format, ist es bei den Subscriptions ein Websocket, der einmal geoeffnet in unregelmaeszigen Abstaenden JSON-Pakete leiefert.

In PHP habe ich wenig bis gar nichts gefunden. Ich wollte mir auch einfach die Nutzung einer GraphQL-Library ersparen. Python habe ich erst einmal an den Schluss verschoben und einem Integration in FHEM den Vorzug gegeben.

Hier der Code der tatsaechlich funktioniert, obwohl er an ein paar Stellen nicht so vorgeht wie ich aus den Tibber-Docs erwartet hatte.

HINWEIS: Dies ist der "nackte" Perl-Code. In der "Origonaldatei" sind noch die "FHEM-Escapes" doppelter Strichpunkt und Zeilenfolgezeichen \ enthalten. Eine Eingabe ueber die FHEM-Kmmandozeile hat bei mir nicht funktioniert. Erst das direkte Einkopieren in die fhem.conf zeigte das nachfolgende, funktionierende Ergebnis.

  1 connect:cmd:.connect {
  2 	my $hash = $defs{$name};
  3 	my $devState = DevIo_IsOpen($hash);
  4 	return "Device already open" if (defined($devState));
  5 	
  6 	# establish connection to websocket
  7 	# format must also include portnumber if a path is to be specified
  8 	$hash->{DeviceName} = AttrVal($name, "websocketURL", "wss:echo.websocket.org:443");
  9 	
 10 	# special headers needed for Tibber, see also Developer Tools in Browser
 11 	$hash->{header}{'Sec-WebSocket-Protocol'} = 'graphql-transport-ws';
 12 	$hash->{header}{'Host'} = 'websocket-api.tibber.com';
 13 	$hash->{header}{'Origin'} = 'https://developer.tibber.com';
 14 	
 15 	# callback function when "select()" signals data for us
 16 	# websocket Ping/Pongs are treated in DevIo but still call this function
 17 	$hash->{directReadFn} = sub () {
 18 		my $hash = $defs{$name};
 19 		
 20 		# we can read without closing the DevIo, because select() signalled data
 21 		my $buf = DevIo_SimpleRead($hash);
 22 		
 23 		# if read fails, close device
 24 		if(!defined($buf)) {
 25 			DevIo_CloseDev($hash);
 26 			$buf = "not_connected";
 27 		}
 28 		
 29 		#Log(3, "$name:$reading: websocket data: >>>$buf<<<");
 30 		
 31 		# only update our reading if buffer is not empty and if last update is older than minInterval
 32 		if ($buf ne "") {
 33 			my $websocketDataAge = ReadingsAge($name, "websocketData", 3600);
 34 			my $minInterval = AttrVal($name, "minInterval", 0);
 35 			my $isNext = ($buf =~ /.*id.*type.*next.*payload.*data.*liveMeasurement.*/s);
 36 			
 37 			readingsBeginUpdate($hash);
 38 			readingsBulkUpdate($hash, "websocketData", "$buf") if ($isNext && $websocketDataAge > $minInterval);
 39 			readingsBulkUpdate($hash, "websocketData", "$buf") if (!$isNext);
 40 			readingsEndUpdate($hash, 1);
 41 		}
 42 	};
 43 	
 44 	# open DevIo websocket
 45 	DevIo_OpenDev($hash, 0, undef, sub(){
 46 		my ($hash, $error) = @_;
 47 		return "$error" if ($error);
 48 		
 49 		my $token = AttrVal($name, "token", "???");
 50 		
 51 		DevIo_SimpleWrite($hash, '{"type":"connection_init","payload":{"token":"'.$token.'"}}', 2);
 52 	});
 53 	readingsBulkUpdate($hash, "websocketData", "");
 54 	
 55 	return POSIX::strftime("%H:%M:%S",localtime(time()));
 56 },
 57 disconnect:cmd:.disconnect {
 58 	my $hash = $defs{$name};
 59 	RemoveInternalTimer($hash);
 60 	DevIo_SimpleRead($hash);
 61 	DevIo_CloseDev($hash);
 62 	
 63 	return POSIX::strftime("%H:%M:%S",localtime(time()));
 64 },
 65 onDisconnect {
 66 	my $myState = ReadingsVal($name, "state", "???");
 67 	my $myData = ReadingsVal($name, "websocketData", "???");
 68 	return if ($myState ne "disconnected" and $myData ne "not_connected");
 69 	
 70 	## timer callback function, called after a few seconds to initiate a reconnect
 71 	my $timerFunction = sub() {
 72 		my ($hash) = @_;
 73 		my $devState = DevIo_IsOpen($hash);
 74 		
 75 		# only re-connect if device is not connected
 76 		readingsSingleUpdate($hash, "cmd", "connect", 1) if (!defined($devState));
 77 	};
 78 	my $hash = $defs{$name};
 79 	RemoveInternalTimer($hash, $timerFunction);
 80 	
 81 	# wait a random time before reconnect (exponential backoff TBD):
 82 	my $rwait = int(rand(200)) + 30;
 83 	InternalTimer(gettimeofday() + $rwait, $timerFunction, $hash);
 84 	readingsBulkUpdate($hash, "cmd", "reconnect attempt in $rwait seconds");
 85 	
 86 	return POSIX::strftime("%H:%M:%S",localtime(time()));
 87 },
 88 onConnectionAck:websocketData:.*connection_ack.* {
 89 	#websocketData contains the string "connection_ack"
 90 	Log(3, "$name:$reading: got connection ack");
 91 	
 92 	# do not proceed if connection is lost
 93 	my $hash = $defs{$name};
 94 	my $devState = DevIo_IsOpen($hash);
 95 	return "Device not open" if (!defined($devState));
 96 	
 97 	readingsBulkUpdate($hash, "cmd", "got connection ack");
 98 	
 99 	my $homeId = AttrVal($name, "homeId", "???");
100 	my $myId = AttrVal($name, "myId", "???");
101 	
102 	# build the query, do it in pieces, the comma at the end caused perl errors
103 	# so we put it together in this not very elegant way
104 	my $json = '{ "id":"'. $myId .'", "type":"subscribe"'.", ";
105 	$json .= '"payload":{';
106 	$json .= '"variables":{}'.", ";
107 	$json .= '"extensions":{}'.", ";
108 	$json .= '"query":"subscription { liveMeasurement( homeId: \"'.$homeId.'\" ) ';
109 	$json .= '{ timestamp power lastMeterConsumption accumulatedConsumption accumulatedProduction ';
110 	$json .= 'accumulatedProductionLastHour accumulatedCost accumulatedReward currency minPower averagePower maxPower ';
111 	$json .= 'powerProduction powerReactive powerProductionReactive minPowerProduction maxPowerProduction lastMeterProduction ';
112 	$json .= 'powerFactor voltagePhase1 voltagePhase2 voltagePhase3 signalStrength }}"';
113 	$json .= '}}';
114 	
115 	#send the string via websocket as ASCII
116 	Log(3, "$name:$reading: sending JSON: >>>$json<<<");
117 	DevIo_SimpleWrite($hash, $json, 2);
118 	
119 	return POSIX::strftime("%H:%M:%S",localtime(time()));
120 },
121 onNextLiveMeasurement:websocketData:.*next.*payload.*data.*liveMeasurement.* {
122 	#websocketData contains next-live-measurement-data
123 	my $val = ReadingsVal($name, "websocketData", "{}");
124 	my %res = %{json2nameValue($val, undef, undef, "payload_data_liveMeasurement.*")};
125 	
126 	my $ret = "got values for:\n";
127 	foreach my $k (sort keys %res) {
128 		$ret .= "$k\n";
129 		readingsBulkUpdate($hash, makeReadingName($k), $res{$k});
130 	}
131 	return $ret;
132 }

Code Analyse

Es gibt 5 Eintrittspunkte:

  • connect
  • disconnect
  • onDisconnect
  • onConnectionAck
  • onNextLiveMeasurement

Die ersten beiden legen die Befehlsfolgen für ein Start-Kommando (set cmd connect, bzw. set cmd disconnect) oder Stop-Kommando fest. Die letzten 3 reagieren auf die entsprechenden Events.

connect

Lexikalische Variablen mittels my, bzw. my()

Jede Variable, die mit our deklariert oder auch "einfach so" ohne eine Deklaration verwendet wird, wird in die Symboltabelle des jeweils aktuellen Packages aufgenommen.
Deklariert man dagegen eine Variable mit dem Operator my, so wird die entsprechende Variable in einer anderen Tabelle abgelegt, auf die kein expliziter Zugriff möglich ist. 
Neben der Tatsache, daß my-Variablen in einer eigenen Tabelle verwaltet werden, ist von besonderer Bedeutung, daß sie nur einen recht beschränkten Gültigkeitsbereich besitzen. Eine durch my erzeugte Variable steht nur in dem aktuellen Block (definiert durch geschweifte Klammern "{...}"), der aktuellen Datei oder innerhalb eines Arguments von eval() zur Verfügung. Außerhalb davon existieren diese Variablen nicht, es ist also (im Gegensatz zu our-Variablen) auch mit Hilfe des Package-Namens dort kein Zugriff möglich
Es kann in einem Package durchaus zwei Variablen gleichen Namens geben: eine in der Symboltabelle und eine, die durch einen Aufruf von my entstanden ist. In einem solchen Falle wird bei einfacher Verwendung des Bezeichners auf die my-Variable zugegriffen. 

In $defs stehen alle Defines drin. In $hash wird also der Name des des aufrufenden Devices lokal hinterlegt und danach mit DevIO_IsOpen() der Status abgefragt. $defs ist ein Hash. Ein Hash ist ein anderer Namen fuer ein assoziatives Array. Also eine ungeordnete Liste von Skalaren (Zahl oder String) die ueber einen Stringwert angesprochen (Lookup) werden koennen.

FHEM-Spezial:

In $defs findet man alle Devices, z.b. $defs{"Rolladen_Tuere"} beinhaltet das Device.
my @alle = keys %defs;
my @teilmenge = defInfo('TYPE=notify','NAME'); liefert ein array mit allen notify devices.
z.B. my @teilmenge = defInfo('NAME=Rollladen.*','NAME');
return "Device already open" if (defined($devState));

Sollte also das Device bereits im State "offen" sein, wird die connect-Routine abgebrochen. Dann erscheint in den Readings nicht Datum/Uhrzeit des Connects sondern die Meldung "Device already open".

# 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");
HASH-Operationen (Beispiel):
$href ={APR => 4,AUG => 8}; #anonymous hash
$el = $href->{APR}; $el = %{$href}{APR}; #access element of hash
$href2 = {%{$href1}}; #copy hash
if (ref($r) eq "HASH") {} #checks if $r points to hash

Dem Internal "DeviceName" wird das Ergebnis von AttrVal() zugewiesen. Bei mir ergab das:

DeviceName wss:websocket-api.tibber.com:443/v1-beta/gql/subscriptions
FHEM-Referenz:
AttrVal(<devicename>,<attribute>,<defaultvalue>)
Gibt das entsprechende Attribut des Gerätes zurück
{ Value("wz") }
{ OldValue("wz") }
{ time_str2num(OldTimestamp("wz")) }
{ ReadingsVal("wz", "measured-temp", "20")+0 }
{ ReadingsTimestamp("wz", "measured-temp", 0)}
{ AttrVal("wz", "room", "none") }

Das Attribut websocketURL ist bei mir wss:websocket-api.tibber.com:443/v1-beta/gql/subscriptions. Somit erklaert sich auch der Eintrag bei DeviceName. "wss:echo.websocket.org:443" ist nur der Defaultwert.

Man haette zum Testen auch die folgenden beiden verwenden/eintragen koennen.

There are free test servers that we can use to experiment with a WebSocket client, such as:
   Hoppscotch – wss://echo-websocket.hoppscotch.io (GUI)
   λ if else – wss://ws.ifelse.io (GUI)
Both provide a GUI for testing via a browser. The former sends a timestamp to the client every second but doesn’t respond to client messages. The latter is a standard echo service that sends a copy of each received message back to the client. Both make sense in testing.

Da curl nicht nativ websocket-Kommunikation unterstuetzt kann man auf folgende Tool ausweichen. Auch chrome bietet ein Websocket-Client-Plugin.

  • wssh3
  • websocat
  • wscat
# 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); } };

Die Callback-Funktion wird direkt, ohne Namen, in das/den (?) Hash unter directReadFn abgelegt. Die Callback-Funktion: Hier wird mit Hilfe der Funktion DevIo_SimpleRead() die Rueckmeldung des Server eingelesen. Siehe [2] Hinweis zum Logging in FHEM: [3] Die eigentliche Aktivitaet geschieht in den sieben Zeilen am Ende der Callback-Fkt.

ReadingsAge(<devicename>,<reading>,<defaultvalue>) #gibt das Alter des Readings in Sekunden zurück. 
AttrVal hatten wir schon.
isNext bekommt seinen Wert aus dem durch einen REGEX geliefert Wert des Buffers. Beispiel ($match) = ($dirty =~ /^(.*)$/s); 
Die Funktion readingsBeginUpdate() bereitet die Definition mit dem Hash $hash auf ein Update von Readings vor. Dies betrifft insbesondere das Setzen von Umgebungsvariablen sowie dem aktuellen Zeitstempel als Änderungszeitpunkt. Der Aufruf dieser Funktion ist notwendig um eigentliche Updates mit der Funktion readingsBulkUpdate() auf der gewünschten Definition durchführen zu können. [4]

Die Funktion readingsEndUpdate() beendet den Bulk-Update Prozess durch die Funktionen readingsBeginUpdate() & readingsBulkUpdate() und triggert optional die entsprechenden Events sämtlicher erzeugter Readings für die Definition $hash. Desweiteren werden nachgelagerte Tasks wie bspw. die Erzeugung von User-Readings (Attribut: userReadings), sowie die Erzeugung des STATE aufgrund des Attributs stateFormat durchgeführt. Sofern $do_trigger gesetzt ist, werden alle anstehenden Events nach Abschluss getriggert.

# 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); });

Das ist die eigentliche Kontaktaufnahme.

Dies dient vermutlich dazu, bis zum ersten Empfang von Daten, das Reading websocketDate zu leeren.

return POSIX::strftime("%H:%M:%S",localtime(time()));

Zum Abschluss ercheint dann im Reading connect die aktuelle Zeit.

readingsBulkUpdate($hash, "websocketData", "");

disconnect

1  disconnect:cmd:.disconnect {
2 	my $hash = $defs{$name};
3 	RemoveInternalTimer($hash);
4 	DevIo_SimpleRead($hash);
5 	DevIo_CloseDev($hash);
6 	return POSIX::strftime("%H:%M:%S",localtime(time()));

Erlaeuterung:

  • Die Funktion RemoveInternalTimer löscht möglicherweise noch anstehende Timer welche mit dem Übergabeparameter $arg gescheduled sind. Optional kann man zusätzlich die Suche auf eine bestimmte Funktion $functionName weiter einschränken. [5]
  • Die Funktion DevIo_SimpleRead() liest anstehende Daten für die Verbindung von $hash ein und gibt diese zurück. [6]
  • Die Funktion DevIo_CloseDev() schließt eine evtl. geöffnete Verbindung für die Definition $hash. [7]

onDisconnect

TODO

onConnectionAck

TODO


onNextLiveMeasurement

TODO


Tests

Siehe hierzu auch den Beitrag Websocket-Tools.

PHP/Python

TODO