2017-05-02 16:18:47 +00:00
|
|
|
"""Helpers for Home Assistant dispatcher & internal component/platform."""
|
2021-09-29 14:32:11 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2022-07-20 02:11:46 +00:00
|
|
|
from collections.abc import Callable, Coroutine
|
2024-01-08 08:45:37 +00:00
|
|
|
from dataclasses import dataclass
|
2023-09-06 01:18:27 +00:00
|
|
|
from functools import partial
|
2017-02-25 01:11:50 +00:00
|
|
|
import logging
|
2024-01-08 08:45:37 +00:00
|
|
|
from typing import Any, Generic, TypeVarTuple, overload
|
2017-02-23 21:02:56 +00:00
|
|
|
|
2021-03-27 11:55:24 +00:00
|
|
|
from homeassistant.core import HassJob, HomeAssistant, callback
|
2017-10-08 15:17:54 +00:00
|
|
|
from homeassistant.loader import bind_hass
|
2018-03-11 17:01:12 +00:00
|
|
|
from homeassistant.util.async_ import run_callback_threadsafe
|
2019-01-17 22:44:57 +00:00
|
|
|
from homeassistant.util.logging import catch_log_exception
|
2017-02-23 21:02:56 +00:00
|
|
|
|
2024-01-08 08:45:37 +00:00
|
|
|
_Ts = TypeVarTuple("_Ts")
|
|
|
|
|
2017-02-25 01:11:50 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
2019-07-31 19:25:30 +00:00
|
|
|
DATA_DISPATCHER = "dispatcher"
|
2017-02-23 21:02:56 +00:00
|
|
|
|
2024-01-08 08:45:37 +00:00
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
class SignalType(Generic[*_Ts]):
|
|
|
|
"""Generic string class for signal to improve typing."""
|
|
|
|
|
|
|
|
name: str
|
|
|
|
|
|
|
|
def __hash__(self) -> int:
|
|
|
|
"""Return hash of name."""
|
|
|
|
|
|
|
|
return hash(self.name)
|
|
|
|
|
|
|
|
def __eq__(self, other: Any) -> bool:
|
|
|
|
"""Check equality for dict keys to be compatible with str."""
|
|
|
|
|
|
|
|
if isinstance(other, str):
|
|
|
|
return self.name == other
|
|
|
|
if isinstance(other, SignalType):
|
|
|
|
return self.name == other.name
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
2023-09-06 01:18:27 +00:00
|
|
|
_DispatcherDataType = dict[
|
2024-01-08 08:45:37 +00:00
|
|
|
SignalType[*_Ts] | str,
|
2023-09-06 01:18:27 +00:00
|
|
|
dict[
|
2024-01-08 08:45:37 +00:00
|
|
|
Callable[[*_Ts], Any] | Callable[..., Any],
|
2023-09-06 01:18:27 +00:00
|
|
|
HassJob[..., None | Coroutine[Any, Any, None]] | None,
|
|
|
|
],
|
|
|
|
]
|
|
|
|
|
2017-02-23 21:02:56 +00:00
|
|
|
|
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]:
|
|
|
|
...
|
|
|
|
|
|
|
|
|
|
|
|
@overload
|
2017-10-08 15:17:54 +00:00
|
|
|
@bind_hass
|
2019-07-31 19:25:30 +00:00
|
|
|
def dispatcher_connect(
|
2021-03-27 11:55:24 +00:00
|
|
|
hass: HomeAssistant, signal: str, target: Callable[..., None]
|
2024-01-08 08:45:37 +00:00
|
|
|
) -> Callable[[], None]:
|
|
|
|
...
|
|
|
|
|
|
|
|
|
|
|
|
@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]:
|
2017-06-08 13:53:12 +00:00
|
|
|
"""Connect a callable function to a signal."""
|
2017-02-25 01:11:50 +00:00
|
|
|
async_unsub = run_callback_threadsafe(
|
2019-07-31 19:25:30 +00:00
|
|
|
hass.loop, async_dispatcher_connect, hass, signal, target
|
|
|
|
).result()
|
2017-02-25 01:11:50 +00:00
|
|
|
|
2018-11-04 21:46:42 +00:00
|
|
|
def remove_dispatcher() -> None:
|
2017-02-25 01:11:50 +00:00
|
|
|
"""Remove signal listener."""
|
|
|
|
run_callback_threadsafe(hass.loop, async_unsub).result()
|
|
|
|
|
|
|
|
return remove_dispatcher
|
2017-02-23 21:02:56 +00:00
|
|
|
|
|
|
|
|
2023-09-06 01:18:27 +00:00
|
|
|
@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],
|
2023-09-06 01:18:27 +00:00
|
|
|
) -> 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]:
|
|
|
|
...
|
|
|
|
|
|
|
|
|
|
|
|
@overload
|
2017-02-23 21:02:56 +00:00
|
|
|
@callback
|
2017-10-08 15:17:54 +00:00
|
|
|
@bind_hass
|
2019-07-31 19:25:30 +00:00
|
|
|
def async_dispatcher_connect(
|
2021-03-27 11:55:24 +00:00
|
|
|
hass: HomeAssistant, signal: str, target: Callable[..., Any]
|
2024-01-08 08:45:37 +00:00
|
|
|
) -> Callable[[], None]:
|
|
|
|
...
|
|
|
|
|
|
|
|
|
|
|
|
@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]:
|
2017-06-08 13:53:12 +00:00
|
|
|
"""Connect a callable function to a signal.
|
2017-02-23 21:02:56 +00:00
|
|
|
|
|
|
|
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]
|
2023-09-06 01:18:27 +00:00
|
|
|
|
|
|
|
if signal not in dispatchers:
|
|
|
|
dispatchers[signal] = {}
|
2017-02-25 01:11:50 +00:00
|
|
|
|
2023-09-06 01:18:27 +00:00
|
|
|
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)
|
2017-02-25 01:11:50 +00:00
|
|
|
|
2017-02-23 21:02:56 +00:00
|
|
|
|
2024-01-08 08:45:37 +00:00
|
|
|
@overload
|
|
|
|
@bind_hass
|
|
|
|
def dispatcher_send(hass: HomeAssistant, signal: SignalType[*_Ts], *args: *_Ts) -> None:
|
|
|
|
...
|
|
|
|
|
|
|
|
|
|
|
|
@overload
|
2017-10-08 15:17:54 +00:00
|
|
|
@bind_hass
|
2021-03-27 11:55:24 +00:00
|
|
|
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:
|
2017-02-23 21:02:56 +00:00
|
|
|
"""Send signal and data."""
|
2017-03-09 14:31:43 +00:00
|
|
|
hass.loop.call_soon_threadsafe(async_dispatcher_send, hass, signal, *args)
|
2017-02-23 21:02:56 +00:00
|
|
|
|
|
|
|
|
2024-01-08 08:45:37 +00:00
|
|
|
def _format_err(
|
|
|
|
signal: SignalType[*_Ts] | str,
|
|
|
|
target: Callable[[*_Ts], Any] | Callable[..., Any],
|
|
|
|
*args: Any,
|
|
|
|
) -> str:
|
2023-12-16 09:16:58 +00:00
|
|
|
"""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,
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2022-07-20 02:11:46 +00:00
|
|
|
def _generate_job(
|
2024-01-08 08:45:37 +00:00
|
|
|
signal: SignalType[*_Ts] | str, target: Callable[[*_Ts], Any] | Callable[..., Any]
|
2022-07-20 02:11:46 +00:00
|
|
|
) -> HassJob[..., None | Coroutine[Any, Any, None]]:
|
2022-07-04 12:58:35 +00:00
|
|
|
"""Generate a HassJob for a signal and target."""
|
|
|
|
return HassJob(
|
2023-12-16 09:16:58 +00:00
|
|
|
catch_log_exception(target, partial(_format_err, signal, target)),
|
2023-03-05 11:46:02 +00:00
|
|
|
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:
|
|
|
|
...
|
|
|
|
|
|
|
|
|
|
|
|
@overload
|
2017-02-23 21:02:56 +00:00
|
|
|
@callback
|
2017-10-08 15:17:54 +00:00
|
|
|
@bind_hass
|
2021-03-27 11:55:24 +00:00
|
|
|
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:
|
2017-02-23 21:02:56 +00:00
|
|
|
"""Send signal and data.
|
|
|
|
|
|
|
|
This method must be run in the event loop.
|
|
|
|
"""
|
2023-09-06 01:18:27 +00:00
|
|
|
if (maybe_dispatchers := hass.data.get(DATA_DISPATCHER)) is None:
|
|
|
|
return
|
2024-01-08 08:45:37 +00:00
|
|
|
dispatchers: _DispatcherDataType[*_Ts] = maybe_dispatchers
|
2023-09-06 01:18:27 +00:00
|
|
|
if (target_list := dispatchers.get(signal)) is None:
|
|
|
|
return
|
2022-07-07 15:39:05 +00:00
|
|
|
|
2023-09-06 01:18:27 +00:00
|
|
|
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
|
2022-07-07 15:39:05 +00:00
|
|
|
hass.async_run_hass_job(job, *args)
|