Skip to content

Commit 694aec8

Browse files
committed
feat(inverter): expand input register map with split-phase, PV4-6, AC couple, smart load, serial, NTC temps
- Add shared registers to SNA map: serial (115-118), load power (114, 170-173), reactive power (139), NTC temps (214-216), remaining charge time (210) - Add 18KPV-family registers: EPS L1/L2 total (135-138), AC couple (153, 206-207), split-phase L1/L2 (193-204), PV4-6 strings (217-231), smart load (232) - Extend 18KPV polling batches to cover 0-232 - Add tests for new registers + 32-bit pair decode Source: docs/reference/luxpower-ha-register-map.md (LuxPower/EG4 shared protocol)
1 parent c5a0f02 commit 694aec8

3 files changed

Lines changed: 105 additions & 3 deletions

File tree

‎collector/drivers/eg4_18kpv.py‎

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,50 @@
4444
150: RegisterDef("afci_max_arc_ch2", "", 1.0, "AFCI max arc CH2"),
4545
151: RegisterDef("afci_max_arc_ch3", "", 1.0, "AFCI max arc CH3"),
4646
152: RegisterDef("afci_max_arc_ch4", "", 1.0, "AFCI max arc CH4"),
47+
48+
# ── Split-phase / hybrid additions (document-derived) ──
49+
# EPS L1/L2 total energy (32-bit pairs)
50+
135: RegisterDef("eps_energy_l1_total", "kWh", 0.1, "EPS L1 total energy (low word)", pair_high=136),
51+
137: RegisterDef("eps_energy_l2_total", "kWh", 0.1, "EPS L2 total energy (low word)", pair_high=138),
52+
# AC couple power
53+
153: RegisterDef("ac_couple_power", "W", 1.0, "AC couple power"),
54+
206: RegisterDef("ac_couple_power_s", "W", 1.0, "AC couple power S-phase"),
55+
207: RegisterDef("ac_couple_power_t", "W", 1.0, "AC couple power T-phase"),
56+
# Split-phase L1/L2 per-leg
57+
193: RegisterDef("grid_voltage_l1n", "V", 0.1, "Grid L1-N voltage"),
58+
194: RegisterDef("grid_voltage_l2n", "V", 0.1, "Grid L2-N voltage"),
59+
195: RegisterDef("gen_voltage_l1n", "V", 0.1, "Generator L1-N voltage"),
60+
196: RegisterDef("gen_voltage_l2n", "V", 0.1, "Generator L2-N voltage"),
61+
197: RegisterDef("inv_power_l1n", "W", 1.0, "Inverting power L1-N"),
62+
198: RegisterDef("inv_power_l2n", "W", 1.0, "Inverting power L2-N"),
63+
199: RegisterDef("rec_power_l1n", "W", 1.0, "Rectifying power L1-N"),
64+
200: RegisterDef("rec_power_l2n", "W", 1.0, "Rectifying power L2-N"),
65+
201: RegisterDef("grid_export_l1n", "W", 1.0, "Grid export L1-N"),
66+
202: RegisterDef("grid_export_l2n", "W", 1.0, "Grid export L2-N"),
67+
203: RegisterDef("grid_import_l1n", "W", 1.0, "Grid import L1-N"),
68+
204: RegisterDef("grid_import_l2n", "W", 1.0, "Grid import L2-N"),
69+
# Additional PV strings (PV4-6)
70+
217: RegisterDef("pv4_voltage", "V", 0.1, "PV4 voltage"),
71+
218: RegisterDef("pv5_voltage", "V", 0.1, "PV5 voltage"),
72+
219: RegisterDef("pv6_voltage", "V", 0.1, "PV6 voltage"),
73+
220: RegisterDef("pv4_power", "W", 1.0, "PV4 power"),
74+
221: RegisterDef("pv5_power", "W", 1.0, "PV5 power"),
75+
222: RegisterDef("pv6_power", "W", 1.0, "PV6 power"),
76+
223: RegisterDef("pv4_energy_today", "kWh", 0.1, "PV4 energy today"),
77+
224: RegisterDef("pv4_energy_total", "kWh", 0.1, "PV4 total energy (low word)", pair_high=225),
78+
226: RegisterDef("pv5_energy_today", "kWh", 0.1, "PV5 energy today"),
79+
227: RegisterDef("pv5_energy_total", "kWh", 0.1, "PV5 total energy (low word)", pair_high=228),
80+
229: RegisterDef("pv6_energy_today", "kWh", 0.1, "PV6 energy today"),
81+
230: RegisterDef("pv6_energy_total", "kWh", 0.1, "PV6 total energy (low word)", pair_high=231),
82+
# Smart load power
83+
232: RegisterDef("smart_load_power", "W", 1.0, "Smart load power"),
4784
}
4885

4986
INPUT_REGISTERS.update(_18KPV_EXTRA)
5087

51-
# Active TCP batches. The 18KPV has 153+ input registers; poll in 40-register
52-
# groups (the protocol requires group-aligned reads).
53-
BATCHES = [(0, 40), (40, 40), (80, 40), (120, 40)]
88+
# Active TCP batches. The 18KPV has 233+ input registers; poll in 40-register
89+
# groups (the protocol requires group-aligned reads). Covers 0-232.
90+
BATCHES = [(0, 40), (40, 40), (80, 40), (120, 40), (160, 40), (200, 40)]
5491

