core/homeassistant/components/tado/binary_sensor.py

279 lines
8.2 KiB
Python

"""Support for Tado sensors for each zone."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
import logging
from typing import Any
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from .const import (
DATA,
DOMAIN,
SIGNAL_TADO_UPDATE_RECEIVED,
TYPE_AIR_CONDITIONING,
TYPE_BATTERY,
TYPE_HEATING,
TYPE_HOT_WATER,
TYPE_POWER,
)
from .entity import TadoDeviceEntity, TadoZoneEntity
_LOGGER = logging.getLogger(__name__)
@dataclass
class TadoBinarySensorEntityDescriptionMixin:
"""Mixin for required keys."""
state_fn: Callable[[Any], bool]
@dataclass
class TadoBinarySensorEntityDescription(
BinarySensorEntityDescription, TadoBinarySensorEntityDescriptionMixin
):
"""Describes Tado binary sensor entity."""
attributes_fn: Callable[[Any], dict[Any, StateType]] | None = None
BATTERY_STATE_ENTITY_DESCRIPTION = TadoBinarySensorEntityDescription(
key="battery state",
name="Battery state",
state_fn=lambda data: data["batteryState"] == "LOW",
device_class=BinarySensorDeviceClass.BATTERY,
)
CONNECTION_STATE_ENTITY_DESCRIPTION = TadoBinarySensorEntityDescription(
key="connection state",
name="Connection state",
state_fn=lambda data: data.get("connectionState", {}).get("value", False),
device_class=BinarySensorDeviceClass.CONNECTIVITY,
)
POWER_ENTITY_DESCRIPTION = TadoBinarySensorEntityDescription(
key="power",
name="Power",
state_fn=lambda data: data.power == "ON",
device_class=BinarySensorDeviceClass.POWER,
)
LINK_ENTITY_DESCRIPTION = TadoBinarySensorEntityDescription(
key="link",
name="Link",
state_fn=lambda data: data.link == "ONLINE",
device_class=BinarySensorDeviceClass.CONNECTIVITY,
)
OVERLAY_ENTITY_DESCRIPTION = TadoBinarySensorEntityDescription(
key="overlay",
name="Overlay",
state_fn=lambda data: data.overlay_active,
attributes_fn=lambda data: {"termination": data.overlay_termination_type}
if data.overlay_active
else {},
device_class=BinarySensorDeviceClass.POWER,
)
OPEN_WINDOW_ENTITY_DESCRIPTION = TadoBinarySensorEntityDescription(
key="open window",
name="Open window",
state_fn=lambda data: bool(data.open_window or data.open_window_detected),
attributes_fn=lambda data: data.open_window_attr,
device_class=BinarySensorDeviceClass.WINDOW,
)
EARLY_START_ENTITY_DESCRIPTION = TadoBinarySensorEntityDescription(
key="early start",
name="Early start",
state_fn=lambda data: data.preparation,
device_class=BinarySensorDeviceClass.POWER,
)
DEVICE_SENSORS = {
TYPE_BATTERY: [
BATTERY_STATE_ENTITY_DESCRIPTION,
CONNECTION_STATE_ENTITY_DESCRIPTION,
],
TYPE_POWER: [
CONNECTION_STATE_ENTITY_DESCRIPTION,
],
}
ZONE_SENSORS = {
TYPE_HEATING: [
POWER_ENTITY_DESCRIPTION,
LINK_ENTITY_DESCRIPTION,
OVERLAY_ENTITY_DESCRIPTION,
OPEN_WINDOW_ENTITY_DESCRIPTION,
EARLY_START_ENTITY_DESCRIPTION,
],
TYPE_AIR_CONDITIONING: [
POWER_ENTITY_DESCRIPTION,
LINK_ENTITY_DESCRIPTION,
OVERLAY_ENTITY_DESCRIPTION,
OPEN_WINDOW_ENTITY_DESCRIPTION,
],
TYPE_HOT_WATER: [
POWER_ENTITY_DESCRIPTION,
LINK_ENTITY_DESCRIPTION,
OVERLAY_ENTITY_DESCRIPTION,
],
}
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the Tado sensor platform."""
tado = hass.data[DOMAIN][entry.entry_id][DATA]
devices = tado.devices
zones = tado.zones
entities: list[BinarySensorEntity] = []
# Create device sensors
for device in devices:
if "batteryState" in device:
device_type = TYPE_BATTERY
else:
device_type = TYPE_POWER
entities.extend(
[
TadoDeviceBinarySensor(tado, device, entity_description)
for entity_description in DEVICE_SENSORS[device_type]
]
)
# Create zone sensors
for zone in zones:
zone_type = zone["type"]
if zone_type not in ZONE_SENSORS:
_LOGGER.warning("Unknown zone type skipped: %s", zone_type)
continue
entities.extend(
[
TadoZoneBinarySensor(tado, zone["name"], zone["id"], entity_description)
for entity_description in ZONE_SENSORS[zone_type]
]
)
async_add_entities(entities, True)
class TadoDeviceBinarySensor(TadoDeviceEntity, BinarySensorEntity):
"""Representation of a tado Sensor."""
entity_description: TadoBinarySensorEntityDescription
_attr_has_entity_name = True
def __init__(
self, tado, device_info, entity_description: TadoBinarySensorEntityDescription
) -> None:
"""Initialize of the Tado Sensor."""
self.entity_description = entity_description
self._tado = tado
super().__init__(device_info)
self._attr_unique_id = (
f"{entity_description.key} {self.device_id} {tado.home_id}"
)
async def async_added_to_hass(self) -> None:
"""Register for sensor updates."""
self.async_on_remove(
async_dispatcher_connect(
self.hass,
SIGNAL_TADO_UPDATE_RECEIVED.format(
self._tado.home_id, "device", self.device_id
),
self._async_update_callback,
)
)
self._async_update_device_data()
@callback
def _async_update_callback(self):
"""Update and write state."""
self._async_update_device_data()
self.async_write_ha_state()
@callback
def _async_update_device_data(self):
"""Handle update callbacks."""
try:
self._device_info = self._tado.data["device"][self.device_id]
except KeyError:
return
self._attr_is_on = self.entity_description.state_fn(self._device_info)
if self.entity_description.attributes_fn is not None:
self._attr_extra_state_attributes = self.entity_description.attributes_fn(
self._device_info
)
class TadoZoneBinarySensor(TadoZoneEntity, BinarySensorEntity):
"""Representation of a tado Sensor."""
entity_description: TadoBinarySensorEntityDescription
_attr_has_entity_name = True
def __init__(
self,
tado,
zone_name,
zone_id,
entity_description: TadoBinarySensorEntityDescription,
) -> None:
"""Initialize of the Tado Sensor."""
self.entity_description = entity_description
self._tado = tado
super().__init__(zone_name, tado.home_id, zone_id)
self._attr_unique_id = f"{entity_description.key} {zone_id} {tado.home_id}"
async def async_added_to_hass(self) -> None:
"""Register for sensor updates."""
self.async_on_remove(
async_dispatcher_connect(
self.hass,
SIGNAL_TADO_UPDATE_RECEIVED.format(
self._tado.home_id, "zone", self.zone_id
),
self._async_update_callback,
)
)
self._async_update_zone_data()
@callback
def _async_update_callback(self):
"""Update and write state."""
self._async_update_zone_data()
self.async_write_ha_state()
@callback
def _async_update_zone_data(self):
"""Handle update callbacks."""
try:
tado_zone_data = self._tado.data["zone"][self.zone_id]
except KeyError:
return
self._attr_is_on = self.entity_description.state_fn(tado_zone_data)
if self.entity_description.attributes_fn is not None:
self._attr_extra_state_attributes = self.entity_description.attributes_fn(
tado_zone_data
)