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
|
braucht die Brücke keinen eigenen Speicher, und ein Ausfall von ein paar Tagen
|
||||||
holt sich von selbst wieder ein.
|
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
|
## Wenn evcc nicht gefunden wird
|
||||||
|
|
||||||
Läuft evcc in einem eigenen Compose-Verbund, gehören beide ins selbe Netz.
|
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