Home Assistant Unofficial Reference 2024.12.1
event.py
Go to the documentation of this file.
1 """Helpers for listening to events."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections import defaultdict
7 from collections.abc import Callable, Coroutine, Iterable, Mapping, Sequence
8 import copy
9 from dataclasses import dataclass
10 from datetime import datetime, timedelta
11 from functools import partial, wraps
12 import logging
13 from random import randint
14 import time
15 from typing import TYPE_CHECKING, Any, Concatenate, Generic, TypeVar
16 
17 from homeassistant.const import (
18  EVENT_CORE_CONFIG_UPDATE,
19  EVENT_STATE_CHANGED,
20  EVENT_STATE_REPORTED,
21  MATCH_ALL,
22  SUN_EVENT_SUNRISE,
23  SUN_EVENT_SUNSET,
24 )
25 from homeassistant.core import (
26  CALLBACK_TYPE,
27  Event,
28  # Explicit reexport of 'EventStateChangedData' for backwards compatibility
29  EventStateChangedData as EventStateChangedData, # noqa: PLC0414
30  EventStateEventData,
31  EventStateReportedData,
32  HassJob,
33  HassJobType,
34  HomeAssistant,
35  State,
36  callback,
37  split_entity_id,
38 )
39 from homeassistant.exceptions import TemplateError
40 from homeassistant.loader import bind_hass
41 from homeassistant.util import dt as dt_util
42 from homeassistant.util.async_ import run_callback_threadsafe
43 from homeassistant.util.event_type import EventType
44 from homeassistant.util.hass_dict import HassKey
45 
46 from . import frame
47 from .device_registry import (
48  EVENT_DEVICE_REGISTRY_UPDATED,
49  EventDeviceRegistryUpdatedData,
50 )
51 from .entity_registry import (
52  EVENT_ENTITY_REGISTRY_UPDATED,
53  EventEntityRegistryUpdatedData,
54 )
55 from .ratelimit import KeyedRateLimit
56 from .sun import get_astral_event_next
57 from .template import RenderInfo, Template, result_as_boolean
58 from .typing import TemplateVarsType
59 
60 _TRACK_STATE_CHANGE_DATA: HassKey[_KeyedEventData[EventStateChangedData]] = HassKey(
61  "track_state_change_data"
62 )
63 _TRACK_STATE_REPORT_DATA: HassKey[_KeyedEventData[EventStateReportedData]] = HassKey(
64  "track_state_report_data"
65 )
66 _TRACK_STATE_ADDED_DOMAIN_DATA: HassKey[_KeyedEventData[EventStateChangedData]] = (
67  HassKey("track_state_added_domain_data")
68 )
69 _TRACK_STATE_REMOVED_DOMAIN_DATA: HassKey[_KeyedEventData[EventStateChangedData]] = (
70  HassKey("track_state_removed_domain_data")
71 )
72 _TRACK_ENTITY_REGISTRY_UPDATED_DATA: HassKey[
73  _KeyedEventData[EventEntityRegistryUpdatedData]
74 ] = HassKey("track_entity_registry_updated_data")
75 _TRACK_DEVICE_REGISTRY_UPDATED_DATA: HassKey[
76  _KeyedEventData[EventDeviceRegistryUpdatedData]
77 ] = HassKey("track_device_registry_updated_data")
78 
79 _ALL_LISTENER = "all"
80 _DOMAINS_LISTENER = "domains"
81 _ENTITIES_LISTENER = "entities"
82 
83 _LOGGER = logging.getLogger(__name__)
84 
85 # Used to spread async_track_utc_time_change listeners and DataUpdateCoordinator
86 # refresh cycles between RANDOM_MICROSECOND_MIN..RANDOM_MICROSECOND_MAX.
87 # The values have been determined experimentally in production testing, background
88 # in PR https://github.com/home-assistant/core/pull/82233
89 RANDOM_MICROSECOND_MIN = 50000
90 RANDOM_MICROSECOND_MAX = 500000
91 
92 _TypedDictT = TypeVar("_TypedDictT", bound=Mapping[str, Any])
93 _StateEventDataT = TypeVar("_StateEventDataT", bound=EventStateEventData)
94 
95 
96 @dataclass(slots=True, frozen=True)
97 class _KeyedEventTracker(Generic[_TypedDictT]):
98  """Class to track events by key."""
99 
100  key: HassKey[_KeyedEventData[_TypedDictT]]
101  event_type: EventType[_TypedDictT] | str
102  dispatcher_callable: Callable[
103  [
104  HomeAssistant,
105  dict[str, list[HassJob[[Event[_TypedDictT]], Any]]],
106  Event[_TypedDictT],
107  ],
108  None,
109  ]
110  filter_callable: Callable[
111  [
112  HomeAssistant,
113  dict[str, list[HassJob[[Event[_TypedDictT]], Any]]],
114  _TypedDictT,
115  ],
116  bool,
117  ]
118 
119 
120 @dataclass(slots=True, frozen=True)
121 class _KeyedEventData(Generic[_TypedDictT]):
122  """Class to track data for events by key."""
123 
124  listener: CALLBACK_TYPE
125  callbacks: defaultdict[str, list[HassJob[[Event[_TypedDictT]], Any]]]
126 
127 
128 @dataclass(slots=True)
130  """Class for keeping track of states being tracked.
131 
132  all_states: All states on the system are being tracked
133  entities: Lowercased entities to track
134  domains: Lowercased domains to track
135  """
136 
137  all_states: bool
138  entities: set[str]
139  domains: set[str]
140 
141 
142 @dataclass(slots=True)
144  """Class for keeping track of a template with variables.
145 
146  The template is template to calculate.
147  The variables are variables to pass to the template.
148  The rate_limit is a rate limit on how often the template is re-rendered.
149  """
150 
151  template: Template
152  variables: TemplateVarsType
153  rate_limit: float | None = None
154 
155 
156 @dataclass(slots=True)
158  """Class for result of template tracking.
159 
160  template
161  The template that has changed.
162  last_result
163  The output from the template on the last successful run, or None
164  if no previous successful run.
165  result
166  Result from the template run. This will be a string or an
167  TemplateError if the template resulted in an error.
168  """
169 
170  template: Template
171  last_result: Any
172  result: Any
173 
174 
175 def threaded_listener_factory[**_P](
176  async_factory: Callable[Concatenate[HomeAssistant, _P], Any],
177 ) -> Callable[Concatenate[HomeAssistant, _P], CALLBACK_TYPE]:
178  """Convert an async event helper to a threaded one."""
179 
180  @wraps(async_factory)
181  def factory(
182  hass: HomeAssistant, *args: _P.args, **kwargs: _P.kwargs
183  ) -> CALLBACK_TYPE:
184  """Call async event helper safely."""
185  if not isinstance(hass, HomeAssistant):
186  raise TypeError("First parameter needs to be a hass instance")
187 
188  async_remove = run_callback_threadsafe(
189  hass.loop, partial(async_factory, hass, *args, **kwargs)
190  ).result()
191 
192  def remove() -> None:
193  """Threadsafe removal."""
194  run_callback_threadsafe(hass.loop, async_remove).result()
195 
196  return remove
197 
198  return factory
199 
200 
201 @callback
202 @bind_hass
204  hass: HomeAssistant,
205  entity_ids: str | Iterable[str],
206  action: Callable[
207  [str, State | None, State | None], Coroutine[Any, Any, None] | None
208  ],
209  from_state: str | Iterable[str] | None = None,
210  to_state: str | Iterable[str] | None = None,
211 ) -> CALLBACK_TYPE:
212  """Track specific state changes.
213 
214  entity_ids, from_state and to_state can be string or list.
215  Use list to match multiple.
216 
217  Returns a function that can be called to remove the listener.
218 
219  If entity_ids are not MATCH_ALL along with from_state and to_state
220  being None, async_track_state_change_event should be used instead
221  as it is slightly faster.
222 
223  This function is deprecated and will be removed in Home Assistant 2025.5.
224 
225  Must be run within the event loop.
226  """
227  frame.report_usage(
228  "calls `async_track_state_change` instead of `async_track_state_change_event`"
229  " which is deprecated and will be removed in Home Assistant 2025.5",
230  core_behavior=frame.ReportBehavior.LOG,
231  )
232 
233  if from_state is not None:
234  match_from_state = process_state_match(from_state)
235  if to_state is not None:
236  match_to_state = process_state_match(to_state)
237 
238  # Ensure it is a lowercase list with entity ids we want to match on
239  if entity_ids == MATCH_ALL:
240  pass
241  elif isinstance(entity_ids, str):
242  entity_ids = (entity_ids.lower(),)
243  else:
244  entity_ids = tuple(entity_id.lower() for entity_id in entity_ids)
245 
246  job = HassJob(action, f"track state change {entity_ids} {from_state} {to_state}")
247 
248  @callback
249  def state_change_filter(event_data: EventStateChangedData) -> bool:
250  """Handle specific state changes."""
251  if from_state is not None:
252  old_state_str: str | None = None
253  if (old_state := event_data["old_state"]) is not None:
254  old_state_str = old_state.state
255 
256  if not match_from_state(old_state_str):
257  return False
258 
259  if to_state is not None:
260  new_state_str: str | None = None
261  if (new_state := event_data["new_state"]) is not None:
262  new_state_str = new_state.state
263 
264  if not match_to_state(new_state_str):
265  return False
266 
267  return True
268 
269  @callback
270  def state_change_dispatcher(event: Event[EventStateChangedData]) -> None:
271  """Handle specific state changes."""
272  hass.async_run_hass_job(
273  job,
274  event.data["entity_id"],
275  event.data["old_state"],
276  event.data["new_state"],
277  )
278 
279  @callback
280  def state_change_listener(event: Event[EventStateChangedData]) -> None:
281  """Handle specific state changes."""
282  if not state_change_filter(event.data):
283  return
284 
285  state_change_dispatcher(event)
286 
287  if entity_ids != MATCH_ALL:
288  # If we have a list of entity ids we use
289  # async_track_state_change_event to route
290  # by entity_id to avoid iterating though state change
291  # events and creating a jobs where the most
292  # common outcome is to return right away because
293  # the entity_id does not match since usually
294  # only one or two listeners want that specific
295  # entity_id.
296  return async_track_state_change_event(hass, entity_ids, state_change_listener)
297 
298  return hass.bus.async_listen(
299  EVENT_STATE_CHANGED,
300  state_change_dispatcher,
301  event_filter=state_change_filter,
302  )
303 
304 
305 track_state_change = threaded_listener_factory(async_track_state_change)
306 
307 
308 @bind_hass
310  hass: HomeAssistant,
311  entity_ids: str | Iterable[str],
312  action: Callable[[Event[EventStateChangedData]], Any],
313  job_type: HassJobType | None = None,
314 ) -> CALLBACK_TYPE:
315  """Track specific state change events indexed by entity_id.
316 
317  Unlike async_track_state_change, async_track_state_change_event
318  passes the full event to the callback.
319 
320  In order to avoid having to iterate a long list
321  of EVENT_STATE_CHANGED and fire and create a job
322  for each one, we keep a dict of entity ids that
323  care about the state change events so we can
324  do a fast dict lookup to route events.
325  The passed in entity_ids will be automatically lower cased.
326 
327  EVENT_STATE_CHANGED is fired on each occasion the state is updated
328  and changed, opposite of EVENT_STATE_REPORTED.
329  """
330  if not (entity_ids := _async_string_to_lower_list(entity_ids)):
331  return _remove_empty_listener
332  return _async_track_state_change_event(hass, entity_ids, action, job_type)
333 
334 
335 @callback
337  hass: HomeAssistant,
338  callbacks: dict[str, list[HassJob[[Event[_StateEventDataT]], Any]]],
339  event: Event[_StateEventDataT],
340 ) -> None:
341  """Dispatch to listeners soon to ensure one event loop runs before dispatch."""
342  hass.loop.call_soon(_async_dispatch_entity_id_event, hass, callbacks, event)
343 
344 
345 @callback
347  hass: HomeAssistant,
348  callbacks: dict[str, list[HassJob[[Event[_StateEventDataT]], Any]]],
349  event: Event[_StateEventDataT],
350 ) -> None:
351  """Dispatch to listeners."""
352  if not (callbacks_list := callbacks.get(event.data["entity_id"])):
353  return
354  for job in callbacks_list.copy():
355  try:
356  hass.async_run_hass_job(job, event)
357  except Exception:
358  _LOGGER.exception(
359  "Error while dispatching event for %s to %s",
360  event.data["entity_id"],
361  job,
362  )
363 
364 
365 @callback
367  hass: HomeAssistant,
368  callbacks: dict[str, list[HassJob[[Event[_StateEventDataT]], Any]]],
369  event_data: _StateEventDataT,
370 ) -> bool:
371  """Filter state changes by entity_id."""
372  return event_data["entity_id"] in callbacks
373 
374 
375 _KEYED_TRACK_STATE_CHANGE = _KeyedEventTracker(
376  key=_TRACK_STATE_CHANGE_DATA,
377  event_type=EVENT_STATE_CHANGED,
378  dispatcher_callable=_async_dispatch_entity_id_event_soon,
379  filter_callable=_async_state_filter,
380 )
381 
382 
383 @bind_hass
385  hass: HomeAssistant,
386  entity_ids: str | Iterable[str],
387  action: Callable[[Event[EventStateChangedData]], Any],
388  job_type: HassJobType | None,
389 ) -> CALLBACK_TYPE:
390  """Faster version of async_track_state_change_event.
391 
392  The passed in entity_ids will not be automatically lower cased.
393  """
394  return _async_track_event(
395  _KEYED_TRACK_STATE_CHANGE, hass, entity_ids, action, job_type
396  )
397 
398 
399 _KEYED_TRACK_STATE_REPORT = _KeyedEventTracker(
400  key=_TRACK_STATE_REPORT_DATA,
401  event_type=EVENT_STATE_REPORTED,
402  dispatcher_callable=_async_dispatch_entity_id_event,
403  filter_callable=_async_state_filter,
404 )
405 
406 
408  hass: HomeAssistant,
409  entity_ids: str | Iterable[str],
410  action: Callable[[Event[EventStateReportedData]], Any],
411  job_type: HassJobType | None = None,
412 ) -> CALLBACK_TYPE:
413  """Track EVENT_STATE_REPORTED by entity_ids.
414 
415  EVENT_STATE_REPORTED is fired on each occasion the state is updated
416  but not changed, opposite of EVENT_STATE_CHANGED.
417  """
418  return _async_track_event(
419  _KEYED_TRACK_STATE_REPORT, hass, entity_ids, action, job_type
420  )
421 
422 
423 @callback
425  """Remove a listener that does nothing."""
426 
427 
428 @callback
430  hass: HomeAssistant,
431  tracker: _KeyedEventTracker[_TypedDictT],
432  keys: Iterable[str],
433  job: HassJob[[Event[_TypedDictT]], Any],
434  callbacks: dict[str, list[HassJob[[Event[_TypedDictT]], Any]]],
435 ) -> None:
436  """Remove listener."""
437  for key in keys:
438  callbacks[key].remove(job)
439  if not callbacks[key]:
440  del callbacks[key]
441 
442  if not callbacks:
443  hass.data.pop(tracker.key).listener()
444 
445 
446 # tracker, not hass is intentionally the first argument here since its
447 # constant and may be used in a partial in the future
449  tracker: _KeyedEventTracker[_TypedDictT],
450  hass: HomeAssistant,
451  keys: str | Iterable[str],
452  action: Callable[[Event[_TypedDictT]], None],
453  job_type: HassJobType | None,
454 ) -> CALLBACK_TYPE:
455  """Track an event by a specific key.
456 
457  This function is intended for internal use only.
458  """
459  if not keys:
460  return _remove_empty_listener
461 
462  hass_data = hass.data
463  tracker_key = tracker.key
464  if tracker_key in hass_data:
465  event_data = hass_data[tracker_key]
466  callbacks = event_data.callbacks
467  else:
468  callbacks = defaultdict(list)
469  listener = hass.bus.async_listen(
470  tracker.event_type,
471  partial(tracker.dispatcher_callable, hass, callbacks),
472  event_filter=partial(tracker.filter_callable, hass, callbacks),
473  )
474  event_data = _KeyedEventData(listener, callbacks)
475  hass_data[tracker_key] = event_data
476 
477  job = HassJob(action, f"track {tracker.event_type} event {keys}", job_type=job_type)
478 
479  if isinstance(keys, str):
480  # Almost all calls to this function use a single key
481  # so we optimize for that case. We don't use setdefault
482  # here because this function gets called ~20000 times
483  # during startup, and we want to avoid the overhead of
484  # creating empty lists and throwing them away.
485  callbacks[keys].append(job)
486  keys = (keys,)
487  else:
488  for key in keys:
489  callbacks[key].append(job)
490 
491  return partial(_remove_listener, hass, tracker, keys, job, callbacks)
492 
493 
494 @callback
496  hass: HomeAssistant,
497  callbacks: dict[str, list[HassJob[[Event[EventEntityRegistryUpdatedData]], Any]]],
498  event: Event[EventEntityRegistryUpdatedData],
499 ) -> None:
500  """Dispatch to listeners."""
501  if not (
502  callbacks_list := callbacks.get( # type: ignore[call-overload] # mypy bug?
503  event.data.get("old_entity_id", event.data["entity_id"])
504  )
505  ):
506  return
507  for job in callbacks_list.copy():
508  try:
509  hass.async_run_hass_job(job, event)
510  except Exception:
511  _LOGGER.exception(
512  "Error while dispatching event for %s to %s",
513  event.data.get("old_entity_id", event.data["entity_id"]),
514  job,
515  )
516 
517 
518 @callback
520  hass: HomeAssistant,
521  callbacks: dict[str, list[HassJob[[Event[EventEntityRegistryUpdatedData]], Any]]],
522  event_data: EventEntityRegistryUpdatedData,
523 ) -> bool:
524  """Filter entity registry updates by entity_id."""
525  return event_data.get("old_entity_id", event_data["entity_id"]) in callbacks
526 
527 
528 _KEYED_TRACK_ENTITY_REGISTRY_UPDATED = _KeyedEventTracker(
529  key=_TRACK_ENTITY_REGISTRY_UPDATED_DATA,
530  event_type=EVENT_ENTITY_REGISTRY_UPDATED,
531  dispatcher_callable=_async_dispatch_old_entity_id_or_entity_id_event,
532  filter_callable=_async_entity_registry_updated_filter,
533 )
534 
535 
536 @bind_hass
537 @callback
539  hass: HomeAssistant,
540  entity_ids: str | Iterable[str],
541  action: Callable[[Event[EventEntityRegistryUpdatedData]], Any],
542  job_type: HassJobType | None = None,
543 ) -> CALLBACK_TYPE:
544  """Track specific entity registry updated events indexed by entity_id.
545 
546  Entities must be lower case.
547 
548  Similar to async_track_state_change_event.
549  """
550  return _async_track_event(
551  _KEYED_TRACK_ENTITY_REGISTRY_UPDATED, hass, entity_ids, action, job_type
552  )
553 
554 
555 @callback
557  hass: HomeAssistant,
558  callbacks: dict[str, list[HassJob[[Event[EventDeviceRegistryUpdatedData]], Any]]],
559  event_data: EventDeviceRegistryUpdatedData,
560 ) -> bool:
561  """Filter device registry updates by device_id."""
562  return event_data["device_id"] in callbacks
563 
564 
565 @callback
567  hass: HomeAssistant,
568  callbacks: dict[str, list[HassJob[[Event[EventDeviceRegistryUpdatedData]], Any]]],
569  event: Event[EventDeviceRegistryUpdatedData],
570 ) -> None:
571  """Dispatch to listeners."""
572  if not (callbacks_list := callbacks.get(event.data["device_id"])):
573  return
574  for job in callbacks_list.copy():
575  try:
576  hass.async_run_hass_job(job, event)
577  except Exception:
578  _LOGGER.exception(
579  "Error while dispatching event for %s to %s",
580  event.data["device_id"],
581  job,
582  )
583 
584 
585 _KEYED_TRACK_DEVICE_REGISTRY_UPDATED = _KeyedEventTracker(
586  key=_TRACK_DEVICE_REGISTRY_UPDATED_DATA,
587  event_type=EVENT_DEVICE_REGISTRY_UPDATED,
588  dispatcher_callable=_async_dispatch_device_id_event,
589  filter_callable=_async_device_registry_updated_filter,
590 )
591 
592 
593 @callback
595  hass: HomeAssistant,
596  device_ids: str | Iterable[str],
597  action: Callable[[Event[EventDeviceRegistryUpdatedData]], Any],
598  job_type: HassJobType | None = None,
599 ) -> CALLBACK_TYPE:
600  """Track specific device registry updated events indexed by device_id.
601 
602  Similar to async_track_entity_registry_updated_event.
603  """
604  return _async_track_event(
605  _KEYED_TRACK_DEVICE_REGISTRY_UPDATED, hass, device_ids, action, job_type
606  )
607 
608 
609 @callback
611  hass: HomeAssistant,
612  callbacks: dict[str, list[HassJob[[Event[EventStateChangedData]], Any]]],
613  event: Event[EventStateChangedData],
614 ) -> None:
615  """Dispatch domain event listeners."""
616  domain = split_entity_id(event.data["entity_id"])[0]
617  for job in callbacks.get(domain, []) + callbacks.get(MATCH_ALL, []):
618  try:
619  hass.async_run_hass_job(job, event)
620  except Exception:
621  _LOGGER.exception(
622  "Error while processing event %s for domain %s", event, domain
623  )
624 
625 
626 @callback
628  hass: HomeAssistant,
629  callbacks: dict[str, list[HassJob[[Event[EventStateChangedData]], Any]]],
630  event_data: EventStateChangedData,
631 ) -> bool:
632  """Filter state changes by entity_id."""
633  return event_data["old_state"] is None and (
634  MATCH_ALL in callbacks
635  or
636  # If old_state is None, new_state must be set but
637  # mypy doesn't know that
638  event_data["new_state"].domain in callbacks # type: ignore[union-attr]
639  )
640 
641 
642 @bind_hass
644  hass: HomeAssistant,
645  domains: str | Iterable[str],
646  action: Callable[[Event[EventStateChangedData]], Any],
647  job_type: HassJobType | None = None,
648 ) -> CALLBACK_TYPE:
649  """Track state change events when an entity is added to domains."""
650  if not (domains := _async_string_to_lower_list(domains)):
651  return _remove_empty_listener
652  return _async_track_state_added_domain(hass, domains, action, job_type)
653 
654 
655 _KEYED_TRACK_STATE_ADDED_DOMAIN = _KeyedEventTracker(
656  key=_TRACK_STATE_ADDED_DOMAIN_DATA,
657  event_type=EVENT_STATE_CHANGED,
658  dispatcher_callable=_async_dispatch_domain_event,
659  filter_callable=_async_domain_added_filter,
660 )
661 
662 
663 @bind_hass
665  hass: HomeAssistant,
666  domains: str | Iterable[str],
667  action: Callable[[Event[EventStateChangedData]], Any],
668  job_type: HassJobType | None,
669 ) -> CALLBACK_TYPE:
670  """Track state change events when an entity is added to domains."""
671  return _async_track_event(
672  _KEYED_TRACK_STATE_ADDED_DOMAIN, hass, domains, action, job_type
673  )
674 
675 
676 @callback
678  hass: HomeAssistant,
679  callbacks: dict[str, list[HassJob[[Event[EventStateChangedData]], Any]]],
680  event_data: EventStateChangedData,
681 ) -> bool:
682  """Filter state changes by entity_id."""
683  return event_data["new_state"] is None and (
684  MATCH_ALL in callbacks
685  or
686  # If new_state is None, old_state must be set but
687  # mypy doesn't know that
688  event_data["old_state"].domain in callbacks # type: ignore[union-attr]
689  )
690 
691 
692 _KEYED_TRACK_STATE_REMOVED_DOMAIN = _KeyedEventTracker(
693  key=_TRACK_STATE_REMOVED_DOMAIN_DATA,
694  event_type=EVENT_STATE_CHANGED,
695  dispatcher_callable=_async_dispatch_domain_event,
696  filter_callable=_async_domain_removed_filter,
697 )
698 
699 
700 @bind_hass
702  hass: HomeAssistant,
703  domains: str | Iterable[str],
704  action: Callable[[Event[EventStateChangedData]], Any],
705  job_type: HassJobType | None = None,
706 ) -> CALLBACK_TYPE:
707  """Track state change events when an entity is removed from domains."""
708  return _async_track_event(
709  _KEYED_TRACK_STATE_REMOVED_DOMAIN, hass, domains, action, job_type
710  )
711 
712 
713 @callback
714 def _async_string_to_lower_list(instr: str | Iterable[str]) -> list[str]:
715  if isinstance(instr, str):
716  return [instr.lower()]
717 
718  return [mstr.lower() for mstr in instr]
719 
720 
722  """Handle removal / refresh of tracker."""
723 
724  def __init__(
725  self,
726  hass: HomeAssistant,
727  track_states: TrackStates,
728  action: Callable[[Event[EventStateChangedData]], Any],
729  ) -> None:
730  """Handle removal / refresh of tracker init."""
731  self.hasshass = hass
732  self._action_action = action
733  self._action_as_hassjob_action_as_hassjob = HassJob(
734  action, f"track state change filtered {track_states}"
735  )
736  self._listeners: dict[str, Callable[[], None]] = {}
737  self._last_track_states_last_track_states: TrackStates = track_states
738 
739  @callback
740  def async_setup(self) -> None:
741  """Create listeners to track states."""
742  track_states = self._last_track_states_last_track_states
743 
744  if (
745  not track_states.all_states
746  and not track_states.domains
747  and not track_states.entities
748  ):
749  return
750 
751  if track_states.all_states:
752  self._setup_all_listener_setup_all_listener()
753  return
754 
755  self._setup_domains_listener_setup_domains_listener(track_states.domains)
756  self._setup_entities_listener_setup_entities_listener(track_states.domains, track_states.entities)
757 
758  @property
759  def listeners(self) -> dict[str, bool | set[str]]:
760  """State changes that will cause a re-render."""
761  track_states = self._last_track_states_last_track_states
762  return {
763  _ALL_LISTENER: track_states.all_states,
764  _ENTITIES_LISTENER: track_states.entities,
765  _DOMAINS_LISTENER: track_states.domains,
766  }
767 
768  @callback
769  def async_update_listeners(self, new_track_states: TrackStates) -> None:
770  """Update the listeners based on the new TrackStates."""
771  last_track_states = self._last_track_states_last_track_states
772  self._last_track_states_last_track_states = new_track_states
773 
774  had_all_listener = last_track_states.all_states
775 
776  if new_track_states.all_states:
777  if had_all_listener:
778  return
779  self._cancel_listener_cancel_listener(_DOMAINS_LISTENER)
780  self._cancel_listener_cancel_listener(_ENTITIES_LISTENER)
781  self._setup_all_listener_setup_all_listener()
782  return
783 
784  if had_all_listener:
785  self._cancel_listener_cancel_listener(_ALL_LISTENER)
786 
787  domains_changed = new_track_states.domains != last_track_states.domains
788 
789  if had_all_listener or domains_changed:
790  domains_changed = True
791  self._cancel_listener_cancel_listener(_DOMAINS_LISTENER)
792  self._setup_domains_listener_setup_domains_listener(new_track_states.domains)
793 
794  if (
795  had_all_listener
796  or domains_changed
797  or new_track_states.entities != last_track_states.entities
798  ):
799  self._cancel_listener_cancel_listener(_ENTITIES_LISTENER)
800  self._setup_entities_listener_setup_entities_listener(
801  new_track_states.domains, new_track_states.entities
802  )
803 
804  @callback
805  def async_remove(self) -> None:
806  """Cancel the listeners."""
807  for key in list(self._listeners):
808  self._listeners.pop(key)()
809 
810  @callback
811  def _cancel_listener(self, listener_name: str) -> None:
812  if listener_name not in self._listeners:
813  return
814 
815  self._listeners.pop(listener_name)()
816 
817  @callback
818  def _setup_entities_listener(self, domains: set[str], entities: set[str]) -> None:
819  if domains:
820  entities = entities.copy()
821  entities.update(self.hasshass.states.async_entity_ids(domains))
822 
823  # Entities has changed to none
824  if not entities:
825  return
826 
827  self._listeners[_ENTITIES_LISTENER] = _async_track_state_change_event(
828  self.hasshass, entities, self._action_action, self._action_as_hassjob_action_as_hassjob.job_type
829  )
830 
831  @callback
832  def _state_added(self, event: Event[EventStateChangedData]) -> None:
833  self._cancel_listener_cancel_listener(_ENTITIES_LISTENER)
834  self._setup_entities_listener_setup_entities_listener(
835  self._last_track_states_last_track_states.domains, self._last_track_states_last_track_states.entities
836  )
837  self.hasshass.async_run_hass_job(self._action_as_hassjob_action_as_hassjob, event)
838 
839  @callback
840  def _setup_domains_listener(self, domains: set[str]) -> None:
841  if not domains:
842  return
843 
844  self._listeners[_DOMAINS_LISTENER] = _async_track_state_added_domain(
845  self.hasshass, domains, self._state_added_state_added, HassJobType.Callback
846  )
847 
848  @callback
849  def _setup_all_listener(self) -> None:
850  self._listeners[_ALL_LISTENER] = self.hasshass.bus.async_listen(
851  EVENT_STATE_CHANGED, self._action_action
852  )
853 
854 
855 @callback
856 @bind_hass
858  hass: HomeAssistant,
859  track_states: TrackStates,
860  action: Callable[[Event[EventStateChangedData]], Any],
861 ) -> _TrackStateChangeFiltered:
862  """Track state changes with a TrackStates filter that can be updated.
863 
864  Parameters
865  ----------
866  hass
867  Home assistant object.
868  track_states
869  A TrackStates data class.
870  action
871  Callable to call with results.
872 
873  Returns
874  -------
875  Object used to update the listeners (async_update_listeners) with a new
876  TrackStates or cancel the tracking (async_remove).
877 
878  """
879  tracker = _TrackStateChangeFiltered(hass, track_states, action)
880  tracker.async_setup()
881  return tracker
882 
883 
884 @callback
885 @bind_hass
887  hass: HomeAssistant,
888  template: Template,
889  action: Callable[
890  [str, State | None, State | None], Coroutine[Any, Any, None] | None
891  ],
892  variables: TemplateVarsType | None = None,
893 ) -> CALLBACK_TYPE:
894  """Add a listener that fires when a template evaluates to 'true'.
895 
896  Listen for the result of the template becoming true, or a true-like
897  string result, such as 'On', 'Open', or 'Yes'. If the template results
898  in an error state when the value changes, this will be logged and not
899  passed through.
900 
901  If the initial check of the template is invalid and results in an
902  exception, the listener will still be registered but will only
903  fire if the template result becomes true without an exception.
904 
905  Action arguments
906  ----------------
907  entity_id
908  ID of the entity that triggered the state change.
909  old_state
910  The old state of the entity that changed.
911  new_state
912  New state of the entity that changed.
913 
914  Parameters
915  ----------
916  hass
917  Home assistant object.
918  template
919  The template to calculate.
920  action
921  Callable to call with results. See above for arguments.
922  variables
923  Variables to pass to the template.
924 
925  Returns
926  -------
927  Callable to unregister the listener.
928 
929  """
930  job = HassJob(action, f"track template {template}")
931 
932  @callback
933  def _template_changed_listener(
934  event: Event[EventStateChangedData] | None,
935  updates: list[TrackTemplateResult],
936  ) -> None:
937  """Check if condition is correct and run action."""
938  track_result = updates.pop()
939 
940  template = track_result.template
941  last_result = track_result.last_result
942  result = track_result.result
943 
944  if isinstance(result, TemplateError):
945  _LOGGER.error(
946  "Error while processing template: %s",
947  template.template,
948  exc_info=result,
949  )
950  return
951 
952  if (
953  not isinstance(last_result, TemplateError)
954  and result_as_boolean(last_result)
955  or not result_as_boolean(result)
956  ):
957  return
958 
959  hass.async_run_hass_job(
960  job,
961  event and event.data["entity_id"],
962  event and event.data["old_state"],
963  event and event.data["new_state"],
964  )
965 
967  hass, [TrackTemplate(template, variables)], _template_changed_listener
968  )
969 
970  return info.async_remove
971 
972 
973 track_template = threaded_listener_factory(async_track_template)
974 
975 
977  """Handle removal / refresh of tracker."""
978 
979  def __init__(
980  self,
981  hass: HomeAssistant,
982  track_templates: Sequence[TrackTemplate],
983  action: TrackTemplateResultListener,
984  has_super_template: bool = False,
985  ) -> None:
986  """Handle removal / refresh of tracker init."""
987  self.hasshass = hass
988  self._job_job = HassJob(action, f"track template result {track_templates}")
989 
990  self._track_templates_track_templates = track_templates
991  self._has_super_template_has_super_template = has_super_template
992 
993  self._last_result: dict[Template, bool | str | TemplateError] = {}
994 
995  for track_template_ in track_templates:
996  if track_template_.template.hass:
997  continue
998 
999  frame.report_usage(
1000  "calls async_track_template_result with template without hass",
1001  core_behavior=frame.ReportBehavior.LOG,
1002  breaks_in_ha_version="2025.10",
1003  )
1004  track_template_.template.hass = hass
1005 
1006  self._rate_limit_rate_limit = KeyedRateLimit(hass)
1007  self._info: dict[Template, RenderInfo] = {}
1008  self._track_state_changes_track_state_changes: _TrackStateChangeFiltered | None = None
1009  self._time_listeners: dict[Template, Callable[[], None]] = {}
1010 
1011  def __repr__(self) -> str:
1012  """Return the representation."""
1013  return f"<TrackTemplateResultInfo {self._info}>"
1014 
1016  self,
1017  strict: bool = False,
1018  log_fn: Callable[[int, str], None] | None = None,
1019  ) -> None:
1020  """Activation of template tracking."""
1021  block_render = False
1022  super_template = self._track_templates_track_templates[0] if self._has_super_template_has_super_template else None
1023 
1024  # Render the super template first
1025  if super_template is not None:
1026  template = super_template.template
1027  variables = super_template.variables
1028  self._info[template] = info = template.async_render_to_info(
1029  variables, strict=strict, log_fn=log_fn
1030  )
1031 
1032  # If the super template did not render to True, don't update other templates
1033  try:
1034  super_result: str | TemplateError = info.result()
1035  except TemplateError as ex:
1036  super_result = ex
1037  if (
1038  super_result is not None
1039  and self._super_template_as_boolean_super_template_as_boolean(super_result) is not True
1040  ):
1041  block_render = True
1042 
1043  # Then update the remaining templates unless blocked by the super template
1044  for track_template_ in self._track_templates_track_templates:
1045  if block_render or track_template_ == super_template:
1046  continue
1047  template = track_template_.template
1048  variables = track_template_.variables
1049  self._info[template] = info = template.async_render_to_info(
1050  variables, strict=strict, log_fn=log_fn
1051  )
1052 
1053  if info.exception:
1054  if not log_fn:
1055  _LOGGER.error(
1056  "Error while processing template: %s",
1057  track_template_.template,
1058  exc_info=info.exception,
1059  )
1060  else:
1061  log_fn(logging.ERROR, str(info.exception))
1062 
1064  self.hasshass, _render_infos_to_track_states(self._info.values()), self._refresh_refresh
1065  )
1066  self._update_time_listeners_update_time_listeners()
1067  _LOGGER.debug(
1068  (
1069  "Template group %s listens for %s, first render blocked by super"
1070  " template: %s"
1071  ),
1072  self._track_templates_track_templates,
1073  self.listenerslisteners,
1074  block_render,
1075  )
1076 
1077  @property
1078  def listeners(self) -> dict[str, bool | set[str]]:
1079  """State changes that will cause a re-render."""
1080  assert self._track_state_changes_track_state_changes
1081  return {
1082  **self._track_state_changes_track_state_changes.listeners,
1083  "time": bool(self._time_listeners),
1084  }
1085 
1086  @callback
1087  def _setup_time_listener(self, template: Template, has_time: bool) -> None:
1088  if not has_time:
1089  if template in self._time_listeners:
1090  # now() or utcnow() has left the scope of the template
1091  self._time_listeners.pop(template)()
1092  return
1093 
1094  if template in self._time_listeners:
1095  return
1096 
1097  track_templates = [
1098  track_template_
1099  for track_template_ in self._track_templates_track_templates
1100  if track_template_.template == template
1101  ]
1102 
1103  @callback
1104  def _refresh_from_time(now: datetime) -> None:
1105  self._refresh_refresh(None, track_templates=track_templates)
1106 
1107  self._time_listeners[template] = async_track_utc_time_change(
1108  self.hasshass, _refresh_from_time, second=0
1109  )
1110 
1111  @callback
1112  def _update_time_listeners(self) -> None:
1113  for template, info in self._info.items():
1114  self._setup_time_listener_setup_time_listener(template, info.has_time)
1115 
1116  @callback
1117  def async_remove(self) -> None:
1118  """Cancel the listener."""
1119  assert self._track_state_changes_track_state_changes
1120  self._track_state_changes_track_state_changes.async_remove()
1121  self._rate_limit_rate_limit.async_remove()
1122  for template in list(self._time_listeners):
1123  self._time_listeners.pop(template)()
1124 
1125  @callback
1126  def async_refresh(self) -> None:
1127  """Force recalculate the template."""
1128  self._refresh_refresh(None)
1129 
1131  self,
1132  track_template_: TrackTemplate,
1133  now: float,
1134  event: Event[EventStateChangedData] | None,
1135  ) -> bool | TrackTemplateResult:
1136  """Re-render the template if conditions match.
1137 
1138  Returns False if the template was not re-rendered.
1139 
1140  Returns True if the template re-rendered and did not
1141  change.
1142 
1143  Returns TrackTemplateResult if the template re-render
1144  generates a new result.
1145  """
1146  template = track_template_.template
1147 
1148  if event:
1149  info = self._info[template]
1150 
1151  if not _event_triggers_rerender(event, info):
1152  return False
1153 
1154  had_timer = self._rate_limit_rate_limit.async_has_timer(template)
1155 
1156  if self._rate_limit_rate_limit.async_schedule_action(
1157  template,
1158  _rate_limit_for_event(event, info, track_template_),
1159  now,
1160  self._refresh_refresh,
1161  event,
1162  (track_template_,),
1163  True,
1164  ):
1165  return not had_timer
1166 
1167  _LOGGER.debug(
1168  "Template update %s triggered by event: %s",
1169  template.template,
1170  event,
1171  )
1172 
1173  self._rate_limit_rate_limit.async_triggered(template, now)
1174  self._info[template] = info = template.async_render_to_info(
1175  track_template_.variables
1176  )
1177 
1178  try:
1179  result: str | TemplateError = info.result()
1180  except TemplateError as ex:
1181  result = ex
1182 
1183  last_result = self._last_result.get(template)
1184 
1185  # Check to see if the result has changed or is new
1186  if result == last_result and template in self._last_result:
1187  return True
1188 
1189  if isinstance(result, TemplateError) and isinstance(last_result, TemplateError):
1190  return True
1191 
1192  return TrackTemplateResult(template, last_result, result)
1193 
1194  @staticmethod
1195  def _super_template_as_boolean(result: bool | str | TemplateError) -> bool:
1196  """Return True if the result is truthy or a TemplateError."""
1197  if isinstance(result, TemplateError):
1198  return True
1199 
1200  return result_as_boolean(result)
1201 
1202  @callback
1204  self,
1205  updates: list[TrackTemplateResult],
1206  update: bool | TrackTemplateResult,
1207  template: Template,
1208  ) -> bool:
1209  """Handle updates of a tracked template."""
1210  if not update:
1211  return False
1212 
1213  self._setup_time_listener_setup_time_listener(template, self._info[template].has_time)
1214 
1215  if isinstance(update, TrackTemplateResult):
1216  updates.append(update)
1217 
1218  return True
1219 
1220  @callback
1222  self,
1223  event: Event[EventStateChangedData] | None,
1224  track_templates: Iterable[TrackTemplate] | None = None,
1225  replayed: bool | None = False,
1226  ) -> None:
1227  """Refresh the template.
1228 
1229  The event is the state_changed event that caused the refresh
1230  to be considered.
1231 
1232  track_templates is an optional list of TrackTemplate objects
1233  to refresh. If not provided, all tracked templates will be
1234  considered.
1235 
1236  replayed is True if the event is being replayed because the
1237  rate limit was hit.
1238  """
1239  updates: list[TrackTemplateResult] = []
1240  info_changed = False
1241  now = event.time_fired_timestamp if not replayed and event else time.time()
1242 
1243  block_updates = False
1244  super_template = self._track_templates_track_templates[0] if self._has_super_template_has_super_template else None
1245 
1246  track_templates = track_templates or self._track_templates_track_templates
1247 
1248  # Update the super template first
1249  if super_template is not None:
1250  update = self._render_template_if_ready_render_template_if_ready(super_template, now, event)
1251  info_changed |= self._apply_update_apply_update(updates, update, super_template.template)
1252 
1253  if isinstance(update, TrackTemplateResult):
1254  super_result = update.result
1255  else:
1256  super_result = self._last_result.get(super_template.template)
1257 
1258  # If the super template did not render to True, don't update other templates
1259  if (
1260  super_result is not None
1261  and self._super_template_as_boolean_super_template_as_boolean(super_result) is not True
1262  ):
1263  block_updates = True
1264 
1265  if (
1266  isinstance(update, TrackTemplateResult)
1267  and self._super_template_as_boolean_super_template_as_boolean(update.last_result) is not True
1268  and self._super_template_as_boolean_super_template_as_boolean(update.result) is True
1269  ):
1270  # Super template changed from not True to True, force re-render
1271  # of all templates in the group
1272  event = None
1273  track_templates = self._track_templates_track_templates
1274 
1275  # Then update the remaining templates unless blocked by the super template
1276  if not block_updates:
1277  for track_template_ in track_templates:
1278  if track_template_ == super_template:
1279  continue
1280 
1281  update = self._render_template_if_ready_render_template_if_ready(track_template_, now, event)
1282  info_changed |= self._apply_update_apply_update(
1283  updates, update, track_template_.template
1284  )
1285 
1286  if info_changed:
1287  assert self._track_state_changes_track_state_changes
1288  self._track_state_changes_track_state_changes.async_update_listeners(
1290  [
1292  if self._rate_limit_rate_limit.async_has_timer(template)
1293  else info
1294  for template, info in self._info.items()
1295  ]
1296  )
1297  )
1298  _LOGGER.debug(
1299  (
1300  "Template group %s listens for %s, re-render blocked by super"
1301  " template: %s"
1302  ),
1303  self._track_templates_track_templates,
1304  self.listenerslisteners,
1305  block_updates,
1306  )
1307 
1308  if not updates:
1309  return
1310 
1311  for track_result in updates:
1312  self._last_result[track_result.template] = track_result.result
1313 
1314  self.hasshass.async_run_hass_job(self._job_job, event, updates)
1315 
1316 
1317 type TrackTemplateResultListener = Callable[
1318  [
1319  Event[EventStateChangedData] | None,
1320  list[TrackTemplateResult],
1321  ],
1322  Coroutine[Any, Any, None] | None,
1323 ]
1324 """Type for the listener for template results.
1325 
1326  Action arguments
1327  ----------------
1328  event
1329  Event that caused the template to change output. None if not
1330  triggered by an event.
1331  updates
1332  A list of TrackTemplateResult
1333 """
1334 
1335 
1336 @callback
1337 @bind_hass
1339  hass: HomeAssistant,
1340  track_templates: Sequence[TrackTemplate],
1341  action: TrackTemplateResultListener,
1342  strict: bool = False,
1343  log_fn: Callable[[int, str], None] | None = None,
1344  has_super_template: bool = False,
1345 ) -> TrackTemplateResultInfo:
1346  """Add a listener that fires when the result of a template changes.
1347 
1348  The action will fire with the initial result from the template, and
1349  then whenever the output from the template changes. The template will
1350  be reevaluated if any states referenced in the last run of the
1351  template change, or if manually triggered. If the result of the
1352  evaluation is different from the previous run, the listener is passed
1353  the result.
1354 
1355  If the template results in an TemplateError, this will be returned to
1356  the listener the first time this happens but not for subsequent errors.
1357  Once the template returns to a non-error condition the result is sent
1358  to the action as usual.
1359 
1360  Parameters
1361  ----------
1362  hass
1363  Home assistant object.
1364  track_templates
1365  An iterable of TrackTemplate.
1366  action
1367  Callable to call with results.
1368  strict
1369  When set to True, raise on undefined variables.
1370  log_fn
1371  If not None, template error messages will logging by calling log_fn
1372  instead of the normal logging facility.
1373  has_super_template
1374  When set to True, the first template will block rendering of other
1375  templates if it doesn't render as True.
1376 
1377  Returns
1378  -------
1379  Info object used to unregister the listener, and refresh the template.
1380 
1381  """
1382  tracker = TrackTemplateResultInfo(hass, track_templates, action, has_super_template)
1383  tracker.async_setup(strict=strict, log_fn=log_fn)
1384  return tracker
1385 
1386 
1387 @callback
1388 @bind_hass
1390  hass: HomeAssistant,
1391  period: timedelta,
1392  action: Callable[[], Coroutine[Any, Any, None] | None],
1393  async_check_same_func: Callable[[str, State | None, State | None], bool],
1394  entity_ids: str | Iterable[str] = MATCH_ALL,
1395 ) -> CALLBACK_TYPE:
1396  """Track the state of entities for a period and run an action.
1397 
1398  If async_check_func is None it use the state of orig_value.
1399  Without entity_ids we track all state changes.
1400  """
1401  async_remove_state_for_cancel: CALLBACK_TYPE | None = None
1402  async_remove_state_for_listener: CALLBACK_TYPE | None = None
1403 
1404  job = HassJob(action, f"track same state {period} {entity_ids}")
1405 
1406  @callback
1407  def clear_listener() -> None:
1408  """Clear all unsub listener."""
1409  nonlocal async_remove_state_for_cancel, async_remove_state_for_listener
1410 
1411  if async_remove_state_for_listener is not None:
1412  async_remove_state_for_listener()
1413  async_remove_state_for_listener = None
1414  if async_remove_state_for_cancel is not None:
1415  async_remove_state_for_cancel()
1416  async_remove_state_for_cancel = None
1417 
1418  @callback
1419  def state_for_listener(now: Any) -> None:
1420  """Fire on state changes after a delay and calls action."""
1421  nonlocal async_remove_state_for_listener
1422  async_remove_state_for_listener = None
1423  clear_listener()
1424  hass.async_run_hass_job(job)
1425 
1426  @callback
1427  def state_for_cancel_listener(event: Event[EventStateChangedData]) -> None:
1428  """Fire on changes and cancel for listener if changed."""
1429  entity = event.data["entity_id"]
1430  from_state = event.data["old_state"]
1431  to_state = event.data["new_state"]
1432 
1433  if not async_check_same_func(entity, from_state, to_state):
1434  clear_listener()
1435 
1436  async_remove_state_for_listener = async_call_later(hass, period, state_for_listener)
1437 
1438  if entity_ids == MATCH_ALL:
1439  async_remove_state_for_cancel = hass.bus.async_listen(
1440  EVENT_STATE_CHANGED, state_for_cancel_listener
1441  )
1442  else:
1443  async_remove_state_for_cancel = async_track_state_change_event(
1444  hass,
1445  entity_ids,
1446  state_for_cancel_listener,
1447  )
1448 
1449  return clear_listener
1450 
1451 
1452 track_same_state = threaded_listener_factory(async_track_same_state)
1453 
1454 
1455 @callback
1456 @bind_hass
1458  hass: HomeAssistant,
1459  action: HassJob[[datetime], Coroutine[Any, Any, None] | None]
1460  | Callable[[datetime], Coroutine[Any, Any, None] | None],
1461  point_in_time: datetime,
1462 ) -> CALLBACK_TYPE:
1463  """Add a listener that fires once at or after a specific point in time.
1464 
1465  The listener is passed the time it fires in local time.
1466  """
1467  job = (
1468  action
1469  if isinstance(action, HassJob)
1470  else HassJob(action, f"track point in time {point_in_time}")
1471  )
1472 
1473  @callback
1474  def utc_converter(utc_now: datetime) -> None:
1475  """Convert passed in UTC now to local now."""
1476  hass.async_run_hass_job(job, dt_util.as_local(utc_now))
1477 
1478  track_job = HassJob(
1479  utc_converter,
1480  name=f"{job.name} UTC converter",
1481  cancel_on_shutdown=job.cancel_on_shutdown,
1482  job_type=HassJobType.Callback,
1483  )
1484  return async_track_point_in_utc_time(hass, track_job, point_in_time)
1485 
1486 
1487 track_point_in_time = threaded_listener_factory(async_track_point_in_time)
1488 
1489 
1490 @dataclass(slots=True)
1492  hass: HomeAssistant
1493  job: HassJob[[datetime], Coroutine[Any, Any, None] | None]
1494  utc_point_in_time: datetime
1495  expected_fire_timestamp: float
1496  _cancel_callback: asyncio.TimerHandle | None = None
1497 
1498  def async_attach(self) -> None:
1499  """Initialize track job."""
1500  loop = self.hass.loop
1501  self._cancel_callback_cancel_callback = loop.call_at(
1502  loop.time() + self.expected_fire_timestamp - time.time(), self
1503  )
1504 
1505  @callback
1506  def __call__(self) -> None:
1507  """Call the action.
1508 
1509  We implement this as __call__ so when debug logging logs the object
1510  it shows the name of the job. This is especially helpful when asyncio
1511  debug logging is enabled as we can see the name of the job that is
1512  being called that is blocking the event loop.
1513  """
1514  # Depending on the available clock support (including timer hardware
1515  # and the OS kernel) it can happen that we fire a little bit too early
1516  # as measured by utcnow(). That is bad when callbacks have assumptions
1517  # about the current time. Thus, we rearm the timer for the remaining
1518  # time.
1519  if (delta := (self.expected_fire_timestamp - time_tracker_timestamp())) > 0:
1520  _LOGGER.debug("Called %f seconds too early, rearming", delta)
1521  loop = self.hass.loop
1522  self._cancel_callback_cancel_callback = loop.call_at(loop.time() + delta, self)
1523  return
1524 
1525  self.hass.async_run_hass_job(self.job, self.utc_point_in_time)
1526 
1527  @callback
1528  def async_cancel(self) -> None:
1529  """Cancel the call_at."""
1530  if TYPE_CHECKING:
1531  assert self._cancel_callback_cancel_callback is not None
1532  self._cancel_callback_cancel_callback.cancel()
1533 
1534 
1535 @callback
1536 @bind_hass
1538  hass: HomeAssistant,
1539  action: HassJob[[datetime], Coroutine[Any, Any, None] | None]
1540  | Callable[[datetime], Coroutine[Any, Any, None] | None],
1541  point_in_time: datetime,
1542 ) -> CALLBACK_TYPE:
1543  """Add a listener that fires once at or after a specific point in time.
1544 
1545  The listener is passed the time it fires in UTC time.
1546  """
1547  # Ensure point_in_time is UTC
1548  utc_point_in_time = dt_util.as_utc(point_in_time)
1549  expected_fire_timestamp = utc_point_in_time.timestamp()
1550  job = (
1551  action
1552  if isinstance(action, HassJob)
1553  else HassJob(action, f"track point in utc time {utc_point_in_time}")
1554  )
1555  track = _TrackPointUTCTime(hass, job, utc_point_in_time, expected_fire_timestamp)
1556  track.async_attach()
1557  return track.async_cancel
1558 
1559 
1560 track_point_in_utc_time = threaded_listener_factory(async_track_point_in_utc_time)
1561 
1562 
1564  hass: HomeAssistant, job: HassJob[[datetime], Coroutine[Any, Any, None] | None]
1565 ) -> None:
1566  """Run action."""
1567  hass.async_run_hass_job(job, time_tracker_utcnow())
1568 
1569 
1570 @callback
1571 @bind_hass
1573  hass: HomeAssistant,
1574  action: HassJob[[datetime], Coroutine[Any, Any, None] | None]
1575  | Callable[[datetime], Coroutine[Any, Any, None] | None],
1576  loop_time: float,
1577 ) -> CALLBACK_TYPE:
1578  """Add a listener that fires at or after <loop_time>.
1579 
1580  The listener is passed the time it fires in UTC time.
1581  """
1582  job = (
1583  action
1584  if isinstance(action, HassJob)
1585  else HassJob(action, f"call_at {loop_time}")
1586  )
1587  return hass.loop.call_at(loop_time, _run_async_call_action, hass, job).cancel
1588 
1589 
1590 @callback
1591 @bind_hass
1593  hass: HomeAssistant,
1594  delay: float | timedelta,
1595  action: HassJob[[datetime], Coroutine[Any, Any, None] | None]
1596  | Callable[[datetime], Coroutine[Any, Any, None] | None],
1597 ) -> CALLBACK_TYPE:
1598  """Add a listener that fires at or after <delay>.
1599 
1600  The listener is passed the time it fires in UTC time.
1601  """
1602  if isinstance(delay, timedelta):
1603  delay = delay.total_seconds()
1604  job = (
1605  action
1606  if isinstance(action, HassJob)
1607  else HassJob(action, f"call_later {delay}")
1608  )
1609  loop = hass.loop
1610  return loop.call_at(loop.time() + delay, _run_async_call_action, hass, job).cancel
1611 
1612 
1613 call_later = threaded_listener_factory(async_call_later)
1614 
1615 
1616 @dataclass(slots=True)
1618  """Helper class to help listen to time interval events."""
1619 
1620  hass: HomeAssistant
1621  seconds: float
1622  job_name: str
1623  action: Callable[[datetime], Coroutine[Any, Any, None] | None]
1624  cancel_on_shutdown: bool | None
1625  _track_job: HassJob[[datetime], Coroutine[Any, Any, None] | None] | None = None
1626  _run_job: HassJob[[datetime], Coroutine[Any, Any, None] | None] | None = None
1627  _timer_handle: asyncio.TimerHandle | None = None
1628 
1629  def async_attach(self) -> None:
1630  """Initialize track job."""
1631  self._track_job_track_job = HassJob(
1632  self._interval_listener_interval_listener,
1633  self.job_name,
1634  job_type=HassJobType.Callback,
1635  cancel_on_shutdown=self.cancel_on_shutdown,
1636  )
1637  self._run_job_run_job = HassJob(
1638  self.action,
1639  f"track time interval {self.seconds}",
1640  cancel_on_shutdown=self.cancel_on_shutdown,
1641  )
1642  self._schedule_timer_schedule_timer()
1643 
1644  def _schedule_timer(self) -> None:
1645  """Schedule the timer."""
1646  if TYPE_CHECKING:
1647  assert self._track_job_track_job is not None
1648  hass = self.hass
1649  loop = hass.loop
1650  self._timer_handle_timer_handle = loop.call_at(
1651  loop.time() + self.seconds, self._interval_listener_interval_listener, self._track_job_track_job
1652  )
1653 
1654  @callback
1655  def _interval_listener(self, _: Any) -> None:
1656  """Handle elapsed intervals."""
1657  if TYPE_CHECKING:
1658  assert self._run_job_run_job is not None
1659  self._schedule_timer_schedule_timer()
1660  self.hass.async_run_hass_job(self._run_job_run_job, dt_util.utcnow(), background=True)
1661 
1662  @callback
1663  def async_cancel(self) -> None:
1664  """Cancel the call_at."""
1665  if TYPE_CHECKING:
1666  assert self._timer_handle_timer_handle is not None
1667  self._timer_handle_timer_handle.cancel()
1668 
1669 
1670 @callback
1671 @bind_hass
1673  hass: HomeAssistant,
1674  action: Callable[[datetime], Coroutine[Any, Any, None] | None],
1675  interval: timedelta,
1676  *,
1677  name: str | None = None,
1678  cancel_on_shutdown: bool | None = None,
1679 ) -> CALLBACK_TYPE:
1680  """Add a listener that fires repetitively at every timedelta interval.
1681 
1682  The listener is passed the time it fires in UTC time.
1683  """
1684  seconds = interval.total_seconds()
1685  job_name = f"track time interval {seconds} {action}"
1686  if name:
1687  job_name = f"{name}: {job_name}"
1688  track = _TrackTimeInterval(hass, seconds, job_name, action, cancel_on_shutdown)
1689  track.async_attach()
1690  return track.async_cancel
1691 
1692 
1693 track_time_interval = threaded_listener_factory(async_track_time_interval)
1694 
1695 
1696 @dataclass(slots=True)
1698  """Helper class to help listen to sun events."""
1699 
1700  hass: HomeAssistant
1701  job: HassJob[[], Coroutine[Any, Any, None] | None]
1702  event: str
1703  offset: timedelta | None
1704  _unsub_sun: CALLBACK_TYPE | None = None
1705  _unsub_config: CALLBACK_TYPE | None = None
1706 
1707  @callback
1708  def async_attach(self) -> None:
1709  """Attach a sun listener."""
1710  assert self._unsub_config_unsub_config is None
1711 
1712  self._unsub_config_unsub_config = self.hass.bus.async_listen(
1713  EVENT_CORE_CONFIG_UPDATE, self._handle_config_event_handle_config_event
1714  )
1715 
1716  self._listen_next_sun_event_listen_next_sun_event()
1717 
1718  @callback
1719  def async_detach(self) -> None:
1720  """Detach the sun listener."""
1721  assert self._unsub_sun_unsub_sun is not None
1722  assert self._unsub_config_unsub_config is not None
1723 
1724  self._unsub_sun_unsub_sun()
1725  self._unsub_sun_unsub_sun = None
1726  self._unsub_config_unsub_config()
1727  self._unsub_config_unsub_config = None
1728 
1729  @callback
1730  def _listen_next_sun_event(self) -> None:
1731  """Set up the sun event listener."""
1732  assert self._unsub_sun_unsub_sun is None
1733 
1734  self._unsub_sun_unsub_sun = async_track_point_in_utc_time(
1735  self.hass,
1736  self._handle_sun_event_handle_sun_event,
1737  get_astral_event_next(self.hass, self.event, offset=self.offset),
1738  )
1739 
1740  @callback
1741  def _handle_sun_event(self, _now: Any) -> None:
1742  """Handle solar event."""
1743  self._unsub_sun_unsub_sun = None
1744  self._listen_next_sun_event_listen_next_sun_event()
1745  self.hass.async_run_hass_job(self.job, background=True)
1746 
1747  @callback
1748  def _handle_config_event(self, _event: Any) -> None:
1749  """Handle core config update."""
1750  assert self._unsub_sun_unsub_sun is not None
1751  self._unsub_sun_unsub_sun()
1752  self._unsub_sun_unsub_sun = None
1753  self._listen_next_sun_event_listen_next_sun_event()
1754 
1755 
1756 @callback
1757 @bind_hass
1759  hass: HomeAssistant, action: Callable[[], None], offset: timedelta | None = None
1760 ) -> CALLBACK_TYPE:
1761  """Add a listener that will fire a specified offset from sunrise daily."""
1762  listener = SunListener(
1763  hass, HassJob(action, "track sunrise"), SUN_EVENT_SUNRISE, offset
1764  )
1765  listener.async_attach()
1766  return listener.async_detach
1767 
1768 
1769 track_sunrise = threaded_listener_factory(async_track_sunrise)
1770 
1771 
1772 @callback
1773 @bind_hass
1775  hass: HomeAssistant, action: Callable[[], None], offset: timedelta | None = None
1776 ) -> CALLBACK_TYPE:
1777  """Add a listener that will fire a specified offset from sunset daily."""
1778  listener = SunListener(
1779  hass, HassJob(action, "track sunset"), SUN_EVENT_SUNSET, offset
1780  )
1781  listener.async_attach()
1782  return listener.async_detach
1783 
1784 
1785 track_sunset = threaded_listener_factory(async_track_sunset)
1786 
1787 # For targeted patching in tests
1788 time_tracker_utcnow = dt_util.utcnow
1789 time_tracker_timestamp = time.time
1790 
1791 
1792 @dataclass(slots=True)
1794  hass: HomeAssistant
1795  time_match_expression: tuple[list[int], list[int], list[int]]
1796  microsecond: int
1797  local: bool
1798  job: HassJob[[datetime], Coroutine[Any, Any, None] | None]
1799  listener_job_name: str
1800  _pattern_time_change_listener_job: HassJob[[datetime], None] | None = None
1801  _cancel_callback: CALLBACK_TYPE | None = None
1802 
1803  def async_attach(self) -> None:
1804  """Initialize track job."""
1805  self._pattern_time_change_listener_job_pattern_time_change_listener_job = HassJob(
1806  self._pattern_time_change_listener_pattern_time_change_listener,
1807  self.listener_job_name,
1808  job_type=HassJobType.Callback,
1809  )
1811  self.hass,
1812  self._pattern_time_change_listener_job_pattern_time_change_listener_job,
1813  self._calculate_next_calculate_next(dt_util.utcnow()),
1814  )
1815 
1816  def _calculate_next(self, utc_now: datetime) -> datetime:
1817  """Calculate and set the next time the trigger should fire."""
1818  localized_now = dt_util.as_local(utc_now) if self.local else utc_now
1819  return dt_util.find_next_time_expression_time(
1820  localized_now, *self.time_match_expression
1821  ).replace(microsecond=self.microsecond)
1822 
1823  @callback
1824  def _pattern_time_change_listener(self, _: datetime) -> None:
1825  """Listen for matching time_changed events."""
1826  hass = self.hass
1827  # Fetch time again because we want the actual time, not the
1828  # time when the timer was scheduled
1829  utc_now = time_tracker_utcnow()
1830  localized_now = dt_util.as_local(utc_now) if self.local else utc_now
1831  if TYPE_CHECKING:
1832  assert self._pattern_time_change_listener_job_pattern_time_change_listener_job is not None
1833  self._cancel_callback_cancel_callback = async_track_point_in_utc_time(
1834  hass,
1835  self._pattern_time_change_listener_job_pattern_time_change_listener_job,
1836  self._calculate_next_calculate_next(utc_now + timedelta(seconds=1)),
1837  )
1838  hass.async_run_hass_job(self.job, localized_now, background=True)
1839 
1840  @callback
1841  def async_cancel(self) -> None:
1842  """Cancel the call_at."""
1843  if TYPE_CHECKING:
1844  assert self._cancel_callback_cancel_callback is not None
1845  self._cancel_callback_cancel_callback()
1846 
1847 
1848 @callback
1849 @bind_hass
1851  hass: HomeAssistant,
1852  action: Callable[[datetime], Coroutine[Any, Any, None] | None],
1853  hour: Any | None = None,
1854  minute: Any | None = None,
1855  second: Any | None = None,
1856  local: bool = False,
1857 ) -> CALLBACK_TYPE:
1858  """Add a listener that will fire every time the UTC or local time matches a pattern.
1859 
1860  The listener is passed the time it fires in UTC or local time.
1861  """
1862  # We do not have to wrap the function with time pattern matching logic
1863  # if no pattern given
1864  if all(val is None or val == "*" for val in (hour, minute, second)):
1865  # Previously this relied on EVENT_TIME_FIRED
1866  # which meant it would not fire right away because
1867  # the caller would always be misaligned with the call
1868  # time vs the fire time by < 1s. To preserve this
1869  # misalignment we use async_track_time_interval here
1870  return async_track_time_interval(hass, action, timedelta(seconds=1))
1871 
1872  job = HassJob(action, f"track time change {hour}:{minute}:{second} local={local}")
1873  matching_seconds = dt_util.parse_time_expression(second, 0, 59)
1874  matching_minutes = dt_util.parse_time_expression(minute, 0, 59)
1875  matching_hours = dt_util.parse_time_expression(hour, 0, 23)
1876  # Avoid aligning all time trackers to the same fraction of a second
1877  # since it can create a thundering herd problem
1878  # https://github.com/home-assistant/core/issues/82231
1879  microsecond = randint(RANDOM_MICROSECOND_MIN, RANDOM_MICROSECOND_MAX)
1880  listener_job_name = f"time change listener {hour}:{minute}:{second} {action}"
1881  track = _TrackUTCTimeChange(
1882  hass,
1883  (matching_seconds, matching_minutes, matching_hours),
1884  microsecond,
1885  local,
1886  job,
1887  listener_job_name,
1888  )
1889  track.async_attach()
1890  return track.async_cancel
1891 
1892 
1893 track_utc_time_change = threaded_listener_factory(async_track_utc_time_change)
1894 
1895 
1896 @callback
1897 @bind_hass
1899  hass: HomeAssistant,
1900  action: Callable[[datetime], Coroutine[Any, Any, None] | None],
1901  hour: Any | None = None,
1902  minute: Any | None = None,
1903  second: Any | None = None,
1904 ) -> CALLBACK_TYPE:
1905  """Add a listener that will fire every time the local time matches a pattern.
1906 
1907  The listener is passed the time it fires in local time.
1908  """
1909  return async_track_utc_time_change(hass, action, hour, minute, second, local=True)
1910 
1911 
1912 track_time_change = threaded_listener_factory(async_track_time_change)
1913 
1914 
1916  parameter: str | Iterable[str] | None, invert: bool = False
1917 ) -> Callable[[str | None], bool]:
1918  """Convert parameter to function that matches input against parameter."""
1919  if parameter is None or parameter == MATCH_ALL:
1920  return lambda _: not invert
1921 
1922  if isinstance(parameter, str) or not hasattr(parameter, "__iter__"):
1923  return lambda state: invert is not (state == parameter)
1924 
1925  parameter_set = set(parameter)
1926  return lambda state: invert is not (state in parameter_set)
1927 
1928 
1929 @callback
1931  render_infos: Iterable[RenderInfo],
1932 ) -> tuple[set[str], set[str]]:
1933  """Combine from multiple RenderInfo."""
1934  entities: set[str] = set()
1935  domains: set[str] = set()
1936 
1937  for render_info in render_infos:
1938  if render_info.entities:
1939  entities.update(render_info.entities)
1940  if render_info.domains:
1941  domains.update(render_info.domains)
1942  if render_info.domains_lifecycle:
1943  domains.update(render_info.domains_lifecycle)
1944  return entities, domains
1945 
1946 
1947 @callback
1948 def _render_infos_needs_all_listener(render_infos: Iterable[RenderInfo]) -> bool:
1949  """Determine if an all listener is needed from RenderInfo."""
1950  for render_info in render_infos:
1951  # Tracking all states
1952  if render_info.all_states or render_info.all_states_lifecycle:
1953  return True
1954 
1955  return False
1956 
1957 
1958 @callback
1959 def _render_infos_to_track_states(render_infos: Iterable[RenderInfo]) -> TrackStates:
1960  """Create a TrackStates dataclass from the latest RenderInfo."""
1961  if _render_infos_needs_all_listener(render_infos):
1962  return TrackStates(True, set(), set())
1963 
1964  return TrackStates(False, *_entities_domains_from_render_infos(render_infos))
1965 
1966 
1967 @callback
1969  event: Event[EventStateChangedData], info: RenderInfo
1970 ) -> bool:
1971  """Determine if a template should be re-rendered from an event."""
1972  entity_id = event.data["entity_id"]
1973 
1974  if info.filter(entity_id):
1975  return True
1976 
1977  if event.data["new_state"] is not None and event.data["old_state"] is not None:
1978  return False
1979 
1980  return bool(info.filter_lifecycle(entity_id))
1981 
1982 
1983 @callback
1985  event: Event[EventStateChangedData],
1986  info: RenderInfo,
1987  track_template_: TrackTemplate,
1988 ) -> float | None:
1989  """Determine the rate limit for an event."""
1990  # Specifically referenced entities are excluded
1991  # from the rate limit
1992  if event.data["entity_id"] in info.entities:
1993  return None
1994 
1995  if track_template_.rate_limit is not None:
1996  return track_template_.rate_limit
1997 
1998  rate_limit: float | None = info.rate_limit
1999  return rate_limit
2000 
2001 
2002 def _suppress_domain_all_in_render_info(render_info: RenderInfo) -> RenderInfo:
2003  """Remove the domains and all_states from render info during a ratelimit."""
2004  rate_limited_render_info = copy.copy(render_info)
2005  rate_limited_render_info.all_states = False
2006  rate_limited_render_info.all_states_lifecycle = False
2007  rate_limited_render_info.domains = set()
2008  rate_limited_render_info.domains_lifecycle = set()
2009  return rate_limited_render_info
None _handle_config_event(self, Any _event)
Definition: event.py:1748
None _handle_sun_event(self, Any _now)
Definition: event.py:1741
None _refresh(self, Event[EventStateChangedData]|None event, Iterable[TrackTemplate]|None track_templates=None, bool|None replayed=False)
Definition: event.py:1226
None _setup_time_listener(self, Template template, bool has_time)
Definition: event.py:1087
dict[str, bool|set[str]] listeners(self)
Definition: event.py:1078
None async_setup(self, bool strict=False, Callable[[int, str], None]|None log_fn=None)
Definition: event.py:1019
None __init__(self, HomeAssistant hass, Sequence[TrackTemplate] track_templates, TrackTemplateResultListener action, bool has_super_template=False)
Definition: event.py:985
bool|TrackTemplateResult _render_template_if_ready(self, TrackTemplate track_template_, float now, Event[EventStateChangedData]|None event)
Definition: event.py:1135
bool _apply_update(self, list[TrackTemplateResult] updates, bool|TrackTemplateResult update, Template template)
Definition: event.py:1208
bool _super_template_as_boolean(bool|str|TemplateError result)
Definition: event.py:1195
None _setup_domains_listener(self, set[str] domains)
Definition: event.py:840
None __init__(self, HomeAssistant hass, TrackStates track_states, Callable[[Event[EventStateChangedData]], Any] action)
Definition: event.py:729
None _cancel_listener(self, str listener_name)
Definition: event.py:811
None _setup_entities_listener(self, set[str] domains, set[str] entities)
Definition: event.py:818
None async_update_listeners(self, TrackStates new_track_states)
Definition: event.py:769
dict[str, bool|set[str]] listeners(self)
Definition: event.py:759
None _state_added(self, Event[EventStateChangedData] event)
Definition: event.py:832
datetime _calculate_next(self, datetime utc_now)
Definition: event.py:1816
None _pattern_time_change_listener(self, datetime _)
Definition: event.py:1824
bool remove(self, _T matcher)
Definition: match.py:214
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
tuple[str, str] split_entity_id(str entity_id)
Definition: core.py:214
tuple[set[str], set[str]] _entities_domains_from_render_infos(Iterable[RenderInfo] render_infos)
Definition: event.py:1932
None _async_dispatch_device_id_event(HomeAssistant hass, dict[str, list[HassJob[[Event[EventDeviceRegistryUpdatedData]], Any]]] callbacks, Event[EventDeviceRegistryUpdatedData] event)
Definition: event.py:570
CALLBACK_TYPE _async_track_state_change_event(HomeAssistant hass, str|Iterable[str] entity_ids, Callable[[Event[EventStateChangedData]], Any] action, HassJobType|None job_type)
Definition: event.py:389
CALLBACK_TYPE async_track_time_change(HomeAssistant hass, Callable[[datetime], Coroutine[Any, Any, None]|None] action, Any|None hour=None, Any|None minute=None, Any|None second=None)
Definition: event.py:1904
CALLBACK_TYPE async_track_device_registry_updated_event(HomeAssistant hass, str|Iterable[str] device_ids, Callable[[Event[EventDeviceRegistryUpdatedData]], Any] action, HassJobType|None job_type=None)
Definition: event.py:599
_TrackStateChangeFiltered async_track_state_change_filtered(HomeAssistant hass, TrackStates track_states, Callable[[Event[EventStateChangedData]], Any] action)
Definition: event.py:861
bool _async_device_registry_updated_filter(HomeAssistant hass, dict[str, list[HassJob[[Event[EventDeviceRegistryUpdatedData]], Any]]] callbacks, EventDeviceRegistryUpdatedData event_data)
Definition: event.py:560
CALLBACK_TYPE async_track_sunset(HomeAssistant hass, Callable[[], None] action, timedelta|None offset=None)
Definition: event.py:1776
None _async_dispatch_domain_event(HomeAssistant hass, dict[str, list[HassJob[[Event[EventStateChangedData]], Any]]] callbacks, Event[EventStateChangedData] event)
Definition: event.py:614
CALLBACK_TYPE async_track_template(HomeAssistant hass, Template template, Callable[[str, State|None, State|None], Coroutine[Any, Any, None]|None] action, TemplateVarsType|None variables=None)
Definition: event.py:893
bool _async_domain_added_filter(HomeAssistant hass, dict[str, list[HassJob[[Event[EventStateChangedData]], Any]]] callbacks, EventStateChangedData event_data)
Definition: event.py:631
CALLBACK_TYPE async_track_state_added_domain(HomeAssistant hass, str|Iterable[str] domains, Callable[[Event[EventStateChangedData]], Any] action, HassJobType|None job_type=None)
Definition: event.py:648
float|None _rate_limit_for_event(Event[EventStateChangedData] event, RenderInfo info, TrackTemplate track_template_)
Definition: event.py:1988
None _run_async_call_action(HomeAssistant hass, HassJob[[datetime], Coroutine[Any, Any, None]|None] job)
Definition: event.py:1565
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_state_added_domain(HomeAssistant hass, str|Iterable[str] domains, Callable[[Event[EventStateChangedData]], Any] action, HassJobType|None job_type)
Definition: event.py:669
TrackStates _render_infos_to_track_states(Iterable[RenderInfo] render_infos)
Definition: event.py:1959
CALLBACK_TYPE async_track_same_state(HomeAssistant hass, timedelta period, Callable[[], Coroutine[Any, Any, None]|None] action, Callable[[str, State|None, State|None], bool] async_check_same_func, str|Iterable[str] entity_ids=MATCH_ALL)
Definition: event.py:1395
CALLBACK_TYPE async_track_state_report_event(HomeAssistant hass, str|Iterable[str] entity_ids, Callable[[Event[EventStateReportedData]], Any] action, HassJobType|None job_type=None)
Definition: event.py:412
CALLBACK_TYPE async_track_utc_time_change(HomeAssistant hass, Callable[[datetime], Coroutine[Any, Any, None]|None] action, Any|None hour=None, Any|None minute=None, Any|None second=None, bool local=False)
Definition: event.py:1857
None _async_dispatch_old_entity_id_or_entity_id_event(HomeAssistant hass, dict[str, list[HassJob[[Event[EventEntityRegistryUpdatedData]], Any]]] callbacks, Event[EventEntityRegistryUpdatedData] event)
Definition: event.py:499
CALLBACK_TYPE async_track_entity_registry_updated_event(HomeAssistant hass, str|Iterable[str] entity_ids, Callable[[Event[EventEntityRegistryUpdatedData]], Any] action, HassJobType|None job_type=None)
Definition: event.py:543
CALLBACK_TYPE _async_track_event(_KeyedEventTracker[_TypedDictT] tracker, HomeAssistant hass, str|Iterable[str] keys, Callable[[Event[_TypedDictT]], None] action, HassJobType|None job_type)
Definition: event.py:454
CALLBACK_TYPE async_track_state_change_event(HomeAssistant hass, str|Iterable[str] entity_ids, Callable[[Event[EventStateChangedData]], Any] action, HassJobType|None job_type=None)
Definition: event.py:314
CALLBACK_TYPE async_track_sunrise(HomeAssistant hass, Callable[[], None] action, timedelta|None offset=None)
Definition: event.py:1760
bool _async_entity_registry_updated_filter(HomeAssistant hass, dict[str, list[HassJob[[Event[EventEntityRegistryUpdatedData]], Any]]] callbacks, EventEntityRegistryUpdatedData event_data)
Definition: event.py:523
Callable[[str|None], bool] process_state_match(str|Iterable[str]|None parameter, bool invert=False)
Definition: event.py:1917
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
CALLBACK_TYPE async_track_point_in_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:1462
bool _async_state_filter(HomeAssistant hass, dict[str, list[HassJob[[Event[_StateEventDataT]], Any]]] callbacks, _StateEventDataT event_data)
Definition: event.py:370
bool _event_triggers_rerender(Event[EventStateChangedData] event, RenderInfo info)
Definition: event.py:1970
CALLBACK_TYPE async_track_state_removed_domain(HomeAssistant hass, str|Iterable[str] domains, Callable[[Event[EventStateChangedData]], Any] action, HassJobType|None job_type=None)
Definition: event.py:706
CALLBACK_TYPE async_call_at(HomeAssistant hass, HassJob[[datetime], Coroutine[Any, Any, None]|None]|Callable[[datetime], Coroutine[Any, Any, None]|None] action, float loop_time)
Definition: event.py:1577
list[str] _async_string_to_lower_list(str|Iterable[str] instr)
Definition: event.py:714
RenderInfo _suppress_domain_all_in_render_info(RenderInfo render_info)
Definition: event.py:2002
bool _async_domain_removed_filter(HomeAssistant hass, dict[str, list[HassJob[[Event[EventStateChangedData]], Any]]] callbacks, EventStateChangedData event_data)
Definition: event.py:681
TrackTemplateResultInfo async_track_template_result(HomeAssistant hass, Sequence[TrackTemplate] track_templates, TrackTemplateResultListener action, bool strict=False, Callable[[int, str], None]|None log_fn=None, bool has_super_template=False)
Definition: event.py:1345
CALLBACK_TYPE async_track_time_interval(HomeAssistant hass, Callable[[datetime], Coroutine[Any, Any, None]|None] action, timedelta interval, *str|None name=None, bool|None cancel_on_shutdown=None)
Definition: event.py:1679
None _async_dispatch_entity_id_event_soon(HomeAssistant hass, dict[str, list[HassJob[[Event[_StateEventDataT]], Any]]] callbacks, Event[_StateEventDataT] event)
Definition: event.py:340
None _remove_listener(HomeAssistant hass, _KeyedEventTracker[_TypedDictT] tracker, Iterable[str] keys, HassJob[[Event[_TypedDictT]], Any] job, dict[str, list[HassJob[[Event[_TypedDictT]], Any]]] callbacks)
Definition: event.py:435
bool _render_infos_needs_all_listener(Iterable[RenderInfo] render_infos)
Definition: event.py:1948
None _async_dispatch_entity_id_event(HomeAssistant hass, dict[str, list[HassJob[[Event[_StateEventDataT]], Any]]] callbacks, Event[_StateEventDataT] event)
Definition: event.py:350
CALLBACK_TYPE async_track_state_change(HomeAssistant hass, str|Iterable[str] entity_ids, Callable[[str, State|None, State|None], Coroutine[Any, Any, None]|None] action, str|Iterable[str]|None from_state=None, str|Iterable[str]|None to_state=None)
Definition: event.py:211
datetime.datetime get_astral_event_next(HomeAssistant hass, str event, datetime.datetime|None utc_point_in_time=None, datetime.timedelta|None offset=None)
Definition: sun.py:60
bool result_as_boolean(Any|None template_result)
Definition: template.py:1277