From 231e03081dff5e9c81c00f9f1e1f1c2b26390549 Mon Sep 17 00:00:00 2001 From: noise Date: Sat, 16 May 2026 07:33:22 -0400 Subject: [PATCH] fixed battery SoC and various other regs --- eg4battery/bin/eg4-battery | 68 ++++++++++++++++++++++++-------------- 1 file changed, 43 insertions(+), 25 deletions(-) diff --git a/eg4battery/bin/eg4-battery b/eg4battery/bin/eg4-battery index dfbbd67..b1078a9 100755 --- a/eg4battery/bin/eg4-battery +++ b/eg4battery/bin/eg4-battery @@ -338,23 +338,28 @@ def decode_eg4_modbus_regs(regs: list[int], expose_raw: bool = False) -> dict[st out["cell_lowest"] = cells_v.index(vmin) + 1 out["cell_highest"] = cells_v.index(vmax) + 1 - # --- temperatures (regs 18-21 = Temp_01..04, reg 24 = Temp_PCB) --- + # --- temperatures (regs 18-20 = Temp_01..03) --- + # Regs 21 and 24 were previously labeled temperature_04 / temperature_pcb + # (carryover from a misread of the lv_host.app schema) but are actually + # the BMS's SoC tracker — see the SoC block below. The pack only exposes + # three real cell temps. out["temperature_01"] = regs[18] out["temperature_02"] = regs[19] out["temperature_03"] = regs[20] - out["temperature_04"] = regs[21] - out["temperature_pcb"] = regs[24] - # --- SoC / SoH (regs 22, 23) --- - out["soc"] = regs[22] - out["soh"] = regs[23] - - # --- heater / status (regs 25-30) --- - # reg 30 has been observed = 1 on a healthy pack; treat as binary - out["heater"] = "on" if regs[30] & 0x01 else "off" - - # --- max charge/discharge current limit (reg 31), A --- - out["max_current_limit"] = round(regs[31] / 100.0, 2) + # --- SoC (regs 21 / 24) --- + # Identified 2026-05-16 by comparing a week of register snapshots against + # the bank's known SoC drift (95% → ~40%): only regs 21 and 24 moved in + # lockstep with the physical state change. regs 22/23 are pegged at 100 + # across all observed conditions — they are emitted below as raw fields + # for visibility but do NOT name them soc/soh until we know what they are. + # r24 stays within ±1 of r21 (probably a re-snapshotted copy from later + # in the BMS poll loop); kept as `soc_alt` (diagnostic) for now so we + # can watch whether the relationship holds. + out["soc"] = regs[21] + out["soc_alt"] = regs[24] + out["bms_field_22"] = regs[22] # was misnamed `soc` — purpose unknown + out["bms_field_23"] = regs[23] # was misnamed `soh` — purpose unknown # --- error / alarm word (reg 35) --- # reg 33/34 used to decode as warning_/protection_ bitfields but were @@ -362,12 +367,28 @@ def decode_eg4_modbus_regs(regs: list[int], expose_raw: bool = False) -> dict[st # reg 35 is the canonical alarm word; 0 on healthy packs. out["error_code"] = regs[35] - # --- static-ish (regs 36, 37) --- + # --- static-ish (regs 36, 37, 38) --- out["cell_count"] = regs[36] out["capacity_ah"] = round(regs[37] / 10.0, 1) out["remaining_ah"] = round(regs[38] / 100.0, 2) - out["cycle_count"] = regs[39] - out["battery_mode"] = regs[40] + # Several other regs in this block were previously decoded with names that + # turned out not to match reality. Removed 2026-05-16, pending re-derivation: + # reg 30 → was "heater" via bit-0 test; bit 0 is always 0 across all + # 6 packs in all observed states, so the heater claim is + # unprovable. The reg itself has values 6 / 38 / 42 that + # vary by pack batch (older packs ~38-42, newer ~6) and + # look like a status-flag bitfield, not heater on/off. + # reg 31 → was "max_current_limit" in A; value 5493 → 54.93 sits + # squarely in the float-voltage range (54-55 V) and is + # identical across all packs regardless of charge state. + # Almost certainly V*100 of a config threshold, not amps. + # reg 39 → was "cycle_count"; pegged at 0 across all packs after + # weeks of cycling. Not the cycle counter. + # reg 40 → was "battery_mode"; pegged at 7 across all packs in all + # states. Possibly a battery-type code or fixed mode flag, + # but value never changes so naming it "mode" was misleading. + # All four entities + their HA discovery configs were retracted at deploy. + # Use --expose-raw / register_NN entities to investigate further. # regs 41-42: u16 version codes — superseded by `firmware_version` ASCII # decode below; available via expose_raw if needed @@ -703,14 +724,12 @@ _FIELD_META.update({ "temperature_01": ("°C", "temperature", "measurement", "mdi:thermometer"), "temperature_02": ("°C", "temperature", "measurement", "mdi:thermometer"), "temperature_03": ("°C", "temperature", "measurement", "mdi:thermometer"), - "temperature_04": ("°C", "temperature", "measurement", "mdi:thermometer"), - "temperature_pcb": ("°C", "temperature", "measurement", "mdi:chip"), - "heater": (None, None, None, "mdi:heating-coil"), - "max_current_limit": ("A", "current", "measurement", "mdi:current-dc"), + "soc_alt": ("%", "battery", "measurement", "mdi:battery-50"), + "bms_field_22": (None, None, "measurement", "mdi:numeric"), + "bms_field_23": (None, None, "measurement", "mdi:numeric"), "error_code": (None, None, None, "mdi:alert-octagon"), "cell_count": (None, None, "measurement", "mdi:numeric"), "remaining_ah": ("Ah", None, "measurement", "mdi:battery-clock"), - "battery_mode": (None, None, None, "mdi:state-machine"), "model": (None, None, None, "mdi:battery-outline"), "firmware_version": (None, None, None, "mdi:chip"), "firmware_date": (None, None, None, "mdi:calendar"), @@ -726,9 +745,10 @@ def field_meta(key: str) -> tuple[str | None, str | None, str | None, str | None _FIELD_PRECISION: dict[str, int] = { "pack_voltage": 2, "pack_current": 2, - "max_current_limit": 2, "soc": 0, - "soh": 0, + "soc_alt": 0, + "bms_field_22": 0, + "bms_field_23": 0, "cycle_count": 0, "cell_voltage_min": 3, "cell_voltage_max": 3, @@ -739,13 +759,11 @@ _FIELD_PRECISION: dict[str, int] = { "capacity_ah": 1, "remaining_ah": 1, "error_code": 0, - "battery_mode": 0, } for _i in range(1, 17): _FIELD_PRECISION[f"cell_{_i:02d}_voltage"] = 3 for _i in range(1, 7): _FIELD_PRECISION[f"temperature_{_i}"] = 0 -_FIELD_PRECISION["temperature_pcb"] = 0 def field_precision(key: str) -> int | None: