core/homeassistant/helpers/dispatcher.py

240 lines
6.9 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
from typing import Any, overload
from homeassistant.core import (
HassJob,
HomeAssistant,
callback,
get_hassjob_callable_job_type,
)
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
_LOGGER = logging.getLogger(__name__)
2019-07-31 19:25:30 +00:00
DATA_DISPATCHER = "dispatcher"
2024-01-08 08:45:37 +00:00
type _DispatcherDataType[*_Ts] = 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[*_Ts](
2024-01-08 08:45:37 +00:00
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[*_Ts](
2024-01-08 08:45:37 +00:00
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[*_Ts](
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[*_Ts](
2024-01-08 08:45:37 +00:00
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[*_Ts](
2024-01-08 08:45:37 +00:00
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[*_Ts](
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[*_Ts](
hass: HomeAssistant, signal: SignalType[*_Ts], *args: *_Ts
) -> None:
"""Send signal and data."""
hass.loop.call_soon_threadsafe(async_dispatcher_send_internal, hass, signal, *args)
def _format_err[*_Ts](
2024-01-08 08:45:37 +00:00
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[*_Ts](
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."""
job_type = get_hassjob_callable_job_type(target)
2022-07-04 12:58:35 +00:00
return HassJob(
catch_log_exception(
target, partial(_format_err, signal, target), job_type=job_type
),
f"dispatcher {signal}",
job_type=job_type,
2022-07-04 12:58:35 +00:00
)
2024-01-08 08:45:37 +00:00
@overload
@callback
@bind_hass
def async_dispatcher_send[*_Ts](
2024-01-08 08:45:37 +00:00
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[*_Ts](
2024-01-08 08:45:37 +00:00
hass: HomeAssistant, signal: SignalType[*_Ts] | str, *args: *_Ts
) -> None:
"""Send signal and data.
This method must be run in the event loop.
"""
# We turned on asyncio debug in April 2024 in the dev containers
# in the hope of catching some of the issues that have been
# reported. It will take a while to get all the issues fixed in
# custom components.
#
# In 2025.5 we should guard the `verify_event_loop_thread`
# check with a check for the `hass.config.debug` flag being set as
# long term we don't want to be checking this in production
# environments since it is a performance hit.
hass.verify_event_loop_thread("async_dispatcher_send")
async_dispatcher_send_internal(hass, signal, *args)
@callback
@bind_hass
def async_dispatcher_send_internal[*_Ts](
hass: HomeAssistant, signal: SignalType[*_Ts] | str, *args: *_Ts
) -> None:
"""Send signal and data.
This method is intended to only be used by core internally
and should not be considered a stable API. We will make
breaking changes to this function in the future and it
should not be used in integrations.
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)