* Rename secondary_temperature with internal_temperature * Prefix binary and sensor descriptions matching on all sensor devices with COMMON_ * Always create entities in the same order Its been reported previously that if the integration is removed and setup again that entity IDs can change if not sorted in the numerical order * Rename alarmsystems to alarm_systems * Use websocket enums * Don't use legacy pydeconz constants * Bump pydeconz to v103 * unsub -> unsubscribe
300 lines
9.3 KiB
Python
300 lines
9.3 KiB
Python
"""Support for deCONZ binary sensors."""
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Callable
|
|
from dataclasses import dataclass
|
|
|
|
from pydeconz.interfaces.sensors import SensorResources
|
|
from pydeconz.models.event import EventType
|
|
from pydeconz.models.sensor.alarm import Alarm
|
|
from pydeconz.models.sensor.carbon_monoxide import CarbonMonoxide
|
|
from pydeconz.models.sensor.fire import Fire
|
|
from pydeconz.models.sensor.generic_flag import GenericFlag
|
|
from pydeconz.models.sensor.open_close import OpenClose
|
|
from pydeconz.models.sensor.presence import Presence
|
|
from pydeconz.models.sensor.vibration import Vibration
|
|
from pydeconz.models.sensor.water import Water
|
|
|
|
from homeassistant.components.binary_sensor import (
|
|
DOMAIN,
|
|
BinarySensorDeviceClass,
|
|
BinarySensorEntity,
|
|
BinarySensorEntityDescription,
|
|
)
|
|
from homeassistant.config_entries import ConfigEntry
|
|
from homeassistant.const import ATTR_TEMPERATURE
|
|
from homeassistant.core import HomeAssistant, callback
|
|
from homeassistant.helpers.entity import EntityCategory
|
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|
import homeassistant.helpers.entity_registry as er
|
|
|
|
from .const import ATTR_DARK, ATTR_ON, DOMAIN as DECONZ_DOMAIN
|
|
from .deconz_device import DeconzDevice
|
|
from .gateway import DeconzGateway, get_gateway_from_config_entry
|
|
|
|
ATTR_ORIENTATION = "orientation"
|
|
ATTR_TILTANGLE = "tiltangle"
|
|
ATTR_VIBRATIONSTRENGTH = "vibrationstrength"
|
|
|
|
PROVIDES_EXTRA_ATTRIBUTES = (
|
|
"alarm",
|
|
"carbon_monoxide",
|
|
"fire",
|
|
"flag",
|
|
"open",
|
|
"presence",
|
|
"vibration",
|
|
"water",
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class DeconzBinarySensorDescriptionMixin:
|
|
"""Required values when describing secondary sensor attributes."""
|
|
|
|
suffix: str
|
|
update_key: str
|
|
value_fn: Callable[[SensorResources], bool | None]
|
|
|
|
|
|
@dataclass
|
|
class DeconzBinarySensorDescription(
|
|
BinarySensorEntityDescription,
|
|
DeconzBinarySensorDescriptionMixin,
|
|
):
|
|
"""Class describing deCONZ binary sensor entities."""
|
|
|
|
|
|
ENTITY_DESCRIPTIONS = {
|
|
Alarm: [
|
|
DeconzBinarySensorDescription(
|
|
key="alarm",
|
|
value_fn=lambda device: device.alarm if isinstance(device, Alarm) else None,
|
|
suffix="",
|
|
update_key="alarm",
|
|
device_class=BinarySensorDeviceClass.SAFETY,
|
|
)
|
|
],
|
|
CarbonMonoxide: [
|
|
DeconzBinarySensorDescription(
|
|
key="carbon_monoxide",
|
|
value_fn=lambda device: device.carbon_monoxide
|
|
if isinstance(device, CarbonMonoxide)
|
|
else None,
|
|
suffix="",
|
|
update_key="carbonmonoxide",
|
|
device_class=BinarySensorDeviceClass.CO,
|
|
)
|
|
],
|
|
Fire: [
|
|
DeconzBinarySensorDescription(
|
|
key="fire",
|
|
value_fn=lambda device: device.fire if isinstance(device, Fire) else None,
|
|
suffix="",
|
|
update_key="fire",
|
|
device_class=BinarySensorDeviceClass.SMOKE,
|
|
),
|
|
DeconzBinarySensorDescription(
|
|
key="in_test_mode",
|
|
value_fn=lambda device: device.in_test_mode
|
|
if isinstance(device, Fire)
|
|
else None,
|
|
suffix="Test Mode",
|
|
update_key="test",
|
|
device_class=BinarySensorDeviceClass.SMOKE,
|
|
entity_category=EntityCategory.DIAGNOSTIC,
|
|
),
|
|
],
|
|
GenericFlag: [
|
|
DeconzBinarySensorDescription(
|
|
key="flag",
|
|
value_fn=lambda device: device.flag
|
|
if isinstance(device, GenericFlag)
|
|
else None,
|
|
suffix="",
|
|
update_key="flag",
|
|
)
|
|
],
|
|
OpenClose: [
|
|
DeconzBinarySensorDescription(
|
|
key="open",
|
|
value_fn=lambda device: device.open
|
|
if isinstance(device, OpenClose)
|
|
else None,
|
|
suffix="",
|
|
update_key="open",
|
|
device_class=BinarySensorDeviceClass.OPENING,
|
|
)
|
|
],
|
|
Presence: [
|
|
DeconzBinarySensorDescription(
|
|
key="presence",
|
|
value_fn=lambda device: device.presence
|
|
if isinstance(device, Presence)
|
|
else None,
|
|
suffix="",
|
|
update_key="presence",
|
|
device_class=BinarySensorDeviceClass.MOTION,
|
|
)
|
|
],
|
|
Vibration: [
|
|
DeconzBinarySensorDescription(
|
|
key="vibration",
|
|
value_fn=lambda device: device.vibration
|
|
if isinstance(device, Vibration)
|
|
else None,
|
|
suffix="",
|
|
update_key="vibration",
|
|
device_class=BinarySensorDeviceClass.VIBRATION,
|
|
)
|
|
],
|
|
Water: [
|
|
DeconzBinarySensorDescription(
|
|
key="water",
|
|
value_fn=lambda device: device.water if isinstance(device, Water) else None,
|
|
suffix="",
|
|
update_key="water",
|
|
device_class=BinarySensorDeviceClass.MOISTURE,
|
|
)
|
|
],
|
|
}
|
|
|
|
COMMON_BINARY_SENSOR_DESCRIPTIONS = [
|
|
DeconzBinarySensorDescription(
|
|
key="tampered",
|
|
value_fn=lambda device: device.tampered,
|
|
suffix="Tampered",
|
|
update_key="tampered",
|
|
device_class=BinarySensorDeviceClass.TAMPER,
|
|
entity_category=EntityCategory.DIAGNOSTIC,
|
|
),
|
|
DeconzBinarySensorDescription(
|
|
key="low_battery",
|
|
value_fn=lambda device: device.low_battery,
|
|
suffix="Low Battery",
|
|
update_key="lowbattery",
|
|
device_class=BinarySensorDeviceClass.BATTERY,
|
|
entity_category=EntityCategory.DIAGNOSTIC,
|
|
),
|
|
]
|
|
|
|
|
|
@callback
|
|
def async_update_unique_id(
|
|
hass: HomeAssistant, unique_id: str, description: DeconzBinarySensorDescription
|
|
) -> None:
|
|
"""Update unique ID to always have a suffix.
|
|
|
|
Introduced with release 2022.7.
|
|
"""
|
|
ent_reg = er.async_get(hass)
|
|
|
|
new_unique_id = f"{unique_id}-{description.key}"
|
|
if ent_reg.async_get_entity_id(DOMAIN, DECONZ_DOMAIN, new_unique_id):
|
|
return
|
|
|
|
if description.suffix:
|
|
unique_id = f'{unique_id.split("-", 1)[0]}-{description.suffix.lower()}'
|
|
|
|
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)
|
|
|
|
|
|
async def async_setup_entry(
|
|
hass: HomeAssistant,
|
|
config_entry: ConfigEntry,
|
|
async_add_entities: AddEntitiesCallback,
|
|
) -> None:
|
|
"""Set up the deCONZ binary sensor."""
|
|
gateway = get_gateway_from_config_entry(hass, config_entry)
|
|
gateway.entities[DOMAIN] = set()
|
|
|
|
@callback
|
|
def async_add_sensor(_: EventType, sensor_id: str) -> None:
|
|
"""Add sensor from deCONZ."""
|
|
sensor = gateway.api.sensors[sensor_id]
|
|
|
|
for description in (
|
|
ENTITY_DESCRIPTIONS.get(type(sensor), [])
|
|
+ COMMON_BINARY_SENSOR_DESCRIPTIONS
|
|
):
|
|
if (
|
|
not hasattr(sensor, description.key)
|
|
or description.value_fn(sensor) is None
|
|
):
|
|
continue
|
|
|
|
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,
|
|
gateway.api.sensors,
|
|
)
|
|
|
|
|
|
class DeconzBinarySensor(DeconzDevice[SensorResources], BinarySensorEntity):
|
|
"""Representation of a deCONZ binary sensor."""
|
|
|
|
TYPE = DOMAIN
|
|
entity_description: DeconzBinarySensorDescription
|
|
|
|
def __init__(
|
|
self,
|
|
device: SensorResources,
|
|
gateway: DeconzGateway,
|
|
description: DeconzBinarySensorDescription,
|
|
) -> None:
|
|
"""Initialize deCONZ binary sensor."""
|
|
self.entity_description: DeconzBinarySensorDescription = description
|
|
super().__init__(device, gateway)
|
|
|
|
if description.suffix:
|
|
self._attr_name = f"{self._device.name} {description.suffix}"
|
|
|
|
self._update_keys = {description.update_key, "reachable"}
|
|
if self.entity_description.key in PROVIDES_EXTRA_ATTRIBUTES:
|
|
self._update_keys.update({"on", "state"})
|
|
|
|
@property
|
|
def unique_id(self) -> str:
|
|
"""Return a unique identifier for this device."""
|
|
return f"{super().unique_id}-{self.entity_description.key}"
|
|
|
|
@callback
|
|
def async_update_callback(self) -> None:
|
|
"""Update the sensor's state."""
|
|
if self._device.changed_keys.intersection(self._update_keys):
|
|
super().async_update_callback()
|
|
|
|
@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.entity_description.key not in PROVIDES_EXTRA_ATTRIBUTES:
|
|
return attr
|
|
|
|
if self._device.on is not None:
|
|
attr[ATTR_ON] = self._device.on
|
|
|
|
if self._device.internal_temperature is not None:
|
|
attr[ATTR_TEMPERATURE] = self._device.internal_temperature
|
|
|
|
if isinstance(self._device, Presence):
|
|
|
|
if self._device.dark is not None:
|
|
attr[ATTR_DARK] = self._device.dark
|
|
|
|
elif isinstance(self._device, Vibration):
|
|
attr[ATTR_ORIENTATION] = self._device.orientation
|
|
attr[ATTR_TILTANGLE] = self._device.tilt_angle
|
|
attr[ATTR_VIBRATIONSTRENGTH] = self._device.vibration_strength
|
|
|
|
return attr
|