Polestar-API zu MQTT im Container (openWB-V1-Anbindung möglich)

Danke für deine Arbeit - läuft wieder!

Zum watchtower muss ich aber „für Blöde“ fragen. Den Abschnitt von #27 in die „docker-compose.yml“ einfügen ohne die Einrückung zu verändert - soweit okay. Ist es egal ob dies an Position 1 oder 2 geschieht?

Oder ist dies durch den Eintrag oberhalb von „2“ schon obsolete?

Ja, ist egal ob vor oder nach dem mqtt Container :slight_smile:

1 „Gefällt mir“

Genau. Bitte das services: weglassen (also nur einmal am Anfang). Das ist die „Überschrift“ des Abschnitts.

Der Eintrag unter labels: sagt dem Watchtower, dass dieser Container aktualisiert werden soll.

Ein Tipp: Bei so Fragen immer erst einmal ChatGPT fragen. Das kann das zum Teil besser als ich. Nutze ich auch gerne.

Frage mal z.B. zum Einstieg „Erkläre mir die folgende docker-compose.yml“ und dahinter die komplette Datei kopieren. Du wirst staunen.

1 „Gefällt mir“

Ist jetzt merged. :+1:

1 „Gefällt mir“

Interessante Erkenntnis:

POLESTAR_CYCLE sollte etwas unter 300 Sekunden (bei mir jetzt 280) konfiguriert werden.

Hintergrund: Beim Abruf des API-Token bekommt man „expires_in = 299“.

Bleibt man unter dieser Zeit, so erfolgt nicht bei jedem Zyklus ein neuer Login, sondern er nutzt das Token weiter.

Naja, fast. :grin:

Da fehlt im Code noch die Refresh-Funktion für das Token. :man_facepalming:

1 „Gefällt mir“

Und hier ein kleines Feature-Update:

Bildschirmfoto 2024-12-13 um 17.13.02

BASIS_TOPIC/container/online wird offline, wenn der Container nicht läuft oder die Verbindung zu MQTT verloren hat (LWT).

Der Topic wird auf online gesetzt, wenn die Verbindung zum MQTT-Server steht.

BASIS_TOPIC/container/last_update wird erst nach erfolgreichem Abruf der drei Datensätze gesetzt. Bricht der Abruf wegen eines Fehlers vorher ab, so wird der Zeitstempel nicht aktualisiert.

@Landmatrose Passt das? Was fehlt?

1 „Gefällt mir“

Das ist genau das, was ich gesucht habe, Abfrage: besteht eine Verbindung zwischen dem HomeSystem und dem Polestar-Server. :+1:

Danke dafür, freue mich schon auf das nächste Update

War wohl doch ein bisschen spät:

image

Der Topic sollte …/connected heißen. Fix ist raus. Sorry.

Hmmm, noch nicht so ganz:

  • Der Status connected zeigt erst einmal, dass der Container aktiv ist.
  • Erst der Zeitstempel zeigt die letzte erfolgreiche Übertragung.

Da muss ich noch ein bisschen bauen, um eine fehlgeschlagene Verbindung zuverlässig zu meiden.

Dann habe ich das (noch) nicht verstanden.

Der Wert ist also „nur Container läuft“ und nicht „Handshake Container - PolestarServer erfolgreich“?
Dann ist es nicht was ist gesucht habe.
Den Zeitstempel der letzten Kommunikation gibt es doch bereits (eventUpdatedTimestamp iso/unix). Mein Problem ist aber, dass dieser in zwei Fällen unverändert bleibt:

  1. es gibt keine veränderten, aktuelleren Daten
  2. es besteht keine gültige Verbindung zum Polestar Server (was letztlich für das HomeSystem wie 1. aussieht)

Diese beiden Fälle möchte ich unterscheiden können. Hintergrund ist meine „Zielladefunktion“. Besteht keine Verbindung (Punkt 2.) errechne ich einen Ersatz-SoC aus der Energie der WB, ansonsten warte ich einfach auf den nächsten, gültigen Wert (Punkt 1).

