Files
Smart-Dashboard/doku/byd.md
T
adminandClaude Opus 5 a4b25ba279 Doku: die Schnittstelle zur BYD-Batterie beschrieben
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>
2026-09-21 12:34:50 +02:00

274 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<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](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 12
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 81128 ab Byte 5 + Fühler 130 ab Byte 103
p8: Fühler 3164 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·(m1)+1 bis 16·m
und die Fühler 8·(m1)+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 20004000 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 |