Home Assistant Unofficial Reference 2024.12.1
dispatcher.py
Go to the documentation of this file.
1 """Helpers for Home Assistant dispatcher & internal component/platform."""
2 
3 from __future__ import annotations
4 
5 from collections import defaultdict
6 from collections.abc import Callable, Coroutine
7 from functools import partial
8 import logging
9 from typing import Any, overload
10 
11 from homeassistant.core import (
12  HassJob,
13  HassJobType,
14  HomeAssistant,
15  callback,
16  get_hassjob_callable_job_type,
17 )
18 from homeassistant.loader import bind_hass
19 from homeassistant.util.async_ import run_callback_threadsafe
20 from homeassistant.util.logging import catch_log_exception, log_exception
21 
22 # Explicit reexport of 'SignalType' for backwards compatibility
23 from homeassistant.util.signal_type import SignalType as SignalType # noqa: PLC0414
24 
25 _LOGGER = logging.getLogger(__name__)
26 DATA_DISPATCHER = "dispatcher"
27 
28 
29 type _DispatcherDataType[*_Ts] = dict[
30  SignalType[*_Ts] | str,
31  dict[
32  Callable[[*_Ts], Any] | Callable[..., Any],
33  HassJob[..., None | Coroutine[Any, Any, None]] | None,
34  ],
35 ]
36 
37 
38 @overload
39 @bind_hass
41  hass: HomeAssistant, signal: SignalType[*_Ts], target: Callable[[*_Ts], None]
42 ) -> Callable[[], None]: ...
43 
44 
45 @overload
46 @bind_hass
48  hass: HomeAssistant, signal: str, target: Callable[..., None]
49 ) -> Callable[[], None]: ...
50 
51 
52 @bind_hass # type: ignore[misc] # workaround; exclude typing of 2 overload in func def
53 def dispatcher_connect[*_Ts](
54  hass: HomeAssistant,
55  signal: SignalType[*_Ts],
56  target: Callable[[*_Ts], None],
57 ) -> Callable[[], None]:
58  """Connect a callable function to a signal."""
59  async_unsub = run_callback_threadsafe(
60  hass.loop, async_dispatcher_connect, hass, signal, target
61  ).result()
62 
63  def remove_dispatcher() -> None:
64  """Remove signal listener."""
65  run_callback_threadsafe(hass.loop, async_unsub).result()
66 
67  return remove_dispatcher
68 
69 
70 @callback
71 def _async_remove_dispatcher[*_Ts](
72  dispatchers: _DispatcherDataType[*_Ts],
73  signal: SignalType[*_Ts] | str,
74  target: Callable[[*_Ts], Any] | Callable[..., Any],
75 ) -> None:
76  """Remove signal listener."""
77  try:
78  signal_dispatchers = dispatchers[signal]
79  del signal_dispatchers[target]
80  # Cleanup the signal dict if it is now empty
81  # to prevent memory leaks
82  if not signal_dispatchers:
83  del dispatchers[signal]
84  except (KeyError, ValueError):
85  # KeyError is key target listener did not exist
86  # ValueError if listener did not exist within signal
87  _LOGGER.warning("Unable to remove unknown dispatcher %s", target)
88 
89 
90 @overload
91 @callback
92 @bind_hass
94  hass: HomeAssistant, signal: SignalType[*_Ts], target: Callable[[*_Ts], Any]
95 ) -> Callable[[], None]: ...
96 
97 
98 @overload
99 @callback
100 @bind_hass
102  hass: HomeAssistant, signal: str, target: Callable[..., Any]
103 ) -> Callable[[], None]: ...
104 
105 
106 @callback
107 @bind_hass
109  hass: HomeAssistant,
110  signal: SignalType[*_Ts] | str,
111  target: Callable[[*_Ts], Any] | Callable[..., Any],
112 ) -> Callable[[], None]:
113  """Connect a callable function to a signal.
114 
115  This method must be run in the event loop.
116  """
117  if DATA_DISPATCHER not in hass.data:
118  hass.data[DATA_DISPATCHER] = defaultdict(dict)
119  dispatchers: _DispatcherDataType[*_Ts] = hass.data[DATA_DISPATCHER]
120  dispatchers[signal][target] = None
121  # Use a partial for the remove since it uses
122  # less memory than a full closure since a partial copies
123  # the body of the function and we don't have to store
124  # many different copies of the same function
125  return partial(_async_remove_dispatcher, dispatchers, signal, target)
126 
127 
128 @overload
129 @bind_hass
130 def dispatcher_send[*_Ts](
131  hass: HomeAssistant, signal: SignalType[*_Ts], *args: *_Ts
132 ) -> None: ...
133 
134 
135 @overload
136 @bind_hass
137 def dispatcher_send(hass: HomeAssistant, signal: str, *args: Any) -> None: ...
138 
139 
140 @bind_hass # type: ignore[misc] # workaround; exclude typing of 2 overload in func def
141 def dispatcher_send[*_Ts](
142  hass: HomeAssistant, signal: SignalType[*_Ts], *args: *_Ts
143 ) -> None:
144  """Send signal and data."""
145  hass.loop.call_soon_threadsafe(async_dispatcher_send_internal, hass, signal, *args)
146 
147 
148 def _format_err[*_Ts](
149  signal: SignalType[*_Ts] | str,
150  target: Callable[[*_Ts], Any] | Callable[..., Any],
151  *args: Any,
152 ) -> str:
153  """Format error message."""
154 
155  return (
156  # Functions wrapped in partial do not have a __name__
157  f"Exception in {getattr(target, "__name__", None) or target} "
158  f"when dispatching '{signal}': {args}"
159  )
160 
161 
162 def _generate_job[*_Ts](
163  signal: SignalType[*_Ts] | str, target: Callable[[*_Ts], Any] | Callable[..., Any]
164 ) -> HassJob[..., Coroutine[Any, Any, None] | None]:
165  """Generate a HassJob for a signal and target."""
166  job_type = get_hassjob_callable_job_type(target)
167  name = f"dispatcher {signal}"
168  if job_type is HassJobType.Callback:
169  # We will catch exceptions in the callback to avoid
170  # wrapping the callback since calling wraps() is more
171  # expensive than the whole dispatcher_send process
172  return HassJob(target, name, job_type=job_type)
173  return HassJob(
174  catch_log_exception(
175  target, partial(_format_err, signal, target), job_type=job_type
176  ),
177  name,
178  job_type=job_type,
179  )
180 
181 
182 @overload
183 @callback
184 @bind_hass
185 def async_dispatcher_send[*_Ts](
186  hass: HomeAssistant, signal: SignalType[*_Ts], *args: *_Ts
187 ) -> None: ...
188 
189 
190 @overload
191 @callback
192 @bind_hass
193 def async_dispatcher_send(hass: HomeAssistant, signal: str, *args: Any) -> None: ...
194 
195 
196 @callback
197 @bind_hass
199  hass: HomeAssistant, signal: SignalType[*_Ts] | str, *args: *_Ts
200 ) -> None:
201  """Send signal and data.
202 
203  This method must be run in the event loop.
204  """
205  # We turned on asyncio debug in April 2024 in the dev containers
206  # in the hope of catching some of the issues that have been
207  # reported. It will take a while to get all the issues fixed in
208  # custom components.
209  #
210  # In 2025.5 we should guard the `verify_event_loop_thread`
211  # check with a check for the `hass.config.debug` flag being set as
212  # long term we don't want to be checking this in production
213  # environments since it is a performance hit.
214  hass.verify_event_loop_thread("async_dispatcher_send")
215  async_dispatcher_send_internal(hass, signal, *args)
216 
217 
218 @callback
219 @bind_hass
220 def async_dispatcher_send_internal[*_Ts](
221  hass: HomeAssistant, signal: SignalType[*_Ts] | str, *args: *_Ts
222 ) -> None:
223  """Send signal and data.
224 
225  This method is intended to only be used by core internally
226  and should not be considered a stable API. We will make
227  breaking changes to this function in the future and it
228  should not be used in integrations.
229 
230  This method must be run in the event loop.
231  """
232  if (maybe_dispatchers := hass.data.get(DATA_DISPATCHER)) is None:
233  return
234  dispatchers: _DispatcherDataType[*_Ts] = maybe_dispatchers
235  if (target_list := dispatchers.get(signal)) is None:
236  return
237 
238  for target, job in list(target_list.items()):
239  if job is None:
240  job = _generate_job(signal, target)
241  target_list[target] = job
242  # We do not wrap Callback jobs in catch_log_exception since
243  # single use dispatchers spend more time wrapping the callback
244  # than the actual callback takes to run in many cases.
245  if job.job_type is HassJobType.Callback:
246  try:
247  job.target(*args)
248  except Exception: # noqa: BLE001
249  log_exception(partial(_format_err, signal, target), *args) # type: ignore[arg-type]
250  else:
251  hass.async_run_hass_job(job, *args)
str _format_err(str name, *Any args)
Definition: __init__.py:143
HassJobType get_hassjob_callable_job_type(Callable[..., Any] target)
Definition: core.py:387
Callable[[], None] dispatcher_connect(HomeAssistant hass, str signal, Callable[..., None] target)
Definition: dispatcher.py:49
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
Definition: dispatcher.py:103
None dispatcher_send(HomeAssistant hass, str signal, *Any args)
Definition: dispatcher.py:137
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)
Definition: dispatcher.py:193