Explorar o código

Expand register coverage to all protocol v1.4.15 telemetry

Now reading every interesting holding register the firmware exposes,
not just the most basic ones.

CHARGER block (15201..15221):
  - State (15201..15203) -- already had these
  - Live readings (15205..15208) -- already had these
  - Sensors (15209..15210) -- NEW: charger radiator temp, external temp
  - Relays (15211..15212) -- NEW: battery and PV relay state
  - Configuration (15215..15216) -- NEW: battery voltage grade, rated current
  - Energy totals (15217+15218) -- NEW: accumulated PV energy (kWh, combined)
  - Runtime (15219..15221) -- NEW: days/hours/minutes since last reset

INVERTER block (25201..25275):
  - State (25201..25203) -- added rated power
  - Voltages (25205..25208) -- added BUS voltage
  - Currents (25209..25212) -- NEW: control/inverter/grid/load current
  - Real power P (25213..25216) -- already had these
  - Apparent power S (25217..25219) -- NEW: complex power
  - Reactive power Q (25221..25223) -- NEW: reactive power
  - Frequencies (25225..25226) -- NEW: inverter + grid frequency
  - Temperatures (25233..25235) -- already had these
  - Relays (25237..25242) -- NEW: all 6 relay states
  - Energy totals (25245..25260) -- NEW: 8 accumulator pairs combined into kWh totals
  - Battery telemetry (25273..25275) -- added battery voltage grade

Implementation:
  - New helper _combine_accumulators() merges each MWh+kWh pair into
    a single total-kWh line so the user doesn't have to do the math.
  - New helper _split_groups() divides a flat snapshot into labelled
    sections (State, Voltages, Relays, Energy totals, etc.) for
    readability. Groups accept a list of prefixes so multi-register
    sections (Currents, Voltages, Relays) all show up together.
  - New RELAY_STATE decoder for 0=Open, 1=Closed.
  - print_snapshot() now takes optional  to render section
    headers; falls back to flat output if no groups given.

Display renames (no protocol change, just human-readable labels):
  - 15206 'Battery voltage' -> 'Battery voltage (charger)'
  - 25205 'Battery voltage' -> 'Battery voltage (inverter)'
  These were ambiguous with 'Battery voltage grade' (15215 / 25275)
  which uses a different register entirely.

docs/register-map.md updated to cover every polled register with
scale, unit, and decode notes. README.md updated with a sample
output that matches the new layout.
lrosales hai 1 mes
pai
achega
e61b818c97
Modificáronse 3 ficheiros con 452 adicións e 85 borrados
  1. 111 21
      README.md
  2. 146 43
      docs/register-map.md
  3. 195 21
      must_pv1800_monitor.py

+ 111 - 21
README.md

@@ -84,36 +84,126 @@ which response belongs to which request.
 ## Output
 
 Example snapshot (panels disconnected, inverter in ByPass mode feeding a
-350W load from grid):
+~350 W load from grid):
 
 ```
-=== 2026-09-10 16:51:55 ===
+=== 2026-09-10 17:13:36 ===
 ---------------
   CHARGER
 ---------------
-  Charger workstate = Standby
-  MPPT state        = Stop
-  Charging state    = Stop
-  PV voltage        = 0.0V
-  Battery voltage   = 27.7V
-  Charger current   = 0.0A
-  Charger power     = 0.0W
+  --- State ---
+  Charger workstate         = SelfTest mode
+  MPPT state                = Stop
+  Charging state            = Stop
+
+  --- Live readings ---
+  PV voltage                = 0.0V
+  Battery voltage (charger) = 27.9V
+  Charger current           = 0.0A
+  Charger power             = 0.0W
+
+  --- Sensors ---
+  Charger radiator temp     = 26.0°C
+  External temp             = 0.0°C
+
+  --- Relays ---
+  Battery Relay (charger)   = Open
+  PV Relay (charger)        = Open
+
+  --- Configuration ---
+  Battery voltage grade     = 24V system
+  Charger rated current     = 60.0A
+
+  --- Energy totals ---
+  Acc PV energy (total)     = 0.1kWh
+
+  --- Runtime ---
+  Acc runtime days          = 0.0d
+  Acc runtime hours         = 0.0h
+  Acc runtime minutes       = 0.0m
+
 
 ---------------
   INVERTER
 ---------------
-  Inverter Work state = ByPass
-  Battery voltage     = 28.2V
-  Inverter voltage    = 115.7V
-  Grid voltage        = 117.0V
-  Inverter power      = -17W
-  Grid power          = -369W
-  Load power          = 351W
-  System load         = 18.0%
-  AC radiator temp    = 33.0°C
-  Transformer temp    = 52.0°C
-  Battery power       = 13W
-  Battery current     = -3A
+  --- State ---
+  Inverter Work state        = ByPass
+
+  --- Configuration ---
+  AC voltage grade           = 120V variant
+  Rated power                = 2000.0VA
+  Battery voltage grade      = 24V system
+
+  --- Voltages ---
+  Battery voltage (inverter) = 28.2V
+  Inverter voltage           = 116.0V
+  Grid voltage               = 117.5V
+  BUS voltage                = 254.9V
+
+  --- Currents ---
+  Control current            = 0.0A
+  Inverter current           = 0.0A
+  Grid current               = 3.3A
+  Load current               = 3.2A
+
+  --- Real power (P) ---
+  PInverter                  = -14W
+  PGrid                      = -363W
+  PLoad                      = 352W
+  Load percent               = 18.0%
+
+  --- Apparent power (S) ---
+  SInverter                  = 0VA
+  SGrid                      = 395VA
+  SLoad                      = 352VA
+
+  --- Reactive power (Q) ---
+  QInverter                  = 0var
+  QGrid                      = 152var
+  QLoad                      = 127var
+
+  --- Frequencies ---
+  Inverter frequency         = 60.0Hz
+  Grid frequency             = 60.0Hz
+
+  --- Temperatures ---
+  AC radiator temp           = 38.0°C
+  Transformer temp           = 60.0°C
+  DC radiator temp           = 0.0°C
+
+  --- Relays ---
+  Inverter relay             = Open
+  Grid relay                 = Closed
+  Load relay                 = Closed
+  N_Line relay               = Open
+  DC relay                   = Open
+  Earth relay                = Open
+
+  --- Energy totals ---
+  Acc charger (total)        = 0.0kWh
+  Acc discharger (total)     = 0.1kWh
+  Acc grid-buy (total)       = 17.0kWh
+  Acc grid-sell (total)      = 0.0kWh
+  Acc load (total)           = 16.0kWh
+  Acc self-use (total)       = 0.1kWh
+  Acc PV-sell (total)        = 0.0kWh
+  Acc grid-charge (total)    = 1.0kWh
+
+  --- Battery telemetry ---
+  Battery power              = 16W
+  Battery current            = -3A
+
+
+---------------
+  SETTINGS
+---------------
+  Float voltage                    = 27.2V
+  Absorb voltage                   = 28.2V
+  Battery stop discharging voltage = 23.0V
+  Battery stop charging voltage    = 26.4V
+  Battery low voltage              = 20.4V
+  Battery high voltage             = 30.0V
+  Charger current                  = 60.0A
 ```
 
 ## References

+ 146 - 43
docs/register-map.md

@@ -9,55 +9,156 @@ the full document covers hundreds more.
 All values are Modbus RTU holding registers (function code 0x03),
 unless otherwise noted. RO = read-only, RW = read/write.
 
+The monitor reads everything below as contiguous blocks:
+- CHARGER block: 15201..15221 (with gaps at 15204, 15213, 15214)
+- INVERTER block: 25201..25275 (with gaps at 25204, 25220, 25224, 25227, 25228, 25232, 25236, 25243, 25244, 25262..25272, 25276)
+- SETTINGS: two short contiguous reads at 10103..10104 and 20118..20132
+
+The firmware ignores the `count` field and dumps the entire block
+regardless, so the monitor always asks for the widest range that
+covers what we care about.
+
+---
+
 ## CHARGER block (start at 15201)
 
 | Addr | RO/RW | Name | Unit | Notes |
 |------|-------|------|------|-------|
