Refactor Tado to use entity descriptions and new naming style (#75750)

* Refactor Tado to use entity descriptions and new naming style

* minor fixes

* typing
pull/90192/head
avee87 2023-03-28 13:24:19 +01:00 committed by GitHub
parent 0eb409cff1
commit cc404cfe77
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 274 additions and 298 deletions

View File

@ -1,14 +1,21 @@
"""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,
@ -24,31 +31,99 @@ 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",
"connection state",
BATTERY_STATE_ENTITY_DESCRIPTION,
CONNECTION_STATE_ENTITY_DESCRIPTION,
],
TYPE_POWER: [
"connection state",
CONNECTION_STATE_ENTITY_DESCRIPTION,
],
}
ZONE_SENSORS = {
TYPE_HEATING: [
"power",
"link",
"overlay",
"early start",
"open window",
POWER_ENTITY_DESCRIPTION,
LINK_ENTITY_DESCRIPTION,
OVERLAY_ENTITY_DESCRIPTION,
OPEN_WINDOW_ENTITY_DESCRIPTION,
EARLY_START_ENTITY_DESCRIPTION,
],
TYPE_AIR_CONDITIONING: [
"power",
"link",
"overlay",
"open window",
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,
],
TYPE_HOT_WATER: ["power", "link", "overlay"],
}
@ -71,8 +146,8 @@ async def async_setup_entry(
entities.extend(
[
TadoDeviceBinarySensor(tado, device, variable)
for variable in DEVICE_SENSORS[device_type]
TadoDeviceBinarySensor(tado, device, entity_description)
for entity_description in DEVICE_SENSORS[device_type]
]
)
@ -85,8 +160,8 @@ async def async_setup_entry(
entities.extend(
[
TadoZoneBinarySensor(tado, zone["name"], zone["id"], variable)
for variable in ZONE_SENSORS[zone_type]
TadoZoneBinarySensor(tado, zone["name"], zone["id"], entity_description)
for entity_description in ZONE_SENSORS[zone_type]
]
)
@ -96,16 +171,21 @@ async def async_setup_entry(
class TadoDeviceBinarySensor(TadoDeviceEntity, BinarySensorEntity):
"""Representation of a tado Sensor."""
def __init__(self, tado, device_info, device_variable):
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.device_variable = device_variable
self._unique_id = f"{device_variable} {self.device_id} {tado.home_id}"
self._state = None
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."""
@ -121,30 +201,6 @@ class TadoDeviceBinarySensor(TadoDeviceEntity, BinarySensorEntity):
)
self._async_update_device_data()
@property
def unique_id(self):
"""Return the unique id."""
return self._unique_id
@property
def name(self):
"""Return the name of the sensor."""
return f"{self.device_name} {self.device_variable}"
@property
def is_on(self):
"""Return true if sensor is on."""
return self._state
@property
def device_class(self):
"""Return the class of this sensor."""
if self.device_variable == "battery state":
return BinarySensorDeviceClass.BATTERY
if self.device_variable == "connection state":
return BinarySensorDeviceClass.CONNECTIVITY
return None
@callback
def _async_update_callback(self):
"""Update and write state."""
@ -159,29 +215,33 @@ class TadoDeviceBinarySensor(TadoDeviceEntity, BinarySensorEntity):
except KeyError:
return
if self.device_variable == "battery state":
self._state = self._device_info["batteryState"] == "LOW"
elif self.device_variable == "connection state":
self._state = self._device_info.get("connectionState", {}).get(
"value", False
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."""
def __init__(self, tado, zone_name, zone_id, zone_variable):
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.zone_variable = zone_variable
self._unique_id = f"{zone_variable} {zone_id} {tado.home_id}"
self._state = None
self._state_attributes = None
self._tado_zone_data = None
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."""
@ -197,41 +257,6 @@ class TadoZoneBinarySensor(TadoZoneEntity, BinarySensorEntity):
)
self._async_update_zone_data()
@property
def unique_id(self):
"""Return the unique id."""
return self._unique_id
@property
def name(self):
"""Return the name of the sensor."""
return f"{self.zone_name} {self.zone_variable}"
@property
def is_on(self):
"""Return true if sensor is on."""
return self._state
@property
def device_class(self):
"""Return the class of this sensor."""
if self.zone_variable == "early start":
return BinarySensorDeviceClass.POWER
if self.zone_variable == "link":
return BinarySensorDeviceClass.CONNECTIVITY
if self.zone_variable == "open window":
return BinarySensorDeviceClass.WINDOW
if self.zone_variable == "overlay":
return BinarySensorDeviceClass.POWER
if self.zone_variable == "power":
return BinarySensorDeviceClass.POWER
return None
@property
def extra_state_attributes(self):
"""Return the state attributes."""
return self._state_attributes
@callback
def _async_update_callback(self):
"""Update and write state."""
@ -242,29 +267,12 @@ class TadoZoneBinarySensor(TadoZoneEntity, BinarySensorEntity):
def _async_update_zone_data(self):
"""Handle update callbacks."""
try:
self._tado_zone_data = self._tado.data["zone"][self.zone_id]
tado_zone_data = self._tado.data["zone"][self.zone_id]
except KeyError:
return
if self.zone_variable == "power":
self._state = self._tado_zone_data.power == "ON"
elif self.zone_variable == "link":
self._state = self._tado_zone_data.link == "ONLINE"
elif self.zone_variable == "overlay":
self._state = self._tado_zone_data.overlay_active
if self._tado_zone_data.overlay_active:
self._state_attributes = {
"termination": self._tado_zone_data.overlay_termination_type
}
elif self.zone_variable == "early start":
self._state = self._tado_zone_data.preparation
elif self.zone_variable == "open window":
self._state = bool(
self._tado_zone_data.open_window
or self._tado_zone_data.open_window_detected
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
)
self._state_attributes = self._tado_zone_data.open_window_attr

