Home Assistant Unofficial Reference 2024.12.1
sensor.py
Go to the documentation of this file.
1 """Allows the creation of a sensor that breaks out state_attributes."""
2 
3 from __future__ import annotations
4 
5 from datetime import date, datetime
6 import logging
7 from typing import Any
8 
9 import voluptuous as vol
10 
12  ATTR_LAST_RESET,
13  CONF_STATE_CLASS,
14  DEVICE_CLASSES_SCHEMA,
15  DOMAIN as SENSOR_DOMAIN,
16  ENTITY_ID_FORMAT,
17  PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
18  RestoreSensor,
19  SensorDeviceClass,
20  SensorEntity,
21  SensorStateClass,
22 )
23 from homeassistant.components.sensor.helpers import async_parse_date_datetime
24 from homeassistant.config_entries import ConfigEntry
25 from homeassistant.const import (
26  ATTR_ENTITY_ID,
27  CONF_DEVICE_CLASS,
28  CONF_DEVICE_ID,
29  CONF_ENTITY_PICTURE_TEMPLATE,
30  CONF_FRIENDLY_NAME,
31  CONF_FRIENDLY_NAME_TEMPLATE,
32  CONF_ICON_TEMPLATE,
33  CONF_NAME,
34  CONF_SENSORS,
35  CONF_STATE,
36  CONF_UNIQUE_ID,
37  CONF_UNIT_OF_MEASUREMENT,
38  CONF_VALUE_TEMPLATE,
39  STATE_UNAVAILABLE,
40  STATE_UNKNOWN,
41 )
42 from homeassistant.core import HomeAssistant, callback
43 from homeassistant.exceptions import TemplateError
44 from homeassistant.helpers import config_validation as cv, selector, template
45 from homeassistant.helpers.device import async_device_info_to_link_from_device_id
46 from homeassistant.helpers.entity import async_generate_entity_id
47 from homeassistant.helpers.entity_platform import AddEntitiesCallback
48 from homeassistant.helpers.trigger_template_entity import TEMPLATE_SENSOR_BASE_SCHEMA
49 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
50 from homeassistant.util import dt as dt_util
51 
52 from . import TriggerUpdateCoordinator
53 from .const import (
54  CONF_ATTRIBUTE_TEMPLATES,
55  CONF_AVAILABILITY_TEMPLATE,
56  CONF_OBJECT_ID,
57  CONF_TRIGGER,
58 )
59 from .template_entity import (
60  TEMPLATE_ENTITY_COMMON_SCHEMA,
61  TemplateEntity,
62  rewrite_common_legacy_to_modern_conf,
63 )
64 from .trigger_entity import TriggerEntity
65 
66 LEGACY_FIELDS = {
67  CONF_FRIENDLY_NAME_TEMPLATE: CONF_NAME,
68  CONF_FRIENDLY_NAME: CONF_NAME,
69  CONF_VALUE_TEMPLATE: CONF_STATE,
70 }
71 
72 
74  """Run extra validation checks."""
75  if (
76  val.get(ATTR_LAST_RESET) is not None
77  and val.get(CONF_STATE_CLASS) != SensorStateClass.TOTAL
78  ):
79  raise vol.Invalid(
80  "last_reset is only valid for template sensors with state_class 'total'"
81  )
82 
83  return val
84 
85 
86 SENSOR_SCHEMA = vol.All(
87  vol.Schema(
88  {
89  vol.Required(CONF_STATE): cv.template,
90  vol.Optional(ATTR_LAST_RESET): cv.template,
91  }
92  )
93  .extend(TEMPLATE_SENSOR_BASE_SCHEMA.schema)
94  .extend(TEMPLATE_ENTITY_COMMON_SCHEMA.schema),
95  validate_last_reset,
96 )
97 
98 
99 SENSOR_CONFIG_SCHEMA = vol.All(
100  vol.Schema(
101  {
102  vol.Required(CONF_STATE): cv.template,
103  vol.Optional(CONF_DEVICE_ID): selector.DeviceSelector(),
104  }
105  ).extend(TEMPLATE_SENSOR_BASE_SCHEMA.schema),
106 )
107 
108 LEGACY_SENSOR_SCHEMA = vol.All(
109  cv.deprecated(ATTR_ENTITY_ID),
110  vol.Schema(
111  {
112  vol.Required(CONF_VALUE_TEMPLATE): cv.template,
113  vol.Optional(CONF_ICON_TEMPLATE): cv.template,
114  vol.Optional(CONF_ENTITY_PICTURE_TEMPLATE): cv.template,
115  vol.Optional(CONF_FRIENDLY_NAME_TEMPLATE): cv.template,
116  vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template,
117  vol.Optional(CONF_ATTRIBUTE_TEMPLATES, default={}): vol.Schema(
118  {cv.string: cv.template}
119  ),
120  vol.Optional(CONF_FRIENDLY_NAME): cv.string,
121  vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
122  vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
123  vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
124  vol.Optional(CONF_UNIQUE_ID): cv.string,
125  }
126  ),
127 )
128 
129 
131  """Run extra validation checks."""
132  if CONF_TRIGGER in val:
133  raise vol.Invalid(
134  "You can only add triggers to template entities if they are defined under"
135  " `template:`. See the template documentation for more information:"
136  " https://www.home-assistant.io/integrations/template/"
137  )
138 
139  if CONF_SENSORS not in val and SENSOR_DOMAIN not in val:
140  raise vol.Invalid(f"Required key {SENSOR_DOMAIN} not defined")
141 
142  return val
143 
144 
146  hass: HomeAssistant, cfg: dict[str, dict]
147 ) -> list[dict]:
148  """Rewrite legacy sensor definitions to modern ones."""
149  sensors = []
150 
151  for object_id, entity_cfg in cfg.items():
152  entity_cfg = {**entity_cfg, CONF_OBJECT_ID: object_id}
153 
155  hass, entity_cfg, LEGACY_FIELDS
156  )
157 
158  if CONF_NAME not in entity_cfg:
159  entity_cfg[CONF_NAME] = template.Template(object_id, hass)
160 
161  sensors.append(entity_cfg)
162 
163  return sensors
164 
165 
166 PLATFORM_SCHEMA = vol.All(
167  SENSOR_PLATFORM_SCHEMA.extend(
168  {
169  vol.Optional(CONF_TRIGGER): cv.match_all, # to raise custom warning
170  vol.Required(CONF_SENSORS): cv.schema_with_slug_keys(LEGACY_SENSOR_SCHEMA),
171  }
172  ),
173  extra_validation_checks,
174 )
175 
176 _LOGGER = logging.getLogger(__name__)
177 
178 
179 @callback
181  async_add_entities: AddEntitiesCallback,
182  hass: HomeAssistant,
183  definitions: list[dict],
184  unique_id_prefix: str | None,
185 ) -> None:
186  """Create the template sensors."""
187  sensors = []
188 
189  for entity_conf in definitions:
190  unique_id = entity_conf.get(CONF_UNIQUE_ID)
191 
192  if unique_id and unique_id_prefix:
193  unique_id = f"{unique_id_prefix}-{unique_id}"
194 
195  sensors.append(
197  hass,
198  entity_conf,
199  unique_id,
200  )
201  )
202 
203  async_add_entities(sensors)
204 
205 
207  hass: HomeAssistant,
208  config: ConfigType,
209  async_add_entities: AddEntitiesCallback,
210  discovery_info: DiscoveryInfoType | None = None,
211 ) -> None:
212  """Set up the template sensors."""
213  if discovery_info is None:
215  async_add_entities,
216  hass,
217  rewrite_legacy_to_modern_conf(hass, config[CONF_SENSORS]),
218  None,
219  )
220  return
221 
222  if "coordinator" in discovery_info:
224  TriggerSensorEntity(hass, discovery_info["coordinator"], config)
225  for config in discovery_info["entities"]
226  )
227  return
228 
230  async_add_entities,
231  hass,
232  discovery_info["entities"],
233  discovery_info["unique_id"],
234  )
235 
236 
238  hass: HomeAssistant,
239  config_entry: ConfigEntry,
240  async_add_entities: AddEntitiesCallback,
241 ) -> None:
242  """Initialize config entry."""
243  _options = dict(config_entry.options)
244  _options.pop("template_type")
245  validated_config = SENSOR_CONFIG_SCHEMA(_options)
246  async_add_entities([SensorTemplate(hass, validated_config, config_entry.entry_id)])
247 
248 
249 @callback
251  hass: HomeAssistant, name: str, config: dict[str, Any]
252 ) -> SensorTemplate:
253  """Create a preview sensor."""
254  validated_config = SENSOR_CONFIG_SCHEMA(config | {CONF_NAME: name})
255  return SensorTemplate(hass, validated_config, None)
256 
257 
259  """Representation of a Template Sensor."""
260 
261  _attr_should_poll = False
262 
263  def __init__(
264  self,
265  hass: HomeAssistant,
266  config: dict[str, Any],
267  unique_id: str | None,
268  ) -> None:
269  """Initialize the sensor."""
270  super().__init__(hass, config=config, fallback_name=None, unique_id=unique_id)
271  self._attr_native_unit_of_measurement_attr_native_unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT)
272  self._attr_device_class_attr_device_class = config.get(CONF_DEVICE_CLASS)
273  self._attr_state_class_attr_state_class = config.get(CONF_STATE_CLASS)
274  self._template: template.Template = config[CONF_STATE]
275  self._attr_last_reset_template: template.Template | None = config.get(
276  ATTR_LAST_RESET
277  )
279  hass,
280  config.get(CONF_DEVICE_ID),
281  )
282  if (object_id := config.get(CONF_OBJECT_ID)) is not None:
284  ENTITY_ID_FORMAT, object_id, hass=hass
285  )
286 
287  @callback
288  def _async_setup_templates(self) -> None:
289  """Set up templates."""
290  self.add_template_attributeadd_template_attribute(
291  "_attr_native_value", self._template, None, self._update_state_update_state_update_state
292  )
293  if self._attr_last_reset_template is not None:
294  self.add_template_attributeadd_template_attribute(
295  "_attr_last_reset",
296  self._attr_last_reset_template,
297  cv.datetime,
298  self._update_last_reset_update_last_reset,
299  )
300 
301  super()._async_setup_templates()
302 
303  @callback
304  def _update_last_reset(self, result):
305  self._attr_last_reset_attr_last_reset = result
306 
307  @callback
308  def _update_state(self, result):
309  super()._update_state(result)
310  if isinstance(result, TemplateError):
311  self._attr_native_value_attr_native_value = None
312  return
313 
314  if result is None or self.device_classdevice_classdevice_class not in (
315  SensorDeviceClass.DATE,
316  SensorDeviceClass.TIMESTAMP,
317  ):
318  self._attr_native_value_attr_native_value = result
319  return
320 
321  self._attr_native_value_attr_native_value = async_parse_date_datetime(
322  result, self.entity_identity_identity_identity_id, self.device_classdevice_classdevice_class
323  )
324 
325 
327  """Sensor entity based on trigger data."""
328 
329  domain = SENSOR_DOMAIN
330  extra_template_keys = (CONF_STATE,)
331 
332  def __init__(
333  self,
334  hass: HomeAssistant,
335  coordinator: TriggerUpdateCoordinator,
336  config: ConfigType,
337  ) -> None:
338  """Initialize."""
339  super().__init__(hass, coordinator, config)
340 
341  if (last_reset_template := config.get(ATTR_LAST_RESET)) is not None:
342  if last_reset_template.is_static:
343  self._static_rendered[ATTR_LAST_RESET] = last_reset_template.template
344  else:
345  self._to_render_simple.append(ATTR_LAST_RESET)
346 
347  self._attr_state_class_attr_state_class = config.get(CONF_STATE_CLASS)
348  self._attr_native_unit_of_measurement_attr_native_unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT)
349 
350  async def async_added_to_hass(self) -> None:
351  """Restore last state."""
352  await super().async_added_to_hass()
353  if (
354  (last_state := await self.async_get_last_state()) is not None
355  and (extra_data := await self.async_get_last_sensor_dataasync_get_last_sensor_data()) is not None
356  and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE)
357  # The trigger might have fired already while we waited for stored data,
358  # then we should not restore state
359  and CONF_STATE not in self._rendered
360  ):
361  self._rendered[CONF_STATE] = extra_data.native_value
362  self.restore_attributes(last_state)
363 
364  @property
365  def native_value(self) -> str | datetime | date | None:
366  """Return state of the sensor."""
367  return self._rendered.get(CONF_STATE)
368 
369  @callback
370  def _process_data(self) -> None:
371  """Process new data."""
372  super()._process_data()
373 
374  # Update last_reset
375  if ATTR_LAST_RESET in self._rendered:
376  parsed_timestamp = dt_util.parse_datetime(self._rendered[ATTR_LAST_RESET])
377  if parsed_timestamp is None:
378  _LOGGER.warning(
379  "%s rendered invalid timestamp for last_reset attribute: %s",
380  self.entity_identity_id,
381  self._rendered.get(ATTR_LAST_RESET),
382  )
383  else:
384  self._attr_last_reset_attr_last_reset = parsed_timestamp
385 
386  if (
387  state := self._rendered.get(CONF_STATE)
388  ) is None or self.device_classdevice_classdevice_class not in (
389  SensorDeviceClass.DATE,
390  SensorDeviceClass.TIMESTAMP,
391  ):
392  return
393 
394  self._rendered[CONF_STATE] = async_parse_date_datetime(
395  state, self.entity_identity_id, self.device_classdevice_classdevice_class
396  )
SensorExtraStoredData|None async_get_last_sensor_data(self)
Definition: __init__.py:934
SensorDeviceClass|None device_class(self)
Definition: __init__.py:313
None __init__(self, HomeAssistant hass, dict[str, Any] config, str|None unique_id)
Definition: sensor.py:268
None __init__(self, HomeAssistant hass, TriggerUpdateCoordinator coordinator, ConfigType config)
Definition: sensor.py:337
None add_template_attribute(self, str attribute, Template template, Callable[[Any], Any]|None validator=None, Callable[[Any], None]|None on_update=None, bool none_on_template_error=False)
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
datetime|date|None async_parse_date_datetime(str value, str entity_id, SensorDeviceClass|str|None device_class)
Definition: helpers.py:19
SensorTemplate async_create_preview_sensor(HomeAssistant hass, str name, dict[str, Any] config)
Definition: sensor.py:252
None async_setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback async_add_entities, DiscoveryInfoType|None discovery_info=None)
Definition: sensor.py:211
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: sensor.py:241
None _async_create_template_tracking_entities(AddEntitiesCallback async_add_entities, HomeAssistant hass, list[dict] definitions, str|None unique_id_prefix)
Definition: sensor.py:185
list[dict] rewrite_legacy_to_modern_conf(HomeAssistant hass, dict[str, dict] cfg)
Definition: sensor.py:147
dict[str, Any] rewrite_common_legacy_to_modern_conf(HomeAssistant hass, dict[str, Any] entity_cfg, dict[str, str]|None extra_legacy_fields=None)
dr.DeviceInfo|None async_device_info_to_link_from_device_id(HomeAssistant hass, str|None device_id)
Definition: device.py:44
str async_generate_entity_id(str entity_id_format, str|None name, Iterable[str]|None current_ids=None, HomeAssistant|None hass=None)
Definition: entity.py:119