docs: Endpunkt vollständig beschreiben, damit der Weg auch ohne diese Brücke nachgebaut werden kann
This commit is contained in:
@@ -70,6 +70,14 @@ bekannte Ladungen an ihrer Kennung und legt sie nicht doppelt an. Dadurch
|
||||
braucht die Brücke keinen eigenen Speicher, und ein Ausfall von ein paar Tagen
|
||||
holt sich von selbst wieder ein.
|
||||
|
||||
## Wenn die Brücke nicht passt
|
||||
|
||||
Wer den Weg lieber selbst baut, weil die Ladedaten aus einer anderen Quelle als
|
||||
evcc kommen oder das Senden in eine vorhandene Anwendung gehört, findet in
|
||||
[SCHNITTSTELLE.md](SCHNITTSTELLE.md) die vollständige Beschreibung des
|
||||
Endpunkts: Adresse, Kopfzeilen, jedes Feld mit seiner Bedeutung, alle
|
||||
Fehlercodes, die Regeln der Zuordnung und ein lauffähiges Beispiel.
|
||||
|
||||
## Wenn evcc nicht gefunden wird
|
||||
|
||||
Läuft evcc in einem eigenen Compose-Verbund, gehören beide ins selbe Netz.
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user