"""Support for SLZB-06 sensors.""" from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass from datetime import datetime, timedelta from itertools import chain from pysmlight import Info, Sensors from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorEntityDescription, SensorStateClass, ) from homeassistant.const import EntityCategory, UnitOfInformation, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.util.dt import utcnow from . import SmConfigEntry from .const import UPTIME_DEVIATION from .coordinator import SmDataUpdateCoordinator from .entity import SmEntity @dataclass(frozen=True, kw_only=True) class SmSensorEntityDescription(SensorEntityDescription): """Class describing SMLIGHT sensor entities.""" value_fn: Callable[[Sensors], float | None] @dataclass(frozen=True, kw_only=True) class SmInfoEntityDescription(SensorEntityDescription): """Class describing SMLIGHT information entities.""" value_fn: Callable[[Info], StateType] INFO: list[SmInfoEntityDescription] = [ SmInfoEntityDescription( key="device_mode", translation_key="device_mode", device_class=SensorDeviceClass.ENUM, options=["eth", "wifi", "usb"], value_fn=lambda x: x.coord_mode, ), SmInfoEntityDescription( key="firmware_channel", translation_key="firmware_channel", device_class=SensorDeviceClass.ENUM, options=["dev", "release"], value_fn=lambda x: x.fw_channel, ), SmInfoEntityDescription( key="zigbee_type", translation_key="zigbee_type", device_class=SensorDeviceClass.ENUM, options=["coordinator", "router", "thread"], value_fn=lambda x: x.zb_type, ), ] SENSORS: list[SmSensorEntityDescription] = [ SmSensorEntityDescription( key="core_temperature", translation_key="core_temperature", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, suggested_display_precision=1, value_fn=lambda x: x.esp32_temp, ), SmSensorEntityDescription( key="zigbee_temperature", translation_key="zigbee_temperature", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, suggested_display_precision=1, value_fn=lambda x: x.zb_temp, ), SmSensorEntityDescription( key="ram_usage", translation_key="ram_usage", device_class=SensorDeviceClass.DATA_SIZE, native_unit_of_measurement=UnitOfInformation.KILOBYTES, entity_registry_enabled_default=False, value_fn=lambda x: x.ram_usage, ), SmSensorEntityDescription( key="fs_usage", translation_key="fs_usage", device_class=SensorDeviceClass.DATA_SIZE, native_unit_of_measurement=UnitOfInformation.KILOBYTES, entity_registry_enabled_default=False, value_fn=lambda x: x.fs_used, ), ] UPTIME: list[SmSensorEntityDescription] = [ SmSensorEntityDescription( key="core_uptime", translation_key="core_uptime", device_class=SensorDeviceClass.TIMESTAMP, entity_registry_enabled_default=False, value_fn=lambda x: x.uptime, ), SmSensorEntityDescription( key="socket_uptime", translation_key="socket_uptime", device_class=SensorDeviceClass.TIMESTAMP, entity_registry_enabled_default=False, value_fn=lambda x: x.socket_uptime, ), ] async def async_setup_entry( hass: HomeAssistant, entry: SmConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up SMLIGHT sensor based on a config entry.""" coordinator = entry.runtime_data.data async_add_entities( chain( (SmInfoSensorEntity(coordinator, description) for description in INFO), (SmSensorEntity(coordinator, description) for description in SENSORS), (SmUptimeSensorEntity(coordinator, description) for description in UPTIME), ) ) class SmSensorEntity(SmEntity, SensorEntity): """Representation of a slzb sensor.""" coordinator: SmDataUpdateCoordinator entity_description: SmSensorEntityDescription _attr_entity_category = EntityCategory.DIAGNOSTIC def __init__( self, coordinator: SmDataUpdateCoordinator, description: SmSensorEntityDescription, ) -> None: """Initiate slzb sensor.""" super().__init__(coordinator) self.entity_description = description self._attr_unique_id = f"{coordinator.unique_id}_{description.key}" @property def native_value(self) -> datetime | str | float | None: """Return the sensor value.""" return self.entity_description.value_fn(self.coordinator.data.sensors) class SmInfoSensorEntity(SmEntity, SensorEntity): """Representation of a slzb info sensor.""" coordinator: SmDataUpdateCoordinator entity_description: SmInfoEntityDescription _attr_entity_category = EntityCategory.DIAGNOSTIC def __init__( self, coordinator: SmDataUpdateCoordinator, description: SmInfoEntityDescription, ) -> None: """Initiate slzb sensor.""" super().__init__(coordinator) self.entity_description = description self._attr_unique_id = f"{coordinator.unique_id}_{description.key}" @property def native_value(self) -> StateType: """Return the sensor value.""" value = self.entity_description.value_fn(self.coordinator.data.info) options = self.entity_description.options if isinstance(value, int) and options is not None: value = options[value] if 0 <= value < len(options) else None return value class SmUptimeSensorEntity(SmSensorEntity): """Representation of a slzb uptime sensor.""" def __init__( self, coordinator: SmDataUpdateCoordinator, description: SmSensorEntityDescription, ) -> None: "Initialize uptime sensor instance." super().__init__(coordinator, description) self._last_uptime: datetime | None = None def get_uptime(self, uptime: float | None) -> datetime | None: """Return device uptime or zigbee socket uptime. Converts uptime from seconds to a datetime value, allow up to 5 seconds deviation. This avoids unnecessary updates to sensor state, that may be caused by clock jitter. """ if uptime is None: # reset to unknown state self._last_uptime = None return None new_uptime = utcnow() - timedelta(seconds=uptime) if ( not self._last_uptime or abs(new_uptime - self._last_uptime) > UPTIME_DEVIATION ): self._last_uptime = new_uptime return self._last_uptime @property def native_value(self) -> datetime | None: """Return the sensor value.""" value = self.entity_description.value_fn(self.coordinator.data.sensors) return self.get_uptime(value)