Tibber Pulse (API)

Aus TippvomTibb
Zur Navigation springen Zur Suche springen

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

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 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

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.

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

Zu Einstieg nutze ich den Api Explorer im TibberDev. Nachdem man sich dort einen Personal Token angelegt hat kanns direkt losgehen. Der GraphiQL hat auch eine Autovervollstaendigung. Sehr praktisch.

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 oder Python) brauche welches die fuer mich interessanten Daten in meine MySQL-DB uebertraegt. Ob ich zuerst anzeige und dann speichere, oder umgekehrt muss ich noch festlegen.


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 4