Full protocol coverage: binary sensors, energy, hardware version

Sensors added:
- energy_stored (kWh = V × Ah / 1000) for energy dashboard

Binary sensors added (all from existing 0x03 frame, no extra BLE requests):
- Charge MOSFET / Discharge MOSFET (MOS gate status)
- Cell Balancing (any balance bit active)
- 13× protection flags: cell/pack over/under-voltage, charge/discharge
  over/under-temperature, charge/discharge over-current, short circuit,
  frontend IC error, software lock

Other:
- Hardware version string fetched once via CMD 0x05, shown in device card
- DeviceInfo centralised on coordinator (sensor + binary_sensor share it)
- CONF_ADDRESS removed from sensor.py (coordinator holds address)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-11 19:52:10 +02:00
parent e9132c33a7
commit 5d527168e2
6 changed files with 240 additions and 38 deletions
@@ -172,6 +172,10 @@ class BmsBluetoothHandler:
raw = struct.unpack_from(">H", p, 23 + i * 2)[0]
temperatures.append(round((raw - 2731) / 10.0, 1))
balance = struct.unpack_from(">H", p, 12)[0] | struct.unpack_from(">H", p, 14)[0]
prot = struct.unpack_from(">H", p, 16)[0]
mos = p[20]
return {
"voltage": round(struct.unpack_from(">H", p, 0)[0] / 100.0, 2),
"current": round(struct.unpack_from(">h", p, 2)[0] / 100.0, 2),
@@ -181,8 +185,33 @@ class BmsBluetoothHandler:
"state_of_charge": p[19],
"cell_count": p[21],
"temperatures": temperatures,
# MOS status
"mos_charge_enabled": bool(mos & 0x01),
"mos_discharge_enabled": bool(mos & 0x02),
# Cell balancing (any cell currently balancing)
"balance_active": balance != 0,
# Protection flags (bit per event, True = protection triggered)
"prot_cell_overvolt": bool(prot & (1 << 0)),
"prot_cell_undervolt": bool(prot & (1 << 1)),
"prot_pack_overvolt": bool(prot & (1 << 2)),
"prot_pack_undervolt": bool(prot & (1 << 3)),
"prot_charge_overtemp": bool(prot & (1 << 4)),
"prot_charge_undertemp": bool(prot & (1 << 5)),
"prot_discharge_overtemp": bool(prot & (1 << 6)),
"prot_discharge_undertemp": bool(prot & (1 << 7)),
"prot_charge_overcurrent": bool(prot & (1 << 8)),
"prot_discharge_overcurrent": bool(prot & (1 << 9)),
"prot_short_circuit": bool(prot & (1 << 10)),
"prot_frontend_ic_error": bool(prot & (1 << 11)),
"prot_software_lock": bool(prot & (1 << 12)),
}
@staticmethod
def parse_version(frame: bytes) -> str:
"""Parse a 0x05 hardware version response frame into an ASCII string."""
p = frame[_HEADER_LEN:-_TRAILER_LEN]
return p.decode("ascii", errors="replace").strip("\x00").strip()
@staticmethod
def parse_cell_info(frame: bytes) -> dict:
"""Parse a 0x04 cell voltage response frame.