Home Assistant Unofficial Reference 2024.12.1
sensor.py
Go to the documentation of this file.
1 """Support for Tado sensors for each zone."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable
6 from dataclasses import dataclass
7 import logging
8 from typing import Any
9 
11  SensorDeviceClass,
12  SensorEntity,
13  SensorEntityDescription,
14  SensorStateClass,
15 )
16 from homeassistant.const import PERCENTAGE, UnitOfTemperature
17 from homeassistant.core import HomeAssistant, callback
18 from homeassistant.helpers.dispatcher import async_dispatcher_connect
19 from homeassistant.helpers.entity_platform import AddEntitiesCallback
20 from homeassistant.helpers.typing import StateType
21 
22 from . import TadoConfigEntry
23 from .const import (
24  CONDITIONS_MAP,
25  SENSOR_DATA_CATEGORY_GEOFENCE,
26  SENSOR_DATA_CATEGORY_WEATHER,
27  SIGNAL_TADO_UPDATE_RECEIVED,
28  TYPE_AIR_CONDITIONING,
29  TYPE_HEATING,
30  TYPE_HOT_WATER,
31 )
32 from .entity import TadoHomeEntity, TadoZoneEntity
33 from .tado_connector import TadoConnector
34 
35 _LOGGER = logging.getLogger(__name__)
36 
37 
38 @dataclass(frozen=True, kw_only=True)
40  """Describes Tado sensor entity."""
41 
42  state_fn: Callable[[Any], StateType]
43 
44  attributes_fn: Callable[[Any], dict[Any, StateType]] | None = None
45  data_category: str | None = None
46 
47 
48 def format_condition(condition: str) -> str:
49  """Return condition from dict CONDITIONS_MAP."""
50  for key, value in CONDITIONS_MAP.items():
51  if condition in value:
52  return key
53  return condition
54 
55 
56 def get_tado_mode(data: dict[str, str]) -> str | None:
57  """Return Tado Mode based on Presence attribute."""
58  if "presence" in data:
59  return data["presence"]
60  return None
61 
62 
63 def get_automatic_geofencing(data: dict[str, str]) -> bool:
64  """Return whether Automatic Geofencing is enabled based on Presence Locked attribute."""
65  if "presenceLocked" in data:
66  if data["presenceLocked"]:
67  return False
68  return True
69  return False
70 
71 
72 def get_geofencing_mode(data: dict[str, str]) -> str:
73  """Return Geofencing Mode based on Presence and Presence Locked attributes."""
74  tado_mode = data.get("presence", "unknown")
75 
76  if "presenceLocked" in data:
77  if data["presenceLocked"]:
78  geofencing_switch_mode = "manual"
79  else:
80  geofencing_switch_mode = "auto"
81  else:
82  geofencing_switch_mode = "manual"
83 
84  return f"{tado_mode.capitalize()} ({geofencing_switch_mode.capitalize()})"
85 
86 
87 HOME_SENSORS = [
89  key="outdoor temperature",
90  translation_key="outdoor_temperature",
91  state_fn=lambda data: data["outsideTemperature"]["celsius"],
92  attributes_fn=lambda data: {
93  "time": data["outsideTemperature"]["timestamp"],
94  },
95  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
96  device_class=SensorDeviceClass.TEMPERATURE,
97  state_class=SensorStateClass.MEASUREMENT,
98  data_category=SENSOR_DATA_CATEGORY_WEATHER,
99  ),
101  key="solar percentage",
102  translation_key="solar_percentage",
103  state_fn=lambda data: data["solarIntensity"]["percentage"],
104  attributes_fn=lambda data: {
105  "time": data["solarIntensity"]["timestamp"],
106  },
107  native_unit_of_measurement=PERCENTAGE,
108  state_class=SensorStateClass.MEASUREMENT,
109  data_category=SENSOR_DATA_CATEGORY_WEATHER,
110  ),
112  key="weather condition",
113  translation_key="weather_condition",
114  state_fn=lambda data: format_condition(data["weatherState"]["value"]),
115  attributes_fn=lambda data: {"time": data["weatherState"]["timestamp"]},
116  data_category=SENSOR_DATA_CATEGORY_WEATHER,
117  ),
119  key="tado mode",
120  translation_key="tado_mode",
121  state_fn=get_tado_mode,
122  data_category=SENSOR_DATA_CATEGORY_GEOFENCE,
123  ),
125  key="geofencing mode",
126  translation_key="geofencing_mode",
127  state_fn=get_geofencing_mode,
128  data_category=SENSOR_DATA_CATEGORY_GEOFENCE,
129  ),
131  key="automatic geofencing",
132  translation_key="automatic_geofencing",
133  state_fn=get_automatic_geofencing,
134  data_category=SENSOR_DATA_CATEGORY_GEOFENCE,
135  ),
136 ]
137 
138 TEMPERATURE_ENTITY_DESCRIPTION = TadoSensorEntityDescription(
139  key="temperature",
140  state_fn=lambda data: data.current_temp,
141  attributes_fn=lambda data: {
142  "time": data.current_temp_timestamp,
143  "setting": 0, # setting is used in climate device
144  },
145  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
146  device_class=SensorDeviceClass.TEMPERATURE,
147  state_class=SensorStateClass.MEASUREMENT,
148 )
149 HUMIDITY_ENTITY_DESCRIPTION = TadoSensorEntityDescription(
150  key="humidity",
151  state_fn=lambda data: data.current_humidity,
152  attributes_fn=lambda data: {"time": data.current_humidity_timestamp},
153  native_unit_of_measurement=PERCENTAGE,
154  device_class=SensorDeviceClass.HUMIDITY,
155  state_class=SensorStateClass.MEASUREMENT,
156 )
157 TADO_MODE_ENTITY_DESCRIPTION = TadoSensorEntityDescription(
158  key="tado mode",
159  translation_key="tado_mode",
160  state_fn=lambda data: data.tado_mode,
161 )
162 HEATING_ENTITY_DESCRIPTION = TadoSensorEntityDescription(
163  key="heating",
164  translation_key="heating",
165  state_fn=lambda data: data.heating_power_percentage,
166  attributes_fn=lambda data: {"time": data.heating_power_timestamp},
167  native_unit_of_measurement=PERCENTAGE,
168  state_class=SensorStateClass.MEASUREMENT,
169 )
170 AC_ENTITY_DESCRIPTION = TadoSensorEntityDescription(
171  key="ac",
172  translation_key="ac",
173  name="AC",
174  state_fn=lambda data: data.ac_power,
175  attributes_fn=lambda data: {"time": data.ac_power_timestamp},
176 )
177 
178 ZONE_SENSORS = {
179  TYPE_HEATING: [
180  TEMPERATURE_ENTITY_DESCRIPTION,
181  HUMIDITY_ENTITY_DESCRIPTION,
182  TADO_MODE_ENTITY_DESCRIPTION,
183  HEATING_ENTITY_DESCRIPTION,
184  ],
185  TYPE_AIR_CONDITIONING: [
186  TEMPERATURE_ENTITY_DESCRIPTION,
187  HUMIDITY_ENTITY_DESCRIPTION,
188  TADO_MODE_ENTITY_DESCRIPTION,
189  AC_ENTITY_DESCRIPTION,
190  ],
191  TYPE_HOT_WATER: [TADO_MODE_ENTITY_DESCRIPTION],
192 }
193 
194 
196  hass: HomeAssistant, entry: TadoConfigEntry, async_add_entities: AddEntitiesCallback
197 ) -> None:
198  """Set up the Tado sensor platform."""
199 
200  tado = entry.runtime_data
201  zones = tado.zones
202  entities: list[SensorEntity] = []
203 
204  # Create home sensors
205  entities.extend(
206  [
207  TadoHomeSensor(tado, entity_description)
208  for entity_description in HOME_SENSORS
209  ]
210  )
211 
212  # Create zone sensors
213  for zone in zones:
214  zone_type = zone["type"]
215  if zone_type not in ZONE_SENSORS:
216  _LOGGER.warning("Unknown zone type skipped: %s", zone_type)
217  continue
218 
219  entities.extend(
220  [
221  TadoZoneSensor(tado, zone["name"], zone["id"], entity_description)
222  for entity_description in ZONE_SENSORS[zone_type]
223  ]
224  )
225 
226  async_add_entities(entities, True)
227 
228 
230  """Representation of a Tado Sensor."""
231 
232  entity_description: TadoSensorEntityDescription
233 
234  def __init__(
235  self, tado: TadoConnector, entity_description: TadoSensorEntityDescription
236  ) -> None:
237  """Initialize of the Tado Sensor."""
238  self.entity_descriptionentity_description = entity_description
239  super().__init__(tado)
240  self._tado_tado = tado
241 
242  self._attr_unique_id_attr_unique_id = f"{entity_description.key} {tado.home_id}"
243 
244  async def async_added_to_hass(self) -> None:
245  """Register for sensor updates."""
246 
247  self.async_on_removeasync_on_remove(
249  self.hasshass,
250  SIGNAL_TADO_UPDATE_RECEIVED.format(self._tado_tado.home_id, "home", "data"),
251  self._async_update_callback_async_update_callback,
252  )
253  )
254  self._async_update_home_data_async_update_home_data()
255 
256  @callback
257  def _async_update_callback(self) -> None:
258  """Update and write state."""
259  self._async_update_home_data_async_update_home_data()
260  self.async_write_ha_stateasync_write_ha_state()
261 
262  @callback
263  def _async_update_home_data(self) -> None:
264  """Handle update callbacks."""
265  try:
266  tado_weather_data = self._tado_tado.data["weather"]
267  tado_geofence_data = self._tado_tado.data["geofence"]
268  except KeyError:
269  return
270 
271  if self.entity_descriptionentity_description.data_category is not None:
272  if self.entity_descriptionentity_description.data_category == SENSOR_DATA_CATEGORY_WEATHER:
273  tado_sensor_data = tado_weather_data
274  else:
275  tado_sensor_data = tado_geofence_data
276  self._attr_native_value_attr_native_value = self.entity_descriptionentity_description.state_fn(tado_sensor_data)
277  if self.entity_descriptionentity_description.attributes_fn is not None:
278  self._attr_extra_state_attributes_attr_extra_state_attributes = self.entity_descriptionentity_description.attributes_fn(
279  tado_sensor_data
280  )
281 
282 
284  """Representation of a tado Sensor."""
285 
286  entity_description: TadoSensorEntityDescription
287 
288  def __init__(
289  self,
290  tado: TadoConnector,
291  zone_name: str,
292  zone_id: int,
293  entity_description: TadoSensorEntityDescription,
294  ) -> None:
295  """Initialize of the Tado Sensor."""
296  self.entity_descriptionentity_description = entity_description
297  self._tado_tado = tado
298  super().__init__(zone_name, tado.home_id, zone_id)
299 
300  self._attr_unique_id_attr_unique_id = f"{entity_description.key} {zone_id} {tado.home_id}"
301 
302  async def async_added_to_hass(self) -> None:
303  """Register for sensor updates."""
304 
305  self.async_on_removeasync_on_remove(
307  self.hasshass,
308  SIGNAL_TADO_UPDATE_RECEIVED.format(
309  self._tado_tado.home_id, "zone", self.zone_idzone_id
310  ),
311  self._async_update_callback_async_update_callback,
312  )
313  )
314  self._async_update_zone_data_async_update_zone_data()
315 
316  @callback
317  def _async_update_callback(self) -> None:
318  """Update and write state."""
319  self._async_update_zone_data_async_update_zone_data()
320  self.async_write_ha_stateasync_write_ha_state()
321 
322  @callback
323  def _async_update_zone_data(self) -> None:
324  """Handle update callbacks."""
325  try:
326  tado_zone_data = self._tado_tado.data["zone"][self.zone_idzone_id]
327  except KeyError:
328  return
329 
330  self._attr_native_value_attr_native_value = self.entity_descriptionentity_description.state_fn(tado_zone_data)
331  if self.entity_descriptionentity_description.attributes_fn is not None:
332  self._attr_extra_state_attributes_attr_extra_state_attributes = self.entity_descriptionentity_description.attributes_fn(
333  tado_zone_data
334  )
None __init__(self, TadoConnector tado, TadoSensorEntityDescription entity_description)
Definition: sensor.py:236
None __init__(self, TadoConnector tado, str zone_name, int zone_id, TadoSensorEntityDescription entity_description)
Definition: sensor.py:294
None async_on_remove(self, CALLBACK_TYPE func)
Definition: entity.py:1331
str get_geofencing_mode(dict[str, str] data)
Definition: sensor.py:72
str|None get_tado_mode(dict[str, str] data)
Definition: sensor.py:56
bool get_automatic_geofencing(dict[str, str] data)
Definition: sensor.py:63
None async_setup_entry(HomeAssistant hass, TadoConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: sensor.py:197
str format_condition(str condition)
Definition: sensor.py:48
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
Definition: dispatcher.py:103