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:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user