-| 15201 | RO | Charger workstate | — | 0=Init, 1=Standby, 2=Charging, 3=Fault, 4=Flash |
-| 15202 | RO | MPPT state | — | 0=Stop, 1=Start, 2=Normal, 3=MPPT limit, 4=Float |
-| 15203 | RO | Charging state | — | 0=Stop, 1=Precharge, 2=CC, 3=CV, 4=Float, 5=Current derating |
-| 15204 | RO | reserved | — | |
+| 15201 | RO | Charger workstate | — | decoded: Init / SelfTest / Work / Stop |
+| 15202 | RO | MPPT state | — | decoded: Stop / MPPT / Current limiting |
+| 15203 | RO | Charging state | — | decoded: Stop / Absorb / Float / EQ |
+| 15204 | RO | reserved | | |
 | 15205 | RO | PV voltage | 0.1 V | |
 | 15206 | RO | Battery voltage (charger side) | 0.1 V | |
 | 15207 | RO | Charger current | 0.1 A | |
 | 15208 | RO | Charger power | W | |
-
-When you ask for register 15201 with `count=1` the inverter dumps the
-entire block (49 registers, addresses 15201..15249). The registers
-listed here are 15201..15208.
+| 15209 | RO | Charger radiator temperature | 1 °C | |
+| 15210 | RO | External temperature | 1 °C | |
+| 15211 | RO | Battery Relay | bool | 0=Open, 1=Closed |
+| 15212 | RO | PV Relay | bool | 0=Open, 1=Closed |
+| 15213 | RO | reserved | | |
+| 15214 | RO | reserved | | |
+| 15215 | RO | Battery voltage grade | V | 12, 24, or 48 |
+| 15216 | RO | Charger rated current | 0.1 A | |
+| 15217 | RO | Accumulated PV energy MWh part | MWh | combine with 15218 for total kWh |
+| 15218 | RO | Accumulated PV energy kWh part | 0.1 kWh | combine with 15217 for total kWh |
+| 15219 | RO | Accumulated runtime days | days | |
+| 15220 | RO | Accumulated runtime hours | hours | |
+| 15221 | RO | Accumulated runtime minutes | minutes | |
+
+The accumulated energy at 15217+15218 combines as `total_kWh = 15217 * 1000 + 15218 * 0.1`.
+
+---
 
 ## INVERTER block (start at 25201)
 
+### Operating state & configuration
+
 | Addr | RO/RW | Name | Unit | Notes |
 |------|-------|------|------|-------|
-| 25201 | RO | Work state | — | 0=PowerOn, 1=SelfTest, 2=OffGrid, 3=GridTie, 4=ByPass, 5=Stop, 6=GridCharging |
-| 25202 | RO | AC voltage grade | V | 230=230V variant, 120=120V variant |
+| 25201 | RO | Work state | — | decoded: PowerOn / SelfTest / OffGrid / GridTie / ByPass / Stop / GridCharging |
+| 25202 | RO | AC voltage grade | V | 230 or 120 |
 | 25203 | RO | Rated power | VA | |
 | 25204 | RO | reserved | | |
