Files
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

12 KiB
Raw Permalink Blame History

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

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 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=verlaufEnergyFlow
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 p5p8 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