diff --git a/README.md b/README.md
index cf1b9a7..df4da78 100644
--- a/README.md
+++ b/README.md
@@ -21,7 +21,7 @@ Für einige Teile reicht diese Landkarte nicht, weil ihr Verhalten aus dem
Zusammenspiel mehrerer Prozesse entsteht. Sie haben eigene, ausführliche
Dokumente unter [`doku/`](doku/README.md):
**[Automatiken](doku/automatiken.md)**, **[Zeitleiste](doku/zeitleiste.md)**,
-**[Benachrichtigungen](doku/benachrichtigungen.md)**,
+**[Benachrichtigungen](doku/benachrichtigungen.md)**, **[Batterie](doku/byd.md)**,
**[Solar-Übersicht](doku/uebersicht.md)**, **[Datenbanken](doku/datenbank.md)**
und **[Einstellungsseite](doku/einstellungen.md)**.
@@ -392,7 +392,8 @@ Verläufe aus `ajax/speicher.php` (Ladestand und Leistung aus `EnergyFlow`,
Spreizung und Temperatur aus `byd`). Live-Werte kommen über MQTT
`solarManager/byd/#` (`gatherBYDData.py`); fehlen sie, zeigt die Ansicht, was
der Wechselrichter weiß. Die Nennenergie je Modul (2,76 kWh, HVM) steht als
-Konstante oben in `speicher.js` — die BMU meldet sie nicht.
+Konstante oben in `speicher.js` — die BMU meldet sie nicht. Wie die Werte
+aus der BMU herauskommen, steht in **[doku/byd.md](doku/byd.md)**.
**`skodaMQTT.js`** — Fahrzeugseite. Holt alles über `ajax/skoda.php?was=…`
(live, ladungen, kurve, gesundheit, fahrten, strecke), zeichnet das Fahrzeug
diff --git a/doku/README.md b/doku/README.md
index e0a26b1..a569ad4 100644
--- a/doku/README.md
+++ b/doku/README.md
@@ -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 |
diff --git a/doku/bilder/speicher.png b/doku/bilder/speicher.png
new file mode 100644
index 0000000..ac576f5
Binary files /dev/null and b/doku/bilder/speicher.png differ
diff --git a/doku/byd.md b/doku/byd.md
new file mode 100644
index 0000000..d366698
--- /dev/null
+++ b/doku/byd.md
@@ -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
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 |
diff --git a/doku/datenbank.md b/doku/datenbank.md
index f858e5d..9181790 100644
--- a/doku/datenbank.md
+++ b/doku/datenbank.md
@@ -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 | — |