Skip to content

Commit ec34719

Browse files
committed
feat(inverter): add full EG4 lineup across SNA + 18KPV families
- SNA family (capture-validated): 6000XP, 12000XP, 6500EX-48, 3000EHV-48 - 18KPV family (document-derived, untested): 18KPV, 12kPV - Add dedicated alias driver modules for clean labels (eg4_12000xp, eg4_12kpv, eg4_6500ex, eg4_3000ehv) - Update settings dropdown with full lineup (legacy models marked) - Document FlexBOSS/GridBOSS as a NEW platform needing a dedicated map (NOT aliased to SNA/18KPV — would silently decode garbage) - Update README (Supported inverter models table) + TODO #7 - 21 tests pass
1 parent 55da9c6 commit ec34719

9 files changed

Lines changed: 142 additions & 12 deletions

File tree

‎README.md‎

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,28 @@ This project is actively running on private hardware monitoring an EG4 6000XP in
4242
| Grafana | ✅ Built | Pre-loaded dashboards and data source |
4343
| Automation rules | ✅ Built | Rule-table model (time + sensor conditions) that writes holding registers |
4444

45+
### Supported inverter models
46+
47+
lux-mon speaks the **Luxpower SNA** and **18KPV** register families. EG4
48+
inverters are Luxpower rebadges, so the full EG4 lineup is supported via two
49+
families:
50+
51+
| Family | Models | Register map | Status |
52+
|--------|--------|--------------|--------|
53+
| **SNA** (2-MPPT, single-phase off-grid) | EG4 6000XP, 12000XP, 6500EX-48, 3000EHV-48, Luxpower SNA | `collector/registers.py` | ✅ Capture-validated on the 6000XP |
54+
| **18KPV** (3-MPPT, split-phase 120/240V, AFCI, generator) | EG4 18KPV, 12kPV | `collector/drivers/eg4_18kpv.py` | ⚠️ Document-derived, not yet capture-validated |
55+
56+
- **SNA family** is the validated reference — the 6000XP map was
57+
reverse-engineered from live captures. The 12000XP, 6500EX-48, and
58+
3000EHV-48 (legacy, discontinued) share the exact same register layout.
59+
- **18KPV family** is derived from the official EG4 Modbus protocol document
60+
and has **not** been validated against a live unit. The 12kPV shares the
61+
18KPV map. Community feedback will identify any gaps.
62+
- **FlexBOSS18/21 and GridBOSS** are a *new* platform (not a Luxpower SNA
63+
rebadge) with an unknown register map. They are intentionally **not**
64+
aliased to either family — doing so would silently decode garbage. A
65+
dedicated driver is needed once their protocol is documented.
66+
4567
### In progress / near-term
4668

4769
- **Inverter Edit Mode page** — manual Read/Set for every editable EG4 6000XP holding register, mirroring the EG4 Monitor Maintenance tab.
@@ -52,7 +74,7 @@ This project is actively running on private hardware monitoring an EG4 6000XP in
5274

5375
- Generator and AC-coupled charge support (generator-charge register path is stubbed)
5476
- Battery protection automations (SOC threshold + restore) — rule-table model supports this, needs UI
55-
- More inverter models via pluggable Modbus RTU drivers
77+
- More inverter models via pluggable Modbus RTU drivers (FlexBOSS/GridBOSS need a dedicated register map)
5678
- Forecast.Solar provider (listed in settings, not yet wired)
5779

5880
## Architecture

‎TODO.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,11 @@
3939
### 6. Holding register map corrections — ⬜ (explicitly "in progress")
4040
- Align `collector/protocol.py` with `LXP_REGISTERS.txt`, add missing registers, fix ranges, correct AC charge current to register 168.
4141