Stimmt. :grin:

  • BASIS_TOPIC/container/connected
    • der Container läuft: online
    • sieht der MQTT-Broker den Container nicht mehr: offline
  • BASIS_TOPIC/container/last_update
    • wird gesetzt, wenn alle 3 Anfragen (getConsumerCarsV2, getBatteryData, getOdometerData) ohne Fehler durchgelaufen sind
    • ist also der Nachweis, dass die API
      a) antwortet und
      b) lesbare Antworten liefert
    • wird vom Python-Programm erzeugt
  • BASIS_TOPIC/getBatteryData/eventUpdatedTimestamp und BASIS_TOPIC /getOdometerData/eventUpdatedTimestamp
    • kommen beide aus der API (und nicht aus dem Python-Programm)
    • geben die Info, wann das letzte Mal vom Fahrzeug geänderte Daten bei Polestar angekommen sind
    • helfen auch nicht weiter, wenn ich die gleichen Daten alle 5 Minuten neu sende
      • mache ja auch nichts anderes als if odometer_data != last_odometer_data: fragen

Doch, im Prinzip schon. Das Programm kommt nur bis hier, wenn die Kommunikation mit Polestar funktioniert.

Aber dann bricht der Container aktuell ab und meldet „offline“. Will ich aber noch schöner machen, im Sinne von

  • online
    • alles gut
  • API offline
    • Container läuft, aber Polestar-Server zickt
  • offline
    • Container läuft nicht (z.B. docker compose down, Host abgestürzt oder ausgeschaltet)

Ganz einfach: Ist BASIS_TOPIC/container/last_update deutlich älter als Deine POLESTAR_CYCLE in Sekunden, so gibt es ein Problem mit der Verbindung.

1 „Gefällt mir“

Und wieder ein kleines Update:

polestar2mqtt  | Polestar_2_MQTT.py startet
polestar2mqtt  | ==========================
polestar2mqtt  | get_token()
polestar2mqtt  |  get_path_token()
polestar2mqtt  |     MQTT connected with result code 'Success': polestar2/container/connected=online
polestar2mqtt  |   code_verifier  = xNELdrOwGj0W4Zvcbq8ZKlKkLrryXn7GQemdxPyuHlY
polestar2mqtt  |   code_challenge = D0i5f17UPw4TQGidrYRQLKJWMc1iotXDUCQkGzCWw14

PKCE Code Verifier und Code Challenge werden jetzt dynamisch generiert und bei jedem Login geändert.

@salkin Bitte gerne aus folgendem Commit klauen. Der Code leht sich ja sehr stark an Deinen Widgets an. :blush:

Deine statische Lösung funktioniert zwar, konterkariert aber natürlich PKCE. :rofl:

Danke für deine ausführliche Erklärung!
Warte dann gespannt auf das Update um es zu probieren.

Übrigens lässt sich die API auch prima benutzen um externe Ladevorgänge zu dokumentieren.
Wenn der „chargerConnectionStatus“ ein angeschlossenes Kabel meldet und gleichzeitig KEIN Auto an der heimischen WB angeschlossen ist, handelt es sich um eine externe Ladung.
Mit einem Script merke ich mir dann die Randbedingungen wie Datum, Uhrzeit, Kilometerstand, Start- und EndeSoC. Die geladene Energie ermittelt das Script indem es die Ladeleistung „chargingPowerWatts“ über die Zeit integriert. Das geht zwar nur im 5 Minuten-Raster, aber für den Hausgebrauch ist es ausreichend.
Die Daten überträgt das Script dann auf ein Google-Spreadsheet.
Vorher hatte ich immer Block und Stift dabei, so ist es wesentlich komfortabler. Wenn ich jetzt noch irgendwie eine ID der Ladestation reinbekomme…

You are welcome!

Och, da kannst Du lange warten. Das ist schon seit gestern online. Schau mal hier:

Das sind die Job-Läufe, die jedes mal, wenn da ein grüner Haken steht, einen neuen Container gebaut und nach docker.com geschickt haben. Das geht vollautomatisch, wenn ich nach GitHub eine Änderung hochlade.

Fortgeschrittene Magie halt! :grin:

Das ist aber cool! :sunglasses:

Zeig mal das Script!

Gerne, aber nicht schimpfen - meine Programmierkenntnisse stammen aus den 80er mit BASIC. Da habe ich auch noch ohne Skrupel „format C:“ eingegeben und mich gewundert warum der PC danach nicht mehr starten wollte…

Hier das (für mich passende) Skript inkl. WhatsApp und Push Nachrichten:

var EnergyCharged, StartSoC, i, AUSGABE, DCCharge, ACDC, ZeileTabelle;