-| 25205 | RO | Battery voltage | 0.1 V | |
-| 25206 | RO | Inverter voltage | 0.1 V | |
-| 25207 | RO | Grid voltage | 0.1 V | |
-| 25208 | RO | BUS voltage | 0.1 V | |
-| 25209 | RO | Control current | 0.1 A | |
-| 25210 | RO | Inverter current | 0.1 A | |
-| 25211 | RO | Grid current | 0.1 A | |
-| 25212 | RO | Load current | 0.1 A | |
-| 25213 | RO | PInverter | W | **signed** in this script; protocol says unsigned 1W |
-| 25214 | RO | PGrid | W | signed (negative = exporting to grid) |
-| 25215 | RO | PLoad | W | signed (negative = ??? on this firmware) |
-| 25216 | RO | Load percent | % | unit is percent (raw integer, NOT ×0.01) |
-| 25217 | RO | SInverter | VA | |
-| ... | | (more S/Q regs, frequencies, temps, relays, accumulators) | | |
-| 25233 | RO | AC radiator temperature | °C | |
-| 25234 | RO | Transformer temperature | °C | |
-| 25235 | RO | DC radiator temperature | °C | often 0 on units without that sensor |
-| ... | | | | |
-| 25273 | RO | Batt power | W | signed |
-| 25274 | RO | Batt current | 0.1 A | signed |
-| 25275 | RO | Batt voltage grade | V | 12, 24, or 48 |
-
-This monitor polls 25201..25274 (74 registers). The inverter returns
-all of them in one response when asked.
+
+### Voltages
+
+| Addr | Name | Unit |
+|------|------|------|
+| 25205 | Battery voltage (inverter side) | 0.1 V |
+| 25206 | Inverter voltage | 0.1 V |
+| 25207 | Grid voltage | 0.1 V |
+| 25208 | BUS voltage | 0.1 V |
+
+### Currents
+
+| Addr | Name | Unit |
+|------|------|------|
+| 25209 | Control current | 0.1 A |
+| 25210 | Inverter current | 0.1 A |
+| 25211 | Grid current | 0.1 A |
+| 25212 | Load current | 0.1 A |
+
+### Power (real / apparent / reactive)
+
+| Addr | Name | Unit |
+|------|------|------|
+| 25213 | PInverter | W (signed in our script) |
+| 25214 | PGrid | W (signed; negative = exporting) |
+| 25215 | PLoad | W (signed) |
+| 25216 | Load percent | % |
+| 25217 | SInverter | VA (signed) |
+| 25218 | SGrid | VA (signed) |
+| 25219 | SLoad | VA (signed) |
+| 25220 | reserved | |
+| 25221 | Qinverter | var (signed) |
+| 25222 | Qgrid | var (signed) |
+| 25223 | Qload | var (signed) |
+
+### Frequencies
+
+| Addr | Name | Unit |
+|------|------|------|
+| 25225 | Inverter frequency | 0.01 Hz |
+| 25226 | Grid frequency | 0.01 Hz |
+
+### Temperatures
+
+| Addr | Name | Unit |
+|------|------|------|
+| 25233 | AC radiator temperature | 1 °C |
+| 25234 | Transformer temperature | 1 °C |
+| 25235 | DC radiator temperature | 1 °C | (often 0 on units without that sensor) |
+
+### Relays
+
+| Addr | Name | Notes |
+|------|------|-------|
+| 25237 | Inverter relay | 0=Disconnect, 1=Connect |
+| 25238 | Grid relay | |
+| 25239 | Load relay | |
+| 25240 | N_Line relay | |
+| 25241 | DC relay | |
+| 25242 | Earth relay | |
+
+### Energy accumulators (each is a HIGH+LOW pair)
+
+| Addr | Name | Scale |
+|------|------|-------|
+| 25245 | Acc charger MWh part | 1000 kWh |
+| 25246 | Acc charger kWh part | 0.1 kWh |
+| 25247 | Acc discharger MWh part | 1000 kWh |
+| 25248 | Acc discharger kWh part | 0.1 kWh |
+| 25249 | Acc grid-buy MWh part | 1000 kWh |
+| 25250 | Acc grid-buy kWh part | 0.1 kWh |
+| 25251 | Acc grid-sell MWh part | 1000 kWh |
+| 25252 | Acc grid-sell kWh part | 0.1 kWh |
+| 25253 | Acc load MWh part | 1000 kWh |
+| 25254 | Acc load kWh part | 0.1 kWh |
+| 25255 | Acc self-use MWh part | 1000 kWh |
+| 25256 | Acc self-use kWh part | 0.1 kWh |
+| 25257 | Acc PV-sell MWh part | 1000 kWh |
+| 25258 | Acc PV-sell kWh part | 0.1 kWh |
+| 25259 | Acc grid-charge MWh part | 1000 kWh |
+| 25260 | Acc grid-charge kWh part | 0.1 kWh |
+
+Each accumulator combines as `total_kWh = HIGH * 1000 + LOW * 0.1`. The
+monitor does this combination in `_combine_accumulators()` and
+displays each as a single `Acc <name> (total) = X.X kWh` line.
+
+### Alarm / warning / status bits
+
+Registers 25261..25268 hold error/warning bits. Not currently polled.
+
+### Battery telemetry
+
+| Addr | Name | Unit |
+|------|------|------|
+| 25273 | Battery power | W (signed) |
+| 25274 | Battery current | 0.1 A (signed) |
+| 25275 | Battery voltage grade | V (12, 24, or 48) |
+
+---
 
 ## SETTINGS blocks (split into two ranges)
 
