Tibber Pulse (API): Unterschied zwischen den Versionen

Aus TippvomTibb
Zur Navigation springen Zur Suche springen
Zeile 146: Zeile 146:
  
 
?
 
?
 +
 +
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.
 +
 +
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 ($hash) = @_;
 +
my $devState = DevIo_IsOpen($hash);
 +
 +
# only re-connect if device is not connected
 +
readingsSingleUpdate($hash, "cmd", "connect", 1) if (!defined($devState));
 +
};
 +
my $hash = $defs{$name};
 +
RemoveInternalTimer($hash, $timerFunction);
 +
 +
# wait a random time before reconnect (exponential backoff TBD):
 +
my $rwait = int(rand(200)) + 30;
 +
InternalTimer(gettimeofday() + $rwait, $timerFunction, $hash);
 +
readingsBulkUpdate($hash, "cmd", "reconnect attempt in $rwait seconds");
 +
 +
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 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;
 +
}

Version vom 15. Januar 2024, 18:59 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

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.

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 ($hash) = @_; my $devState = DevIo_IsOpen($hash);

# only re-connect if device is not connected readingsSingleUpdate($hash, "cmd", "connect", 1) if (!defined($devState)); }; my $hash = $defs{$name}; RemoveInternalTimer($hash, $timerFunction);

# wait a random time before reconnect (exponential backoff TBD): my $rwait = int(rand(200)) + 30; InternalTimer(gettimeofday() + $rwait, $timerFunction, $hash); readingsBulkUpdate($hash, "cmd", "reconnect attempt in $rwait seconds");

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