on({ id: [].concat(['mqtt.0.MeinPolestar2.getBatteryData.chargerConnectionStatus']), change: 'ne' }, async (obj) => {
  let value = obj.state.val;
  let oldValue = obj.oldState.val;
  await wait(10000);
  if ((getState('go-e.0.car').val == 1) && ((getState('mqtt.0.MeinPolestar2.getBatteryData.chargingPowerWatts').val != 'null') || (parseFloat(getState('mqtt.0.MeinPolestar2.getBatteryData.chargingPowerWatts').val) > 0))) {
    setState('0_userdata.0.Common.SST01_Ctl' /* SST01_Ctl */, 1);
    EnergyCharged = 0;
    StartSoC = getState('0_userdata.0.Polestar.ChgBeginCarSoC').val;
    i = 0;
    AUSGABE = 'Externer ';
    if (parseFloat(getState('mqtt.0.MeinPolestar2.getBatteryData.chargingPowerWatts').val) <= 11000) {
      AUSGABE += 'AC-';
      DCCharge = false;
      ACDC = 'AC3';
    } else {
      AUSGABE += 'DC-';
      DCCharge = true;
      ACDC = 'DC';
    }
    AUSGABE += 'Ladevorgang gestartet!';
    AUSGABE += '\n';
    AUSGABE += 'Startzeit: ';
    AUSGABE += String(formatDate(new Date(), 'hh:mm'));
    AUSGABE += '\n';
    AUSGABE += 'Start-Soc: ';
    AUSGABE += String(StartSoC);
    AUSGABE += ' %';
    AUSGABE += '\n';
    AUSGABE += 'Kilometerstand: ';
    AUSGABE += String(Math.round((getState('mqtt.0.MeinPolestar2.getOdometerData.odometerMeters').val / 1000) * 10) / 10);
    AUSGABE += ' km';
    AUSGABE += '\n';
    sendTo("whatsapp-cmb", "send", {
        text: AUSGABE
    });
    console.info(AUSGABE);
    while ((getState('mqtt.0.MeinPolestar2.getBatteryData.chargingPowerWatts').val != 'null') || (parseFloat(getState('mqtt.0.MeinPolestar2.getBatteryData.chargingPowerWatts').val) > 0)) {
      await wait(150000);
      //
      // Wird der Ladevorgang während der Pause beendet ist die Leistung "null"

      if (getState('mqtt.0.MeinPolestar2.getBatteryData.chargingPowerWatts').val != 'null') {
        EnergyCharged = EnergyCharged + ((parseFloat(getState('mqtt.0.MeinPolestar2.getBatteryData.chargingPowerWatts').val) / 1000) / 3600) * 300;
        AUSGABE = 'gel. Energie: ';
        AUSGABE += String(EnergyCharged);
        AUSGABE += ' kWh';
        AUSGABE += '\n';
        AUSGABE += 'akt. SoC: ';
        AUSGABE += String(Math.round(getState('0_userdata.0.Polestar.CalcActualCarSoC').val * 10) / 10);
        AUSGABE += ' %';
        AUSGABE += '\n';
        AUSGABE += 'akt. Ladeleistung: ';
        AUSGABE += String(Math.round(getState('mqtt.0.MeinPolestar2.getBatteryData.chargingPowerWatts').val / 1000));
        AUSGABE += ' kW';
        AUSGABE += '\n';
        console.info(AUSGABE);
        if (DCCharge && (i == 2)) {
          i = 0;
          sendTo('pushover', 'send', {
            message: AUSGABE,
            sound: '',
          });
        } else {
          if (i == 3) {
            i = 0;
            sendTo('pushover', 'send', {
              message: AUSGABE,
              sound: '',
            });
          }
        }
        i = (typeof i === 'number' ? i : 0) + 1;
      }
      await wait(150000);
    }
    await wait(10000);
    setState('0_userdata.0.Common.SST01_Ctl' /* SST01_Ctl */, 2);
    AUSGABE = 'Externer ';
    if (DCCharge) {
      AUSGABE += 'DC-';
    } else {
      AUSGABE += 'AC-';
    }
    AUSGABE += 'Ladevorgang beendet!';
    AUSGABE += '\n';
    AUSGABE += 'Stoppzeit: ';
    AUSGABE += String(formatDate(new Date(), 'hh:mm'));
    AUSGABE += '\n';
    AUSGABE += 'End-Soc: ';
    AUSGABE += String(Math.round(getState('0_userdata.0.Polestar.CalcActualCarSoC').val * 10) / 10);
    AUSGABE += ' %';
    AUSGABE += '\n';
    AUSGABE += 'Reichweite: ';
    AUSGABE += String(getState('mqtt.0.MeinPolestar2.getBatteryData.estimatedDistanceToEmptyKm').val);
    AUSGABE += ' km';
    AUSGABE += '\n';
    AUSGABE += 'gel. Energie: ';
    AUSGABE += String(Math.round(EnergyCharged * 10) / 10);
    AUSGABE += ' kWh';
    AUSGABE += '\n';
    AUSGABE += 'Ladedauer: ';
    AUSGABE += String(getState('0_userdata.0.Common.SST01_Output').val);
    AUSGABE += '\n';
    sendTo("whatsapp-cmb", "send", {
        text: AUSGABE
    });
    console.info(AUSGABE);
    ZeileTabelle = [formatDate(new Date(), 'DD.MM.YYYY'), StartSoC, Math.round(getState('0_userdata.0.Polestar.CalcActualCarSoC').val * 10) / 10, '', ACDC, Math.round(EnergyCharged * 10) / 10, '0,00', '100', getState('0_userdata.0.Common.SST01_Output').val, '', '', '', '', '', '', '', getState('mqtt.0.MeinPolestar2.getBatteryData.estimatedDistanceToEmptyKm').val, '', '', '', '', '', '', '', Math.round(getState('mqtt.0.MeinPolestar2.getOdometerData.odometerMeters').val / 1000), 'Externer Ladevorgang'];
    sendTo("google-spreadsheet.0", "append", {"sheetName":'Ladungen', "data":ZeileTabelle});
    setState('0_userdata.0.Common.SST01_Ctl' /* SST01_Ctl */, 0);
  }
});

