doku/byd.md: warum der Umweg über die BMU überhaupt nötig ist (der Gen24 kennt weder Zellen noch SOH noch die Zähler), der Ablauf der sechs Anfragen in einer einzigen Verbindung, und vor allem die Eigenheit, aus der sich der ganze Aufbau des Sammlers ergibt — das Netzwerkmodul startet alle ~102 Sekunden neu und bedient danach nur die ersten ein, zwei Verbindungen. Dazu die Byte-Lagen samt der einen Abweichung vom ioBroker-Adapter (wortvertauschte Gesamtzähler), die Aufteilung der 128 Zellen auf vier Antworten, was unter solarManager/byd/… herauskommt, die beiden Tabellen in solarLog und wie speicher.js daraus die Ansicht baut. Dabei aufgefallen und vermerkt: ajax/speicher.php?was=gesundheit hat keinen Aufrufer. Die Tageswerte für ein Alterungsdiagramm liegen also schon bereit, es fehlt nur die Anzeige. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
12 KiB
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.
flowchart LR
BMU["BMU der Battery-Box<br/><small>192.168.16.254:8080</small>"] -->|"Modbus-RTU in TCP"| G["gatherBYDData.py<br/><small>im solarManager.py-Prozess</small>"]
G -->|"solarManager/byd/…"| M[["MQTT"]]
G -->|"alle 5 / 15 Min."| DB[("solarLog<br/>byd · byd_zellen")]
M --> B["speicher.js<br/><small>Live</small>"]
DB --> A["ajax/speicher.php"] --> B
WR["Gen24<br/><small>Modbus</small>"] -->|"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 (MIT). Sie sind nirgends offiziell dokumentiert — wer hier etwas ändert, ändert an einer nachgebauten Schnittstelle.
2. Der Ablauf: alles in einer Verbindung
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 undConnectionErrorlanden im Debug-Protokoll, nicht als Warnung. Erst wenn zehn Minuten (_VERALTET) gar nichts durchkommt, gibt es eine Warnung — undokfä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 mit0xdavor, 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
IntersectionObserverlö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 |