View File

@ -240,7 +240,11 @@ class TadoClimate(TadoZoneEntity, ClimateEntity):
self.zone_id = zone_id
self.zone_type = zone_type
self._unique_id = f"{zone_type} {zone_id} {tado.home_id}"
self._attr_unique_id = f"{zone_type} {zone_id} {tado.home_id}"
self._attr_name = zone_name
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
self._device_info = device_info
self._device_id = self._device_info["shortSerialNo"]
@ -288,16 +292,6 @@ class TadoClimate(TadoZoneEntity, ClimateEntity):
)
)
@property
def name(self):
"""Return the name of the entity."""
return self.zone_name
@property
def unique_id(self):
"""Return the unique id."""
return self._unique_id
@property
def current_humidity(self):
"""Return the current humidity."""

View File

@ -33,6 +33,8 @@ class TadoDeviceEntity(Entity):
class TadoHomeEntity(Entity):
"""Base implementation for Tado home."""
_attr_should_poll = False
def __init__(self, tado):
"""Initialize a Tado home."""
super().__init__()

View File

@ -1,9 +1,15 @@
"""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.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
@ -11,6 +17,7 @@ from homeassistant.const import PERCENTAGE, UnitOfTemperature
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 (
CONDITIONS_MAP,
@ -25,26 +32,108 @@ from .entity import TadoHomeEntity, TadoZoneEntity
_LOGGER = logging.getLogger(__name__)
HOME_SENSORS = {
"outdoor temperature",
"solar percentage",
"weather condition",
}
@dataclass
class TadoSensorEntityDescriptionMixin:
"""Mixin for required keys."""
state_fn: Callable[[Any], StateType]
@dataclass
class TadoSensorEntityDescription(
SensorEntityDescription, TadoSensorEntityDescriptionMixin
):
"""Describes Tado sensor entity."""
attributes_fn: Callable[[Any], dict[Any, StateType]] | None = None
HOME_SENSORS = [
TadoSensorEntityDescription(
key="outdoor temperature",
name="Outdoor temperature",
state_fn=lambda data: data["outsideTemperature"]["celsius"],
attributes_fn=lambda data: {
"time": data["outsideTemperature"]["timestamp"],
},
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
),
TadoSensorEntityDescription(
key="solar percentage",
name="Solar percentage",
state_fn=lambda data: data["solarIntensity"]["percentage"],
attributes_fn=lambda data: {
"time": data["solarIntensity"]["timestamp"],
},
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
TadoSensorEntityDescription(
key="weather condition",
name="Weather condition",
state_fn=lambda data: format_condition(data["weatherState"]["value"]),
attributes_fn=lambda data: {"time": data["weatherState"]["timestamp"]},
),
]
TEMPERATURE_ENTITY_DESCRIPTION = TadoSensorEntityDescription(
key="temperature",
name="Temperature",
state_fn=lambda data: data.current_temp,
attributes_fn=lambda data: {
"time": data.current_temp_timestamp,
"setting": 0, # setting is used in climate device
},
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
)
HUMIDITY_ENTITY_DESCRIPTION = TadoSensorEntityDescription(
key="humidity",
name="Humidity",
state_fn=lambda data: data.current_humidity,
attributes_fn=lambda data: {"time": data.current_humidity_timestamp},
native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
)
TADO_MODE_ENTITY_DESCRIPTION = TadoSensorEntityDescription(
key="tado mode",
name="Tado mode",
state_fn=lambda data: data.tado_mode,
)
HEATING_ENTITY_DESCRIPTION = TadoSensorEntityDescription(
key="heating",
name="Heating",
state_fn=lambda data: data.heating_power_percentage,
attributes_fn=lambda data: {"time": data.heating_power_timestamp},
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
)
AC_ENTITY_DESCRIPTION = TadoSensorEntityDescription(
key="ac",
name="AC",
state_fn=lambda data: data.ac_power,
attributes_fn=lambda data: {"time": data.ac_power_timestamp},
)
ZONE_SENSORS = {
TYPE_HEATING: [
"temperature",
"humidity",
"heating",
"tado mode",
TEMPERATURE_ENTITY_DESCRIPTION,
HUMIDITY_ENTITY_DESCRIPTION,
TADO_MODE_ENTITY_DESCRIPTION,
HEATING_ENTITY_DESCRIPTION,
],
TYPE_AIR_CONDITIONING: [
"temperature",
"humidity",
"ac",
"tado mode",
TEMPERATURE_ENTITY_DESCRIPTION,
HUMIDITY_ENTITY_DESCRIPTION,
TADO_MODE_ENTITY_DESCRIPTION,
AC_ENTITY_DESCRIPTION,
],
TYPE_HOT_WATER: ["tado mode"],
TYPE_HOT_WATER: [TADO_MODE_ENTITY_DESCRIPTION],
}
@ -66,7 +155,12 @@ async def async_setup_entry(
entities: list[SensorEntity] = []
# Create home sensors
entities.extend([TadoHomeSensor(tado, variable) for variable in HOME_SENSORS])
entities.extend(
[
TadoHomeSensor(tado, entity_description)
for entity_description in HOME_SENSORS
]
)
# Create zone sensors
for zone in zones:
@ -77,8 +171,8 @@ async def async_setup_entry(
entities.extend(
[
TadoZoneSensor(tado, zone["name"], zone["id"], variable)
for variable in ZONE_SENSORS[zone_type]
TadoZoneSensor(tado, zone["name"], zone["id"], entity_description)
for entity_description in ZONE_SENSORS[zone_type]
]
)
@ -88,18 +182,17 @@ async def async_setup_entry(
class TadoHomeSensor(TadoHomeEntity, SensorEntity):
"""Representation of a Tado Sensor."""
def __init__(self, tado, home_variable):
entity_description: TadoSensorEntityDescription
_attr_has_entity_name = True
def __init__(self, tado, entity_description: TadoSensorEntityDescription) -> None:
"""Initialize of the Tado Sensor."""
self.entity_description = entity_description
super().__init__(tado)
self._tado = tado
self.home_variable = home_variable
self._unique_id = f"{home_variable} {tado.home_id}"
self._state = None
self._state_attributes = None
self._tado_weather_data = self._tado.data["weather"]
self._attr_unique_id = f"{entity_description.key} {tado.home_id}"
async def async_added_to_hass(self) -> None:
"""Register for sensor updates."""
@ -115,50 +208,6 @@ class TadoHomeSensor(TadoHomeEntity, SensorEntity):
)
self._async_update_home_data()
@property
def unique_id(self):
"""Return the unique id."""
return self._unique_id
@property
def name(self):
"""Return the name of the sensor."""
return f"{self._tado.home_name} {self.home_variable}"
@property
def native_value(self):
"""Return the state of the sensor."""
return self._state
@property
def extra_state_attributes(self):
"""Return the state attributes."""
return self._state_attributes
@property
def native_unit_of_measurement(self):
"""Return the unit of measurement."""
if self.home_variable in ["temperature", "outdoor temperature"]:
return UnitOfTemperature.CELSIUS
if self.home_variable == "solar percentage":
return PERCENTAGE
if self.home_variable == "weather condition":
return None
@property
def device_class(self):
"""Return the device class."""
if self.home_variable == "outdoor temperature":
return SensorDeviceClass.TEMPERATURE
return None
@property
def state_class(self):
"""Return the state class."""
if self.home_variable in ["outdoor temperature", "solar percentage"]:
return SensorStateClass.MEASUREMENT
return None
@callback
def _async_update_callback(self):
"""Update and write state."""
@ -169,46 +218,37 @@ class TadoHomeSensor(TadoHomeEntity, SensorEntity):
def _async_update_home_data(self):
"""Handle update callbacks."""
try:
self._tado_weather_data = self._tado.data["weather"]
tado_weather_data = self._tado.data["weather"]
except KeyError:
return
if self.home_variable == "outdoor temperature":
self._state = self._tado_weather_data["outsideTemperature"]["celsius"]
self._state_attributes = {
"time": self._tado_weather_data["outsideTemperature"]["timestamp"],
}
elif self.home_variable == "solar percentage":
self._state = self._tado_weather_data["solarIntensity"]["percentage"]
self._state_attributes = {
"time": self._tado_weather_data["solarIntensity"]["timestamp"],
}
elif self.home_variable == "weather condition":
self._state = format_condition(
self._tado_weather_data["weatherState"]["value"]
self._attr_native_value = self.entity_description.state_fn(tado_weather_data)
if self.entity_description.attributes_fn is not None:
self._attr_extra_state_attributes = self.entity_description.attributes_fn(
tado_weather_data
)
self._state_attributes = {
"time": self._tado_weather_data["weatherState"]["timestamp"]
}
class TadoZoneSensor(TadoZoneEntity, SensorEntity):
"""Representation of a tado Sensor."""
def __init__(self, tado, zone_name, zone_id, zone_variable):
entity_description: TadoSensorEntityDescription
_attr_has_entity_name = True
def __init__(
self,
tado,
zone_name,
zone_id,
entity_description: TadoSensorEntityDescription,
) -> None:
"""Initialize of the Tado Sensor."""
self.entity_description = entity_description
self._tado = tado
super().__init__(zone_name, tado.home_id, zone_id)
self.zone_variable = zone_variable
self._unique_id = f"{zone_variable} {zone_id} {tado.home_id}"
self._state = None
self._state_attributes = None
self._tado_zone_data = None
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."""
@ -224,54 +264,6 @@ class TadoZoneSensor(TadoZoneEntity, SensorEntity):
)
self._async_update_zone_data()
@property
def unique_id(self):
"""Return the unique id."""
return self._unique_id
@property
def name(self):
"""Return the name of the sensor."""
return f"{self.zone_name} {self.zone_variable}"
@property
def native_value(self):
"""Return the state of the sensor."""
return self._state
@property
def extra_state_attributes(self):
"""Return the state attributes."""
return self._state_attributes
@property
def native_unit_of_measurement(self):
"""Return the unit of measurement."""
if self.zone_variable == "temperature":
return UnitOfTemperature.CELSIUS
if self.zone_variable == "humidity":
return PERCENTAGE
if self.zone_variable == "heating":
return PERCENTAGE
if self.zone_variable == "ac":
return None
@property
def device_class(self):
"""Return the device class."""
if self.zone_variable == "humidity":
return SensorDeviceClass.HUMIDITY
if self.zone_variable == "temperature":
return SensorDeviceClass.TEMPERATURE
return None
@property
def state_class(self):
"""Return the state class."""
if self.zone_variable in ["heating", "humidity", "temperature"]:
return SensorStateClass.MEASUREMENT
return None
@callback
def _async_update_callback(self):
"""Update and write state."""
@ -282,32 +274,12 @@ class TadoZoneSensor(TadoZoneEntity, SensorEntity):
def _async_update_zone_data(self):
"""Handle update callbacks."""
try:
self._tado_zone_data = self._tado.data["zone"][self.zone_id]
tado_zone_data = self._tado.data["zone"][self.zone_id]
except KeyError:
return
if self.zone_variable == "temperature":
self._state = self._tado_zone_data.current_temp
self._state_attributes = {
"time": self._tado_zone_data.current_temp_timestamp,
"setting": 0, # setting is used in climate device
}
elif self.zone_variable == "humidity":
self._state = self._tado_zone_data.current_humidity
self._state_attributes = {
"time": self._tado_zone_data.current_humidity_timestamp
}
elif self.zone_variable == "heating":
self._state = self._tado_zone_data.heating_power_percentage
self._state_attributes = {
"time": self._tado_zone_data.heating_power_timestamp
}
elif self.zone_variable == "ac":
self._state = self._tado_zone_data.ac_power
self._state_attributes = {"time": self._tado_zone_data.ac_power_timestamp}
elif self.zone_variable == "tado mode":
self._state = self._tado_zone_data.tado_mode
self._attr_native_value = 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
)