# 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 , 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: ``` 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 ```json { "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-` 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-` 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 ```json { "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: ```bash 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`: ```python 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.