Repository navigation
Register Findings
Die Register-Map des SG04LP3 ist nicht offiziell dokumentiert. Dieses Projekt trägt drei Interpretationen desselben Geräts mit sich, und die weichen an einigen Stellen voneinander ab:
-
Parser -
src/deye_master.cppzusammen mit den Adressen ininclude/config.h. Bestimmt, was per MQTT nach Home Assistant geht. -
Web-UI-Tabelle -
REG_INFOinsrc/web_ui.cpp. Bestimmt nur, wie der Register-Explorer einen manuell gelesenen Rohwert beschriftet. -
Referenzen - kellerza/sunsynk
(
three_phase_common.py,three_phase_lv.py) und kbialek/deye-inverter-mqtt (deye_sensors_deye_sg04lp3.py).
Wo Parser und Web-UI sich widersprechen, ist genau eine der beiden Varianten das, was auf dem Bus tatsächlich ankommt. Unten steht je Fall, welche das nach Aktenlage ist, was das praktisch bedeutet und wie man es am eigenen Gerät entscheidet.
Stand der Quellenprüfung: 26.07.2026, jeweils Branch main.
| ID | Register | Thema | Betroffen | Schwere | Status |
|---|---|---|---|---|---|
| F-1 | 591 | Batteriestrom-Faktor 0,02 statt 0,01 | Parser, MQTT | hoch | am Gerät zu verifizieren |
| F-2 | 142 | Work-Mode-Enum dreifach verschieden | Web-UI-Anzeige | mittel | offen |
| F-3 | 501/502, 504..507 | Wirk- vs. Blindenergie | Web-UI-Tabelle | mittel | Parser bestätigt |
| F-4 | 500 | Betriebszustände unvollständig | Kommentar, Web-UI | niedrig | offen |
| F-5 | 604..659 | Sensor16-High-Words ungenutzt | Parser | niedrig | akzeptiert |
| F-6 | 672 / 673 | doppelt belegt in der Quelle | Quellenkonflikt | niedrig | akzeptiert |
Schwere: hoch - betrifft einen publizierten Messwert.
Der Parser rechnet den Batteriestrom mit 0,02 A pro LSB:
// src/deye_master.cpp, parseHot()
s_buf.batt_current = float(asS16(HOT(r, DEYE_REG_BATT_CURRENT))) * 0.02f;include/config.h begründet das mit "LV-Modell; Doku nennt 0.01, real 0.02".
Beide Referenzen nennen übereinstimmend 0,01, und zwar in genau den modellspezifischen Dateien, die für den SG04LP3 gelten:
| Quelle | Definition |
|---|---|
sunsynk three_phase_lv.py
|
Sensor(591, "Battery current", AMPS, -0.01) |
kbialek deye_sensors_deye_sg04lp3.py
|
SingleRegisterSensor("Battery Current", 591, 0.01, signed=True) |
Das negative Vorzeichen bei sunsynk ist kein Rechenvorzeichen, sondern die
Kennzeichnung eines vorzeichenbehafteten Registers -
unpack_value(regs, signed=self.factor < 0) und danach abs(self.factor).
Gemeint ist also: S16 x 0,01.
Damit steht die Firmware allein gegen beide Referenzen und liefert einen doppelt so großen Strom.
batt_current in State-JSON und HA ist im Verdachtsfall um Faktor 2 zu hoch.
Nicht betroffen ist batt_power: die Leistung kommt aus dem eigenen
Register 590 und wird nicht aus U x I gerechnet. Energiebilanzen und
Dashboards, die auf Leistung basieren, stimmen also unabhängig davon.
Genau diese Unabhängigkeit macht die Prüfung einfach - Register 590 ist die Kontrollgröße:
- Web-UI öffnen, Sektion Live-Daten, möglichst bei ruhigem Lade- oder Entladebetrieb (mehrere hundert Watt, nicht nahe null).
-
batt_voltagexbatt_currentbilden und mitbatt_powervergleichen. - Auswertung:
- Produkt passt zu
batt_power(wenige Prozent Abweichung) -> 0,02 ist richtig, der Kommentar inconfig.hstimmt. - Produkt ist etwa doppelt so groß wie
batt_power-> 0,02 ist falsch, korrekt sind 0,01.
- Produkt passt zu
Beispiel: Rohwert 1000 bei 51,2 V ergibt mit 0,02 einen Strom von 20 A und
damit 1024 W; mit 0,01 sind es 10 A und 512 W. Welcher der beiden Werte in
batt_power steht, entscheidet die Frage.
Falls sich 0,01 bestätigt, in src/deye_master.cpp in parseHot()
* 0.02f auf * 0.01f ändern und den Kommentar in include/config.h
mitziehen. Sonst nichts ändern und den Kommentar um das Messergebnis
ergänzen, damit die Abweichung belegt ist.
Schwere: mittel - betrifft nur die Anzeige, aber mit irreführendem Ergebnis.
Für dasselbe Register kursieren drei Zuordnungen:
| Wert | Web-UI (web_ui.cpp) |
Kommentar in config.h
|
sunsynk 3-phase ("Load Limit") |
|---|---|---|---|
| 0 | Self Use | Selling First | Allow Export |
| 1 | Zero Export to Load | Zero Export to Load | Essentials |
| 2 | Zero Export to CT | Zero Export to CT | Zero Export |
| 3 | Selling First | - | - |
config.h und sunsynk sagen inhaltlich dasselbe: 0 erlaubt Export, 1 begrenzt
auf den Hausverbrauch, 2 regelt auf die externe CT-Messung. Die vierwertige
Web-UI-Tabelle sieht nach dem generischen bzw. einphasigen Deye-Dokument aus
und ist gegenüber den anderen beiden bei Wert 0 verschoben.
sunsynk führt für den 3-Phase-Zweig außerdem 141 als Schalter "Priority Load", während die Web-UI-Tabelle 141 als "Energy Mgmt Mode (Bitmask)" beschriftet.
Schreibend unkritisch: applyBatteryControl() schreibt den Wert 2, und der
bedeutet in allen drei Lesarten eine Zero-Export-Betriebsart.
Lesend irreführend: meldet der Wechselrichter eine 0, zeigt der Register-Explorer "Self Use", gemeint ist nach den beiden anderen Quellen aber "Selling First" / "Allow Export" - also das gegenteilige Verhalten.
Betriebsart am Wechselrichter-Display umstellen und Register 142 jeweils per Probe im Web-UI lesen. Zwei, drei Stellungen genügen, um die Zuordnung festzunageln.
Die Enum-Tabelle in web_ui.cpp auf die 3-Phase-Lesart ziehen oder den
Eintrag als mehrdeutig kennzeichnen, damit niemand die Anzeige für bare
Münze nimmt.
Schwere: mittel - Parser ist korrekt, die Web-UI-Beschriftung ist der Ausreißer.
Der Parser liest 502 als Tages-Wirkenergie und 506/507 als Gesamt-Wirkenergie:
// src/deye_master.cpp, parseCold()
s_buf.e_day_active = float(asS16(COLD(r, DEYE_REG_E_DAY_ACTIVE))) * 0.1f; // 502
s_buf.e_tot_active = float(asU32(COLD(r, DEYE_REG_E_TOT_ACTIVE_LO), // 506/507
COLD(r, DEYE_REG_E_TOT_ACTIVE_LO + 1))) * 0.1f;Die Web-UI-Tabelle beschriftet dieselben Register als Blindenergie und setzt die Wirkenergie zwei Register davor auf 501 bzw. 504/505.
sunsynk three_phase_common.py stützt den Parser:
Sensor(502, "Day Active Energy", KWH, -0.1),
Sensor((506, 507), "Total Active Energy", KWH, 0.1),Register 501, 504 und 505 kommen dort überhaupt nicht vor. Auch die
Datentypen decken sich exakt: -0.1 heißt bei sunsynk vorzeichenbehaftet mit
Faktor 0,1, und genau das macht der Parser mit asS16(...) * 0.1f; 506/507
liest er als vorzeichenloses U32 x 0,1.
Ergebnis: der Parser ist richtig. Die Beschriftung im Register-Explorer ist die abweichende Variante - vermutlich aus einer Spec-Fassung mit anderer Zählung übernommen.
Was per MQTT nach Home Assistant geht, stimmt. Der Fehler tritt nur beim manuellen Nachmessen auf: wer 502 oder 506 im Register-Explorer liest, bekommt sie als Blindenergie beschriftet und hält den korrekten Wert fälschlich für den falschen.
Tageszähler 501 und 502 im Register-Explorer nebeneinander lesen und mit dem Display vergleichen. Die Wirkenergie läuft mit der Erzeugung mit, die Blindenergie bleibt vergleichsweise klein und träge.
Die Einträge 501, 502 und 504..507 in REG_INFO nach der sunsynk-Zählung
beschriften - oder beide Lesarten im Namen führen, solange es am Gerät nicht
gegengeprüft ist.
Schwere: niedrig.
| Wert | sunsynk InverterStateSensor
|
Web-UI | Kommentar in config.h
|
|---|---|---|---|
| 0 | standby | Standby | stby |
| 1 | selfcheck | Selfcheck | selfchk |
| 2 | ok | Normal | normal |
| 3 | alarm | Alarm | fault |
| 4 | fault | Fault | - |
| 5 | activating | - | - |
Der Kommentar in config.h bildet 3 auf fault ab, obwohl 3 alarm ist und
fault erst bei 4 kommt. Der Web-UI-Tabelle fehlt der Zustand 5.
run_state geht als blanke Zahl nach MQTT. Wer in Home Assistant eine eigene
Zuordnung baut und sich dabei am Kommentar orientiert, meldet einen Alarm als
Fault. Ein Gerät in Zustand 5 erscheint im Register-Explorer als unbekannt.
Kommentar in include/config.h korrigieren, 5: 'Activating' in die
Web-UI-Tabelle aufnehmen. Optional die Zuordnung auch in der HA-Discovery
mitgeben, statt die Rohzahl zu publizieren.
Schwere: niedrig - für dieses Gerät ohne Folgen.
sunsynk definiert die Leistungsregister als 32-Bit-Paare, deren High-Word nicht benachbart liegt:
| Low-Word | High-Word | Größe |
|---|---|---|
| 604..608 | 700..704 | Grid Inner Power L1..L3, Total, Apparent |
| 616..619 | 705..708 | Grid CT Power L1..L3, Total |
| 622..625 | 687..690 | Grid Power L1..L3, Total |
| 650..653 | 656..659 | Load Power L1..L3, Total |
Der Parser liest jeweils nur das Low-Word als S16. Oberhalb von rund
+-32,7 kW wickelt der Wert um.
sunsynk selbst wertet das High-Word nur heuristisch aus - Sensor16 nimmt es
erst, wenn es über die letzten zehn Messungen durchgängig ungleich null war,
weil Deye es sonst mehrdeutig belegt.
Keine für einen SG04LP3 mit maximal 12 kW. Relevant erst im Parallelverbund oder bei größeren Maschinen.
Akzeptiert. Die High-Words liegen außerhalb der HOT- und COLD2-Blöcke, ein Nachziehen kostet also zusätzliche Bus-Requests. Adressen sind oben dokumentiert, falls es doch einmal gebraucht wird.
Schwere: niedrig - Konflikt in der Quelle, nicht im Projekt.
three_phase_common.py definiert dieselben Adressen zweimal:
Sensor(671, "Gen L1 current", AMPS, -0.01, alias="AUX L1 current"),
Sensor(672, "Gen L2 current", AMPS, -0.01, alias="AUX L2 current"),
Sensor(673, "Gen L3 current", AMPS, -0.01, alias="AUX L3 current"),
...
Sensor(672, "PV1 power", WATT, -1),
Sensor(673, "PV2 power", WATT, -1),In der Sensor-Sammlung gewinnt die spätere Definition, faktisch gelten 672 und 673 dort also als PV-Leistung. Projekt und Web-UI folgen dieser Lesart und führen nur 671 als Generatorstrom.
Generatorstrom L2/L3 ist auf diesem Modell nicht sinnvoll auslesbar. Die PV-Lesart ist die praktisch verwendete und deckt sich mit kbialek.
Akzeptiert und hier festgehalten, damit die Doppelbelegung nicht später in die falsche Richtung "korrigiert" wird.
Beim Abgleich mit beiden Referenzen unauffällig - hier stimmen Parser und Quellen überein:
| Thema | Register | Befund |
|---|---|---|
Temperatur-Offset x0,1 - 100
|
540, 541, 586, 217 | sunsynk TempSensor(offset=100), kbialek offset=-100.0
|
| U32-Wortreihenfolge Low-Word zuerst | alle Total-Zähler | sunsynk helpers.py: "All inverters are little-endian"
|
Batterieleistung S16 x 1
|
590 | beide Referenzen |
Batteriespannung x0,01, SOC x1
|
587, 588 | beide Referenzen |
Inverter-Ströme S16 x 0,01
|
630..632 | sunsynk |
PV-Leistung x1, PV-Spannung/-Strom x0,1
|
672, 673, 676..679 | beide Referenzen |
| Trennung CT-Messung / interne Messung | 616..619 vs. 622..625 | sunsynk führt beide Gruppen getrennt |
| Load-Block | 644..655 | sunsynk |
| Energie-Tages- und Gesamtzähler | 514..535 | beide Referenzen |
| Seriennummer als ASCII | 3..7 | sunsynk SerialSensor
|
Eine Randnotiz ohne praktische Folge: die Firmware liest die
Temperaturregister als int16_t, beide Referenzen behandeln sie
vorzeichenlos. Im realistischen Bereich (Rohwerte um 1000..1500, also 0..50 °C)
macht das keinen Unterschied.
- F-1 ist der einzige Punkt, der einen publizierten Messwert verfälschen kann, und mit dem U-mal-I-Vergleich in wenigen Minuten entschieden.
- F-2, F-3 und F-4 sind Beschriftungs- und Kommentarfehler; sie ändern nichts an den MQTT-Daten, kosten aber Zeit beim Debuggen am Gerät.
- F-5 und F-6 sind bewusst so und brauchen nur dann Arbeit, wenn das Projekt auf größere Maschinen oder den Generatoreingang ausgeweitet wird.
Alle Register mit Typ, Skalierung und Enums stehen auf Modbus-Register.