Files
evcc-bruecke/SCHNITTSTELLE.md

11 KiB

Die Schnittstelle im Energie-Cockpit, vollständig beschrieben

Diese Datei richtet sich an jemanden, der die Brücke nicht benutzt, sondern den Weg selbst nachbaut. Zum Beispiel, weil bruecke.py in einer bestimmten Umgebung nicht läuft, weil die Ladedaten aus einer anderen Quelle als evcc kommen, oder weil das Senden in eine vorhandene Anwendung eingebaut werden soll. Es steht hier alles, was der Server erwartet und was er zurückgibt.

Wer nur die fertige Brücke starten will, liest README.md.

Was der Weg leisten soll

Das Energie-Cockpit kennt die Ladevorgänge des Fahrzeugs aus der Tesla-API. Was es von dort nicht erfährt, ist der Sonnenanteil einer Ladung und die genaue Energiemenge an der Wallbox. Beides weiß die Wallbox-Software im Heimnetz, hier evcc. Der Weg schickt deshalb die Ladungen der Wallbox an das Cockpit, das sie den passenden Tesla-Ladungen zuordnet und dann mit dem echten Sonnenanteil rechnet.

Das Heimnetz ruft dabei nach außen. Das Cockpit kommt nie ins Heimnetz herein, und es muss dort nichts geöffnet werden.

Voraussetzung im Cockpit, ohne die nichts ankommt

  1. Zugangsschlüssel anlegen unter https://energie.schnee.co.at/module/api, Bereich Wallbox. Der Schlüssel wird nur einmal im Klartext gezeigt. Ein Schlüssel eines anderen Bereichs wird mit 403 abgewiesen.
  2. Ladequelle auf evcc stellen im Modul Tesla unter Einstellungen. Steht dort weiter Wattpilot, nimmt der Server die Daten zwar an und legt sie ab, ordnet sie aber nicht zu. Die Antwort sagt das mit quelle_aktiv: false. Das ist Absicht, damit eine vergessene Brücke die Wattpilot-Werte nicht still überschreibt.

Der Endpunkt

POST https://energie.schnee.co.at/api/ladequelle/v1/sessions
Content-Type: application/json
X-API-Key: <der angelegte Schlüssel>

Die Reihenfolge im Pfad ist ladequelle vor v1. /api/v1/ladequelle/... gibt es nicht und antwortet mit 404.

Der Schlüssel gehört in die Kopfzeile. In der Adresszeile wird er hier bewusst nicht angenommen, weil er sonst im Zugriffsprotokoll jedes Zwischenrechners stünde.

Eine maschinenlesbare Beschreibung liegt unter GET https://energie.schnee.co.at/api/ladequelle/v1/openapi.json.

Der Körper der Anfrage

{
  "quelle": "evcc",
  "sessions": [
    {
      "id": 412,
      "created": "2026-09-05T14:02:11Z",
      "finished": "2026-09-05T17:41:58Z",
      "chargedEnergy": 23.84,
      "solarPercentage": 61.5,
      "loadpoint": "Garage",
      "vehicle": "Model 3"
    }
  ]
}

sessions muss eine Liste sein, auch bei einer einzigen Ladung. Fehlt sie oder ist sie etwas anderes als eine Liste, kommt 400 mit dem Code sessions_fehlen.

quelle darf entfallen, dann gilt evcc. Ein anderer Wert als evcc wird zur Zeit mit 400 und quelle_unbekannt abgewiesen.

Die Feldnamen sind genau die, die evcc unter GET /api/sessions liefert. Wer aus einer anderen Quelle sendet, muss seine Daten auf diese Namen bringen.

Die einzelnen Felder

Feld Pflicht Bedeutung
created ja Beginn der Ladung, RFC 3339. Ohne Zeitzone im Text wird UTC angenommen.
finished ja Ende der Ladung, gleiches Format. Muss nach created liegen.
chargedEnergy ja Geladene Energie in kWh, Kommazahl.
solarPercentage nein Sonnenanteil in Prozent, 0 bis 100.
id faktisch ja Kennung der Ladung in evcc. Wird zu evcc-<id> und verhindert, dass dieselbe Ladung doppelt abgelegt wird.
loadpoint nein Name des Ladepunkts, erscheint in der Anzeige.
vehicle nein Name des Fahrzeugs in evcc, erscheint in der Anzeige.

