Home Assistant Unofficial Reference 2024.12.1
binary_sensor.py
Go to the documentation of this file.
1 """Support for exposing a templated binary sensor."""
2 
3 from __future__ import annotations
4 
5 from dataclasses import dataclass
6 from datetime import datetime, timedelta
7 from functools import partial
8 import logging
9 from typing import Any, Self
10 
11 import voluptuous as vol
12 
14  DEVICE_CLASSES_SCHEMA,
15  DOMAIN as BINARY_SENSOR_DOMAIN,
16  ENTITY_ID_FORMAT,
17  PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA,
18  BinarySensorEntity,
19 )
20 from homeassistant.config_entries import ConfigEntry
21 from homeassistant.const import (
22  ATTR_ENTITY_ID,
23  ATTR_FRIENDLY_NAME,
24  CONF_DEVICE_CLASS,
25  CONF_DEVICE_ID,
26  CONF_ENTITY_PICTURE_TEMPLATE,
27  CONF_FRIENDLY_NAME,
28  CONF_FRIENDLY_NAME_TEMPLATE,
29  CONF_ICON,
30  CONF_ICON_TEMPLATE,
31  CONF_NAME,
32  CONF_SENSORS,
33  CONF_STATE,
34  CONF_UNIQUE_ID,
35  CONF_UNIT_OF_MEASUREMENT,
36  CONF_VALUE_TEMPLATE,
37  STATE_ON,
38  STATE_UNAVAILABLE,
39  STATE_UNKNOWN,
40 )
41 from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
42 from homeassistant.exceptions import TemplateError
43 from homeassistant.helpers import 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.event import async_call_later, async_track_point_in_utc_time
49 from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity
50 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
51 from homeassistant.util import dt as dt_util
52 
53 from . import TriggerUpdateCoordinator
54 from .const import (
55  CONF_ATTRIBUTES,
56  CONF_AVAILABILITY,
57  CONF_AVAILABILITY_TEMPLATE,
58  CONF_OBJECT_ID,
59  CONF_PICTURE,
60 )
61 from .template_entity import (
62  TEMPLATE_ENTITY_COMMON_SCHEMA,
63  TemplateEntity,
64  rewrite_common_legacy_to_modern_conf,
65 )
66 from .trigger_entity import TriggerEntity
67 
68 CONF_DELAY_ON = "delay_on"
69 CONF_DELAY_OFF = "delay_off"
70 CONF_AUTO_OFF = "auto_off"
71 CONF_ATTRIBUTE_TEMPLATES = "attribute_templates"
72 
73 LEGACY_FIELDS = {
74  CONF_ICON_TEMPLATE: CONF_ICON,
75  CONF_ENTITY_PICTURE_TEMPLATE: CONF_PICTURE,
76  CONF_AVAILABILITY_TEMPLATE: CONF_AVAILABILITY,
77  CONF_ATTRIBUTE_TEMPLATES: CONF_ATTRIBUTES,
78  CONF_FRIENDLY_NAME_TEMPLATE: CONF_NAME,
79  CONF_FRIENDLY_NAME: CONF_NAME,
80  CONF_VALUE_TEMPLATE: CONF_STATE,
81 }
82 
83 BINARY_SENSOR_SCHEMA = vol.Schema(
84  {
85  vol.Optional(CONF_AUTO_OFF): vol.Any(cv.positive_time_period, cv.template),
86  vol.Optional(CONF_DELAY_OFF): vol.Any(cv.positive_time_period, cv.template),
87  vol.Optional(CONF_DELAY_ON): vol.Any(cv.positive_time_period, cv.template),
88  vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
89  vol.Required(CONF_STATE): cv.template,
90  vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
91  }
92 ).extend(TEMPLATE_ENTITY_COMMON_SCHEMA.schema)
93 
94 BINARY_SENSOR_CONFIG_SCHEMA = BINARY_SENSOR_SCHEMA.extend(
95  {
96  vol.Optional(CONF_DEVICE_ID): selector.DeviceSelector(),
97  }
98 )
99 
100 LEGACY_BINARY_SENSOR_SCHEMA = vol.All(
101  cv.deprecated(ATTR_ENTITY_ID),
102  vol.Schema(
103  {
104  vol.Required(CONF_VALUE_TEMPLATE): cv.template,
105  vol.Optional(CONF_ICON_TEMPLATE): cv.template,
106  vol.Optional(CONF_ENTITY_PICTURE_TEMPLATE): cv.template,
107  vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template,
108  vol.Optional(CONF_ATTRIBUTE_TEMPLATES): vol.Schema(
109  {cv.string: cv.template}
110  ),
111  vol.Optional(ATTR_FRIENDLY_NAME): cv.string,
112  vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
113  vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
114  vol.Optional(CONF_DELAY_ON): vol.Any(cv.positive_time_period, cv.template),
115  vol.Optional(CONF_DELAY_OFF): vol.Any(cv.positive_time_period, cv.template),
116  vol.Optional(CONF_UNIQUE_ID): cv.string,
117  }
118  ),
119 )
120 
121 
123  hass: HomeAssistant, cfg: dict[str, dict]
124 ) -> list[dict]:
125  """Rewrite legacy binary sensor definitions to modern ones."""
126  sensors = []
127 
128  for object_id, entity_cfg in cfg.items():
129  entity_cfg = {**entity_cfg, CONF_OBJECT_ID: object_id}
130 
132  hass, entity_cfg, LEGACY_FIELDS
133  )
134 
135  if CONF_NAME not in entity_cfg:
136  entity_cfg[CONF_NAME] = template.Template(object_id, hass)
137 
138  sensors.append(entity_cfg)
139 
140  return sensors
141 
142 
143 PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend(
144  {
145  vol.Required(CONF_SENSORS): cv.schema_with_slug_keys(
146  LEGACY_BINARY_SENSOR_SCHEMA
147  ),
148  }
149 )
150 
151 
152 @callback
154  async_add_entities: AddEntitiesCallback,
155  hass: HomeAssistant,
156  definitions: list[dict],
157  unique_id_prefix: str | None,
158 ) -> None:
159  """Create the template binary sensors."""
160  sensors = []
161 
162  for entity_conf in definitions:
163  unique_id = entity_conf.get(CONF_UNIQUE_ID)
164 
165  if unique_id and unique_id_prefix:
166  unique_id = f"{unique_id_prefix}-{unique_id}"
167 
168  sensors.append(
170  hass,
171  entity_conf,
172  unique_id,
173  )
174  )
175 
176  async_add_entities(sensors)
177 
178 
180  hass: HomeAssistant,
181  config: ConfigType,
182  async_add_entities: AddEntitiesCallback,
183  discovery_info: DiscoveryInfoType | None = None,
184 ) -> None:
185  """Set up the template binary sensors."""
186  if discovery_info is None:
188  async_add_entities,
189  hass,
190  rewrite_legacy_to_modern_conf(hass, config[CONF_SENSORS]),
191  None,
192  )
193  return
194 
195  if "coordinator" in discovery_info:
197  TriggerBinarySensorEntity(hass, discovery_info["coordinator"], config)
198  for config in discovery_info["entities"]
199  )
200  return
201 
203  async_add_entities,
204  hass,
205  discovery_info["entities"],
206  discovery_info["unique_id"],
207  )
208 
209 
211  hass: HomeAssistant,
212  config_entry: ConfigEntry,
213  async_add_entities: AddEntitiesCallback,
214 ) -> None:
215  """Initialize config entry."""
216  _options = dict(config_entry.options)
217  _options.pop("template_type")
218  validated_config = BINARY_SENSOR_CONFIG_SCHEMA(_options)
220  [BinarySensorTemplate(hass, validated_config, config_entry.entry_id)]
221  )
222 
223 
224 @callback
226  hass: HomeAssistant, name: str, config: dict[str, Any]
227 ) -> BinarySensorTemplate:
228  """Create a preview sensor."""
229  validated_config = BINARY_SENSOR_CONFIG_SCHEMA(config | {CONF_NAME: name})
230  return BinarySensorTemplate(hass, validated_config, None)
231 
232 
234  """A virtual binary sensor that triggers from another sensor."""
235 
236  _attr_should_poll = False
237 
238  def __init__(
239  self,
240  hass: HomeAssistant,
241  config: dict[str, Any],
242  unique_id: str | None,
243  ) -> None:
244  """Initialize the Template binary sensor."""
245  super().__init__(hass, config=config, unique_id=unique_id)
246  if (object_id := config.get(CONF_OBJECT_ID)) is not None:
248  ENTITY_ID_FORMAT, object_id, hass=hass
249  )
250 
251  self._attr_device_class_attr_device_class = config.get(CONF_DEVICE_CLASS)
252  self._template_template = config[CONF_STATE]
253  self._delay_cancel_delay_cancel = None
254  self._delay_on_delay_on = None
255  self._delay_on_raw_delay_on_raw = config.get(CONF_DELAY_ON)
256  self._delay_off_delay_off = None
257  self._delay_off_raw_delay_off_raw = config.get(CONF_DELAY_OFF)
259  hass,
260  config.get(CONF_DEVICE_ID),
261  )
262 
263  async def async_added_to_hass(self) -> None:
264  """Restore state."""
265  if (
266  (self._delay_on_raw_delay_on_raw is not None or self._delay_off_raw_delay_off_raw is not None)
267  and (last_state := await self.async_get_last_stateasync_get_last_state()) is not None
268  and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE)
269  ):
270  self._attr_is_on_attr_is_on = last_state.state == STATE_ON
271  await super().async_added_to_hass()
272 
273  @callback
274  def _async_setup_templates(self) -> None:
275  """Set up templates."""
276  self.add_template_attributeadd_template_attribute("_state", self._template_template, None, self._update_state_update_state_update_state)
277 
278  if self._delay_on_raw_delay_on_raw is not None:
279  try:
280  self._delay_on_delay_on = cv.positive_time_period(self._delay_on_raw_delay_on_raw)
281  except vol.Invalid:
282  self.add_template_attributeadd_template_attribute(
283  "_delay_on", self._delay_on_raw_delay_on_raw, cv.positive_time_period
284  )
285 
286  if self._delay_off_raw_delay_off_raw is not None:
287  try:
288  self._delay_off_delay_off = cv.positive_time_period(self._delay_off_raw_delay_off_raw)
289  except vol.Invalid:
290  self.add_template_attributeadd_template_attribute(
291  "_delay_off", self._delay_off_raw_delay_off_raw, cv.positive_time_period
292  )
293 
294  super()._async_setup_templates()
295 
296  @callback
297  def _update_state(self, result):
298  super()._update_state(result)
299 
300  if self._delay_cancel_delay_cancel:
301  self._delay_cancel_delay_cancel()
302  self._delay_cancel_delay_cancel = None
303 
304  state = (
305  None
306  if isinstance(result, TemplateError)
307  else template.result_as_boolean(result)
308  )
309 
310  if state == self._attr_is_on_attr_is_on:
311  return
312 
313  # state without delay
314  if (
315  state is None
316  or (state and not self._delay_on_delay_on)
317  or (not state and not self._delay_off_delay_off)
318  ):
319  self._attr_is_on_attr_is_on = state
320  return
321 
322  @callback
323  def _set_state(_):
324  """Set state of template binary sensor."""
325  self._attr_is_on_attr_is_on = state
326  self.async_write_ha_stateasync_write_ha_state()
327 
328  delay = (self._delay_on_delay_on if state else self._delay_off_delay_off).total_seconds()
329  # state with delay. Cancelled if template result changes.
330  self._delay_cancel_delay_cancel = async_call_later(self.hasshass, delay, _set_state)
331 
332 
334  """Sensor entity based on trigger data."""
335 
336  domain = BINARY_SENSOR_DOMAIN
337  extra_template_keys = (CONF_STATE,)
338 
339  def __init__(
340  self,
341  hass: HomeAssistant,
342  coordinator: TriggerUpdateCoordinator,
343  config: dict,
344  ) -> None:
345  """Initialize the entity."""
346  super().__init__(hass, coordinator, config)
347 
348  for key in (CONF_DELAY_ON, CONF_DELAY_OFF, CONF_AUTO_OFF):
349  if isinstance(config.get(key), template.Template):
350  self._to_render_simple.append(key)
351  self._parse_result.add(key)
352 
353  self._delay_cancel_delay_cancel: CALLBACK_TYPE | None = None
354  self._auto_off_cancel_auto_off_cancel: CALLBACK_TYPE | None = None
355  self._auto_off_time_auto_off_time: datetime | None = None
356 
357  async def async_added_to_hass(self) -> None:
358  """Restore last state."""
359  await super().async_added_to_hass()
360  if (
361  (last_state := await self.async_get_last_stateasync_get_last_state()) is not None
362  and (extra_data := await self.async_get_last_binary_sensor_dataasync_get_last_binary_sensor_data())
363  is not None
364  and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE)
365  # The trigger might have fired already while we waited for stored data,
366  # then we should not restore state
367  and self._attr_is_on_attr_is_on is None
368  ):
369  self._attr_is_on_attr_is_on = last_state.state == STATE_ON
370  self.restore_attributes(last_state)
371 
372  if CONF_AUTO_OFF not in self._config:
373  return
374 
375  if (
376  auto_off_time := extra_data.auto_off_time
377  ) is not None and auto_off_time <= dt_util.utcnow():
378  # It's already past the saved auto off time
379  self._attr_is_on_attr_is_on = False
380 
381  if self._attr_is_on_attr_is_on and auto_off_time is not None:
382  self._set_auto_off_set_auto_off(auto_off_time)
383 
384  @callback
385  def _handle_coordinator_update(self) -> None:
386  """Handle update of the data."""
387  self._process_data()
388 
389  if self._delay_cancel_delay_cancel:
390  self._delay_cancel_delay_cancel()
391  self._delay_cancel_delay_cancel = None
392 
393  if self._auto_off_cancel_auto_off_cancel:
394  self._auto_off_cancel_auto_off_cancel()
395  self._auto_off_cancel_auto_off_cancel = None
396  self._auto_off_time_auto_off_time = None
397 
398  if not self.availableavailable:
399  self.async_write_ha_stateasync_write_ha_state()
400  return
401 
402  raw = self._rendered.get(CONF_STATE)
403  state = template.result_as_boolean(raw)
404 
405  key = CONF_DELAY_ON if state else CONF_DELAY_OFF
406  delay = self._rendered.get(key) or self._config.get(key)
407 
408  # state without delay. None means rendering failed.
409  if self._attr_is_on_attr_is_on == state or state is None or delay is None:
410  self._set_state_set_state(state)
411  return
412 
413  if not isinstance(delay, timedelta):
414  try:
415  delay = cv.positive_time_period(delay)
416  except vol.Invalid as err:
417  logging.getLogger(__name__).warning(
418  "Error rendering %s template: %s", key, err
419  )
420  return
421 
422  # state with delay. Cancelled if new trigger received
423  self._delay_cancel_delay_cancel = async_call_later(
424  self.hasshass, delay.total_seconds(), partial(self._set_state_set_state, state)
425  )
426 
427  @callback
428  def _set_state(self, state, _=None):
429  """Set up auto off."""
430  self._attr_is_on_attr_is_on = state
431  self.async_set_contextasync_set_context(self.coordinator.data["context"])
432  self.async_write_ha_stateasync_write_ha_state()
433 
434  if not state:
435  return
436 
437  auto_off_delay = self._rendered.get(CONF_AUTO_OFF) or self._config.get(
438  CONF_AUTO_OFF
439  )
440 
441  if auto_off_delay is None:
442  return
443 
444  if not isinstance(auto_off_delay, timedelta):
445  try:
446  auto_off_delay = cv.positive_time_period(auto_off_delay)
447  except vol.Invalid as err:
448  logging.getLogger(__name__).warning(
449  "Error rendering %s template: %s", CONF_AUTO_OFF, err
450  )
451  return
452 
453  auto_off_time = dt_util.utcnow() + auto_off_delay
454  self._set_auto_off_set_auto_off(auto_off_time)
455 
456  def _set_auto_off(self, auto_off_time: datetime) -> None:
457  @callback
458  def _auto_off(_):
459  """Reset state of template binary sensor."""
460  self._attr_is_on_attr_is_on = False
461  self.async_write_ha_stateasync_write_ha_state()
462 
463  self._auto_off_time_auto_off_time = auto_off_time
464  self._auto_off_cancel_auto_off_cancel = async_track_point_in_utc_time(
465  self.hasshass, _auto_off, self._auto_off_time_auto_off_time
466  )
467 
468  @property
469  def extra_restore_state_data(self) -> AutoOffExtraStoredData:
470  """Return specific state data to be restored."""
471  return AutoOffExtraStoredData(self._auto_off_time_auto_off_time)
472 
474  self,
475  ) -> AutoOffExtraStoredData | None:
476  """Restore auto_off_time."""
477  if (restored_last_extra_data := await self.async_get_last_extra_dataasync_get_last_extra_data()) is None:
478  return None
479  return AutoOffExtraStoredData.from_dict(restored_last_extra_data.as_dict())
480 
481 
482 @dataclass
484  """Object to hold extra stored data."""
485 
486  auto_off_time: datetime | None
487 
488  def as_dict(self) -> dict[str, Any]:
489  """Return a dict representation of additional data."""
490  auto_off_time: datetime | dict[str, str] | None = self.auto_off_time
491  if isinstance(auto_off_time, datetime):
492  auto_off_time = {
493  "__type": str(type(auto_off_time)),
494  "isoformat": auto_off_time.isoformat(),
495  }
496  return {
497  "auto_off_time": auto_off_time,
498  }
499 
500  @classmethod
501  def from_dict(cls, restored: dict[str, Any]) -> Self | None:
502  """Initialize a stored binary sensor state from a dict."""
503  try:
504  auto_off_time = restored["auto_off_time"]
505  except KeyError:
506  return None
507  try:
508  type_ = auto_off_time["__type"]
509  if type_ == "<class 'datetime.datetime'>":
510  auto_off_time = dt_util.parse_datetime(auto_off_time["isoformat"])
511  except TypeError:
512  # native_value is not a dict
513  pass
514  except KeyError:
515  # native_value is a dict, but does not have all values
516  return None
517 
518  return cls(auto_off_time)
None __init__(self, HomeAssistant hass, dict[str, Any] config, str|None unique_id)
None __init__(self, HomeAssistant hass, TriggerUpdateCoordinator coordinator, dict config)
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)
None async_set_context(self, Context context)
Definition: entity.py:937
ExtraStoredData|None async_get_last_extra_data(self)
bool add(self, _T matcher)
Definition: match.py:185
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
None _async_create_template_tracking_entities(AddEntitiesCallback async_add_entities, HomeAssistant hass, list[dict] definitions, str|None unique_id_prefix)
None async_setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback async_add_entities, DiscoveryInfoType|None discovery_info=None)
list[dict] rewrite_legacy_to_modern_conf(HomeAssistant hass, dict[str, dict] cfg)
BinarySensorTemplate async_create_preview_binary_sensor(HomeAssistant hass, str name, dict[str, Any] config)
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
CALLBACK_TYPE async_call_later(HomeAssistant hass, float|timedelta delay, HassJob[[datetime], Coroutine[Any, Any, None]|None]|Callable[[datetime], Coroutine[Any, Any, None]|None] action)
Definition: event.py:1597
CALLBACK_TYPE async_track_point_in_utc_time(HomeAssistant hass, HassJob[[datetime], Coroutine[Any, Any, None]|None]|Callable[[datetime], Coroutine[Any, Any, None]|None] action, datetime point_in_time)
Definition: event.py:1542