diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index f495fef45c3..6e0c4c86d21 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -1,7 +1,9 @@ """Support for deCONZ binary sensors.""" from __future__ import annotations -from typing import TYPE_CHECKING, TypeVar +from collections.abc import Callable +from dataclasses import dataclass +from typing import Generic, TypeVar from pydeconz.interfaces.sensors import SensorResources from pydeconz.models.event import EventType @@ -19,6 +21,7 @@ from homeassistant.components.binary_sensor import ( DOMAIN, BinarySensorDeviceClass, BinarySensorEntity, + BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE @@ -48,10 +51,130 @@ PROVIDES_EXTRA_ATTRIBUTES = ( "water", ) +T = TypeVar( + "T", + Alarm, + CarbonMonoxide, + Fire, + GenericFlag, + OpenClose, + Presence, + Vibration, + Water, + PydeconzSensorBase, +) + + +@dataclass +class DeconzBinarySensorDescriptionMixin(Generic[T]): + """Required values when describing secondary sensor attributes.""" + + update_key: str + value_fn: Callable[[T], bool | None] + + +@dataclass +class DeconzBinarySensorDescription( + BinarySensorEntityDescription, + DeconzBinarySensorDescriptionMixin[T], +): + """Class describing deCONZ binary sensor entities.""" + + instance_check: type[T] | None = None + name_suffix: str = "" + old_unique_id_suffix: str = "" + + +ENTITY_DESCRIPTIONS: tuple[DeconzBinarySensorDescription, ...] = ( + DeconzBinarySensorDescription[Alarm]( + key="alarm", + update_key="alarm", + value_fn=lambda device: device.alarm, + instance_check=Alarm, + device_class=BinarySensorDeviceClass.SAFETY, + ), + DeconzBinarySensorDescription[CarbonMonoxide]( + key="carbon_monoxide", + update_key="carbonmonoxide", + value_fn=lambda device: device.carbon_monoxide, + instance_check=CarbonMonoxide, + device_class=BinarySensorDeviceClass.CO, + ), + DeconzBinarySensorDescription[Fire]( + key="fire", + update_key="fire", + value_fn=lambda device: device.fire, + instance_check=Fire, + device_class=BinarySensorDeviceClass.SMOKE, + ), + DeconzBinarySensorDescription[Fire]( + key="in_test_mode", + update_key="test", + value_fn=lambda device: device.in_test_mode, + instance_check=Fire, + name_suffix="Test Mode", + old_unique_id_suffix="test mode", + device_class=BinarySensorDeviceClass.SMOKE, + entity_category=EntityCategory.DIAGNOSTIC, + ), + DeconzBinarySensorDescription[GenericFlag]( + key="flag", + update_key="flag", + value_fn=lambda device: device.flag, + instance_check=GenericFlag, + ), + DeconzBinarySensorDescription[OpenClose]( + key="open", + update_key="open", + value_fn=lambda device: device.open, + instance_check=OpenClose, + device_class=BinarySensorDeviceClass.OPENING, + ), + DeconzBinarySensorDescription[Presence]( + key="presence", + update_key="presence", + value_fn=lambda device: device.presence, + instance_check=Presence, + device_class=BinarySensorDeviceClass.MOTION, + ), + DeconzBinarySensorDescription[Vibration]( + key="vibration", + update_key="vibration", + value_fn=lambda device: device.vibration, + instance_check=Vibration, + device_class=BinarySensorDeviceClass.VIBRATION, + ), + DeconzBinarySensorDescription[Water]( + key="water", + update_key="water", + value_fn=lambda device: device.water, + instance_check=Water, + device_class=BinarySensorDeviceClass.MOISTURE, + ), + DeconzBinarySensorDescription[SensorResources]( + key="tampered", + update_key="tampered", + value_fn=lambda device: device.tampered, + name_suffix="Tampered", + old_unique_id_suffix="tampered", + device_class=BinarySensorDeviceClass.TAMPER, + entity_category=EntityCategory.DIAGNOSTIC, + ), + DeconzBinarySensorDescription[SensorResources]( + key="low_battery", + update_key="lowbattery", + value_fn=lambda device: device.low_battery, + name_suffix="Low Battery", + old_unique_id_suffix="low battery", + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + ), +) + @callback def async_update_unique_id( - hass: HomeAssistant, unique_id: str, entity_class: DeconzBinarySensor + hass: HomeAssistant, unique_id: str, description: DeconzBinarySensorDescription ) -> None: """Update unique ID to always have a suffix. @@ -59,12 +182,12 @@ def async_update_unique_id( """ ent_reg = er.async_get(hass) - new_unique_id = f"{unique_id}-{entity_class.unique_id_suffix}" + new_unique_id = f"{unique_id}-{description.key}" if ent_reg.async_get_entity_id(DOMAIN, DECONZ_DOMAIN, new_unique_id): return - if entity_class.old_unique_id_suffix: - unique_id = f'{unique_id.split("-", 1)[0]}-{entity_class.old_unique_id_suffix}' + if description.old_unique_id_suffix: + unique_id = f'{unique_id.split("-", 1)[0]}-{description.old_unique_id_suffix}' if entity_id := ent_reg.async_get_entity_id(DOMAIN, DECONZ_DOMAIN, unique_id): ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id) @@ -84,19 +207,14 @@ async def async_setup_entry( """Add sensor from deCONZ.""" sensor = gateway.api.sensors[sensor_id] - for sensor_type, entity_class in ENTITY_CLASSES: - if TYPE_CHECKING: - assert isinstance(entity_class, DeconzBinarySensor) + for description in ENTITY_DESCRIPTIONS: if ( - not isinstance(sensor, sensor_type) - or entity_class.unique_id_suffix is not None - and getattr(sensor, entity_class.unique_id_suffix) is None - ): + description.instance_check + and not isinstance(sensor, description.instance_check) + ) or description.value_fn(sensor) is None: continue - - async_update_unique_id(hass, sensor.unique_id, entity_class) - - async_add_entities([entity_class(sensor, gateway)]) + async_update_unique_id(hass, sensor.unique_id, description) + async_add_entities([DeconzBinarySensor(sensor, gateway, description)]) gateway.register_platform_add_device_callback( async_add_sensor, @@ -104,28 +222,43 @@ async def async_setup_entry( ) -class DeconzBinarySensor(DeconzDevice[_SensorDeviceT], BinarySensorEntity): +class DeconzBinarySensor(DeconzDevice[SensorResources], BinarySensorEntity): """Representation of a deCONZ binary sensor.""" - old_unique_id_suffix = "" TYPE = DOMAIN + entity_description: DeconzBinarySensorDescription - def __init__(self, device: _SensorDeviceT, gateway: DeconzGateway) -> None: + def __init__( + self, + device: SensorResources, + gateway: DeconzGateway, + description: DeconzBinarySensorDescription, + ) -> None: """Initialize deCONZ binary sensor.""" + self.entity_description = description + self.unique_id_suffix = description.key + self._update_key = description.update_key + if description.name_suffix: + self._name_suffix = description.name_suffix super().__init__(device, gateway) if ( - self.unique_id_suffix in PROVIDES_EXTRA_ATTRIBUTES + self.entity_description.key in PROVIDES_EXTRA_ATTRIBUTES and self._update_keys is not None ): self._update_keys.update({"on", "state"}) + @property + def is_on(self) -> bool | None: + """Return the state of the sensor.""" + return self.entity_description.value_fn(self._device) + @property def extra_state_attributes(self) -> dict[str, bool | float | int | list | None]: """Return the state attributes of the sensor.""" attr: dict[str, bool | float | int | list | None] = {} - if self.unique_id_suffix not in PROVIDES_EXTRA_ATTRIBUTES: + if self.entity_description.key not in PROVIDES_EXTRA_ATTRIBUTES: return attr if self._device.on is not None: @@ -145,179 +278,3 @@ class DeconzBinarySensor(DeconzDevice[_SensorDeviceT], BinarySensorEntity): attr[ATTR_VIBRATIONSTRENGTH] = self._device.vibration_strength return attr - - -class DeconzAlarmBinarySensor(DeconzBinarySensor[Alarm]): - """Representation of a deCONZ alarm binary sensor.""" - - unique_id_suffix = "alarm" - _update_key = "alarm" - - _attr_device_class = BinarySensorDeviceClass.SAFETY - - @property - def is_on(self) -> bool: - """Return the state of the sensor.""" - return self._device.alarm - - -class DeconzCarbonMonoxideBinarySensor(DeconzBinarySensor[CarbonMonoxide]): - """Representation of a deCONZ carbon monoxide binary sensor.""" - - unique_id_suffix = "carbon_monoxide" - _update_key = "carbonmonoxide" - - _attr_device_class = BinarySensorDeviceClass.CO - - @property - def is_on(self) -> bool: - """Return the state of the sensor.""" - return self._device.carbon_monoxide - - -class DeconzFireBinarySensor(DeconzBinarySensor[Fire]): - """Representation of a deCONZ fire binary sensor.""" - - unique_id_suffix = "fire" - _update_key = "fire" - - _attr_device_class = BinarySensorDeviceClass.SMOKE - - @property - def is_on(self) -> bool: - """Return the state of the sensor.""" - return self._device.fire - - -class DeconzFireInTestModeBinarySensor(DeconzBinarySensor[Fire]): - """Representation of a deCONZ fire in-test-mode binary sensor.""" - - _name_suffix = "Test Mode" - unique_id_suffix = "in_test_mode" - old_unique_id_suffix = "test mode" - _update_key = "test" - - _attr_device_class = BinarySensorDeviceClass.SMOKE - _attr_entity_category = EntityCategory.DIAGNOSTIC - - @property - def is_on(self) -> bool: - """Return the state of the sensor.""" - return self._device.in_test_mode - - -class DeconzFlagBinarySensor(DeconzBinarySensor[GenericFlag]): - """Representation of a deCONZ generic flag binary sensor.""" - - unique_id_suffix = "flag" - _update_key = "flag" - - @property - def is_on(self) -> bool: - """Return the state of the sensor.""" - return self._device.flag - - -class DeconzOpenCloseBinarySensor(DeconzBinarySensor[OpenClose]): - """Representation of a deCONZ open/close binary sensor.""" - - unique_id_suffix = "open" - _update_key = "open" - - _attr_device_class = BinarySensorDeviceClass.OPENING - - @property - def is_on(self) -> bool: - """Return the state of the sensor.""" - return self._device.open - - -class DeconzPresenceBinarySensor(DeconzBinarySensor[Presence]): - """Representation of a deCONZ presence binary sensor.""" - - unique_id_suffix = "presence" - _update_key = "presence" - - _attr_device_class = BinarySensorDeviceClass.MOTION - - @property - def is_on(self) -> bool: - """Return the state of the sensor.""" - return self._device.presence - - -class DeconzVibrationBinarySensor(DeconzBinarySensor[Vibration]): - """Representation of a deCONZ vibration binary sensor.""" - - unique_id_suffix = "vibration" - _update_key = "vibration" - - _attr_device_class = BinarySensorDeviceClass.VIBRATION - - @property - def is_on(self) -> bool: - """Return the state of the sensor.""" - return self._device.vibration - - -class DeconzWaterBinarySensor(DeconzBinarySensor[Water]): - """Representation of a deCONZ water binary sensor.""" - - unique_id_suffix = "water" - _update_key = "water" - - _attr_device_class = BinarySensorDeviceClass.MOISTURE - - @property - def is_on(self) -> bool: - """Return the state of the sensor.""" - return self._device.water - - -class DeconzTamperedCommonBinarySensor(DeconzBinarySensor[SensorResources]): - """Representation of a deCONZ tampered binary sensor.""" - - _name_suffix = "Tampered" - unique_id_suffix = "tampered" - old_unique_id_suffix = "tampered" - _update_key = "tampered" - - _attr_device_class = BinarySensorDeviceClass.TAMPER - _attr_entity_category = EntityCategory.DIAGNOSTIC - - @property - def is_on(self) -> bool | None: - """Return the state of the sensor.""" - return self._device.tampered - - -class DeconzLowBatteryCommonBinarySensor(DeconzBinarySensor[SensorResources]): - """Representation of a deCONZ low battery binary sensor.""" - - _name_suffix = "Low Battery" - unique_id_suffix = "low_battery" - old_unique_id_suffix = "low battery" - _update_key = "lowbattery" - - _attr_device_class = BinarySensorDeviceClass.BATTERY - _attr_entity_category = EntityCategory.DIAGNOSTIC - - @property - def is_on(self) -> bool | None: - """Return the state of the sensor.""" - return self._device.low_battery - - -ENTITY_CLASSES = ( - (Alarm, DeconzAlarmBinarySensor), - (CarbonMonoxide, DeconzCarbonMonoxideBinarySensor), - (Fire, DeconzFireBinarySensor), - (Fire, DeconzFireInTestModeBinarySensor), - (GenericFlag, DeconzFlagBinarySensor), - (OpenClose, DeconzOpenCloseBinarySensor), - (Presence, DeconzPresenceBinarySensor), - (Vibration, DeconzVibrationBinarySensor), - (Water, DeconzWaterBinarySensor), - (PydeconzSensorBase, DeconzTamperedCommonBinarySensor), - (PydeconzSensorBase, DeconzLowBatteryCommonBinarySensor), -)