CEZ_rele_box/custom_components/xt211_han/sensor.py

159 lines
4.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

"""Sensor platform for XT211 HAN integration."""
from __future__ import annotations
import logging
from typing import Any
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_NAME,
UnitOfEnergy,
UnitOfPower,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import XT211Coordinator
from .dlms_parser import OBIS_DESCRIPTIONS
_LOGGER = logging.getLogger(__name__)
# Map OBIS "class" strings → HA SensorDeviceClass + StateClass + unit
SENSOR_META: dict[str, dict] = {
"power": {
"device_class": SensorDeviceClass.POWER,
"state_class": SensorStateClass.MEASUREMENT,
"unit": UnitOfPower.WATT,
},
"energy": {
"device_class": SensorDeviceClass.ENERGY,
"state_class": SensorStateClass.TOTAL_INCREASING,
"unit": UnitOfEnergy.WATT_HOUR,
},
"sensor": {
"device_class": None,
"state_class": SensorStateClass.MEASUREMENT,
"unit": None,
},
}
# OBIS codes that are NOT numeric sensors (text / binary) handled separately
NON_SENSOR_CLASSES = {"text", "binary"}
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up XT211 HAN sensors from a config entry."""
coordinator: XT211Coordinator = hass.data[DOMAIN][entry.entry_id]
# We create entities for all known OBIS codes upfront.
# Unknown codes that arrive later will be added dynamically.
entities: list[XT211SensorEntity] = []
registered_obis: set[str] = set()
for obis, meta in OBIS_DESCRIPTIONS.items():
if meta.get("class") in NON_SENSOR_CLASSES:
continue
entities.append(
XT211SensorEntity(coordinator, entry, obis, meta)
)
registered_obis.add(obis)
async_add_entities(entities)
@callback
def _handle_new_obis(obis: str, data: dict) -> None:
"""Dynamically add sensor for a previously unknown OBIS code."""
if obis in registered_obis:
return
if data.get("class") in NON_SENSOR_CLASSES:
return
registered_obis.add(obis)
async_add_entities([XT211SensorEntity(coordinator, entry, obis, data)])
# Subscribe to coordinator updates to detect new OBIS codes
@callback
def _on_update() -> None:
if coordinator.data:
for obis, data in coordinator.data.items():
_handle_new_obis(obis, data)
coordinator.async_add_listener(_on_update)
class XT211SensorEntity(CoordinatorEntity[XT211Coordinator], SensorEntity):
"""A single numeric sensor entity backed by an OBIS code."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: XT211Coordinator,
entry: ConfigEntry,
obis: str,
meta: dict,
) -> None:
super().__init__(coordinator)
self._obis = obis
self._meta = meta
self._entry = entry
sensor_type = meta.get("class", "sensor")
sm = SENSOR_META.get(sensor_type, SENSOR_META["sensor"])
self._attr_unique_id = f"{entry.entry_id}_{obis}"
self._attr_name = meta.get("name", obis)
self._attr_device_class = sm["device_class"]
self._attr_state_class = sm["state_class"]
self._attr_native_unit_of_measurement = sm["unit"] or meta.get("unit")
# Energy sensors: convert Wh → kWh for HA Energy dashboard
if sensor_type == "energy":
self._attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
self._wh_to_kwh = True
else:
self._wh_to_kwh = False
@property
def device_info(self) -> DeviceInfo:
return DeviceInfo(
identifiers={(DOMAIN, self._entry.entry_id)},
name=self._entry.data.get(CONF_NAME, "XT211 HAN"),
manufacturer="Sagemcom",
model="XT211 AMM",
)
@property
def native_value(self) -> float | None:
if self.coordinator.data is None:
return None
obj = self.coordinator.data.get(self._obis)
if obj is None:
return None
raw = obj.get("value")
if raw is None:
return None
try:
val = float(raw)
if self._wh_to_kwh:
val = val / 1000.0
return round(val, 3)
except (TypeError, ValueError):
return None
@property
def available(self) -> bool:
return self.coordinator.connected and self.coordinator.data is not None