diff --git a/README.md b/README.md index cf1b9a7..df4da78 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Für einige Teile reicht diese Landkarte nicht, weil ihr Verhalten aus dem Zusammenspiel mehrerer Prozesse entsteht. Sie haben eigene, ausführliche Dokumente unter [`doku/`](doku/README.md): **[Automatiken](doku/automatiken.md)**, **[Zeitleiste](doku/zeitleiste.md)**, -**[Benachrichtigungen](doku/benachrichtigungen.md)**, +**[Benachrichtigungen](doku/benachrichtigungen.md)**, **[Batterie](doku/byd.md)**, **[Solar-Übersicht](doku/uebersicht.md)**, **[Datenbanken](doku/datenbank.md)** und **[Einstellungsseite](doku/einstellungen.md)**. @@ -392,7 +392,8 @@ Verläufe aus `ajax/speicher.php` (Ladestand und Leistung aus `EnergyFlow`, Spreizung und Temperatur aus `byd`). Live-Werte kommen über MQTT `solarManager/byd/#` (`gatherBYDData.py`); fehlen sie, zeigt die Ansicht, was der Wechselrichter weiß. Die Nennenergie je Modul (2,76 kWh, HVM) steht als -Konstante oben in `speicher.js` — die BMU meldet sie nicht. +Konstante oben in `speicher.js` — die BMU meldet sie nicht. Wie die Werte +aus der BMU herauskommen, steht in **[doku/byd.md](doku/byd.md)**. **`skodaMQTT.js`** — Fahrzeugseite. Holt alles über `ajax/skoda.php?was=…` (live, ladungen, kurve, gesundheit, fahrten, strecke), zeichnet das Fahrzeug diff --git a/doku/README.md b/doku/README.md index e0a26b1..a569ad4 100644 --- a/doku/README.md +++ b/doku/README.md @@ -15,6 +15,7 @@ abzulesen sind. | [zeitleiste.md](zeitleiste.md) | Die Zeitleiste der Automatiken: wie der Server den Tag ausrechnet und wie der Browser ihn zeichnet | | [benachrichtigungen.md](benachrichtigungen.md) | Meldungen aus Automatiken: Web Push ohne App, E-Mail, das Gerät „Benachrichtigungen“ | | [uebersicht.md](uebersicht.md) | Die Solar-Übersicht: Katalog und Renderer, wie aus Watt ein Ring, ein Fluss und ein Füllstand wird | +| [byd.md](byd.md) | Die Batterie bis zur Zelle: das nachgebaute BMU-Protokoll, das Zeitfenster des Netzwerkmoduls, Byte-Lagen, Historie, Anzeige | | [datenbank.md](datenbank.md) | Die drei Schemata: wer was schreibt, wie Messreihen verdichtet werden, welche Tabelle man für welche Auswertung nimmt | | [einstellungen.md](einstellungen.md) | Die Einstellungsseite: was jeder Reiter speichert, welche Regeln das Modell durchsetzt und was die Seite bewusst nicht kann | diff --git a/doku/bilder/speicher.png b/doku/bilder/speicher.png new file mode 100644 index 0000000..ac576f5 Binary files /dev/null and b/doku/bilder/speicher.png differ diff --git a/doku/byd.md b/doku/byd.md new file mode 100644 index 0000000..d366698 --- /dev/null +++ b/doku/byd.md @@ -0,0 +1,273 @@ +# Die Batterie: BYD Battery-Box HVM + +Der Wechselrichter weiß von der Batterie nur, was er zum Laden braucht. +Alles darunter — 128 Zellspannungen, 64 Temperaturfühler, Gesundheit, +Gesamtzähler, Zellausgleich — kennt nur die **BMU**, die Steuerung im +Batterieschrank. `gatherBYDData.py` im SolarManager spricht sie direkt an. + +![Die Speicher-Ansicht](bilder/speicher.png) + +```mermaid +flowchart LR + BMU["BMU der Battery-Box
192.168.16.254:8080"] -->|"Modbus-RTU in TCP"| G["gatherBYDData.py
im solarManager.py-Prozess"] + G -->|"solarManager/byd/…"| M[["MQTT"]] + G -->|"alle 5 / 15 Min."| DB[("solarLog
byd · byd_zellen")] + M --> B["speicher.js
Live"] + DB --> A["ajax/speicher.php"] --> B + WR["Gen24
Modbus"] -->|"SOC, P_Akku"| M +``` + +--- + +## 1. Warum überhaupt direkt an die BMU + +| Frage | Gen24 über Modbus | BMU über Port 8080 | +|---|---|---| +| Wie voll ist sie? | ja (`SOC`) | ja, oft eine Nachkommastelle anders | +| Wie viel lädt sie gerade? | ja (`P_Akku`) | ja, als Spannung × Strom | +| Wie alt ist sie? | — | **SOH** in % | +| Welche Zelle schwächelt? | — | **128 Zellspannungen** in mV | +| Wie warm wird sie? | eine Zahl | **64 Fühler** einzeln | +| Wie viel ging je durch? | — | **Gesamtzähler** geladen/entladen | +| Gleicht sie gerade aus? | — | Bitmaske je Zelle | +| Meldet sie einen Fehler? | — | 16 Fehlerbits im Klartext | + +Die linke Spalte reicht für den Energiefluss. Für die Frage „hält die +Batterie noch?“ reicht sie nicht — deshalb der zweite Weg. + +Das Protokoll ist dasselbe, das die Hersteller-App **BE Connect** spricht: +Modbus-RTU, in TCP verpackt. Ablauf und Byte-Lagen stammen aus dem +ioBroker-Adapter [bydhvs](https://github.com/christianh17/ioBroker.bydhvs) +(MIT). Sie sind nirgends offiziell dokumentiert — wer hier etwas ändert, +ändert an einer nachgebauten Schnittstelle. + +--- + +## 2. Der Ablauf: alles in **einer** Verbindung + +```mermaid +sequenceDiagram + participant S as gatherBYDData + participant B as BMU + + S->>B: TCP verbinden (Timeout 1,5 s) + S->>B: 0 stamm – Seriennummer, Module, Firmware + B-->>S: p0 + S->>B: 1 echt – SOC, SOH, Strom, Spannung, Fehler, Zähler + B-->>S: p1 + S->>B: 2 typ – HVM oder HVS + B-->>S: p2 + S->>B: 3 messen – Zellmessung starten + Note over S,B: 8 s warten – so lange misst die BMU + S->>B: 4 status + S->>B: 5 zellen (viermal) + B-->>S: p5 p6 p7 p8 + S->>B: Verbindung schließen +``` + +| Name | Bytes | Was zurückkommt | +|---|---|---| +| `stamm` | `010300000066c5e0` | Seriennummer, Modulzahl, BMS-Version | +| `echt` | `01030500001984cc` | die laufenden Werte | +| `typ` | `010300100003040e` | HVM (16 Zellen/Modul) oder HVS (32) | +| `messen` | `0110055000020400018100f853` | Zellmessung anstoßen | +| `status` | `010305510001d517` | Quittung nach der Messung | +| `zellen` | `01030558004104e5` | viermal nacheinander, jedes Mal der nächste Block | + +**Die Reihenfolge ist Pflicht, und die Verbindung darf dazwischen nicht +abreißen.** Eine neue Verbindung, die gleich mit `echt` anfängt, schließt die +BMU wortlos. Auch die acht Sekunden Messpause werden in derselben Verbindung +abgewartet — sie offen zu halten ist billiger, als sich neu anzumelden. + +Jede Antwort wird geprüft, bevor sie gedeutet wird: Rahmenlänge aus Byte 2, +CRC-16 (Modbus, Polynom `0xA001`) über das Ganze, und das Ausnahmebit +`0x80` in Byte 1. Eine Antwort, die zu kurz ankommt, wird weitergelesen; eine +mit falscher Prüfsumme fliegt raus. Schließt die Gegenseite ohne Antwort, +gilt das nicht als Fehler, sondern als `_Abgewiesen` — siehe nächster +Abschnitt. + +--- + +## 3. Die Eigenheit, die den ganzen Aufbau bestimmt + +Zwischen Netzwerk und BMU sitzt ein Serien-Server **Hi-Link HLK-RM08K**. Der +startet **etwa alle 102 Sekunden neu** — von allein, ob jemand fragt oder +nicht. Beobachtet am 15.09.2026: + +``` + ├──────── ~102 s ────────┤ + ▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░▓▓▓▓▓▓▓▓▓░░░░ + └─ ~15 s ─┘└── offen ──┘ └ Neustart ┘ + Neustart: die ersten 1–2 + niemand Verbindungen werden + antwortet bedient, alle weiteren + sofort mit 0 Bytes +``` + +Daraus folgt der Aufbau des Sammlers: + +* **Er klopft**, statt zu pollen: jede Sekunde ein Verbindungsversuch mit 1,5 + s Timeout (`_KLOPFEN`, `_VERBINDEN`). Der 3-Sekunden-Takt des Managers wäre + zu grob, das Fenster ist nur wenige Sekunden breit. +* **Nach einem Erfolg ruht er 80 Sekunden** (`_RUHE`) und ist damit kurz vor + dem nächsten Neustart wieder da. Ergebnis: rund alle 100 Sekunden ein + vollständiger Satz. +* **Eine zugeschlagene Tür ist kein Fehler.** `_Abgewiesen`, Timeouts und + `ConnectionError` landen im Debug-Protokoll, nicht als Warnung. Erst wenn + zehn Minuten (`_VERALTET`) gar nichts durchkommt, gibt es eine Warnung — + und `ok` fällt auf 0, damit die Oberfläche alte Zahlen nicht als frisch + ausgibt. +* **Nie breit im Netz scannen.** Das Modul verträgt keine parallelen + Verbindungen; ein Portscan macht das Fenster für den Sammler zu. + +> Die BMU hängt unter **192.168.16.254**. Das NAS-Netz ist ein /16, deshalb +> ist sie ohne Routing erreichbar. Port 80 desselben Moduls zeigt dessen +> Weboberfläche (Basic-Auth) — für uns ohne Bedeutung. + +Wie alle langsamen Sammler blockiert `gatherData()` nie: Die Schleife läuft +als Hintergrund-Task, zurück kommt immer der letzte Stand. Stirbt die +Schleife, startet der nächste Aufruf sie neu. + +--- + +## 4. Wo welcher Wert im Paket steht + +Die laufenden Werte stehen alle in `p1`: + +| Wert | Lage | Umrechnung | +|---|---|---| +| SOC | 3 | `int16` | +| SOH | 9 | `int16` | +| Strom | 11 | `int16 / 10` → A, positiv = laden | +| T max / T min | 15 / 17 | `int16`, nur Rückfall ohne Fühlerwerte | +| Fehlerbits | 29 | `uint16`, 16 Bits mit festem Wortlaut | +| Spannung | 35 | `uint16 / 100` → V | +| geladen | 37 | **wortvertauscht** `/ 10` → kWh | +| entladen | 41 | **wortvertauscht** `/ 10` → kWh | + +**Die eine Abweichung vom Adapter:** Die Gesamtzähler stehen bei dieser HVM +mit dem niedrigen Wort zuerst. Mit der Lesart des Adapters kämen 31 Millionen +kWh heraus — ein Wert, der so offensichtlich falsch ist, dass er die +Byte-Lage verrät. + +Die Zellen kommen über vier Antworten verteilt, weil ein Modbus-Rahmen sie +nicht fasst: + +``` +p5: Zellen 1– 16 ab Byte 101 p5[17..32]: Ausgleichsbits (roh) +p6: Zellen 17– 80 ab Byte 5 +p7: Zellen 81–128 ab Byte 5 + Fühler 1–30 ab Byte 103 +p8: Fühler 31–64 ab Byte 5 +``` + +Wie viele es sind, sagt nicht die Konstante, sondern das Gerät: Modulzahl aus +`p0[36] % 16`, Bauart aus `p2[5]`. HVM hat 16 Zellen und 8 Fühler je Modul, +HVS 32 und 12. Bei acht Modulen also 128 und 64. + +Zwei Stellen sind bewusst vorsichtig: + +* **Als gültig zählt nur, was zwischen 2000 und 4000 mV liegt.** Minimum, + Maximum und Spreizung werden aus diesen Werten gebildet — nicht belegte + Plätze fallen sonst als „0 mV“ ins Minimum. +* **Die Ausgleichsbits gehen roh weiter** (`ausgleichRoh`, 32 Hexzeichen mit + `0x` davor, damit MQTT sie nicht als Zahl liest). Welches Bit zu welcher + Zelle gehört, ist **nicht bestätigt** — dafür müsste man zusehen, während + die BMU wirklich ausgleicht. Gezählt wird deshalb nur, *wie viele* Bits + gesetzt sind. + +--- + +## 5. Was herauskommt + +Alles unter `solarManager/byd/…`, geschrieben aus der Datenklasse `BYDData`: + +| Topic | Bedeutung | +|---|---| +| `ok` | 1, solange der Stand jünger als zehn Minuten ist | +| `stand` | Unix-Zeit des letzten vollständigen Zyklus | +| `soc`, `soh` | % laut BMU | +| `spannung`, `strom`, `leistung` | V, A, W — positiv = laden | +| `tMin`, `tMax` | °C über alle Fühler | +| `zelleMin`, `zelleMax`, `zelleMinNr`, `zelleMaxNr`, `spreizung` | mV bzw. 1…128 | +| `ausgleich`, `ausgleichRoh` | Zahl der ausgeglichenen Zellen, dazu die rohe Maske | +| `fehler`, `fehlertext` | Bitmaske und derselbe Inhalt als Satz | +| `geladen`, `entladen` | kWh seit Inbetriebnahme | +| `zellen`, `temperaturen` | JSON-Listen, 128 bzw. 64 Werte | +| `module`, `firmware`, `seriennummer` | Stammdaten | + +Historie in `solarLog` (Schema: `solarLog_byd.sql` im Web-Repo): + +| Tabelle | Takt | Inhalt | +|---|---|---| +| `byd` | alle 5 Min. | eine Zeile mit SOC, SOH, Spannung, Strom, Temperaturen, Spreizung, Zählern — knapp 290 am Tag | +| `byd_zellen` | alle 15 Min. | alle Zellspannungen und Fühlerwerte als JSON, rund 1 kB je Zeile | + +Warum JSON und nicht 192 Spalten? Weil diese Werte nie einzeln abgefragt, +sondern immer als Satz gezeichnet werden. Und warum wird `byd` nie +ausgedünnt? Weil die Tabelle genau dafür da ist, zu zeigen, wie sich SOH, +Spreizung und Temperaturen über **Jahre** entwickeln — das ist der Zweck, +nicht der Platzverbrauch. + +--- + +## 6. Die Ansicht im Browser + +Auf der Solar-Seite, dritter Reiter der Realtime-Karte („Speicher“), gebaut +von `js/solar/speicher.js`: + +| Teil | Quelle | +|---|---| +| Kennzahlen-Kacheln | MQTT, live | +| Turm aus Modulen und Zellen | `zellen`/`temperaturen` aus MQTT | +| Detailspalte rechts (gewähltes Modul) | dasselbe, nur ausschnittweise | +| Ladestand und Leistung über die Zeit | `ajax/speicher.php?was=verlauf` → `EnergyFlow` | +| Spreizung und Temperatur über die Zeit | dieselbe Antwort → `byd` | + +`ajax/speicher.php` kann noch etwas, das niemand abruft: `?was=gesundheit` +liefert SOH, größte Spreizung und die Zähler **je Tag** — die Zahlenreihe für +ein Alterungsdiagramm, das es noch nicht gibt. Wer es bauen will, braucht +also nur die Anzeige. + +Drei Entscheidungen, die man beim Lesen des Codes sonst sucht: + +* **Ohne BMU bleibt die Ansicht nützlich.** Fehlen die Topics, zeigt sie + Ladestand, Leistung und deren Verlauf aus dem Wechselrichter und schreibt + darüber, woher die Zahlen kommen. Turm und Zellwerte machen einem Hinweis + Platz, statt leer dazustehen. +* **Die Nennenergie ist eine Konstante im Skript** (`KWH_JE_MODUL`, HVM 2,76 + kWh, HVS 2,56). Die BMU meldet die Modulzahl, aber nicht diese Größe — + ohne sie gäbe es kein „≈ 10,6 von 22,1 kWh“ und keine Zyklenzahl. +* **Der Verlauf wird erst beim Hinsehen geholt.** Ein `IntersectionObserver` + löst den ersten Abruf aus, danach alle fünf Minuten — und nur, solange der + Reiter sichtbar und das Fenster im Vordergrund ist. + +Die Zellen sind überall gleich durchgezählt, in der BMU wie in der +Datenbank wie in der Anzeige: **Modul m hat die Zellen 16·(m−1)+1 bis 16·m +und die Fühler 8·(m−1)+1 bis 8·m.** + +--- + +## 7. Fehlerbilder + +| Beobachtung | Wahrscheinliche Ursache | +|---|---| +| Ansicht sagt „keine Daten von der BMU“ | Sammler aus, Adresse falsch, oder etwas belegt die einzige Verbindung des Moduls | +| „Die Werte der BMU sind veraltet“ | Seit über zehn Minuten kein Zyklus durchgekommen — Protokoll (`solarOutput.log`) nach „BYD:“ durchsehen | +| Zähler in Millionenhöhe | Wortvertauschung (siehe Abschnitt 4) — trifft andere Battery-Box-Bauarten anders | +| Spreizung springt auf hunderte mV | Eine Zelle liefert 0 mV: Filter 2000–4000 greift nicht mehr, weil zu viele Plätze leer sind | +| `zellen` kommt als leere Liste | `p5`–`p8` kamen an, aber die Messung lief noch — die acht Sekunden Pause sind knapp bemessen | +| Nichts geht mehr, seit jemand das Netz gescannt hat | Das Modul bedient nur ein, zwei Verbindungen; abwarten, bis es das nächste Mal neu startet | + +--- + +## 8. Wo fange ich an, wenn ich … + +| Vorhaben | Ort | +|---|---| +| … eine andere Battery-Box anschließen (HVS, andere Modulzahl) | nichts — Modulzahl und Bauart kommen aus `p0`/`p2`; nur `KWH_JE_MODUL` in `speicher.js` stimmt dann nicht mehr | +| … die Adresse ändern | `[byd] host` in der `config.ini` des SolarManagers | +| … seltener oder öfter abfragen | `_RUHE` in `gatherBYDData.py` — kleiner als ~80 s bringt nichts, das Fenster kommt nicht öfter | +| … die Historie feiner aufzeichnen | `_DB_ZEILE` / `_DB_ZELLEN`, dazu den Platzbedarf in `solarLog_byd.sql` gegenrechnen | +| … die Bitlage des Zellausgleichs bestätigen | `byd_zellen.ausgleich` (roh) zu einem Zeitpunkt auswerten, an dem `ausgleich > 0` war | +| … einen weiteren BMU-Wert anzeigen | Feld in `BYDData` ergänzen (geht von selbst nach MQTT), dann `speicher.js` | +| … verstehen, warum die Zahlen der BMU und des Gen24 nicht gleich sind | sie messen an verschiedenen Stellen — die BMU an den Zellen, der Wechselrichter hinter dem Wandler | diff --git a/doku/datenbank.md b/doku/datenbank.md index f858e5d..9181790 100644 --- a/doku/datenbank.md +++ b/doku/datenbank.md @@ -101,7 +101,7 @@ Stand heute rund 170 MB, verteilt auf wenige große Reihen: | `weatherStation`, `weatherHours`, `weatherDays` | ~220.000 | eigene Wetterstation, Vorhersage | Wetterbrücke, Open-Meteo | | `daylight` | 663 | Sonnenauf- und -untergang je Tag | Vorhersage-Job | | `simPower` | ~82.000 | Ertragsprognose | Prognose-Job | -| `byd`, `byd_zellen` | 1.559 / 492 | BYD-Speicher: alle 5 min Ladestand, SOH, Temperaturen; alle 15 min 128 Zellspannungen und 64 Temperaturen | `gatherBYDData.py` | +| `byd`, `byd_zellen` | 1.559 / 492 | BYD-Speicher: alle 5 min Ladestand, SOH, Temperaturen; alle 15 min 128 Zellspannungen und 64 Temperaturen (→ [byd.md](byd.md)) | `gatherBYDData.py` | | `skoda`, `skoda_raw`, `skoda_ladepunkte` | ~1.900 | Fahrzeugzustand, Rohantwort, Ladeverlauf im Minutentakt | `gatherSkodaData.py` | | `gridCosts`, `gasCosts`, `fuelCosts` | 13 | Preiszeitreihen | Einstellungsseite | | `car`, `Status`, `actors`, `sensors`, `autoActions*`, `WindradLog` | — | Altlasten aus der Vorgängerfassung | — |