docs: Endpunkt vollständig beschreiben, damit der Weg auch ohne diese Brücke nachgebaut werden kann

This commit is contained in:
2026-09-06 00:56:40 +02:00
parent 5bf37fa813
commit 2bb76219ba
2 changed files with 267 additions and 0 deletions
+259
View File
@@ -0,0 +1,259 @@
# 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
```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-<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
```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.