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

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