Skip to content

Commit 2dfb456

Browse files
committed
fix: read-back verify automation + quick-charge writes; remove dead register map
1. Automation engine (_do_write) and quick charge (_set_enable_bit) now read the register back after writing and only report success if the value actually stuck on the inverter. Previously they trusted the dongle's write echo, which can ACK without forwarding — the same failure mode that caused the AC-first writes to silently not persist. Mirrors the read-back verification already in the API PUT handler. 2. Remove dead code in protocol.py: INPUT_REGISTERS / INPUT_REGISTERS_2 / INPUT_REGISTERS_3 / ALL_INPUT_REGISTERS / decode_register_values were never called (live decoding uses collector/registers.py via the model drivers) and their register map diverged from the authoritative one (reg 0 = 'v_pv_1' here vs 'state' in registers.py).
1 parent de425c1 commit 2dfb456

3 files changed

Lines changed: 40 additions & 118 deletions

File tree

‎collector/automation.py‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -965,9 +965,32 @@ def _do_write(
965965
ok, msg = _write_holding_register(
966966
dongle_host, dongle_port, datalog_serial, inverter_serial, reg_addr, raw
967967
)
968-
self._log(automation_id, automation_name, setting_name, raw, clamped, False, ok, msg)
969968
if not ok:
969+
self._log(automation_id, automation_name, setting_name, raw, clamped, False, False, msg)
970970
logger.warning("Automation %s write failed: %s", automation_id, msg)
971+
return
972+
973+
# VERIFY: the dongle can echo a write without actually forwarding it to
974+
# the inverter. Read the register back and confirm it stuck before
975+
# reporting success — the same read-back verification the API PUT
976+
# handler performs. This is the difference between "the dongle ACKed"
977+
# and "the inverter actually applied it".
978+
verified = False
979+
for _ in range(4):
980+
rok, rval, _ = _read_holding_register(
981+
dongle_host, dongle_port, datalog_serial, inverter_serial, reg_addr
982+
)
983+
if rok and rval == raw:
984+
verified = True
985+
break
986+
time.sleep(0.5)
987+
if not verified:
988+
self._log(automation_id, automation_name, setting_name, raw, clamped, False, False,
989+
f"Write to {reg_name} not confirmed on inverter (echoed but read-back mismatch)")
990+
logger.warning("Automation %s write to %s not confirmed on inverter", automation_id, reg_name)
991+
return
992+
993+
self._log(automation_id, automation_name, setting_name, raw, clamped, False, True, msg)
971994

972995
def _send_notification(self, auto: Automation) -> None:
973996
if self._notifiers is None:

‎collector/protocol.py‎

Lines changed: 0 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -871,120 +871,3 @@ def find_frames(data: bytes) -> list[LuxFrame]:
871871
return frames
872872

873873

874-
# ── Register Maps ───────────────────────────────────────────────────
875-
876-
# Input registers (read-only, live data) — from lxp-bridge doc/inputs.md
877-
# These are the register *offsets* within each batch
878-
INPUT_REGISTERS = {
879-
# Batch 1: registers 0-39
880-
0: {"name": "v_pv_1", "unit": "V", "scale": 0.1, "desc": "PV1 voltage"},
881-
1: {"name": "v_pv_2", "unit": "V", "scale": 0.1, "desc": "PV2 voltage"},
882-
2: {"name": "v_pv_3", "unit": "V", "scale": 0.1, "desc": "PV3 voltage"},
883-
3: {"name": "v_bat", "unit": "V", "scale": 0.1, "desc": "Battery voltage"},
884-
4: {"name": "soc", "unit": "%", "scale": 1, "desc": "Battery state of charge"},
885-
5: {"name": "soh", "unit": "%", "scale": 1, "desc": "Battery state of health"},
886-
6: {"name": "p_pv_1", "unit": "W", "scale": 1, "desc": "PV1 power"},
887-
7: {"name": "p_pv_2", "unit": "W", "scale": 1, "desc": "PV2 power"},
888-
8: {"name": "p_pv_3", "unit": "W", "scale": 1, "desc": "PV3 power"},
889-
9: {"name": "p_pv", "unit": "W", "scale": 1, "desc": "Total PV power"},
890-
10: {"name": "p_charge", "unit": "W", "scale": 1, "desc": "Battery charge power"},
891-
11: {"name": "p_discharge", "unit": "W", "scale": 1, "desc": "Battery discharge power"},
892-
12: {"name": "v_ac_r", "unit": "V", "scale": 0.1, "desc": "Grid voltage R"},
893-
13: {"name": "v_ac_s", "unit": "V", "scale": 0.1, "desc": "Grid voltage S"},
894-
14: {"name": "v_ac_t", "unit": "V", "scale": 0.1, "desc": "Grid voltage T"},
895-
15: {"name": "f_ac", "unit": "Hz", "scale": 0.01, "desc": "Grid frequency"},
896-
16: {"name": "p_inv", "unit": "W", "scale": 1, "desc": "Inverter power"},
897-
17: {"name": "p_rec", "unit": "W", "scale": 1, "desc": "Rectifier power"},
898-
18: {"name": "pf", "unit": "", "scale": 0.001,"desc": "Power factor"},
899-
19: {"name": "v_eps_r", "unit": "V", "scale": 0.1, "desc": "EPS voltage R"},
900-
20: {"name": "v_eps_s", "unit": "V", "scale": 0.1, "desc": "EPS voltage S"},
901-
21: {"name": "v_eps_t", "unit": "V", "scale": 0.1, "desc": "EPS voltage T"},
902-
22: {"name": "f_eps", "unit": "Hz", "scale": 0.01, "desc": "EPS frequency"},
903-
23: {"name": "p_to_grid", "unit": "W", "scale": 1, "desc": "Power to grid"},
904-
24: {"name": "p_to_user", "unit": "W", "scale": 1, "desc": "Power to user/load"},
905-
25: {"name": "e_pv_day", "unit": "kWh", "scale": 0.1, "desc": "PV energy today"},
906-
26: {"name": "e_pv_day_1", "unit": "kWh", "scale": 0.1, "desc": "PV1 energy today"},
907-
27: {"name": "e_pv_day_2", "unit": "kWh", "scale": 0.1, "desc": "PV2 energy today"},
908-
28: {"name": "e_pv_day_3", "unit": "kWh", "scale": 0.1, "desc": "PV3 energy today"},
909-
29: {"name": "e_inv_day", "unit": "kWh", "scale": 0.1, "desc": "Inverter energy today"},
910-
30: {"name": "e_rec_day", "unit": "kWh", "scale": 0.1, "desc": "Rectifier energy today"},
911-
31: {"name": "e_chg_day", "unit": "kWh", "scale": 0.1, "desc": "Charge energy today"},
912-
32: {"name": "e_dischg_day", "unit": "kWh", "scale": 0.1, "desc": "Discharge energy today"},
913-
33: {"name": "e_eps_day", "unit": "kWh", "scale": 0.1, "desc": "EPS energy today"},
914-
34: {"name": "e_to_grid_day", "unit": "kWh", "scale": 0.1, "desc": "Grid export today"},
915-
35: {"name": "e_to_user_day", "unit": "kWh", "scale": 0.1, "desc": "Grid import today"},
916-
36: {"name": "v_bus_1", "unit": "V", "scale": 0.1, "desc": "Bus voltage 1"},
917-
37: {"name": "v_bus_2", "unit": "V", "scale": 0.1, "desc": "Bus voltage 2"},
918-
38: {"name": "status", "unit": "", "scale": 1, "desc": "Status bitfield"},
919-
39: {"name": "v_pv", "unit": "V", "scale": 0.1, "desc": "Total PV voltage"},
920-
}
921-
922-
# Batch 2: registers 40-79
923-
INPUT_REGISTERS_2 = {
924-
40: {"name": "e_pv_all", "unit": "kWh", "scale": 0.1, "desc": "PV energy all-time"},
925-
41: {"name": "e_pv_all_1", "unit": "kWh", "scale": 0.1, "desc": "PV1 energy all-time"},
926-
42: {"name": "e_pv_all_2", "unit": "kWh", "scale": 0.1, "desc": "PV2 energy all-time"},
927-
43: {"name": "e_pv_all_3", "unit": "kWh", "scale": 0.1, "desc": "PV3 energy all-time"},
928-
44: {"name": "e_inv_all", "unit": "kWh", "scale": 0.1, "desc": "Inverter energy all-time"},
929-
45: {"name": "e_rec_all", "unit": "kWh", "scale": 0.1, "desc": "Rectifier energy all-time"},
930-
46: {"name": "e_chg_all", "unit": "kWh", "scale": 0.1, "desc": "Charge energy all-time"},
931-
47: {"name": "e_dischg_all", "unit": "kWh", "scale": 0.1, "desc": "Discharge energy all-time"},
932-
48: {"name": "e_eps_all", "unit": "kWh", "scale": 0.1, "desc": "EPS energy all-time"},
933-
49: {"name": "e_to_grid_all", "unit": "kWh", "scale": 0.1, "desc": "Grid export all-time"},
934-
50: {"name": "e_to_user_all", "unit": "kWh", "scale": 0.1, "desc": "Grid import all-time"},
935-
51: {"name": "t_inner", "unit": "°C", "scale": 1, "desc": "Inverter temperature"},
936-
52: {"name": "t_rad_1", "unit": "°C", "scale": 1, "desc": "Radiator temp 1"},
937-
53: {"name": "t_rad_2", "unit": "°C", "scale": 1, "desc": "Radiator temp 2"},
938-
54: {"name": "t_bat", "unit": "°C", "scale": 1, "desc": "Battery temperature"},
939-
55: {"name": "runtime_lo", "unit": "s", "scale": 1, "desc": "Runtime (low word)"},
940-
56: {"name": "runtime_hi", "unit": "s", "scale": 1, "desc": "Runtime (high word)"},
941-
}
942-
943-
# Batch 3: registers 80-119
944-
INPUT_REGISTERS_3 = {
945-
80: {"name": "max_chg_curr", "unit": "A", "scale": 0.1, "desc": "Max charge current"},
946-
81: {"name": "max_dischg_curr","unit": "A", "scale": 0.1, "desc": "Max discharge current"},
947-
82: {"name": "charge_volt_ref","unit": "V", "scale": 0.1, "desc": "Charge voltage ref"},
948-
83: {"name": "dischg_cut_volt","unit": "V", "scale": 0.1, "desc": "Discharge cutoff voltage"},
949-
84: {"name": "bat_status_0", "unit": "", "scale": 1, "desc": "Battery status 0"},
950-
85: {"name": "bat_status_1", "unit": "", "scale": 1, "desc": "Battery status 1"},
951-
86: {"name": "bat_status_2", "unit": "", "scale": 1, "desc": "Battery status 2"},
952-
87: {"name": "bat_status_3", "unit": "", "scale": 1, "desc": "Battery status 3"},
953-
88: {"name": "bat_status_4", "unit": "", "scale": 1, "desc": "Battery status 4"},
954-
89: {"name": "bat_status_5", "unit": "", "scale": 1, "desc": "Battery status 5"},
955-
90: {"name": "bat_status_6", "unit": "", "scale": 1, "desc": "Battery status 6"},
956-
91: {"name": "bat_status_7", "unit": "", "scale": 1, "desc": "Battery status 7"},
957-
92: {"name": "bat_status_8", "unit": "", "scale": 1, "desc": "Battery status 8"},
958-
93: {"name": "bat_status_9", "unit": "", "scale": 1, "desc": "Battery status 9"},
959-
94: {"name": "bat_status_inv", "unit": "", "scale": 1, "desc": "Battery status inverter"},
960-
95: {"name": "bat_count", "unit": "", "scale": 1, "desc": "Battery count"},
961-
96: {"name": "bat_capacity", "unit": "Ah", "scale": 1, "desc": "Battery capacity"},
962-
}
963-
964-
# Combined map
965-
ALL_INPUT_REGISTERS = {}
966-
ALL_INPUT_REGISTERS.update(INPUT_REGISTERS)
967-
ALL_INPUT_REGISTERS.update(INPUT_REGISTERS_2)
968-
ALL_INPUT_REGISTERS.update(INPUT_REGISTERS_3)
969-
970-
971-
def decode_register_values(frame: LuxFrame) -> dict:
972-
"""
973-
Decode register values from a parsed frame into named fields with scaling applied.
974-
"""
975-
result = {}
976-
reg_map = ALL_INPUT_REGISTERS if frame.is_read_input else {}
977-
978-
for i, raw_val in enumerate(frame.values):
979-
reg_num = frame.register + i
980-
if reg_num in reg_map:
981-
info = reg_map[reg_num]
982-
scaled = raw_val * info["scale"]
983-
result[info["name"]] = {
984-
"value": scaled,
985-
"raw": raw_val,
986-
"unit": info["unit"],
987-
"desc": info["desc"],
988-
}
989-
990-
return result

‎collector/quick_charge.py‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,22 @@ def _set_enable_bit(
9191
)
9292
if not ok:
9393
return False, msg, None
94+
95+
# VERIFY: the dongle can echo a write without forwarding it to the
96+
# inverter. Read the register back and confirm bit 0 actually flipped
97+
# before reporting success.
98+
verified = False
99+
for _ in range(4):
100+
rok, rval, _ = _read_holding_register(
101+
host, port, datalog_serial, inverter_serial, QC_ENABLE_REG
102+
)
103+
if rok and rval == new:
104+
verified = True
105+
break
106+
time.sleep(0.5)
107+
if not verified:
108+
return False, f"Enable register write not confirmed on inverter (echoed but read-back mismatch)", None
109+
94110
return True, f"bit 0 {'set' if enable else 'cleared'} (was {cur}, now {new})", new
95111

96112

0 commit comments

Comments
 (0)