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
+19 -33
View File
@@ -12,19 +12,19 @@ from homeassistant.const import (
PERCENTAGE,
UnitOfElectricCurrent,
UnitOfElectricPotential,
UnitOfEnergy,
UnitOfPower,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import CONF_ADDRESS, DOMAIN
from .const import DOMAIN
from .coordinator import BmsCoordinator
# ---------------------------------------------------------------------------
# Static sensor definitions — one entity per key in coordinator.data
# Static sensor definitions — one entity per scalar key in coordinator.data
# ---------------------------------------------------------------------------
STATIC_SENSORS: tuple[SensorEntityDescription, ...] = (
@@ -52,6 +52,15 @@ STATIC_SENSORS: tuple[SensorEntityDescription, ...] = (
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
),
SensorEntityDescription(
key="energy_stored",
name="Energy Stored",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY_STORAGE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=3,
icon="mdi:battery-charging",
),
SensorEntityDescription(
key="state_of_charge",
name="State of Charge",
@@ -103,10 +112,9 @@ async def async_setup_entry(
) -> None:
"""Create all BMS sensor entities after the first successful poll."""
coordinator: BmsCoordinator = hass.data[DOMAIN][entry.entry_id]
address: str = entry.data[CONF_ADDRESS]
entities: list[SensorEntity] = [
BmsStaticSensor(coordinator, description, address)
BmsStaticSensor(coordinator, description)
for description in STATIC_SENSORS
]
@@ -115,14 +123,12 @@ async def async_setup_entry(
entities.append(
BmsDynamicSensor(
coordinator=coordinator,
address=address,
unique_key=f"temperature_{i + 1}",
friendly_name=f"Temperature {i + 1}",
data_key="temperatures",
index=i,
unit=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
icon=None,
)
)
@@ -131,33 +137,18 @@ async def async_setup_entry(
entities.append(
BmsDynamicSensor(
coordinator=coordinator,
address=address,
unique_key=f"cell_voltage_{i + 1}",
friendly_name=f"Cell {i + 1} Voltage",
data_key="cell_voltages",
index=i,
unit=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
icon=None,
)
)
async_add_entities(entities)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _device_info(address: str) -> DeviceInfo:
return DeviceInfo(
identifiers={(DOMAIN, address)},
name="Xiaoxiang Smart BMS",
manufacturer="Xiaoxiang",
model="Smart BMS",
)
# ---------------------------------------------------------------------------
# Entity classes
# ---------------------------------------------------------------------------
@@ -171,12 +162,11 @@ class BmsStaticSensor(CoordinatorEntity[BmsCoordinator], SensorEntity):
self,
coordinator: BmsCoordinator,
description: SensorEntityDescription,
address: str,
) -> None:
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{address}_{description.key}"
self._attr_device_info = _device_info(address)
self._attr_unique_id = f"{coordinator.address}_{description.key}"
self._attr_device_info = coordinator.device_info
@property
def native_value(self):
@@ -184,9 +174,9 @@ class BmsStaticSensor(CoordinatorEntity[BmsCoordinator], SensorEntity):
class BmsDynamicSensor(CoordinatorEntity[BmsCoordinator], SensorEntity):
"""A sensor that reads an element from a list inside coordinator.data.
"""A sensor that reads an indexed element from a list inside coordinator.data.
Used for per-cell voltages and per-probe temperatures, whose count is
Used for per-cell voltages and per-probe temperatures whose count is
only known after the first successful BMS poll.
"""
@@ -196,25 +186,21 @@ class BmsDynamicSensor(CoordinatorEntity[BmsCoordinator], SensorEntity):
def __init__(
self,
coordinator: BmsCoordinator,
address: str,
unique_key: str,
friendly_name: str,
data_key: str,
index: int,
unit: str,
device_class: SensorDeviceClass | None,
icon: str | None,
) -> None:
super().__init__(coordinator)
self._data_key = data_key
self._index = index
self._attr_unique_id = f"{address}_{unique_key}"
self._attr_unique_id = f"{coordinator.address}_{unique_key}"
self._attr_name = friendly_name
self._attr_native_unit_of_measurement = unit
self._attr_device_class = device_class
self._attr_device_info = _device_info(address)
if icon:
self._attr_icon = icon
self._attr_device_info = coordinator.device_info
if device_class == SensorDeviceClass.VOLTAGE:
self._attr_suggested_display_precision = 3
elif device_class == SensorDeviceClass.TEMPERATURE: