Skip to content

Register Findings

Falk Thumm edited this page Jul 26, 2026 · 1 revision

Findings: Register-Abweichungen

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:

  1. Parser - src/deye_master.cpp zusammen mit den Adressen in include/config.h. Bestimmt, was per MQTT nach Home Assistant geht.
  2. Web-UI-Tabelle - REG_INFO in src/web_ui.cpp. Bestimmt nur, wie der Register-Explorer einen manuell gelesenen Rohwert beschriftet.
  3. 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.

Übersicht

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

F-1: Batteriestrom (591), Faktor 0,02 statt 0,01

Schwere: hoch - betrifft einen publizierten Messwert.

Beobachtung

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".

Belege

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.

Auswirkung

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.

Verifikation am Gerät

Genau diese Unabhängigkeit macht die Prüfung einfach - Register 590 ist die Kontrollgröße:

  1. Web-UI öffnen, Sektion Live-Daten, möglichst bei ruhigem Lade- oder Entladebetrieb (mehrere hundert Watt, nicht nahe null).
  2. batt_voltage x batt_current bilden und mit batt_power vergleichen.
  3. Auswertung:
    • Produkt passt zu batt_power (wenige Prozent Abweichung) -> 0,02 ist richtig, der Kommentar in config.h stimmt.
    • Produkt ist etwa doppelt so groß wie batt_power -> 0,02 ist falsch, korrekt sind 0,01.

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.

Behebung

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.


F-2: System Work Mode (142), drei verschiedene Enums

Schwere: mittel - betrifft nur die Anzeige, aber mit irreführendem Ergebnis.

Beobachtung

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.

Auswirkung

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.

Verifikation am Gerät

Betriebsart am Wechselrichter-Display umstellen und Register 142 jeweils per Probe im Web-UI lesen. Zwei, drei Stellungen genügen, um die Zuordnung festzunageln.

Behebung

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.


F-3: Wirk- vs. Blindenergie (501/502 und 504..507)

Schwere: mittel - Parser ist korrekt, die Web-UI-Beschriftung ist der Ausreißer.

Beobachtung

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.

Belege

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.

Auswirkung

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.

Verifikation am Gerät

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.

Behebung

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.


F-4: Betriebszustand (500) unvollständig und verschoben

Schwere: niedrig.

Beobachtung

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.

Auswirkung

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.

Behebung

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.


F-5: Sensor16-High-Words werden nicht gelesen

Schwere: niedrig - für dieses Gerät ohne Folgen.

Beobachtung

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.

Auswirkung

Keine für einen SG04LP3 mit maximal 12 kW. Relevant erst im Parallelverbund oder bei größeren Maschinen.

Status

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.


F-6: 672/673 doppelt belegt (PV-Leistung vs. Generatorstrom)

Schwere: niedrig - Konflikt in der Quelle, nicht im Projekt.

Beobachtung

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.

Auswirkung

Generatorstrom L2/L3 ist auf diesem Modell nicht sinnvoll auslesbar. Die PV-Lesart ist die praktisch verwendete und deckt sich mit kbialek.

Status

Akzeptiert und hier festgehalten, damit die Doppelbelegung nicht später in die falsche Richtung "korrigiert" wird.


Gegenprobe: bestätigt korrekt

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.


Weiterarbeiten

  • 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.