@@ -66,7 +167,7 @@ all of them in one response when asked.
 | Addr | RO/RW | Name | Unit |
 |------|-------|------|------|
 | 10103 | RW | Float voltage | 0.1 V |
-| 10104 | RW | Absorb voltage | 0.1 V |
+| 10104 | RW | Absorption voltage | 0.1 V |
 
 ### 20118..20132
 
@@ -78,6 +179,8 @@ all of them in one response when asked.
 | 20128 | RW | Battery high voltage | 0.1 V |
 | 20132 | RW | Max combine charger current | 0.1 A |
 
+---
+
 ## Decoded state values
 
 These are the canonical values from protocol v1.4.15, verified by
@@ -124,17 +227,17 @@ against the same document uploaded to
 | 5 | Stop |
 | 6 | GridCharging |
 
-> **Note:** Earlier versions of this file and the monitor script had
-> incorrect values for 15201–15203. They were originally derived from
-> a misreading of the protocol document. If you ran the script before
-> the 2026-09-10 fix, you may have seen "Standby", "Charging",
-> "Float" etc. where the correct values are "SelfTest mode", "Work
-> mode", "Float charge". The protocol doc and the live readings agree;
-> the old decode tables did not.
+---
 
 ## Notes on this unit
 
 - Battery voltage grade is 24 V (24V battery system).
 - AC voltage grade is 120 V (120V variant, not the 230V default).
 - Confirmed against live readings: `Battery voltage = 28.2 V`,
-  `Inverter voltage = 115.7 V`, `Grid voltage = 117.0 V`.
+  `Inverter voltage = 115.5 V`, `Grid voltage = 117.0 V`.
+- Frequencies are 60 Hz (US-style).
+- The `Acc grid-charge (total) = 1.0 kWh` reading shows the inverter
+  has charged the battery from grid once for a total of ~1 kWh.
+- The `Acc grid-buy (total) = 17.0 kWh` reading shows the inverter
+  has bought 17 kWh from the grid over its lifetime.
+- These lifetime counters survive power cycles.

+ 195 - 21
must_pv1800_monitor.py

@@ -694,6 +694,14 @@ INVERTER_WORKSTATE = {
     6: "GridCharging",
 }
 
