Use entity_description in shelly block sensors (#64897)

* Use entity_description in shelly block sensors

* Re-order binary sensor

* Add async_added_to_hass for BlockSleepingBinarySensor

* Undo None

* Build description when restoring block attribute entities

* Move async_added_to_hass back to base class

Co-authored-by: epenet <epenet@users.noreply.github.com>
This commit is contained in:
epenet 2022-01-26 15:07:15 +01:00 committed by GitHub
parent 982580b95a
commit dfdbeba7be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 259 additions and 205 deletions

View File

@ -14,10 +14,11 @@ from homeassistant.const import STATE_ON
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.entity_registry import RegistryEntry
from .const import CONF_SLEEP_PERIOD from .const import CONF_SLEEP_PERIOD
from .entity import ( from .entity import (
BlockAttributeDescription, BlockEntityDescription,
RestEntityDescription, RestEntityDescription,
RpcEntityDescription, RpcEntityDescription,
ShellyBlockAttributeEntity, ShellyBlockAttributeEntity,
@ -35,6 +36,13 @@ from .utils import (
) )
@dataclass
class BlockBinarySensorDescription(
BlockEntityDescription, BinarySensorEntityDescription
):
"""Class to describe a BLOCK binary sensor."""
@dataclass @dataclass
class RpcBinarySensorDescription(RpcEntityDescription, BinarySensorEntityDescription): class RpcBinarySensorDescription(RpcEntityDescription, BinarySensorEntityDescription):
"""Class to describe a RPC binary sensor.""" """Class to describe a RPC binary sensor."""
@ -46,71 +54,83 @@ class RestBinarySensorDescription(RestEntityDescription, BinarySensorEntityDescr
SENSORS: Final = { SENSORS: Final = {
("device", "overtemp"): BlockAttributeDescription( ("device", "overtemp"): BlockBinarySensorDescription(
key="device|overtemp",
name="Overheating", name="Overheating",
device_class=BinarySensorDeviceClass.PROBLEM, device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
), ),
("device", "overpower"): BlockAttributeDescription( ("device", "overpower"): BlockBinarySensorDescription(
key="device|overpower",
name="Overpowering", name="Overpowering",
device_class=BinarySensorDeviceClass.PROBLEM, device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
), ),
("light", "overpower"): BlockAttributeDescription( ("light", "overpower"): BlockBinarySensorDescription(
key="light|overpower",
name="Overpowering", name="Overpowering",
device_class=BinarySensorDeviceClass.PROBLEM, device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
), ),
("relay", "overpower"): BlockAttributeDescription( ("relay", "overpower"): BlockBinarySensorDescription(
key="relay|overpower",
name="Overpowering", name="Overpowering",
device_class=BinarySensorDeviceClass.PROBLEM, device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
), ),
("sensor", "dwIsOpened"): BlockAttributeDescription( ("sensor", "dwIsOpened"): BlockBinarySensorDescription(
key="sensor|dwIsOpened",
name="Door", name="Door",
device_class=BinarySensorDeviceClass.OPENING, device_class=BinarySensorDeviceClass.OPENING,
available=lambda block: cast(int, block.dwIsOpened) != -1, available=lambda block: cast(int, block.dwIsOpened) != -1,
), ),
("sensor", "flood"): BlockAttributeDescription( ("sensor", "flood"): BlockBinarySensorDescription(
name="Flood", device_class=BinarySensorDeviceClass.MOISTURE key="sensor|flood", name="Flood", device_class=BinarySensorDeviceClass.MOISTURE
), ),
("sensor", "gas"): BlockAttributeDescription( ("sensor", "gas"): BlockBinarySensorDescription(
key="sensor|gas",
name="Gas", name="Gas",
device_class=BinarySensorDeviceClass.GAS, device_class=BinarySensorDeviceClass.GAS,
value=lambda value: value in ["mild", "heavy"], value=lambda value: value in ["mild", "heavy"],
extra_state_attributes=lambda block: {"detected": block.gas}, extra_state_attributes=lambda block: {"detected": block.gas},
), ),
("sensor", "smoke"): BlockAttributeDescription( ("sensor", "smoke"): BlockBinarySensorDescription(
name="Smoke", device_class=BinarySensorDeviceClass.SMOKE key="sensor|smoke", name="Smoke", device_class=BinarySensorDeviceClass.SMOKE
), ),
("sensor", "vibration"): BlockAttributeDescription( ("sensor", "vibration"): BlockBinarySensorDescription(
name="Vibration", device_class=BinarySensorDeviceClass.VIBRATION key="sensor|vibration",
name="Vibration",
device_class=BinarySensorDeviceClass.VIBRATION,
), ),
("input", "input"): BlockAttributeDescription( ("input", "input"): BlockBinarySensorDescription(
key="input|input",
name="Input", name="Input",
device_class=BinarySensorDeviceClass.POWER, device_class=BinarySensorDeviceClass.POWER,
default_enabled=False, entity_registry_enabled_default=False,
removal_condition=is_block_momentary_input, removal_condition=is_block_momentary_input,
), ),
("relay", "input"): BlockAttributeDescription( ("relay", "input"): BlockBinarySensorDescription(
key="relay|input",
name="Input", name="Input",
device_class=BinarySensorDeviceClass.POWER, device_class=BinarySensorDeviceClass.POWER,
default_enabled=False, entity_registry_enabled_default=False,
removal_condition=is_block_momentary_input, removal_condition=is_block_momentary_input,
), ),
("device", "input"): BlockAttributeDescription( ("device", "input"): BlockBinarySensorDescription(
key="device|input",
name="Input", name="Input",
device_class=BinarySensorDeviceClass.POWER, device_class=BinarySensorDeviceClass.POWER,
default_enabled=False, entity_registry_enabled_default=False,
removal_condition=is_block_momentary_input, removal_condition=is_block_momentary_input,
), ),
("sensor", "extInput"): BlockAttributeDescription( ("sensor", "extInput"): BlockBinarySensorDescription(
key="sensor|extInput",
name="External Input", name="External Input",
device_class=BinarySensorDeviceClass.POWER, device_class=BinarySensorDeviceClass.POWER,
default_enabled=False, entity_registry_enabled_default=False,
), ),
("sensor", "motion"): BlockAttributeDescription( ("sensor", "motion"): BlockBinarySensorDescription(
name="Motion", device_class=BinarySensorDeviceClass.MOTION 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( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: ConfigEntry,
@ -189,10 +219,16 @@ async def async_setup_entry(
async_add_entities, async_add_entities,
SENSORS, SENSORS,
BlockSleepingBinarySensor, BlockSleepingBinarySensor,
_build_block_description,
) )
else: else:
await async_setup_entry_attribute_entities( 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( await async_setup_entry_rest(
hass, hass,
@ -206,6 +242,8 @@ async def async_setup_entry(
class BlockBinarySensor(ShellyBlockAttributeEntity, BinarySensorEntity): class BlockBinarySensor(ShellyBlockAttributeEntity, BinarySensorEntity):
"""Represent a block binary sensor entity.""" """Represent a block binary sensor entity."""
entity_description: BlockBinarySensorDescription
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return true if sensor state is on.""" """Return true if sensor state is on."""
@ -241,6 +279,8 @@ class RpcBinarySensor(ShellyRpcAttributeEntity, BinarySensorEntity):
class BlockSleepingBinarySensor(ShellySleepingBlockAttributeEntity, BinarySensorEntity): class BlockSleepingBinarySensor(ShellySleepingBlockAttributeEntity, BinarySensorEntity):
"""Represent a block sleeping binary sensor.""" """Represent a block sleeping binary sensor."""
entity_description: BlockBinarySensorDescription
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return true if sensor state is on.""" """Return true if sensor state is on."""

View File

@ -10,7 +10,6 @@ from typing import Any, Final, cast
from aioshelly.block_device import Block from aioshelly.block_device import Block
import async_timeout import async_timeout
from homeassistant.components.sensor import ATTR_STATE_CLASS
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import ( from homeassistant.helpers import (
@ -19,7 +18,7 @@ from homeassistant.helpers import (
entity_registry, entity_registry,
update_coordinator, 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.entity_platform import AddEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import StateType from homeassistant.helpers.typing import StateType
@ -53,8 +52,11 @@ async def async_setup_entry_attribute_entities(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
sensors: dict[tuple[str, str], BlockAttributeDescription], sensors: Mapping[tuple[str, str], BlockEntityDescription],
sensor_class: Callable, sensor_class: Callable,
description_class: Callable[
[entity_registry.RegistryEntry], BlockEntityDescription
],
) -> None: ) -> None:
"""Set up entities for attributes.""" """Set up entities for attributes."""
wrapper: BlockDeviceWrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][ wrapper: BlockDeviceWrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][
@ -67,7 +69,13 @@ async def async_setup_entry_attribute_entities(
) )
else: else:
await async_restore_block_attribute_entities( 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, hass: HomeAssistant,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
wrapper: BlockDeviceWrapper, wrapper: BlockDeviceWrapper,
sensors: dict[tuple[str, str], BlockAttributeDescription], sensors: Mapping[tuple[str, str], BlockEntityDescription],
sensor_class: Callable, sensor_class: Callable,
) -> None: ) -> None:
"""Set up entities for block attributes.""" """Set up entities for block attributes."""
@ -119,8 +127,11 @@ async def async_restore_block_attribute_entities(
config_entry: ConfigEntry, config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
wrapper: BlockDeviceWrapper, wrapper: BlockDeviceWrapper,
sensors: dict[tuple[str, str], BlockAttributeDescription], sensors: Mapping[tuple[str, str], BlockEntityDescription],
sensor_class: Callable, sensor_class: Callable,
description_class: Callable[
[entity_registry.RegistryEntry], BlockEntityDescription
],
) -> None: ) -> None:
"""Restore block attributes entities.""" """Restore block attributes entities."""
entities = [] entities = []
@ -137,12 +148,7 @@ async def async_restore_block_attribute_entities(
continue continue
attribute = entry.unique_id.split("-")[-1] attribute = entry.unique_id.split("-")[-1]
description = BlockAttributeDescription( description = description_class(entry)
name="",
icon=entry.original_icon,
unit=entry.unit_of_measurement,
device_class=entry.original_device_class,
)
entities.append( entities.append(
sensor_class(wrapper, None, attribute, description, entry, sensors) sensor_class(wrapper, None, attribute, description, entry, sensors)
@ -232,22 +238,16 @@ async def async_setup_entry_rest(
@dataclass @dataclass
class BlockAttributeDescription: class BlockEntityDescription(EntityDescription):
"""Class to describe a sensor.""" """Class to describe a BLOCK entity."""
name: str icon_fn: Callable[[dict], str] | None = None
# Callable = lambda attr_info: unit unit_fn: Callable[[dict], str] | None = None
icon: str | None = None
unit: None | str | Callable[[dict], str] = None
value: Callable[[Any], Any] = lambda val: val 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 available: Callable[[Block], bool] | None = None
# Callable (settings, block), return true if entity should be removed # Callable (settings, block), return true if entity should be removed
removal_condition: Callable[[dict, Block], bool] | None = None removal_condition: Callable[[dict, Block], bool] | None = None
extra_state_attributes: Callable[[Block], dict | None] | None = None extra_state_attributes: Callable[[Block], dict | None] | None = None
entity_category: EntityCategory | None = None
@dataclass @dataclass
@ -283,35 +283,18 @@ class ShellyBlockEntity(entity.Entity):
"""Initialize Shelly entity.""" """Initialize Shelly entity."""
self.wrapper = wrapper self.wrapper = wrapper
self.block = block self.block = block
self._name = get_block_entity_name(wrapper.device, block) self._attr_name = get_block_entity_name(wrapper.device, block)
self._attr_should_poll = False
@property self._attr_device_info = DeviceInfo(
def name(self) -> str: connections={(device_registry.CONNECTION_NETWORK_MAC, wrapper.mac)}
"""Name of entity.""" )
return self._name self._attr_unique_id = f"{wrapper.mac}-{block.description}"
@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)}
}
@property @property
def available(self) -> bool: def available(self) -> bool:
"""Available.""" """Available."""
return self.wrapper.last_update_success 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: async def async_added_to_hass(self) -> None:
"""When entity is added to HASS.""" """When entity is added to HASS."""
self.async_on_remove(self.wrapper.async_add_listener(self._update_callback)) 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): class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity):
"""Helper class to represent a block attribute.""" """Helper class to represent a block attribute."""
entity_description: BlockEntityDescription
def __init__( def __init__(
self, self,
wrapper: BlockDeviceWrapper, wrapper: BlockDeviceWrapper,
block: Block, block: Block,
attribute: str, attribute: str,
description: BlockAttributeDescription, description: BlockEntityDescription,
) -> None: ) -> None:
"""Initialize sensor.""" """Initialize sensor."""
super().__init__(wrapper, block) super().__init__(wrapper, block)
self.attribute = attribute self.attribute = attribute
self.description = description self.entity_description = description
unit = self.description.unit self._attr_unique_id: str = f"{super().unique_id}-{self.attribute}"
self._attr_name = get_block_entity_name(wrapper.device, block, description.name)
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
@property @property
def attribute_value(self) -> StateType: def attribute_value(self) -> StateType:
@ -446,40 +410,25 @@ class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity):
if (value := getattr(self.block, self.attribute)) is None: if (value := getattr(self.block, self.attribute)) is None:
return None return None
return cast(StateType, self.description.value(value)) return cast(StateType, self.entity_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
@property @property
def available(self) -> bool: def available(self) -> bool:
"""Available.""" """Available."""
available = super().available available = super().available
if not available or not self.description.available: if not available or not self.entity_description.available:
return available return available
return self.description.available(self.block) return self.entity_description.available(self.block)
@property @property
def extra_state_attributes(self) -> dict[str, Any] | None: def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return the state attributes.""" """Return the state attributes."""
if self.description.extra_state_attributes is None: if self.entity_description.extra_state_attributes is None:
return None return None
return self.description.extra_state_attributes(self.block) return self.entity_description.extra_state_attributes(self.block)
@property
def entity_category(self) -> EntityCategory | None:
"""Return category of entity."""
return self.description.entity_category
class ShellyRestAttributeEntity(update_coordinator.CoordinatorEntity): class ShellyRestAttributeEntity(update_coordinator.CoordinatorEntity):
@ -601,9 +550,9 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti
wrapper: BlockDeviceWrapper, wrapper: BlockDeviceWrapper,
block: Block | None, block: Block | None,
attribute: str, attribute: str,
description: BlockAttributeDescription, description: BlockEntityDescription,
entry: entity_registry.RegistryEntry | None = None, entry: entity_registry.RegistryEntry | None = None,
sensors: dict[tuple[str, str], BlockAttributeDescription] | None = None, sensors: Mapping[tuple[str, str], BlockEntityDescription] | None = None,
) -> None: ) -> None:
"""Initialize the sleeping sensor.""" """Initialize the sleeping sensor."""
self.sensors = sensors self.sensors = sensors
@ -611,20 +560,16 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti
self.wrapper = wrapper self.wrapper = wrapper
self.attribute = attribute self.attribute = attribute
self.block: Block | None = block # type: ignore[assignment] self.block: Block | None = block # type: ignore[assignment]
self.description = description self.entity_description = description
self._unit = self.description.unit
if block is not None: if block is not None:
if callable(self._unit): self._attr_unique_id = f"{self.wrapper.mac}-{block.description}-{attribute}"
self._unit = self._unit(block.info(attribute)) self._attr_name = get_block_entity_name(
self.wrapper.device, block, self.entity_description.name
self._unique_id = f"{self.wrapper.mac}-{block.description}-{attribute}"
self._name = get_block_entity_name(
self.wrapper.device, block, self.description.name
) )
elif entry is not None: elif entry is not None:
self._unique_id = entry.unique_id self._attr_unique_id = entry.unique_id
self._name = cast(str, entry.original_name) self._attr_name = cast(str, entry.original_name)
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Handle entity which will be added.""" """Handle entity which will be added."""
@ -634,7 +579,6 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti
if last_state is not None: if last_state is not None:
self.last_state = last_state.state self.last_state = last_state.state
self.description.state_class = last_state.attributes.get(ATTR_STATE_CLASS)
@callback @callback
def _update_callback(self) -> None: def _update_callback(self) -> None:
@ -647,7 +591,7 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti
super()._update_callback() super()._update_callback()
return return
_, entity_block, entity_sensor = self.unique_id.split("-") _, entity_block, entity_sensor = self._attr_unique_id.split("-")
assert self.wrapper.device.blocks assert self.wrapper.device.blocks
@ -664,7 +608,7 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti
continue continue
self.block = block self.block = block
self.description = description self.entity_description = description
_LOGGER.debug("Entity %s attached to block", self.name) _LOGGER.debug("Entity %s attached to block", self.name)
super()._update_callback() super()._update_callback()

View File

@ -1,9 +1,12 @@
"""Sensor for Shelly.""" """Sensor for Shelly."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass from dataclasses import dataclass
from typing import Final, cast from typing import Final, cast
from aioshelly.block_device import Block
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
SensorDeviceClass, SensorDeviceClass,
SensorEntity, SensorEntity,
@ -26,11 +29,13 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.entity_registry import RegistryEntry
from homeassistant.helpers.typing import StateType from homeassistant.helpers.typing import StateType
from . import BlockDeviceWrapper
from .const import CONF_SLEEP_PERIOD, SHAIR_MAX_WORK_HOURS from .const import CONF_SLEEP_PERIOD, SHAIR_MAX_WORK_HOURS
from .entity import ( from .entity import (
BlockAttributeDescription, BlockEntityDescription,
RestEntityDescription, RestEntityDescription,
RpcEntityDescription, RpcEntityDescription,
ShellyBlockAttributeEntity, ShellyBlockAttributeEntity,
@ -44,6 +49,11 @@ from .entity import (
from .utils import get_device_entry_gen, get_device_uptime, temperature_unit from .utils import get_device_entry_gen, get_device_uptime, temperature_unit
@dataclass
class BlockSensorDescription(BlockEntityDescription, SensorEntityDescription):
"""Class to describe a BLOCK sensor."""
@dataclass @dataclass
class RpcSensorDescription(RpcEntityDescription, SensorEntityDescription): class RpcSensorDescription(RpcEntityDescription, SensorEntityDescription):
"""Class to describe a RPC sensor.""" """Class to describe a RPC sensor."""
@ -55,170 +65,193 @@ class RestSensorDescription(RestEntityDescription, SensorEntityDescription):
SENSORS: Final = { SENSORS: Final = {
("device", "battery"): BlockAttributeDescription( ("device", "battery"): BlockSensorDescription(
key="device|battery",
name="Battery", name="Battery",
unit=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.BATTERY, device_class=SensorDeviceClass.BATTERY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
removal_condition=lambda settings, _: settings.get("external_power") == 1, removal_condition=lambda settings, _: settings.get("external_power") == 1,
available=lambda block: cast(int, block.battery) != -1, available=lambda block: cast(int, block.battery) != -1,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
), ),
("device", "deviceTemp"): BlockAttributeDescription( ("device", "deviceTemp"): BlockSensorDescription(
key="device|deviceTemp",
name="Device Temperature", name="Device Temperature",
unit=temperature_unit, unit_fn=temperature_unit,
value=lambda value: round(value, 1), value=lambda value: round(value, 1),
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
default_enabled=False, entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
), ),
("emeter", "current"): BlockAttributeDescription( ("emeter", "current"): BlockSensorDescription(
key="emeter|current",
name="Current", name="Current",
unit=ELECTRIC_CURRENT_AMPERE, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE,
value=lambda value: value, value=lambda value: value,
device_class=SensorDeviceClass.CURRENT, device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
("light", "power"): BlockAttributeDescription( ("light", "power"): BlockSensorDescription(
key="light|power",
name="Power", name="Power",
unit=POWER_WATT, native_unit_of_measurement=POWER_WATT,
value=lambda value: round(value, 1), value=lambda value: round(value, 1),
device_class=SensorDeviceClass.POWER, device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
default_enabled=False, entity_registry_enabled_default=False,
), ),
("device", "power"): BlockAttributeDescription( ("device", "power"): BlockSensorDescription(
key="device|power",
name="Power", name="Power",
unit=POWER_WATT, native_unit_of_measurement=POWER_WATT,
value=lambda value: round(value, 1), value=lambda value: round(value, 1),
device_class=SensorDeviceClass.POWER, device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
("emeter", "power"): BlockAttributeDescription( ("emeter", "power"): BlockSensorDescription(
key="emeter|power",
name="Power", name="Power",
unit=POWER_WATT, native_unit_of_measurement=POWER_WATT,
value=lambda value: round(value, 1), value=lambda value: round(value, 1),
device_class=SensorDeviceClass.POWER, device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
("device", "voltage"): BlockAttributeDescription( ("device", "voltage"): BlockSensorDescription(
key="device|voltage",
name="Voltage", name="Voltage",
unit=ELECTRIC_POTENTIAL_VOLT, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
value=lambda value: round(value, 1), value=lambda value: round(value, 1),
device_class=SensorDeviceClass.VOLTAGE, device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
default_enabled=False, entity_registry_enabled_default=False,
), ),
("emeter", "voltage"): BlockAttributeDescription( ("emeter", "voltage"): BlockSensorDescription(
key="emeter|voltage",
name="Voltage", name="Voltage",
unit=ELECTRIC_POTENTIAL_VOLT, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
value=lambda value: round(value, 1), value=lambda value: round(value, 1),
device_class=SensorDeviceClass.VOLTAGE, device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
("emeter", "powerFactor"): BlockAttributeDescription( ("emeter", "powerFactor"): BlockSensorDescription(
key="emeter|powerFactor",
name="Power Factor", name="Power Factor",
unit=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
value=lambda value: round(value * 100, 1), value=lambda value: round(value * 100, 1),
device_class=SensorDeviceClass.POWER_FACTOR, device_class=SensorDeviceClass.POWER_FACTOR,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
("relay", "power"): BlockAttributeDescription( ("relay", "power"): BlockSensorDescription(
key="relay|power",
name="Power", name="Power",
unit=POWER_WATT, native_unit_of_measurement=POWER_WATT,
value=lambda value: round(value, 1), value=lambda value: round(value, 1),
device_class=SensorDeviceClass.POWER, device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
("roller", "rollerPower"): BlockAttributeDescription( ("roller", "rollerPower"): BlockSensorDescription(
key="roller|rollerPower",
name="Power", name="Power",
unit=POWER_WATT, native_unit_of_measurement=POWER_WATT,
value=lambda value: round(value, 1), value=lambda value: round(value, 1),
device_class=SensorDeviceClass.POWER, device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
("device", "energy"): BlockAttributeDescription( ("device", "energy"): BlockSensorDescription(
key="device|energy",
name="Energy", name="Energy",
unit=ENERGY_KILO_WATT_HOUR, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
value=lambda value: round(value / 60 / 1000, 2), value=lambda value: round(value / 60 / 1000, 2),
device_class=SensorDeviceClass.ENERGY, device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING, state_class=SensorStateClass.TOTAL_INCREASING,
), ),
("emeter", "energy"): BlockAttributeDescription( ("emeter", "energy"): BlockSensorDescription(
key="emeter|energy",
name="Energy", name="Energy",
unit=ENERGY_KILO_WATT_HOUR, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
value=lambda value: round(value / 1000, 2), value=lambda value: round(value / 1000, 2),
device_class=SensorDeviceClass.ENERGY, device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING, state_class=SensorStateClass.TOTAL_INCREASING,
), ),
("emeter", "energyReturned"): BlockAttributeDescription( ("emeter", "energyReturned"): BlockSensorDescription(
key="emeter|energyReturned",
name="Energy Returned", name="Energy Returned",
unit=ENERGY_KILO_WATT_HOUR, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
value=lambda value: round(value / 1000, 2), value=lambda value: round(value / 1000, 2),
device_class=SensorDeviceClass.ENERGY, device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING, state_class=SensorStateClass.TOTAL_INCREASING,
), ),
("light", "energy"): BlockAttributeDescription( ("light", "energy"): BlockSensorDescription(
key="light|energy",
name="Energy", name="Energy",
unit=ENERGY_KILO_WATT_HOUR, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
value=lambda value: round(value / 60 / 1000, 2), value=lambda value: round(value / 60 / 1000, 2),
device_class=SensorDeviceClass.ENERGY, device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING, state_class=SensorStateClass.TOTAL_INCREASING,
default_enabled=False, entity_registry_enabled_default=False,
), ),
("relay", "energy"): BlockAttributeDescription( ("relay", "energy"): BlockSensorDescription(
key="relay|energy",
name="Energy", name="Energy",
unit=ENERGY_KILO_WATT_HOUR, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
value=lambda value: round(value / 60 / 1000, 2), value=lambda value: round(value / 60 / 1000, 2),
device_class=SensorDeviceClass.ENERGY, device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING, state_class=SensorStateClass.TOTAL_INCREASING,
), ),
("roller", "rollerEnergy"): BlockAttributeDescription( ("roller", "rollerEnergy"): BlockSensorDescription(
key="roller|rollerEnergy",
name="Energy", name="Energy",
unit=ENERGY_KILO_WATT_HOUR, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
value=lambda value: round(value / 60 / 1000, 2), value=lambda value: round(value / 60 / 1000, 2),
device_class=SensorDeviceClass.ENERGY, device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING, state_class=SensorStateClass.TOTAL_INCREASING,
), ),
("sensor", "concentration"): BlockAttributeDescription( ("sensor", "concentration"): BlockSensorDescription(
key="sensor|concentration",
name="Gas Concentration", name="Gas Concentration",
unit=CONCENTRATION_PARTS_PER_MILLION, native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
icon="mdi:gauge", icon="mdi:gauge",
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
("sensor", "extTemp"): BlockAttributeDescription( ("sensor", "extTemp"): BlockSensorDescription(
key="sensor|extTemp",
name="Temperature", name="Temperature",
unit=temperature_unit, unit_fn=temperature_unit,
value=lambda value: round(value, 1), value=lambda value: round(value, 1),
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
available=lambda block: cast(int, block.extTemp) != 999, available=lambda block: cast(int, block.extTemp) != 999,
), ),
("sensor", "humidity"): BlockAttributeDescription( ("sensor", "humidity"): BlockSensorDescription(
key="sensor|humidity",
name="Humidity", name="Humidity",
unit=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
value=lambda value: round(value, 1), value=lambda value: round(value, 1),
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
available=lambda block: cast(int, block.humidity) != 999, available=lambda block: cast(int, block.humidity) != 999,
), ),
("sensor", "luminosity"): BlockAttributeDescription( ("sensor", "luminosity"): BlockSensorDescription(
key="sensor|luminosity",
name="Luminosity", name="Luminosity",
unit=LIGHT_LUX, native_unit_of_measurement=LIGHT_LUX,
device_class=SensorDeviceClass.ILLUMINANCE, device_class=SensorDeviceClass.ILLUMINANCE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
available=lambda block: cast(int, block.luminosity) != -1, available=lambda block: cast(int, block.luminosity) != -1,
), ),
("sensor", "tilt"): BlockAttributeDescription( ("sensor", "tilt"): BlockSensorDescription(
key="sensor|tilt",
name="Tilt", name="Tilt",
unit=DEGREE, native_unit_of_measurement=DEGREE,
icon="mdi:angle-acute", icon="mdi:angle-acute",
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
("relay", "totalWorkTime"): BlockAttributeDescription( ("relay", "totalWorkTime"): BlockSensorDescription(
key="relay|totalWorkTime",
name="Lamp Life", name="Lamp Life",
unit=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
icon="mdi:progress-wrench", icon="mdi:progress-wrench",
value=lambda value: round(100 - (value / 3600 / SHAIR_MAX_WORK_HOURS), 1), value=lambda value: round(100 - (value / 3600 / SHAIR_MAX_WORK_HOURS), 1),
extra_state_attributes=lambda block: { extra_state_attributes=lambda block: {
@ -226,14 +259,16 @@ SENSORS: Final = {
}, },
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
), ),
("adc", "adc"): BlockAttributeDescription( ("adc", "adc"): BlockSensorDescription(
key="adc|adc",
name="ADC", name="ADC",
unit=ELECTRIC_POTENTIAL_VOLT, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
value=lambda value: round(value, 1), value=lambda value: round(value, 1),
device_class=SensorDeviceClass.VOLTAGE, device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
("sensor", "sensorOp"): BlockAttributeDescription( ("sensor", "sensorOp"): BlockSensorDescription(
key="sensor|sensorOp",
name="Operation", name="Operation",
icon="mdi:cog-transfer", icon="mdi:cog-transfer",
value=lambda value: value, 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( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: ConfigEntry,
@ -341,11 +387,21 @@ async def async_setup_entry(
if config_entry.data[CONF_SLEEP_PERIOD]: if config_entry.data[CONF_SLEEP_PERIOD]:
await async_setup_entry_attribute_entities( 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: else:
await async_setup_entry_attribute_entities( 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( await async_setup_entry_rest(
hass, config_entry, async_add_entities, REST_SENSORS, RestSensor hass, config_entry, async_add_entities, REST_SENSORS, RestSensor
@ -355,21 +411,27 @@ async def async_setup_entry(
class BlockSensor(ShellyBlockAttributeEntity, SensorEntity): class BlockSensor(ShellyBlockAttributeEntity, SensorEntity):
"""Represent a block sensor.""" """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 @property
def native_value(self) -> StateType: def native_value(self) -> StateType:
"""Return value of sensor.""" """Return value of sensor."""
return self.attribute_value 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): class RestSensor(ShellyRestAttributeEntity, SensorEntity):
"""Represent a REST sensor.""" """Represent a REST sensor."""
@ -396,6 +458,24 @@ class RpcSensor(ShellyRpcAttributeEntity, SensorEntity):
class BlockSleepingSensor(ShellySleepingBlockAttributeEntity, SensorEntity): class BlockSleepingSensor(ShellySleepingBlockAttributeEntity, SensorEntity):
"""Represent a block sleeping sensor.""" """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 @property
def native_value(self) -> StateType: def native_value(self) -> StateType:
"""Return value of sensor.""" """Return value of sensor."""
@ -403,13 +483,3 @@ class BlockSleepingSensor(ShellySleepingBlockAttributeEntity, SensorEntity):
return self.attribute_value return self.attribute_value
return self.last_state 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)