Die ZeileTabelle enthält soviele Leerfelder weil ich die Tabelle auch für Heimladungen nutze und da im Sheet selbst ein paar Werte, wie z.B. Verbrauch berechne.

…und nicht dass jetzt jemand glaubt ich könnte JS! Ich habe nur diesen Export erzeugt. Bei mir sieht das so aus:

Also die grafischen Blockly-Elemente kann ich kapieren, den daraus erzeugten Code nicht…

1 „Gefällt mir“

Jein…

  • BASIS_TOPIC/container/last_update wird aktualisiert
  • BASIS_TOPIC/container/connected bleibt seit dem Update (19:33) unverändert auf (null) → kein Wert erhalten.

Und das nächste Update:

Jetzt verwenden wir das Refresh Token, damit nicht jedesmal ein neues Login erfolgen muss, sobald das Token abgelaufen ist:

polestar2mqtt  | Polestar_2_MQTT.py startet
polestar2mqtt  | ==========================
polestar2mqtt  | ensure_valid_token()
polestar2mqtt  | get_token(), no refresh token available
polestar2mqtt  |  get_path_token()
...
polestar2mqtt  |  perform_login()
...
polestar2mqtt  |  get_api_token()
...
polestar2mqtt  |   expires_in    = 299
polestar2mqtt  |   expiry_time   = 2024-12-14 19:55:50 CET+0100
polestar2mqtt  | ********************************************************************************
polestar2mqtt  | wait for 270 seconds
polestar2mqtt  | ensure_valid_token()
polestar2mqtt  | get_car_data()
polestar2mqtt  | get_battery_data()
polestar2mqtt  | get_odometer_data()
polestar2mqtt  | ********************************************************************************
polestar2mqtt  | wait for 270 seconds
polestar2mqtt  | ensure_valid_token()
polestar2mqtt  |  refresh_access_token()
polestar2mqtt  |   access_token  = eyJhbGciOiJSUzI1NiIsImtpZCI6IldVWWp5dEN...
polestar2mqtt  |   refresh_token = GUtLK4SEwNuwh1LjIxpWgrkg5lQjCAEXu0ksiaMN5K
polestar2mqtt  |   expires_in    = 299 seconds
polestar2mqtt  |   expiry_time   = 2024-12-14 20:05:18 CET+0100
polestar2mqtt  | get_car_data()
polestar2mqtt  | get_battery_data()
polestar2mqtt  | get_odometer_data()

Jetzt wird das Ganze doch langsam rund.

:partying_face:

1 „Gefällt mir“

Jetzt steht auch „online“ da :heart_eyes:

1 „Gefällt mir“