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>
This commit is contained in:
@@ -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 |
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 126 KiB |
+273
@@ -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.
|
||||
|
||||

|
||||
|
||||
```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 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 |
|
||||
+1
-1
@@ -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 | — |
|
||||
|
||||
Reference in New Issue
Block a user