# 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 |