+
+def RELAY_STATE(v: int) -> str:
+    """Decode relay-state registers (25237..25242, 15211, 15212).
+
+    Per the protocol: 0=Disconnect, 1=Connect.
+    """
+    return "Closed" if v == 1 else "Open"
+
 BATTERY_TYPE = {
     0: "AGM",
     1: "Flooded",
@@ -730,33 +738,103 @@ class Register:
     unit: str = ""
 
 
-# CHARGER block
+# CHARGER block (registers 15201..15221; gaps at 15204, 15213, 15214
+# are reserved / unused by the firmware)
 CHARGER_REGS: List[Register] = [
+    # ----- Operating state -----
     Register(15201, "Charger workstate",         decode=lambda v: CHARGER_WORKSTATE.get(v, f"unknown({v})")),
     Register(15202, "MPPT state",                decode=lambda v: MPPT_STATE.get(v, f"unknown({v})")),
     Register(15203, "Charging state",            decode=lambda v: CHARGING_STATE.get(v, f"unknown({v})")),
+    # ----- Live measurements -----
     Register(15205, "PV voltage",                scale=0.1, unit="V"),
-    Register(15206, "Battery voltage",           scale=0.1, unit="V"),
+    Register(15206, "Battery voltage (charger)",  scale=0.1, unit="V"),
     Register(15207, "Charger current",           scale=0.1, unit="A"),
     Register(15208, "Charger power",             unit="W"),
-    # 15211 Battery Relay, 15212 PV Relay -- not exposed by reference config
+    # ----- Sensors -----
+    Register(15209, "Charger radiator temp",     unit="°C"),
+    Register(15210, "External temp",             unit="°C"),
+    # ----- Relays -----
+    Register(15211, "Battery Relay (charger)",   decode=RELAY_STATE),
+    Register(15212, "PV Relay (charger)",        decode=RELAY_STATE),
+    # ----- Configuration / identity -----
+    Register(15215, "Battery voltage grade",     decode=lambda v: f"{v}V system"),
+    Register(15216, "Charger rated current",     scale=0.1, unit="A"),
+    # ----- Energy accumulator (high * 1000 kWh + low * 0.1 kWh) -----
+    Register(15217, "Acc PV energy MWh part",    unit="MWh part"),
+    Register(15218, "Acc PV energy kWh part",    scale=0.1, unit="kWh part"),
+    # ----- Runtime since last reset -----
+    Register(15219, "Acc runtime days",          unit="d"),
+    Register(15220, "Acc runtime hours",         unit="h"),
+    Register(15221, "Acc runtime minutes",       unit="m"),
 ]
 
-# INVERTER block (read-only telemetry)
+
+# INVERTER block (registers 25201..25274 with gaps at 25204, 25220,
+# 25224, 25227, 25228, 25232, 25236, 25243, 25244, 25262..25268, 25270..25272,
+# 25275, 25276).
 INVERTER_REGS: List[Register] = [
+    # ----- Operating state -----
     Register(25201, "Inverter Work state",       decode=lambda v: INVERTER_WORKSTATE.get(v, f"unknown({v})")),
-    Register(25205, "Battery voltage",           scale=0.1, unit="V"),
+    Register(25202, "AC voltage grade",          decode=lambda v: f"{v}V variant"),
+    Register(25203, "Rated power",               unit="VA"),
+    # ----- Voltages -----
+    Register(25205, "Battery voltage (inverter)", scale=0.1, unit="V"),
     Register(25206, "Inverter voltage",          scale=0.1, unit="V"),
     Register(25207, "Grid voltage",              scale=0.1, unit="V"),
-    Register(25213, "Inverter power",            signed=True, unit="W"),
-    Register(25214, "Grid power",                signed=True, unit="W"),
-    Register(25215, "Load power",                signed=True, unit="W"),
-    Register(25216, "System load",               unit="%"),
-    Register(25233, "AC radiator temp",         unit="°C"),
+    Register(25208, "BUS voltage",               scale=0.1, unit="V"),
+    # ----- Currents -----
+    Register(25209, "Control current",           scale=0.1, unit="A"),
+    Register(25210, "Inverter current",          scale=0.1, unit="A"),
+    Register(25211, "Grid current",              scale=0.1, unit="A"),
+    Register(25212, "Load current",              scale=0.1, unit="A"),
+    # ----- Real power (P) -----
+    Register(25213, "PInverter",                 signed=True, unit="W"),
+    Register(25214, "PGrid",                     signed=True, unit="W"),
+    Register(25215, "PLoad",                     signed=True, unit="W"),
+    Register(25216, "Load percent",              unit="%"),
+    # ----- Apparent power (S) -----
+    Register(25217, "SInverter",                 signed=True, unit="VA"),
+    Register(25218, "SGrid",                     signed=True, unit="VA"),
+    Register(25219, "SLoad",                     signed=True, unit="VA"),
+    # ----- Reactive power (Q) -----
+    Register(25221, "QInverter",                 signed=True, unit="var"),
+    Register(25222, "QGrid",                     signed=True, unit="var"),
+    Register(25223, "QLoad",                     signed=True, unit="var"),
+    # ----- Frequencies -----
+    Register(25225, "Inverter frequency",        scale=0.01, unit="Hz"),
+    Register(25226, "Grid frequency",            scale=0.01, unit="Hz"),
+    # ----- Temperatures -----
+    Register(25233, "AC radiator temp",          unit="°C"),
     Register(25234, "Transformer temp",          unit="°C"),
     Register(25235, "DC radiator temp",          unit="°C"),
+    # ----- Relays -----
+    Register(25237, "Inverter relay",            decode=RELAY_STATE),
+    Register(25238, "Grid relay",                decode=RELAY_STATE),
+    Register(25239, "Load relay",                decode=RELAY_STATE),
+    Register(25240, "N_Line relay",              decode=RELAY_STATE),
+    Register(25241, "DC relay",                  decode=RELAY_STATE),
+    Register(25242, "Earth relay",               decode=RELAY_STATE),
+    # ----- Energy accumulators (each = high*1000 kWh + low*0.1 kWh) -----
+    Register(25245, "Acc charger MWh part",      unit="MWh part"),
+    Register(25246, "Acc charger kWh part",      scale=0.1, unit="kWh part"),
+    Register(25247, "Acc discharger MWh part",   unit="MWh part"),
+    Register(25248, "Acc discharger kWh part",   scale=0.1, unit="kWh part"),
+    Register(25249, "Acc grid-buy MWh part",     unit="MWh part"),
+    Register(25250, "Acc grid-buy kWh part",     scale=0.1, unit="kWh part"),
+    Register(25251, "Acc grid-sell MWh part",    unit="MWh part"),
+    Register(25252, "Acc grid-sell kWh part",    scale=0.1, unit="kWh part"),
+    Register(25253, "Acc load MWh part",         unit="MWh part"),
+    Register(25254, "Acc load kWh part",         scale=0.1, unit="kWh part"),
+    Register(25255, "Acc self-use MWh part",     unit="MWh part"),
+    Register(25256, "Acc self-use kWh part",     scale=0.1, unit="kWh part"),
+    Register(25257, "Acc PV-sell MWh part",      unit="MWh part"),
+    Register(25258, "Acc PV-sell kWh part",      scale=0.1, unit="kWh part"),
+    Register(25259, "Acc grid-charge MWh part",  unit="MWh part"),
+    Register(25260, "Acc grid-charge kWh part",  scale=0.1, unit="kWh part"),
+    # ----- Battery telemetry (live) -----
     Register(25273, "Battery power",             signed=True, unit="W"),
-    Register(25274, "Battery current",          signed=True, scale=0.1, unit="A"),
+    Register(25274, "Battery current",           signed=True, scale=0.1, unit="A"),
+    Register(25275, "Battery voltage grade",     decode=lambda v: f"{v}V system"),
 ]
 
 # SETTINGS block (read-only mirror of writable config)
@@ -830,19 +908,86 @@ def poll_individual(t: Transport, slave: int,
     return results
 
 
-def print_snapshot(snapshot: List[Tuple[str, Any, str]], title: str) -> None:
+def _split_groups(snapshot: List[Tuple[str, Any, str]],
+                 headings: List[Tuple[str, Any]]) -> List[Tuple[str, List[Tuple[str, Any, str]]]]:
+    """Split a flat snapshot into labelled groups.
+
+    `headings` is a list of `(heading_text, name_prefix_or_prefixes)`
+    pairs. Each register whose name starts with one of the listed
+    prefixes goes into that group. A prefix can also be a `str` for
+    the common single-prefix case. Registers that don't match any
+    prefix are silently dropped.
+    """
+    out: List[Tuple[str, List[Tuple[str, Any, str]]]] = []
+    for header, prefixes in headings:
+        if isinstance(prefixes, str):
+            prefixes = [prefixes]
+        items = [(n, v, u) for n, v, u in snapshot
+                 if any(n.startswith(p) for p in prefixes)]
+        if items:
+            out.append((header, items))
+    return out
+
+
+def _combine_accumulators(snapshot: List[Tuple[str, Any, str]]) -> List[Tuple[str, Any, str]]:
+    """Merge every 'Acc <name> MWh part' / 'kWh part' pair into a single
+    'Acc <name> (total) = X.X kWh' line. Returns the merged list (in
+    the original order, with the combined line replacing the two
+    originals)."""
+    by_name = {n: (v, u) for n, v, u in snapshot}
+    out: List[Tuple[str, Any, str]] = []
+    skip = set()
+    for name, value, unit in snapshot:
+        if name in skip:
+            continue
+        if name.endswith(" MWh part"):
+            base = name[: -len(" MWh part")]
+            kwh_name = base + " kWh part"
+            if kwh_name in by_name:
+                kwh_val = by_name[kwh_name][0]
+                total_kwh = value * 1000.0 + kwh_val * 0.1
+                # `base` looks like "Acc <name>"; we want "<name>".
+                if base.startswith("Acc "):
+                    label = base[len("Acc "):]
+                else:
+                    label = base
+                out.append((f"Acc {label.strip()} (total)",
+                            round(total_kwh, 2), "kWh"))
+                skip.add(name)
+                skip.add(kwh_name)
+                continue
+        out.append((name, value, unit))
+    return out
+
+
+def print_snapshot(snapshot: List[Tuple[str, Any, str]], title: str,
+                   groups: Optional[List[Tuple[str, List[Tuple[str, Any, str]]]]] = None) -> None:
+    """Print a snapshot. If `groups` is given, print section headers between
+    groups; otherwise dump the whole list flat."""
     bar = "-" * max(15, len(title) + 4)
     print(bar)
     print(f"  {title}")
     print(bar)
     name_w = max(len(n) for n, _, _ in snapshot)
-    for name, value, unit in snapshot:
-        if isinstance(value, float):
-            vstr = f"{value:.1f}"
-        else:
-            vstr = str(value)
-        suffix = f"{unit}" if unit else ""
-        print(f"  {name:<{name_w}} = {vstr}{suffix}")
+    if groups:
+        for header, items in groups:
+            print(f"  --- {header} ---")
+            for name, value, unit in items:
+                if isinstance(value, float):
+                    vstr = f"{value:.1f}"
+                else:
+                    vstr = str(value)
+                suffix = f"{unit}" if unit else ""
+                print(f"  {name:<{name_w}} = {vstr}{suffix}")
+            print()
+    else:
+        for name, value, unit in snapshot:
+            if isinstance(value, float):
+                vstr = f"{value:.1f}"
+            else:
+                vstr = str(value)
+            suffix = f"{unit}" if unit else ""
+            print(f"  {name:<{name_w}} = {vstr}{suffix}")
     print()
 
 
@@ -998,8 +1143,37 @@ def main() -> int:
                 print(json.dumps(payload, ensure_ascii=False))
             else:
                 print(f"=== {now} ===")
-                print_snapshot(charger, "CHARGER")
-                print_snapshot(inverter, "INVERTER")
+                # Combine accumulator pairs (MWh part + kWh part -> total kWh)
+                # and split CHARGER/INVERTER into readable sub-sections.
+                charger = _combine_accumulators(charger)
+                inverter = _combine_accumulators(inverter)
+
+                charger_groups = _split_groups(charger, [
+                    ("State",          ["Charger workstate", "MPPT state", "Charging state"]),
+                    ("Live readings",  ["PV voltage", "Battery voltage (charger)", "Charger current", "Charger power"]),
+                    ("Sensors",        ["Charger radiator temp", "External temp"]),
+                    ("Relays",         ["Battery Relay (charger)", "PV Relay (charger)"]),
+                    ("Configuration",  ["Battery voltage grade", "Charger rated current"]),
+                    ("Energy totals",  ["Acc PV energy"]),
+                    ("Runtime",        ["Acc runtime "]),
+                ])
+                inverter_groups = _split_groups(inverter, [
+                    ("State",           ["Inverter Work state"]),
+                    ("Configuration",   ["AC voltage grade", "Rated power", "Battery voltage grade"]),
+                    ("Voltages",        ["Battery voltage (inverter)", "Inverter voltage", "Grid voltage", "BUS voltage"]),
+                    ("Currents",        ["Control current", "Inverter current", "Grid current", "Load current"]),
+                    ("Real power (P)",  ["PInverter", "PGrid", "PLoad", "Load percent"]),
+                    ("Apparent power (S)", ["SInverter", "SGrid", "SLoad"]),
+                    ("Reactive power (Q)", ["QInverter", "QGrid", "QLoad"]),
+                    ("Frequencies",     ["Inverter frequency", "Grid frequency"]),
+                    ("Temperatures",    ["AC radiator temp", "Transformer temp", "DC radiator temp"]),
+                    ("Relays",          ["Inverter relay", "Grid relay", "Load relay", "N_Line relay", "DC relay", "Earth relay"]),
+                    ("Energy totals",   ["Acc "]),
+                    ("Battery telemetry", ["Battery power", "Battery current"]),
+                ])
+
+                print_snapshot(charger, "CHARGER", groups=charger_groups)
+                print_snapshot(inverter, "INVERTER", groups=inverter_groups)
                 print_snapshot(settings, "SETTINGS")
             sys.stdout.flush()