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
+28 -4
View File
@@ -6,10 +6,11 @@ from datetime import timedelta
from homeassistant.components.bluetooth import async_ble_device_from_address
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .bluetooth_handler import BmsBluetoothHandler
from .const import CMD_CELL, CMD_GENERAL, DOMAIN
from .const import CMD_CELL, CMD_GENERAL, CMD_VERSION, DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -31,6 +32,20 @@ class BmsCoordinator(DataUpdateCoordinator[dict]):
)
self.address = address
self._handler = BmsBluetoothHandler(address)
self.hw_version: str | None = None # populated on first successful poll
# ------------------------------------------------------------------
# Device info — centralised so sensor + binary_sensor share it
# ------------------------------------------------------------------
@property
def device_info(self) -> DeviceInfo:
return DeviceInfo(
identifiers={(DOMAIN, self.address)},
name="Xiaoxiang Smart BMS",
manufacturer="Xiaoxiang",
model=self.hw_version or "Smart BMS",
)
# ------------------------------------------------------------------
# Lifecycle
@@ -39,8 +54,7 @@ class BmsCoordinator(DataUpdateCoordinator[dict]):
def _get_ble_device(self):
"""Resolve the best available BLE device via HA's Bluetooth subsystem.
This automatically uses ESPHome BLE proxies if they can reach the BMS
and the local adapter cannot (or vice versa).
Automatically uses ESPHome BLE proxies if they can reach the BMS.
"""
device = async_ble_device_from_address(self.hass, self.address, connectable=True)
if device is None:
@@ -79,6 +93,14 @@ class BmsCoordinator(DataUpdateCoordinator[dict]):
cell_frame = await self._handler.request(CMD_CELL)
if cell_frame is None:
raise UpdateFailed("No response to cell info request (0x04)")
# Fetch hardware version string once — used in DeviceInfo model field
if self.hw_version is None:
version_frame = await self._handler.request(CMD_VERSION)
if version_frame:
self.hw_version = BmsBluetoothHandler.parse_version(version_frame)
_LOGGER.debug("BMS hardware version: %s", self.hw_version)
except UpdateFailed:
raise
except Exception as exc:
@@ -89,6 +111,7 @@ class BmsCoordinator(DataUpdateCoordinator[dict]):
# Derived fields
data["power"] = round(data["voltage"] * data["current"], 2)
data["energy_stored"] = round(data["voltage"] * data["residual_capacity"] / 1000, 3)
if data["cell_voltages"]:
v_max = max(data["cell_voltages"])
@@ -98,11 +121,12 @@ class BmsCoordinator(DataUpdateCoordinator[dict]):
data["cell_delta"] = None
_LOGGER.debug(
"BMS data: %.2fV %.2fA %d%% %.2fAh %d cells",
"BMS data: %.2fV %.2fA %d%% %.2fAh %.3fkWh %d cells",
data["voltage"],
data["current"],
data["state_of_charge"],
data["residual_capacity"],
data["energy_stored"],
len(data["cell_voltages"]),
)
return data