Eine Ladung wird stillschweigend verworfen, wenn Beginn, Ende oder Energie fehlen oder unlesbar sind, oder wenn das Ende vor dem Beginn liegt. Die Zahl der verworfenen Ladungen steht in der Antwort.

Der Sonnenanteil darf fehlen. Er bleibt dann offen und wird beim Mitteln übersprungen. Ihn ersatzweise als 0 zu senden ist ein Fehler: Das drückt den Schnitt und weist am Ende zu hohe Stromkosten aus. Lieber weglassen.

Ohne id fallen mehrere Ladungen zusammen. Der Server bildet daraus die Kennung evcc-<id> und erkennt an ihr die Wiedervorlage derselben Ladung. Fehlt id, heißen alle Einträge gleich, und im Speicher bleibt am Ende genau einer übrig. Wer aus einer anderen Quelle sendet, vergibt deshalb eine eigene, über die Läufe hinweg stabile Kennung je Ladung.

Zusätzliche Felder im Körper stören nicht, der Server geht über sie hinweg.

Was der Server damit macht

Er speichert nichts auf Vorrat. Jede eingehende Ladung wird sofort den Tesla-Ladungen desselben Kontos gegenübergestellt:

  • Betrachtet werden nur Tesla-Ladungen mit Status complete, deren Ladeort als Zuhause oder Wohnung eingestuft ist.
  • Zugeordnet wird über die Zeitüberschneidung mit 5 Minuten Toleranz. Mehrere Wallbox-Ladungen dürfen zu einer Tesla-Ladung gehören, ihre Energie wird dann addiert und der Sonnenanteil nach Energie gewichtet gemittelt.
  • Bei einem Treffer übernimmt das Cockpit die Energiemenge der Wallbox, weil sie genauer ist als die der Tesla-API, dazu den Sonnenanteil.

Daraus folgt die wichtigste Regel für den Sender: Bei jedem Lauf ein ganzes Fenster zurückliegender Ladungen mitschicken, nicht nur die neuen. Eine Tesla-Ladung wird oft erst Stunden später fertig verarbeitet und findet ihren Partner deshalb erst in einem späteren Lauf. Die Brücke sendet standardmäßig die letzten 30 Tage bei jedem Durchgang. Doppelte Ladungen sind harmlos, sie werden über id erkannt.

Die Antwort bei Erfolg

{
  "empfangen": 42,
  "verwertbar": 40,
  "verworfen": 2,
  "neu": 3,
  "uebernommen": 3,
  "quelle_aktiv": true
}
Feld Bedeutung
empfangen So viele Einträge lagen in sessions.
verwertbar So viele hatten Beginn, Ende und Energie.
verworfen Differenz dazu, siehe oben.
neu So viele waren dem Cockpit noch nicht bekannt.
uebernommen So viele Tesla-Ladungen wurden dadurch aktualisiert.
quelle_aktiv false heißt: angenommen, aber nicht zugeordnet, weil im Cockpit eine andere Ladequelle eingestellt ist.

uebernommen: 0 bei neu: 3 ist kein Fehler. Es heißt, dass zu diesen Wallbox-Ladungen noch keine passende Tesla-Ladung vorliegt. Beim nächsten Lauf kann sich das ändern, genau dafür ist das Rückblickfenster da.

Die Fehler, alle mit demselben Aufbau

Jeder Fehler kommt als JSON nach RFC 9457 und enthält neben title und status immer ein Feld fehler mit code, meldung und tu. In tu steht in einem Satz, was zu tun ist. Ein Programm wertet fehler.code aus.

Status Code Ursache und Abhilfe
400 sessions_fehlen Feld sessions fehlt oder ist keine Liste.
400 zu_viele_sessions Mehr als 2000 Ladungen in einem Aufruf. Rückblick verkleinern oder aufteilen.
400 quelle_unbekannt quelle ist etwas anderes als evcc.
401 schluessel_fehlt Kopfzeile X-API-Key fehlt.
401 schluessel_ungueltig Schlüssel gibt es nicht oder er wurde widerrufen.
401 schluessel_ohne_konto Das Konto hinter dem Schlüssel gibt es nicht mehr.
403 schluessel_falscher_bereich Der Schlüssel gehört zu einem anderen Bereich. Es muss Wallbox sein.
429 Begrenzung Mehr als 60 Aufrufe je Stunde je Konto.
500 zuordnung_fehlgeschlagen Fehler im Cockpit. Im nächsten Takt erneut senden.
404 kein Code Falscher Pfad. Fast immer /api/v1/ladequelle/... statt /api/ladequelle/v1/....

Ein Sender sollte bei 4xx nicht sofort erneut senden, außer bei 429. Bei 429 und 5xx ist der nächste reguläre Takt der richtige Zeitpunkt.

Das kürzeste vollständige Beispiel

Zum Prüfen von Hand, ob Schlüssel und Pfad stimmen:

curl -sS -X POST https://energie.schnee.co.at/api/ladequelle/v1/sessions \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $TOKEN" \
  -d '{"sessions": []}'

Antwort bei richtigem Schlüssel: {"empfangen": 0, ...} mit Status 200. Eine leere Liste ist erlaubt und ändert nichts, sie ist der ungefährliche Test.

Und der ganze Weg in Python, ohne Abhängigkeiten außer requests:

import requests

EVCC = "http://192.168.2.233:7070"
ZIEL = "https://energie.schnee.co.at/api/ladequelle/v1/sessions"
TOKEN = "es_..."
RUECKBLICK_TAGE = 30

# 1. Ladungen aus evcc holen. evcc antwortet je nach Fassung entweder
#    direkt mit einer Liste oder mit {"result": [...]}.
antwort = requests.get(f"{EVCC}/api/sessions", timeout=30)
antwort.raise_for_status()
daten = antwort.json()
ladungen = daten.get("result", daten) if isinstance(daten, dict) else daten

# 2. Auf das Rückblickfenster eindampfen, und zwar nach dem ENDE der Ladung.
#    Eine Ladung ohne "finished" läuft noch und wird vom Server ohnehin
#    verworfen, sie kommt beim nächsten Lauf mit.
from datetime import datetime, timedelta, timezone
grenze = datetime.now(timezone.utc) - timedelta(days=RUECKBLICK_TAGE)

def ende(ladung):
    roh = str(ladung.get("finished") or "").replace("Z", "+00:00")
    try:
        zeit = datetime.fromisoformat(roh)
    except ValueError:
        return None
    return zeit if zeit.tzinfo else zeit.replace(tzinfo=timezone.utc)

fenster = [l for l in ladungen if (e := ende(l)) and e >= grenze]

# 3. Senden. Der Schlüssel gehört in die Kopfzeile, nie in die Adresse.
ergebnis = requests.post(
    ZIEL,
    json={"quelle": "evcc", "sessions": fenster},
    headers={"X-API-Key": TOKEN},
    timeout=60,
)
ergebnis.raise_for_status()
print(ergebnis.json())

Wenn nichts ankommt, in dieser Reihenfolge prüfen

  1. 404 in der Antwort: Der Pfad ist verdreht. Es heißt /api/ladequelle/v1/sessions.
  2. 401 oder 403: Schlüssel fehlt, ist widerrufen oder gehört zum falschen Bereich. Im Cockpit unter /module/api einen für Wallbox anlegen.
  3. 200, aber quelle_aktiv: false: Im Modul Tesla ist die Ladequelle nicht auf evcc gestellt.
  4. 200 und verwertbar: 0: Die gesendeten Einträge haben keinen Beginn, kein Ende oder keine Energie. Meist wurde die evcc-Antwort nicht ausgepackt und statt der Liste das umgebende Objekt gesendet.
  5. 200 und uebernommen: 0 über mehrere Tage: Es kommen Ladungen an, aber sie treffen keine Tesla-Ladung. Zu prüfen ist dann, ob der Ladeort im Cockpit als Zuhause oder Wohnung eingestuft ist und ob die Uhren auseinanderlaufen. Eine falsch gesetzte Zeitzone in der eigenen Anwendung verschiebt die Ladungen um Stunden, und dann greift die Toleranz von fünf Minuten nicht mehr.
  6. Erreicht die Anfrage den Server überhaupt? Ein Aufruf mit leerer Liste, siehe oben, beantwortet das in einer Sekunde.

Was sich nicht ändern wird

Der Pfad, die Feldnamen und die Bedeutung der Antwortfelder sind Teil der Zusage an fremde Sender. Kommt etwas dazu, kommt es als neues Feld daneben. Wer auf Felder zugreift, die er nicht kennt, sollte sie ignorieren statt daran abzubrechen.