5592

5693
def _decode(raw: Dict[int, int]) -> dict:

‎collector/registers.py‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,27 @@ def decode(self, raw: int, high_raw: int = 0) -> float:
162162
132:RegisterDef("eps_apparent_l2", "VA", 1.0, "EPS L2 apparent power"),
163163
133:RegisterDef("eps_energy_l1_today","kWh", 0.1, "EPS L1 energy today"),
164164
134:RegisterDef("eps_energy_l2_today","kWh", 0.1, "EPS L2 energy today"),
165+
166+
# ── Load power / reactive / serial (114, 115-118, 139) ──
167+
# NOTE: register 114 is on-grid load power on the 12k; register 170 is the
168+
# general on-grid load power. Both are documented; 170 is the canonical one.
169+
114:RegisterDef("load_power_12k", "W", 1.0, "On-grid load power (12k)"),
170+
115:RegisterDef("serial_ascii_0_3", "", 1.0, "Serial number ASCII chars 0-3"),
171+
116:RegisterDef("serial_ascii_4_5", "", 1.0, "Serial number ASCII chars 4-5"),
172+
117:RegisterDef("serial_ascii_6_7", "", 1.0, "Serial number ASCII chars 6-7"),
173+
118:RegisterDef("serial_ascii_8_9", "", 1.0, "Serial number ASCII chars 8-9"),
174+
139:RegisterDef("reactive_power", "Var", 1.0, "Reactive power"),
175+
176+
# ── Load power / energy (170-173) ──
177+
170:RegisterDef("load_power", "W", 1.0, "On-grid load power"),
178+
171:RegisterDef("load_energy_today", "kWh", 0.1, "Load energy today"),
179+
172:RegisterDef("load_energy_total", "kWh", 0.1, "Load total energy (low word)", pair_high=173),
180+
181+
# ── NTC temps / remaining charge time (210, 214-216) ──
182+
210:RegisterDef("remaining_charge_time", "min", 1.0, "Remaining charge time (one-click)"),
183+
214:RegisterDef("ntc_temp_indc", "°C", 1.0, "NTC temperature INDC"),
184+
215:RegisterDef("ntc_temp_dcdcl", "°C", 1.0, "NTC temperature DCDCL"),
185+
216:RegisterDef("ntc_temp_dcdch", "°C", 1.0, "NTC temperature DCDCH"),
165186
}
166187

167188
# SOH is packed in register 5 high byte

‎tests/test_drivers.py‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,47 @@ def test_18kpv_decodes_shared_sna_registers():
6868
dec = d.decode(raw)
6969
assert dec["pv1_voltage"]["value"] == 350.0
7070
assert dec["pv1_power"]["value"] == 1200.0
71+
72+
73+
def test_sna_has_shared_extended_registers():
74+
# Serial, load power, reactive, NTC temps are shared across the family.
75+
sna = get_driver("luxpower_sna")
76+
assert 115 in sna.input_registers # serial
77+
assert 170 in sna.input_registers # load power
78+
assert 139 in sna.input_registers # reactive power
79+
assert 214 in sna.input_registers # NTC temp
80+
81+
82+
def test_sna_does_not_claim_split_phase_or_three_phase():
83+
# 2-MPPT single-phase SNA hardware has no PV4-6, split-phase, or 3-phase regs.
84+
sna = get_driver("luxpower_sna")
85+
assert 217 not in sna.input_registers # PV4
86+
assert 193 not in sna.input_registers # split-phase L1-N
87+
assert 180 not in sna.input_registers # three-phase S
88+
89+
90+
def test_18kpv_has_split_phase_and_pv4_6():
91+
kpv = get_driver("eg4_18kpv")
92+
assert 217 in kpv.input_registers # PV4
93+
assert 193 in kpv.input_registers # split-phase L1-N
94+
assert 153 in kpv.input_registers # AC couple
95+
assert 232 in kpv.input_registers # smart load
96+
97+
98+
def test_18kpv_decodes_pv4_and_split_phase():
99+
d = get_driver("eg4_18kpv")
100+
raw = {217: 3500, 220: 1200, 193: 2400, 232: 500}
101+
dec = d.decode(raw)
102+
assert dec["pv4_voltage"]["value"] == 350.0
103+
assert dec["pv4_power"]["value"] == 1200.0
104+
assert dec["grid_voltage_l1n"]["value"] == 240.0
105+
assert dec["smart_load_power"]["value"] == 500.0
106+
107+
108+
def test_18kpv_decodes_32bit_pair():
109+
d = get_driver("eg4_18kpv")
110+
# PV4 total energy: low word 224, high word 225
111+
raw = {224: 0x1234, 225: 0x0001}
112+
dec = d.decode(raw)
113+
# 0x00011234 = 70196, scale 0.1 -> 7019.6 kWh
114+
assert dec["pv4_energy_total"]["value"] == 7019.6

0 commit comments

Comments
 (0)