diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index 8eaeafcd3f4..80ac8133415 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -14,10 +14,11 @@ from homeassistant.const import STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity_registry import RegistryEntry from .const import CONF_SLEEP_PERIOD from .entity import ( - BlockAttributeDescription, + BlockEntityDescription, RestEntityDescription, RpcEntityDescription, ShellyBlockAttributeEntity, @@ -35,6 +36,13 @@ from .utils import ( ) +@dataclass +class BlockBinarySensorDescription( + BlockEntityDescription, BinarySensorEntityDescription +): + """Class to describe a BLOCK binary sensor.""" + + @dataclass class RpcBinarySensorDescription(RpcEntityDescription, BinarySensorEntityDescription): """Class to describe a RPC binary sensor.""" @@ -46,71 +54,83 @@ class RestBinarySensorDescription(RestEntityDescription, BinarySensorEntityDescr SENSORS: Final = { - ("device", "overtemp"): BlockAttributeDescription( + ("device", "overtemp"): BlockBinarySensorDescription( + key="device|overtemp", name="Overheating", device_class=BinarySensorDeviceClass.PROBLEM, entity_category=EntityCategory.DIAGNOSTIC, ), - ("device", "overpower"): BlockAttributeDescription( + ("device", "overpower"): BlockBinarySensorDescription( + key="device|overpower", name="Overpowering", device_class=BinarySensorDeviceClass.PROBLEM, entity_category=EntityCategory.DIAGNOSTIC, ), - ("light", "overpower"): BlockAttributeDescription( + ("light", "overpower"): BlockBinarySensorDescription( + key="light|overpower", name="Overpowering", device_class=BinarySensorDeviceClass.PROBLEM, entity_category=EntityCategory.DIAGNOSTIC, ), - ("relay", "overpower"): BlockAttributeDescription( + ("relay", "overpower"): BlockBinarySensorDescription( + key="relay|overpower", name="Overpowering", device_class=BinarySensorDeviceClass.PROBLEM, entity_category=EntityCategory.DIAGNOSTIC, ), - ("sensor", "dwIsOpened"): BlockAttributeDescription( + ("sensor", "dwIsOpened"): BlockBinarySensorDescription( + key="sensor|dwIsOpened", name="Door", device_class=BinarySensorDeviceClass.OPENING, available=lambda block: cast(int, block.dwIsOpened) != -1, ), - ("sensor", "flood"): BlockAttributeDescription( - name="Flood", device_class=BinarySensorDeviceClass.MOISTURE + ("sensor", "flood"): BlockBinarySensorDescription( + key="sensor|flood", name="Flood", device_class=BinarySensorDeviceClass.MOISTURE ), - ("sensor", "gas"): BlockAttributeDescription( + ("sensor", "gas"): BlockBinarySensorDescription( + key="sensor|gas", name="Gas", device_class=BinarySensorDeviceClass.GAS, value=lambda value: value in ["mild", "heavy"], extra_state_attributes=lambda block: {"detected": block.gas}, ), - ("sensor", "smoke"): BlockAttributeDescription( - name="Smoke", device_class=BinarySensorDeviceClass.SMOKE + ("sensor", "smoke"): BlockBinarySensorDescription( + key="sensor|smoke", name="Smoke", device_class=BinarySensorDeviceClass.SMOKE ), - ("sensor", "vibration"): BlockAttributeDescription( - name="Vibration", device_class=BinarySensorDeviceClass.VIBRATION + ("sensor", "vibration"): BlockBinarySensorDescription( + key="sensor|vibration", + name="Vibration", + device_class=BinarySensorDeviceClass.VIBRATION, ), - ("input", "input"): BlockAttributeDescription( + ("input", "input"): BlockBinarySensorDescription( + key="input|input", name="Input", device_class=BinarySensorDeviceClass.POWER, - default_enabled=False, + entity_registry_enabled_default=False, removal_condition=is_block_momentary_input, ), - ("relay", "input"): BlockAttributeDescription( + ("relay", "input"): BlockBinarySensorDescription( + key="relay|input", name="Input", device_class=BinarySensorDeviceClass.POWER, - default_enabled=False, + entity_registry_enabled_default=False, removal_condition=is_block_momentary_input, ), - ("device", "input"): BlockAttributeDescription( + ("device", "input"): BlockBinarySensorDescription( + key="device|input", name="Input", device_class=BinarySensorDeviceClass.POWER, - default_enabled=False, + entity_registry_enabled_default=False, removal_condition=is_block_momentary_input, ), - ("sensor", "extInput"): BlockAttributeDescription( + ("sensor", "extInput"): BlockBinarySensorDescription( + key="sensor|extInput", name="External Input", device_class=BinarySensorDeviceClass.POWER, - default_enabled=False, + entity_registry_enabled_default=False, ), - ("sensor", "motion"): BlockAttributeDescription( - name="Motion", device_class=BinarySensorDeviceClass.MOTION + ("sensor", "motion"): BlockBinarySensorDescription( + key="sensor|motion", name="Motion", device_class=BinarySensorDeviceClass.MOTION ), } @@ -171,6 +191,16 @@ RPC_SENSORS: Final = { } +def _build_block_description(entry: RegistryEntry) -> BlockBinarySensorDescription: + """Build description when restoring block attribute entities.""" + return BlockBinarySensorDescription( + key="", + name="", + icon=entry.original_icon, + device_class=entry.original_device_class, + ) + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -189,10 +219,16 @@ async def async_setup_entry( async_add_entities, SENSORS, BlockSleepingBinarySensor, + _build_block_description, ) else: await async_setup_entry_attribute_entities( - hass, config_entry, async_add_entities, SENSORS, BlockBinarySensor + hass, + config_entry, + async_add_entities, + SENSORS, + BlockBinarySensor, + _build_block_description, ) await async_setup_entry_rest( hass, @@ -206,6 +242,8 @@ async def async_setup_entry( class BlockBinarySensor(ShellyBlockAttributeEntity, BinarySensorEntity): """Represent a block binary sensor entity.""" + entity_description: BlockBinarySensorDescription + @property def is_on(self) -> bool: """Return true if sensor state is on.""" @@ -241,6 +279,8 @@ class RpcBinarySensor(ShellyRpcAttributeEntity, BinarySensorEntity): class BlockSleepingBinarySensor(ShellySleepingBlockAttributeEntity, BinarySensorEntity): """Represent a block sleeping binary sensor.""" + entity_description: BlockBinarySensorDescription + @property def is_on(self) -> bool: """Return true if sensor state is on.""" diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 2b0c310e5a7..a8546cb43dd 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -10,7 +10,6 @@ from typing import Any, Final, cast from aioshelly.block_device import Block import async_timeout -from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import ( @@ -19,7 +18,7 @@ from homeassistant.helpers import ( entity_registry, update_coordinator, ) -from homeassistant.helpers.entity import DeviceInfo, EntityCategory, EntityDescription +from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import StateType @@ -53,8 +52,11 @@ async def async_setup_entry_attribute_entities( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, - sensors: dict[tuple[str, str], BlockAttributeDescription], + sensors: Mapping[tuple[str, str], BlockEntityDescription], sensor_class: Callable, + description_class: Callable[ + [entity_registry.RegistryEntry], BlockEntityDescription + ], ) -> None: """Set up entities for attributes.""" wrapper: BlockDeviceWrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][ @@ -67,7 +69,13 @@ async def async_setup_entry_attribute_entities( ) else: await async_restore_block_attribute_entities( - hass, config_entry, async_add_entities, wrapper, sensors, sensor_class + hass, + config_entry, + async_add_entities, + wrapper, + sensors, + sensor_class, + description_class, ) @@ -75,7 +83,7 @@ async def async_setup_block_attribute_entities( hass: HomeAssistant, async_add_entities: AddEntitiesCallback, wrapper: BlockDeviceWrapper, - sensors: dict[tuple[str, str], BlockAttributeDescription], + sensors: Mapping[tuple[str, str], BlockEntityDescription], sensor_class: Callable, ) -> None: """Set up entities for block attributes.""" @@ -119,8 +127,11 @@ async def async_restore_block_attribute_entities( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, wrapper: BlockDeviceWrapper, - sensors: dict[tuple[str, str], BlockAttributeDescription], + sensors: Mapping[tuple[str, str], BlockEntityDescription], sensor_class: Callable, + description_class: Callable[ + [entity_registry.RegistryEntry], BlockEntityDescription + ], ) -> None: """Restore block attributes entities.""" entities = [] @@ -137,12 +148,7 @@ async def async_restore_block_attribute_entities( continue attribute = entry.unique_id.split("-")[-1] - description = BlockAttributeDescription( - name="", - icon=entry.original_icon, - unit=entry.unit_of_measurement, - device_class=entry.original_device_class, - ) + description = description_class(entry) entities.append( sensor_class(wrapper, None, attribute, description, entry, sensors) @@ -232,22 +238,16 @@ async def async_setup_entry_rest( @dataclass -class BlockAttributeDescription: - """Class to describe a sensor.""" +class BlockEntityDescription(EntityDescription): + """Class to describe a BLOCK entity.""" - name: str - # Callable = lambda attr_info: unit - icon: str | None = None - unit: None | str | Callable[[dict], str] = None + icon_fn: Callable[[dict], str] | None = None + unit_fn: Callable[[dict], str] | None = None value: Callable[[Any], Any] = lambda val: val - device_class: str | None = None - state_class: str | None = None - default_enabled: bool = True available: Callable[[Block], bool] | None = None # Callable (settings, block), return true if entity should be removed removal_condition: Callable[[dict, Block], bool] | None = None extra_state_attributes: Callable[[Block], dict | None] | None = None - entity_category: EntityCategory | None = None @dataclass @@ -283,35 +283,18 @@ class ShellyBlockEntity(entity.Entity): """Initialize Shelly entity.""" self.wrapper = wrapper self.block = block - self._name = get_block_entity_name(wrapper.device, block) - - @property - def name(self) -> str: - """Name of entity.""" - return self._name - - @property - def should_poll(self) -> bool: - """If device should be polled.""" - return False - - @property - def device_info(self) -> DeviceInfo: - """Device info.""" - return { - "connections": {(device_registry.CONNECTION_NETWORK_MAC, self.wrapper.mac)} - } + self._attr_name = get_block_entity_name(wrapper.device, block) + self._attr_should_poll = False + self._attr_device_info = DeviceInfo( + connections={(device_registry.CONNECTION_NETWORK_MAC, wrapper.mac)} + ) + self._attr_unique_id = f"{wrapper.mac}-{block.description}" @property def available(self) -> bool: """Available.""" return self.wrapper.last_update_success - @property - def unique_id(self) -> str: - """Return unique ID of entity.""" - return f"{self.wrapper.mac}-{self.block.description}" - async def async_added_to_hass(self) -> None: """When entity is added to HASS.""" self.async_on_remove(self.wrapper.async_add_listener(self._update_callback)) @@ -404,41 +387,22 @@ class ShellyRpcEntity(entity.Entity): class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity): """Helper class to represent a block attribute.""" + entity_description: BlockEntityDescription + def __init__( self, wrapper: BlockDeviceWrapper, block: Block, attribute: str, - description: BlockAttributeDescription, + description: BlockEntityDescription, ) -> None: """Initialize sensor.""" super().__init__(wrapper, block) self.attribute = attribute - self.description = description + self.entity_description = description - unit = self.description.unit - - if callable(unit): - unit = unit(block.info(attribute)) - - self._unit: None | str | Callable[[dict], str] = unit - self._unique_id: str = f"{super().unique_id}-{self.attribute}" - self._name = get_block_entity_name(wrapper.device, block, self.description.name) - - @property - def unique_id(self) -> str: - """Return unique ID of entity.""" - return self._unique_id - - @property - def name(self) -> str: - """Name of sensor.""" - return self._name - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if it should be enabled by default.""" - return self.description.default_enabled + self._attr_unique_id: str = f"{super().unique_id}-{self.attribute}" + self._attr_name = get_block_entity_name(wrapper.device, block, description.name) @property def attribute_value(self) -> StateType: @@ -446,40 +410,25 @@ class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity): if (value := getattr(self.block, self.attribute)) is None: return None - return cast(StateType, self.description.value(value)) - - @property - def device_class(self) -> str | None: - """Device class of sensor.""" - return self.description.device_class - - @property - def icon(self) -> str | None: - """Icon of sensor.""" - return self.description.icon + return cast(StateType, self.entity_description.value(value)) @property def available(self) -> bool: """Available.""" available = super().available - if not available or not self.description.available: + if not available or not self.entity_description.available: return available - return self.description.available(self.block) + return self.entity_description.available(self.block) @property def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes.""" - if self.description.extra_state_attributes is None: + if self.entity_description.extra_state_attributes is None: return None - return self.description.extra_state_attributes(self.block) - - @property - def entity_category(self) -> EntityCategory | None: - """Return category of entity.""" - return self.description.entity_category + return self.entity_description.extra_state_attributes(self.block) class ShellyRestAttributeEntity(update_coordinator.CoordinatorEntity): @@ -601,9 +550,9 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti wrapper: BlockDeviceWrapper, block: Block | None, attribute: str, - description: BlockAttributeDescription, + description: BlockEntityDescription, entry: entity_registry.RegistryEntry | None = None, - sensors: dict[tuple[str, str], BlockAttributeDescription] | None = None, + sensors: Mapping[tuple[str, str], BlockEntityDescription] | None = None, ) -> None: """Initialize the sleeping sensor.""" self.sensors = sensors @@ -611,20 +560,16 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti self.wrapper = wrapper self.attribute = attribute self.block: Block | None = block # type: ignore[assignment] - self.description = description - self._unit = self.description.unit + self.entity_description = description if block is not None: - if callable(self._unit): - self._unit = self._unit(block.info(attribute)) - - self._unique_id = f"{self.wrapper.mac}-{block.description}-{attribute}" - self._name = get_block_entity_name( - self.wrapper.device, block, self.description.name + self._attr_unique_id = f"{self.wrapper.mac}-{block.description}-{attribute}" + self._attr_name = get_block_entity_name( + self.wrapper.device, block, self.entity_description.name ) elif entry is not None: - self._unique_id = entry.unique_id - self._name = cast(str, entry.original_name) + self._attr_unique_id = entry.unique_id + self._attr_name = cast(str, entry.original_name) async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" @@ -634,7 +579,6 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti if last_state is not None: self.last_state = last_state.state - self.description.state_class = last_state.attributes.get(ATTR_STATE_CLASS) @callback def _update_callback(self) -> None: @@ -647,7 +591,7 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti super()._update_callback() return - _, entity_block, entity_sensor = self.unique_id.split("-") + _, entity_block, entity_sensor = self._attr_unique_id.split("-") assert self.wrapper.device.blocks @@ -664,7 +608,7 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti continue self.block = block - self.description = description + self.entity_description = description _LOGGER.debug("Entity %s attached to block", self.name) super()._update_callback() diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 4339fc1c6fa..1d88efe897e 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -1,9 +1,12 @@ """Sensor for Shelly.""" from __future__ import annotations +from collections.abc import Mapping from dataclasses import dataclass from typing import Final, cast +from aioshelly.block_device import Block + from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -26,11 +29,13 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity_registry import RegistryEntry from homeassistant.helpers.typing import StateType +from . import BlockDeviceWrapper from .const import CONF_SLEEP_PERIOD, SHAIR_MAX_WORK_HOURS from .entity import ( - BlockAttributeDescription, + BlockEntityDescription, RestEntityDescription, RpcEntityDescription, ShellyBlockAttributeEntity, @@ -44,6 +49,11 @@ from .entity import ( from .utils import get_device_entry_gen, get_device_uptime, temperature_unit +@dataclass +class BlockSensorDescription(BlockEntityDescription, SensorEntityDescription): + """Class to describe a BLOCK sensor.""" + + @dataclass class RpcSensorDescription(RpcEntityDescription, SensorEntityDescription): """Class to describe a RPC sensor.""" @@ -55,170 +65,193 @@ class RestSensorDescription(RestEntityDescription, SensorEntityDescription): SENSORS: Final = { - ("device", "battery"): BlockAttributeDescription( + ("device", "battery"): BlockSensorDescription( + key="device|battery", name="Battery", - unit=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, removal_condition=lambda settings, _: settings.get("external_power") == 1, available=lambda block: cast(int, block.battery) != -1, entity_category=EntityCategory.DIAGNOSTIC, ), - ("device", "deviceTemp"): BlockAttributeDescription( + ("device", "deviceTemp"): BlockSensorDescription( + key="device|deviceTemp", name="Device Temperature", - unit=temperature_unit, + unit_fn=temperature_unit, value=lambda value: round(value, 1), device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, - default_enabled=False, + entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, ), - ("emeter", "current"): BlockAttributeDescription( + ("emeter", "current"): BlockSensorDescription( + key="emeter|current", name="Current", - unit=ELECTRIC_CURRENT_AMPERE, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, value=lambda value: value, device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, ), - ("light", "power"): BlockAttributeDescription( + ("light", "power"): BlockSensorDescription( + key="light|power", name="Power", - unit=POWER_WATT, + native_unit_of_measurement=POWER_WATT, value=lambda value: round(value, 1), device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, - default_enabled=False, + entity_registry_enabled_default=False, ), - ("device", "power"): BlockAttributeDescription( + ("device", "power"): BlockSensorDescription( + key="device|power", name="Power", - unit=POWER_WATT, + native_unit_of_measurement=POWER_WATT, value=lambda value: round(value, 1), device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), - ("emeter", "power"): BlockAttributeDescription( + ("emeter", "power"): BlockSensorDescription( + key="emeter|power", name="Power", - unit=POWER_WATT, + native_unit_of_measurement=POWER_WATT, value=lambda value: round(value, 1), device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), - ("device", "voltage"): BlockAttributeDescription( + ("device", "voltage"): BlockSensorDescription( + key="device|voltage", name="Voltage", - unit=ELECTRIC_POTENTIAL_VOLT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, value=lambda value: round(value, 1), device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, - default_enabled=False, + entity_registry_enabled_default=False, ), - ("emeter", "voltage"): BlockAttributeDescription( + ("emeter", "voltage"): BlockSensorDescription( + key="emeter|voltage", name="Voltage", - unit=ELECTRIC_POTENTIAL_VOLT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, value=lambda value: round(value, 1), device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, ), - ("emeter", "powerFactor"): BlockAttributeDescription( + ("emeter", "powerFactor"): BlockSensorDescription( + key="emeter|powerFactor", name="Power Factor", - unit=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, value=lambda value: round(value * 100, 1), device_class=SensorDeviceClass.POWER_FACTOR, state_class=SensorStateClass.MEASUREMENT, ), - ("relay", "power"): BlockAttributeDescription( + ("relay", "power"): BlockSensorDescription( + key="relay|power", name="Power", - unit=POWER_WATT, + native_unit_of_measurement=POWER_WATT, value=lambda value: round(value, 1), device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), - ("roller", "rollerPower"): BlockAttributeDescription( + ("roller", "rollerPower"): BlockSensorDescription( + key="roller|rollerPower", name="Power", - unit=POWER_WATT, + native_unit_of_measurement=POWER_WATT, value=lambda value: round(value, 1), device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), - ("device", "energy"): BlockAttributeDescription( + ("device", "energy"): BlockSensorDescription( + key="device|energy", name="Energy", - unit=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, value=lambda value: round(value / 60 / 1000, 2), device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), - ("emeter", "energy"): BlockAttributeDescription( + ("emeter", "energy"): BlockSensorDescription( + key="emeter|energy", name="Energy", - unit=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, value=lambda value: round(value / 1000, 2), device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), - ("emeter", "energyReturned"): BlockAttributeDescription( + ("emeter", "energyReturned"): BlockSensorDescription( + key="emeter|energyReturned", name="Energy Returned", - unit=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, value=lambda value: round(value / 1000, 2), device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), - ("light", "energy"): BlockAttributeDescription( + ("light", "energy"): BlockSensorDescription( + key="light|energy", name="Energy", - unit=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, value=lambda value: round(value / 60 / 1000, 2), device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, - default_enabled=False, + entity_registry_enabled_default=False, ), - ("relay", "energy"): BlockAttributeDescription( + ("relay", "energy"): BlockSensorDescription( + key="relay|energy", name="Energy", - unit=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, value=lambda value: round(value / 60 / 1000, 2), device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), - ("roller", "rollerEnergy"): BlockAttributeDescription( + ("roller", "rollerEnergy"): BlockSensorDescription( + key="roller|rollerEnergy", name="Energy", - unit=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, value=lambda value: round(value / 60 / 1000, 2), device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), - ("sensor", "concentration"): BlockAttributeDescription( + ("sensor", "concentration"): BlockSensorDescription( + key="sensor|concentration", name="Gas Concentration", - unit=CONCENTRATION_PARTS_PER_MILLION, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, icon="mdi:gauge", state_class=SensorStateClass.MEASUREMENT, ), - ("sensor", "extTemp"): BlockAttributeDescription( + ("sensor", "extTemp"): BlockSensorDescription( + key="sensor|extTemp", name="Temperature", - unit=temperature_unit, + unit_fn=temperature_unit, value=lambda value: round(value, 1), device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, available=lambda block: cast(int, block.extTemp) != 999, ), - ("sensor", "humidity"): BlockAttributeDescription( + ("sensor", "humidity"): BlockSensorDescription( + key="sensor|humidity", name="Humidity", - unit=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, value=lambda value: round(value, 1), device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, available=lambda block: cast(int, block.humidity) != 999, ), - ("sensor", "luminosity"): BlockAttributeDescription( + ("sensor", "luminosity"): BlockSensorDescription( + key="sensor|luminosity", name="Luminosity", - unit=LIGHT_LUX, + native_unit_of_measurement=LIGHT_LUX, device_class=SensorDeviceClass.ILLUMINANCE, state_class=SensorStateClass.MEASUREMENT, available=lambda block: cast(int, block.luminosity) != -1, ), - ("sensor", "tilt"): BlockAttributeDescription( + ("sensor", "tilt"): BlockSensorDescription( + key="sensor|tilt", name="Tilt", - unit=DEGREE, + native_unit_of_measurement=DEGREE, icon="mdi:angle-acute", state_class=SensorStateClass.MEASUREMENT, ), - ("relay", "totalWorkTime"): BlockAttributeDescription( + ("relay", "totalWorkTime"): BlockSensorDescription( + key="relay|totalWorkTime", name="Lamp Life", - unit=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, icon="mdi:progress-wrench", value=lambda value: round(100 - (value / 3600 / SHAIR_MAX_WORK_HOURS), 1), extra_state_attributes=lambda block: { @@ -226,14 +259,16 @@ SENSORS: Final = { }, entity_category=EntityCategory.DIAGNOSTIC, ), - ("adc", "adc"): BlockAttributeDescription( + ("adc", "adc"): BlockSensorDescription( + key="adc|adc", name="ADC", - unit=ELECTRIC_POTENTIAL_VOLT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, value=lambda value: round(value, 1), device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, ), - ("sensor", "sensorOp"): BlockAttributeDescription( + ("sensor", "sensorOp"): BlockSensorDescription( + key="sensor|sensorOp", name="Operation", icon="mdi:cog-transfer", value=lambda value: value, @@ -328,6 +363,17 @@ RPC_SENSORS: Final = { } +def _build_block_description(entry: RegistryEntry) -> BlockSensorDescription: + """Build description when restoring block attribute entities.""" + return BlockSensorDescription( + key="", + name="", + icon=entry.original_icon, + native_unit_of_measurement=entry.unit_of_measurement, + device_class=entry.original_device_class, + ) + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -341,11 +387,21 @@ async def async_setup_entry( if config_entry.data[CONF_SLEEP_PERIOD]: await async_setup_entry_attribute_entities( - hass, config_entry, async_add_entities, SENSORS, BlockSleepingSensor + hass, + config_entry, + async_add_entities, + SENSORS, + BlockSleepingSensor, + _build_block_description, ) else: await async_setup_entry_attribute_entities( - hass, config_entry, async_add_entities, SENSORS, BlockSensor + hass, + config_entry, + async_add_entities, + SENSORS, + BlockSensor, + _build_block_description, ) await async_setup_entry_rest( hass, config_entry, async_add_entities, REST_SENSORS, RestSensor @@ -355,21 +411,27 @@ async def async_setup_entry( class BlockSensor(ShellyBlockAttributeEntity, SensorEntity): """Represent a block sensor.""" + entity_description: BlockSensorDescription + + def __init__( + self, + wrapper: BlockDeviceWrapper, + block: Block, + attribute: str, + description: BlockSensorDescription, + ) -> None: + """Initialize sensor.""" + super().__init__(wrapper, block, attribute, description) + + self._attr_native_unit_of_measurement = description.native_unit_of_measurement + if unit_fn := description.unit_fn: + self._attr_native_unit_of_measurement = unit_fn(block.info(attribute)) + @property def native_value(self) -> StateType: """Return value of sensor.""" return self.attribute_value - @property - def state_class(self) -> str | None: - """State class of sensor.""" - return self.description.state_class - - @property - def native_unit_of_measurement(self) -> str | None: - """Return unit of sensor.""" - return cast(str, self._unit) - class RestSensor(ShellyRestAttributeEntity, SensorEntity): """Represent a REST sensor.""" @@ -396,6 +458,24 @@ class RpcSensor(ShellyRpcAttributeEntity, SensorEntity): class BlockSleepingSensor(ShellySleepingBlockAttributeEntity, SensorEntity): """Represent a block sleeping sensor.""" + entity_description: BlockSensorDescription + + def __init__( + self, + wrapper: BlockDeviceWrapper, + block: Block | None, + attribute: str, + description: BlockSensorDescription, + entry: RegistryEntry | None = None, + sensors: Mapping[tuple[str, str], BlockSensorDescription] | None = None, + ) -> None: + """Initialize the sleeping sensor.""" + super().__init__(wrapper, block, attribute, description, entry, sensors) + + self._attr_native_unit_of_measurement = description.native_unit_of_measurement + if block and (unit_fn := description.unit_fn): + self._attr_native_unit_of_measurement = unit_fn(block.info(attribute)) + @property def native_value(self) -> StateType: """Return value of sensor.""" @@ -403,13 +483,3 @@ class BlockSleepingSensor(ShellySleepingBlockAttributeEntity, SensorEntity): return self.attribute_value return self.last_state - - @property - def state_class(self) -> str | None: - """State class of sensor.""" - return self.description.state_class - - @property - def native_unit_of_measurement(self) -> str | None: - """Return unit of sensor.""" - return cast(str, self._unit)