42-
### 7. More inverter models — 🚧 (aliases only)
43-
- `collector/drivers/registry.py` maps many model names (`eg4_3000ehv`, `eg4_6500ex`, `luxpower_12k`, `growatt`, `solis`, etc.) to the `eg4_6000xp` driver as a fallback.
44-
- **To do:** write dedicated drivers for models that differ from the 6000XP register family.
42+
### 7. More inverter models — 🚧 (SNA + 18KPV families done; FlexBOSS/GridBOSS pending)
43+
- `collector/drivers/registry.py` now maps the full EG4 lineup across two families:
44+
- **SNA family** (capture-validated): `eg4_6000xp`, `eg4_12000xp`, `eg4_6500ex`, `eg4_3000ehv`, `luxpower_sna`.
45+
- **18KPV family** (document-derived, untested): `eg4_18kpv`, `eg4_12kpv`.
46+
- **To do:** FlexBOSS18/21 and GridBOSS are a *new* platform (not a Luxpower SNA rebadge) with an unknown register map — need a dedicated driver once their protocol is documented. Do NOT alias them to SNA/18KPV.
4547

4648
### 8. RS-485 transport for the main collector — 🚧 (separate daemon only)
4749
- The main collector's `transport` options are `tcp_passive`, `tcp_active`, `replay`. RS-485 is a **separate** daemon (`lux-mon-rs485`), not a pluggable transport of the main collector.

‎collector/drivers/eg4_12000xp.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"""EG4 12000XP inverter driver.
2+
3+
The EG4 12000XP is a 12 kW off-grid inverter in the Luxpower SNA family
4+
(2-MPPT, single-phase). This driver is an alias of the canonical
5+
`luxpower_sna` driver — same register map, batches, and decode path.
6+
"""
7+
from __future__ import annotations
8+
9+
from . import ModelDriver
10+
from .luxpower_sna import create_driver as _sna
11+
12+
13+
def create_driver() -> ModelDriver:
14+
drv = _sna()
15+
drv.name = "eg4_12000xp"
16+
drv.label = "EG4 12000XP"
17+
return drv

‎collector/drivers/eg4_12kpv.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"""EG4 12kPV inverter driver.
2+
3+
The EG4 12kPV is the 12 kW sibling of the 18KPV — same 3-MPPT, split-phase
4+
120/240V, AFCI, generator-input register family. This driver is an alias of
5+
the `eg4_18kpv` driver (document-derived, not yet capture-validated).
6+
"""
7+
from __future__ import annotations
8+
9+
from . import ModelDriver
10+
from .eg4_18kpv import create_driver as _18kpv
11+
12+
13+
def create_driver() -> ModelDriver:
14+
drv = _18kpv()
15+
drv.name = "eg4_12kpv"
16+
drv.label = "EG4 12kPV"
17+
return drv

‎collector/drivers/eg4_3000ehv.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"""EG4 3000EHV-48 inverter driver.
2+
3+
The EG4 3000EHV-48 is a legacy small off-grid inverter in the Luxpower SNA
4+
family (2-MPPT, single-phase). Discontinued but still in the field. This
5+
driver is an alias of the canonical `luxpower_sna` driver.
6+
"""
7+
from __future__ import annotations
8+
9+
from . import ModelDriver
10+
from .luxpower_sna import create_driver as _sna
11+
12+
13+
def create_driver() -> ModelDriver:
14+
drv = _sna()
15+
drv.name = "eg4_3000ehv"
16+
drv.label = "EG4 3000EHV-48"
17+
return drv

‎collector/drivers/eg4_6500ex.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"""EG4 6500EX-48 inverter driver.
2+
3+
The EG4 6500EX-48 is a legacy 6.5 kW off-grid inverter in the Luxpower SNA
4+
family (2-MPPT, single-phase). Discontinued but still in the field. This
5+
driver is an alias of the canonical `luxpower_sna` driver.
6+
"""
7+
from __future__ import annotations
8+
9+
from . import ModelDriver
10+
from .luxpower_sna import create_driver as _sna
11+
12+
13+
def create_driver() -> ModelDriver:
14+
drv = _sna()
15+
drv.name = "eg4_6500ex"
16+
drv.label = "EG4 6500EX-48"
17+
return drv

‎collector/drivers/registry.py‎

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
from . import ModelDriver
88
from .eg4_6000xp import create_driver as _eg4_6000xp
99
from .eg4_18kpv import create_driver as _eg4_18kpv
10+
from .eg4_12kpv import create_driver as _eg4_12kpv
11+
from .eg4_12000xp import create_driver as _eg4_12000xp
12+
from .eg4_6500ex import create_driver as _eg4_6500ex
13+
from .eg4_3000ehv import create_driver as _eg4_3000ehv
1014
from .luxpower_sna import create_driver as _luxpower_sna
1115

