core/homeassistant/helpers/dispatcher.py

204 lines
5.7 KiB
Python
Raw Normal View History

"""Helpers for Home Assistant dispatcher & internal component/platform."""
from __future__ import annotations
from collections.abc import Callable, Coroutine
from functools import partial
import logging
2024-03-27 07:41:44 +00:00
from typing import Any, TypeVarTuple, overload
from homeassistant.core import HassJob, HomeAssistant, callback
from homeassistant.loader import bind_hass
from homeassistant.util.async_ import run_callback_threadsafe
from homeassistant.util.logging import catch_log_exception
2024-03-27 07:41:44 +00:00
# Explicit reexport of 'SignalType' for backwards compatibility
from homeassistant.util.signal_type import SignalType as SignalType # noqa: PLC0414
2024-01-08 08:45:37 +00:00
_Ts = TypeVarTuple("_Ts")
_LOGGER = logging.getLogger(__name__)
2019-07-31 19:25:30 +00:00
DATA_DISPATCHER = "dispatcher"
2024-01-08 08:45:37 +00:00
_DispatcherDataType = dict[
2024-01-08 08:45:37 +00:00
SignalType[*_Ts] | str,
dict[
2024-01-08 08:45:37 +00:00
Callable[[*_Ts], Any] | Callable[..., Any],
HassJob[..., None | Coroutine[Any, Any, None]] | None,
],
]
2024-01-08 08:45:37 +00:00
@overload
@bind_hass
def dispatcher_connect(
hass: HomeAssistant, signal: SignalType[*_Ts], target: Callable[[*_Ts], None]
) -> Callable[[], None]: ...
2024-01-08 08:45:37 +00:00
@overload
@bind_hass
2019-07-31 19:25:30 +00:00
def dispatcher_connect(
hass: HomeAssistant, signal: str, target: Callable[..., None]
) -> Callable[[], None]: ...
2024-01-08 08:45:37 +00:00
@bind_hass # type: ignore[misc] # workaround; exclude typing of 2 overload in func def
def dispatcher_connect(
hass: HomeAssistant,
signal: SignalType[*_Ts],
target: Callable[[*_Ts], None],
2019-07-31 19:25:30 +00:00
) -> Callable[[], None]:
"""Connect a callable function to a signal."""
async_unsub = run_callback_threadsafe(
2019-07-31 19:25:30 +00:00
hass.loop, async_dispatcher_connect, hass, signal, target
).result()
def remove_dispatcher() -> None:
"""Remove signal listener."""
run_callback_threadsafe(hass.loop, async_unsub).result()
return remove_dispatcher
@callback
def _async_remove_dispatcher(
2024-01-08 08:45:37 +00:00
dispatchers: _DispatcherDataType[*_Ts],
signal: SignalType[*_Ts] | str,
target: Callable[[*_Ts], Any] | Callable[..., Any],
) -> None:
"""Remove signal listener."""
try:
signal_dispatchers = dispatchers[signal]
del signal_dispatchers[target]
# Cleanup the signal dict if it is now empty
# to prevent memory leaks
if not signal_dispatchers:
del dispatchers[signal]
except (KeyError, ValueError):
# KeyError is key target listener did not exist
# ValueError if listener did not exist within signal
_LOGGER.warning("Unable to remove unknown dispatcher %s", target)
2024-01-08 08:45:37 +00:00
@overload
@callback
@bind_hass
def async_dispatcher_connect(
hass: HomeAssistant, signal: SignalType[*_Ts], target: Callable[[*_Ts], Any]
) -> Callable[[], None]: ...
2024-01-08 08:45:37 +00:00
@overload
@callback
@bind_hass
2019-07-31 19:25:30 +00:00
def async_dispatcher_connect(
hass: HomeAssistant, signal: str, target: Callable[..., Any]
) -> Callable[[], None]: ...
2024-01-08 08:45:37 +00:00
@callback
@bind_hass
def async_dispatcher_connect(
hass: HomeAssistant,
signal: SignalType[*_Ts] | str,
target: Callable[[*_Ts], Any] | Callable[..., Any],
2019-07-31 19:25:30 +00:00
) -> Callable[[], None]:
"""Connect a callable function to a signal.
This method must be run in the event loop.
"""
if DATA_DISPATCHER not in hass.data:
hass.data[DATA_DISPATCHER] = {}
2024-01-08 08:45:37 +00:00
dispatchers: _DispatcherDataType[*_Ts] = hass.data[DATA_DISPATCHER]
if signal not in dispatchers:
dispatchers[signal] = {}
dispatchers[signal][target] = None
# Use a partial for the remove since it uses
# less memory than a full closure since a partial copies
# the body of the function and we don't have to store
# many different copies of the same function
return partial(_async_remove_dispatcher, dispatchers, signal, target)
2024-01-08 08:45:37 +00:00
@overload
@bind_hass
def dispatcher_send(
hass: HomeAssistant, signal: SignalType[*_Ts], *args: *_Ts
) -> None: ...
2024-01-08 08:45:37 +00:00
@overload
@bind_hass
def dispatcher_send(hass: HomeAssistant, signal: str, *args: Any) -> None: ...
2024-01-08 08:45:37 +00:00
@bind_hass # type: ignore[misc] # workaround; exclude typing of 2 overload in func def
def dispatcher_send(hass: HomeAssistant, signal: SignalType[*_Ts], *args: *_Ts) -> None:
"""Send signal and data."""
hass.loop.call_soon_threadsafe(async_dispatcher_send, hass, signal, *args)
2024-01-08 08:45:37 +00:00
def _format_err(
signal: SignalType[*_Ts] | str,
target: Callable[[*_Ts], Any] | Callable[..., Any],
*args: Any,
) -> str:
"""Format error message."""
return "Exception in {} when dispatching '{}': {}".format(
# Functions wrapped in partial do not have a __name__
getattr(target, "__name__", None) or str(target),
signal,
args,
)
def _generate_job(
2024-01-08 08:45:37 +00:00
signal: SignalType[*_Ts] | str, target: Callable[[*_Ts], Any] | Callable[..., Any]
) -> HassJob[..., None | Coroutine[Any, Any, None]]:
2022-07-04 12:58:35 +00:00
"""Generate a HassJob for a signal and target."""
return HassJob(
catch_log_exception(target, partial(_format_err, signal, target)),
f"dispatcher {signal}",
2022-07-04 12:58:35 +00:00
)
2024-01-08 08:45:37 +00:00
@overload
@callback
@bind_hass
def async_dispatcher_send(
hass: HomeAssistant, signal: SignalType[*_Ts], *args: *_Ts
) -> None: ...
2024-01-08 08:45:37 +00:00
@overload
@callback
@bind_hass
def async_dispatcher_send(hass: HomeAssistant, signal: str, *args: Any) -> None: ...
2024-01-08 08:45:37 +00:00
@callback
@bind_hass
def async_dispatcher_send(
hass: HomeAssistant, signal: SignalType[*_Ts] | str, *args: *_Ts
) -> None:
"""Send signal and data.
This method must be run in the event loop.
"""
if (maybe_dispatchers := hass.data.get(DATA_DISPATCHER)) is None:
return
2024-01-08 08:45:37 +00:00
dispatchers: _DispatcherDataType[*_Ts] = maybe_dispatchers
if (target_list := dispatchers.get(signal)) is None:
return
for target, job in list(target_list.items()):
2022-07-04 12:58:35 +00:00
if job is None:
job = _generate_job(signal, target)
target_list[target] = job
Run coroutines as eager tasks in async_run_hass_job (#111683) * Run coroutines as eager tasks in async_run_hass_job Note that this does not change async_add_hass_job Do not merge this. For test run only * Phase out periodic tasks * false by default or some tests will block forever, will need to fix each one manually * kwarg works * kwarg works * kwarg works * fixes * fix more tests * fix more tests * fix lifx * opensky * pvpc_hourly_pricing * adjust more * adjust more * smarttub * adjust more * adjust more * adjust more * adjust more * adjust * no eager executor * zha * qnap_qsw * fix more * fix fix * docs * its a wrapper now * add more coverage * coverage * cover all combos * more fixes * more fixes * more fixes * remaining issues are legit bugs in tests * make tplink test more predictable * more fixes * feedreader * grind out some more * make test race safe * limit first scope to triggers * one more * Start tasks eagerly in for async_at_start(ed) A few of these can avoid being scheduled on the loop during startup * fix cloud * Revert "fix cloud" This reverts commit 5eb3ce695da788bcae649f82c9527c0f9307139c. * fix test to do what start does * flip flag * flip flag * Fix here_travel_time creating many refresh requests at startup - Each entity would try to refresh the coordinator which created many tasks. Move the refresh to a single async_at_started - The tests fired the EVENT_HOMEASSISTANT_START event but the code used async_at_started which only worked because the tests did not set CoreState to not_running * fix azure * remove kw * remove kw * rip * cover * more rips * more rips * more rips
2024-03-12 00:05:08 +00:00
hass.async_run_hass_job(job, *args)