1216
logger = logging.getLogger(__name__)
@@ -21,13 +25,25 @@
2125
# so would silently decode garbage. They are intentionally absent until a
2226
# dedicated driver with a validated map is written.
2327
DRIVERS: Dict[str, Callable[[], ModelDriver]] = {
28+
# ── SNA family (capture-validated on the 6000XP) ──
29+
# 2-MPPT, single-phase off-grid inverters. All share the exact same
30+
# register family; the 6000XP is the validated reference.
2431
"eg4_6000xp": _eg4_6000xp,
2532
"luxpower_sna": _luxpower_sna,
33+
"eg4_12000xp": _eg4_12000xp,
34+
"eg4_6500ex": _eg4_6500ex,
35+
"eg4_3000ehv": _eg4_3000ehv,
36+
37+
# ── 18KPV family (document-derived, NOT yet capture-validated) ──
38+
# 3-MPPT, split-phase 120/240V, AFCI, generator input. The 18KPV map is
39+
# derived from the official EG4 Modbus protocol doc; the 12kPV shares it.
2640
"eg4_18kpv": _eg4_18kpv,
27-
# Rebadges of the Luxpower SNA that share the exact same register family.
28-
"eg4_3000ehv": _luxpower_sna,
29-
"eg4_6500ex": _luxpower_sna,
30-
"luxpower_12k": _luxpower_sna,
41+
"eg4_12kpv": _eg4_12kpv,
42+
43+
# ── FlexBOSS / GridBOSS ──
44+
# NEW platform (not a Luxpower SNA rebadge). Register map unknown; do NOT
45+
# alias to either family above. Intentionally absent until a dedicated
46+
# driver with a validated map is written.
3147
}
3248

3349
DEFAULT_MODEL = "eg4_6000xp"

‎collector/settings.py‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -227,13 +227,14 @@ def effective_value(name: str, db_value: Optional[str]):
227227
"section": "inverter",
228228
"options": [
229229
("eg4_6000xp", "EG4 6000XP"),
230+
("eg4_12000xp", "EG4 12000XP"),
230231
("eg4_18kpv", "EG4 18KPV"),
232+
("eg4_12kpv", "EG4 12kPV"),
231233
("luxpower_sna", "Luxpower SNA"),
232-
("eg4_3000ehv", "EG4 3000EHV"),
233-
("eg4_6500ex", "EG4 6500EX"),
234-
("luxpower_12k", "Luxpower 12K"),
234+
("eg4_6500ex", "EG4 6500EX-48 (legacy)"),
235+
("eg4_3000ehv", "EG4 3000EHV-48 (legacy)"),
235236
],
236-
"hint": "Inverter model for protocol selection (Luxpower SNA family only)",
237+
"hint": "Inverter model (Luxpower SNA / 18KPV families only)",
237238
},
238239
"pv_max_power": {
239240
"label": "Max Solar PV Power",

‎tests/test_drivers.py‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,27 @@ def test_registry_has_sna_family():
99
assert "eg4_6000xp" in DRIVERS
1010
assert "luxpower_sna" in DRIVERS
1111
assert "eg4_18kpv" in DRIVERS
12+
assert "eg4_12kpv" in DRIVERS
13+
assert "eg4_12000xp" in DRIVERS
14+
assert "eg4_6500ex" in DRIVERS
15+
assert "eg4_3000ehv" in DRIVERS
16+
17+
18+
def test_sna_aliases_share_register_family():
19+
# 12000XP / 6500EX / 3000EHV are SNA rebadges → same map as 6000XP
20+
base = get_driver("eg4_6000xp")
21+
for model in ("eg4_12000xp", "eg4_6500ex", "eg4_3000ehv"):
22+
d = get_driver(model)
23+
assert d.input_registers is base.input_registers
24+
assert d.batches == base.batches
25+
26+
27+
def test_12kpv_is_18kpv_family():
28+
kpv = get_driver("eg4_18kpv")
29+
pv = get_driver("eg4_12kpv")
30+
assert pv.input_registers is kpv.input_registers
31+
assert pv.batches == kpv.batches
32+
assert pv.label == "EG4 12kPV"
1233

1334

1435
def test_unsupported_model_raises():

0 commit comments

Comments
 (0)