2016-03-07 22:39:52 +00:00
|
|
|
"""Helpers for listening to events."""
|
2019-09-07 06:48:58 +00:00
|
|
|
from datetime import datetime, timedelta
|
2015-07-26 08:45:49 +00:00
|
|
|
import functools as ft
|
2020-06-28 00:48:27 +00:00
|
|
|
import logging
|
2020-06-29 16:39:24 +00:00
|
|
|
import time
|
2020-06-27 21:46:45 +00:00
|
|
|
from typing import Any, Awaitable, Callable, Dict, Iterable, Optional, Union
|
2019-06-10 23:05:32 +00:00
|
|
|
|
|
|
|
import attr
|
2015-07-26 08:45:49 +00:00
|
|
|
|
2019-06-10 23:05:32 +00:00
|
|
|
from homeassistant.const import (
|
2019-07-31 19:25:30 +00:00
|
|
|
ATTR_NOW,
|
2019-12-09 15:42:10 +00:00
|
|
|
EVENT_CORE_CONFIG_UPDATE,
|
2019-07-31 19:25:30 +00:00
|
|
|
EVENT_STATE_CHANGED,
|
|
|
|
EVENT_TIME_CHANGED,
|
|
|
|
MATCH_ALL,
|
|
|
|
SUN_EVENT_SUNRISE,
|
|
|
|
SUN_EVENT_SUNSET,
|
|
|
|
)
|
2019-12-09 15:42:10 +00:00
|
|
|
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, State, callback
|
|
|
|
from homeassistant.helpers.sun import get_astral_event_next
|
|
|
|
from homeassistant.helpers.template import Template
|
|
|
|
from homeassistant.loader import bind_hass
|
2019-06-10 23:05:32 +00:00
|
|
|
from homeassistant.util import dt as dt_util
|
|
|
|
from homeassistant.util.async_ import run_callback_threadsafe
|
2015-07-26 08:45:49 +00:00
|
|
|
|
2020-06-27 21:46:45 +00:00
|
|
|
TRACK_STATE_CHANGE_CALLBACKS = "track_state_change_callbacks"
|
|
|
|
TRACK_STATE_CHANGE_LISTENER = "track_state_change_listener"
|
|
|
|
|
2020-06-28 00:48:27 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2016-10-04 05:39:27 +00:00
|
|
|
# PyLint does not like the use of threaded_listener_factory
|
2016-10-01 08:22:13 +00:00
|
|
|
# pylint: disable=invalid-name
|
2015-07-26 08:45:49 +00:00
|
|
|
|
2016-03-07 22:39:52 +00:00
|
|
|
|
2019-10-28 20:36:26 +00:00
|
|
|
def threaded_listener_factory(async_factory: Callable[..., Any]) -> CALLBACK_TYPE:
|
2016-10-01 08:22:13 +00:00
|
|
|
"""Convert an async event helper to a threaded one."""
|
2019-07-31 19:25:30 +00:00
|
|
|
|
2016-10-01 08:22:13 +00:00
|
|
|
@ft.wraps(async_factory)
|
2019-10-28 20:36:26 +00:00
|
|
|
def factory(*args: Any, **kwargs: Any) -> CALLBACK_TYPE:
|
2016-10-01 08:22:13 +00:00
|
|
|
"""Call async event helper safely."""
|
|
|
|
hass = args[0]
|
2015-07-26 08:45:49 +00:00
|
|
|
|
2016-10-01 08:22:13 +00:00
|
|
|
if not isinstance(hass, HomeAssistant):
|
2019-07-31 19:25:30 +00:00
|
|
|
raise TypeError("First parameter needs to be a hass instance")
|
2016-10-01 08:22:13 +00:00
|
|
|
|
|
|
|
async_remove = run_callback_threadsafe(
|
2019-07-31 19:25:30 +00:00
|
|
|
hass.loop, ft.partial(async_factory, *args, **kwargs)
|
|
|
|
).result()
|
2016-09-30 19:57:24 +00:00
|
|
|
|
2019-10-28 20:36:26 +00:00
|
|
|
def remove() -> None:
|
2016-10-01 08:22:13 +00:00
|
|
|
"""Threadsafe removal."""
|
|
|
|
run_callback_threadsafe(hass.loop, async_remove).result()
|
2016-09-30 19:57:24 +00:00
|
|
|
|
2016-10-01 08:22:13 +00:00
|
|
|
return remove
|
|
|
|
|
|
|
|
return factory
|
2016-09-30 19:57:24 +00:00
|
|
|
|
|
|
|
|
2017-02-18 22:17:18 +00:00
|
|
|
@callback
|
2017-10-08 15:17:54 +00:00
|
|
|
@bind_hass
|
2019-10-28 20:36:26 +00:00
|
|
|
def async_track_state_change(
|
|
|
|
hass: HomeAssistant,
|
|
|
|
entity_ids: Union[str, Iterable[str]],
|
|
|
|
action: Callable[[str, State, State], None],
|
|
|
|
from_state: Union[None, str, Iterable[str]] = None,
|
|
|
|
to_state: Union[None, str, Iterable[str]] = None,
|
|
|
|
) -> CALLBACK_TYPE:
|
2016-09-30 19:57:24 +00:00
|
|
|
"""Track specific state changes.
|
|
|
|
|
|
|
|
entity_ids, from_state and to_state can be string or list.
|
|
|
|
Use list to match multiple.
|
|
|
|
|
|
|
|
Returns a function that can be called to remove the listener.
|
|
|
|
|
2020-07-05 22:31:33 +00:00
|
|
|
If entity_ids are not MATCH_ALL along with from_state and to_state
|
|
|
|
being None, async_track_state_change_event should be used instead
|
|
|
|
as it is slightly faster.
|
|
|
|
|
2016-09-30 19:57:24 +00:00
|
|
|
Must be run within the event loop.
|
|
|
|
"""
|
2020-07-05 22:31:33 +00:00
|
|
|
if from_state is not None:
|
|
|
|
match_from_state = process_state_match(from_state)
|
|
|
|
if to_state is not None:
|
|
|
|
match_to_state = process_state_match(to_state)
|
2015-07-26 08:45:49 +00:00
|
|
|
|
|
|
|
# Ensure it is a lowercase list with entity ids we want to match on
|
2016-04-21 20:59:42 +00:00
|
|
|
if entity_ids == MATCH_ALL:
|
|
|
|
pass
|
|
|
|
elif isinstance(entity_ids, str):
|
2015-07-26 08:45:49 +00:00
|
|
|
entity_ids = (entity_ids.lower(),)
|
|
|
|
else:
|
|
|
|
entity_ids = tuple(entity_id.lower() for entity_id in entity_ids)
|
|
|
|
|
2016-10-05 03:44:32 +00:00
|
|
|
@callback
|
2019-10-28 20:36:26 +00:00
|
|
|
def state_change_listener(event: Event) -> None:
|
2017-05-02 16:18:47 +00:00
|
|
|
"""Handle specific state changes."""
|
2020-07-05 22:31:33 +00:00
|
|
|
if from_state is not None:
|
|
|
|
old_state = event.data.get("old_state")
|
|
|
|
if old_state is not None:
|
|
|
|
old_state = old_state.state
|
|
|
|
|
|
|
|
if not match_from_state(old_state):
|
|
|
|
return
|
|
|
|
if to_state is not None:
|
|
|
|
new_state = event.data.get("new_state")
|
|
|
|
if new_state is not None:
|
|
|
|
new_state = new_state.state
|
|
|
|
|
|
|
|
if not match_to_state(new_state):
|
|
|
|
return
|
|
|
|
|
|
|
|
hass.async_run_job(
|
|
|
|
action,
|
|
|
|
event.data.get("entity_id"),
|
|
|
|
event.data.get("old_state"),
|
|
|
|
event.data.get("new_state"),
|
|
|
|
)
|
2015-07-26 08:45:49 +00:00
|
|
|
|
2020-06-27 21:46:45 +00:00
|
|
|
if entity_ids != MATCH_ALL:
|
|
|
|
# If we have a list of entity ids we use
|
|
|
|
# async_track_state_change_event to route
|
|
|
|
# by entity_id to avoid iterating though state change
|
|
|
|
# events and creating a jobs where the most
|
|
|
|
# common outcome is to return right away because
|
|
|
|
# the entity_id does not match since usually
|
|
|
|
# only one or two listeners want that specific
|
|
|
|
# entity_id.
|
|
|
|
return async_track_state_change_event(hass, entity_ids, state_change_listener)
|
|
|
|
|
2016-09-30 19:57:24 +00:00
|
|
|
return hass.bus.async_listen(EVENT_STATE_CHANGED, state_change_listener)
|
2015-07-26 08:45:49 +00:00
|
|
|
|
|
|
|
|
2016-10-04 05:39:27 +00:00
|
|
|
track_state_change = threaded_listener_factory(async_track_state_change)
|
2016-10-01 08:22:13 +00:00
|
|
|
|
|
|
|
|
2020-06-27 21:46:45 +00:00
|
|
|
@bind_hass
|
|
|
|
def async_track_state_change_event(
|
2020-07-17 00:50:06 +00:00
|
|
|
hass: HomeAssistant,
|
|
|
|
entity_ids: Union[str, Iterable[str]],
|
|
|
|
action: Callable[[Event], None],
|
2020-06-27 21:46:45 +00:00
|
|
|
) -> Callable[[], None]:
|
|
|
|
"""Track specific state change events indexed by entity_id.
|
|
|
|
|
|
|
|
Unlike async_track_state_change, async_track_state_change_event
|
|
|
|
passes the full event to the callback.
|
|
|
|
|
|
|
|
In order to avoid having to iterate a long list
|
|
|
|
of EVENT_STATE_CHANGED and fire and create a job
|
|
|
|
for each one, we keep a dict of entity ids that
|
|
|
|
care about the state change events so we can
|
|
|
|
do a fast dict lookup to route events.
|
|
|
|
"""
|
|
|
|
|
|
|
|
entity_callbacks = hass.data.setdefault(TRACK_STATE_CHANGE_CALLBACKS, {})
|
|
|
|
|
|
|
|
if TRACK_STATE_CHANGE_LISTENER not in hass.data:
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def _async_state_change_dispatcher(event: Event) -> None:
|
|
|
|
"""Dispatch state changes by entity_id."""
|
|
|
|
entity_id = event.data.get("entity_id")
|
|
|
|
|
|
|
|
if entity_id not in entity_callbacks:
|
|
|
|
return
|
|
|
|
|
2020-07-17 00:50:06 +00:00
|
|
|
for action in entity_callbacks[entity_id][:]:
|
2020-06-28 00:48:27 +00:00
|
|
|
try:
|
|
|
|
hass.async_run_job(action, event)
|
|
|
|
except Exception: # pylint: disable=broad-except
|
|
|
|
_LOGGER.exception(
|
|
|
|
"Error while processing state changed for %s", entity_id
|
|
|
|
)
|
2020-06-27 21:46:45 +00:00
|
|
|
|
|
|
|
hass.data[TRACK_STATE_CHANGE_LISTENER] = hass.bus.async_listen(
|
|
|
|
EVENT_STATE_CHANGED, _async_state_change_dispatcher
|
|
|
|
)
|
|
|
|
|
2020-07-17 00:50:06 +00:00
|
|
|
if isinstance(entity_ids, str):
|
|
|
|
entity_ids = [entity_ids]
|
|
|
|
|
2020-06-27 21:46:45 +00:00
|
|
|
entity_ids = [entity_id.lower() for entity_id in entity_ids]
|
|
|
|
|
|
|
|
for entity_id in entity_ids:
|
2020-07-17 00:50:06 +00:00
|
|
|
entity_callbacks.setdefault(entity_id, []).append(action)
|
2020-06-27 21:46:45 +00:00
|
|
|
|
|
|
|
@callback
|
|
|
|
def remove_listener() -> None:
|
|
|
|
"""Remove state change listener."""
|
|
|
|
_async_remove_state_change_listeners(hass, entity_ids, action)
|
|
|
|
|
|
|
|
return remove_listener
|
|
|
|
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def _async_remove_state_change_listeners(
|
|
|
|
hass: HomeAssistant, entity_ids: Iterable[str], action: Callable[[Event], None]
|
|
|
|
) -> None:
|
|
|
|
"""Remove a listener."""
|
|
|
|
entity_callbacks = hass.data[TRACK_STATE_CHANGE_CALLBACKS]
|
|
|
|
|
|
|
|
for entity_id in entity_ids:
|
|
|
|
entity_callbacks[entity_id].remove(action)
|
|
|
|
if len(entity_callbacks[entity_id]) == 0:
|
|
|
|
del entity_callbacks[entity_id]
|
|
|
|
|
|
|
|
if not entity_callbacks:
|
|
|
|
hass.data[TRACK_STATE_CHANGE_LISTENER]()
|
|
|
|
del hass.data[TRACK_STATE_CHANGE_LISTENER]
|
|
|
|
|
|
|
|
|
2017-02-18 22:17:18 +00:00
|
|
|
@callback
|
2017-10-08 15:17:54 +00:00
|
|
|
@bind_hass
|
2019-10-28 20:36:26 +00:00
|
|
|
def async_track_template(
|
|
|
|
hass: HomeAssistant,
|
|
|
|
template: Template,
|
|
|
|
action: Callable[[str, State, State], None],
|
|
|
|
variables: Optional[Dict[str, Any]] = None,
|
|
|
|
) -> CALLBACK_TYPE:
|
2017-02-12 21:27:53 +00:00
|
|
|
"""Add a listener that track state changes with template condition."""
|
2020-04-04 15:07:36 +00:00
|
|
|
from . import condition # pylint: disable=import-outside-toplevel
|
2017-02-12 21:27:53 +00:00
|
|
|
|
|
|
|
# Local variable to keep track of if the action has already been triggered
|
|
|
|
already_triggered = False
|
|
|
|
|
|
|
|
@callback
|
2019-10-28 20:36:26 +00:00
|
|
|
def template_condition_listener(entity_id: str, from_s: State, to_s: State) -> None:
|
2017-02-12 21:27:53 +00:00
|
|
|
"""Check if condition is correct and run action."""
|
|
|
|
nonlocal already_triggered
|
|
|
|
template_result = condition.async_template(hass, template, variables)
|
|
|
|
|
|
|
|
# Check to see if template returns true
|
|
|
|
if template_result and not already_triggered:
|
|
|
|
already_triggered = True
|
|
|
|
hass.async_run_job(action, entity_id, from_s, to_s)
|
|
|
|
elif not template_result:
|
|
|
|
already_triggered = False
|
|
|
|
|
|
|
|
return async_track_state_change(
|
2019-07-31 19:25:30 +00:00
|
|
|
hass, template.extract_entities(variables), template_condition_listener
|
|
|
|
)
|
2017-02-12 21:27:53 +00:00
|
|
|
|
|
|
|
|
|
|
|
track_template = threaded_listener_factory(async_track_template)
|
|
|
|
|
|
|
|
|
2017-09-05 00:01:01 +00:00
|
|
|
@callback
|
2017-10-08 15:17:54 +00:00
|
|
|
@bind_hass
|
2019-07-31 19:25:30 +00:00
|
|
|
def async_track_same_state(
|
2019-10-28 20:36:26 +00:00
|
|
|
hass: HomeAssistant,
|
|
|
|
period: timedelta,
|
|
|
|
action: Callable[..., None],
|
2020-07-17 00:50:06 +00:00
|
|
|
async_check_same_func: Callable[[str, Optional[State], Optional[State]], bool],
|
2019-10-28 20:36:26 +00:00
|
|
|
entity_ids: Union[str, Iterable[str]] = MATCH_ALL,
|
|
|
|
) -> CALLBACK_TYPE:
|
2018-01-27 19:58:27 +00:00
|
|
|
"""Track the state of entities for a period and run an action.
|
2017-09-05 00:01:01 +00:00
|
|
|
|
|
|
|
If async_check_func is None it use the state of orig_value.
|
|
|
|
Without entity_ids we track all state changes.
|
|
|
|
"""
|
2019-10-28 20:36:26 +00:00
|
|
|
async_remove_state_for_cancel: Optional[CALLBACK_TYPE] = None
|
|
|
|
async_remove_state_for_listener: Optional[CALLBACK_TYPE] = None
|
2017-09-05 00:01:01 +00:00
|
|
|
|
|
|
|
@callback
|
2019-10-28 20:36:26 +00:00
|
|
|
def clear_listener() -> None:
|
2017-09-05 00:01:01 +00:00
|
|
|
"""Clear all unsub listener."""
|
|
|
|
nonlocal async_remove_state_for_cancel, async_remove_state_for_listener
|
|
|
|
|
|
|
|
if async_remove_state_for_listener is not None:
|
|
|
|
async_remove_state_for_listener()
|
|
|
|
async_remove_state_for_listener = None
|
|
|
|
if async_remove_state_for_cancel is not None:
|
|
|
|
async_remove_state_for_cancel()
|
|
|
|
async_remove_state_for_cancel = None
|
|
|
|
|
|
|
|
@callback
|
2019-10-28 20:36:26 +00:00
|
|
|
def state_for_listener(now: Any) -> None:
|
2017-09-05 00:01:01 +00:00
|
|
|
"""Fire on state changes after a delay and calls action."""
|
|
|
|
nonlocal async_remove_state_for_listener
|
|
|
|
async_remove_state_for_listener = None
|
|
|
|
clear_listener()
|
|
|
|
hass.async_run_job(action)
|
|
|
|
|
|
|
|
@callback
|
2020-07-17 00:50:06 +00:00
|
|
|
def state_for_cancel_listener(event: Event) -> None:
|
2017-09-05 00:01:01 +00:00
|
|
|
"""Fire on changes and cancel for listener if changed."""
|
2020-07-17 00:50:06 +00:00
|
|
|
entity: str = event.data["entity_id"]
|
|
|
|
from_state: Optional[State] = event.data.get("old_state")
|
|
|
|
to_state: Optional[State] = event.data.get("new_state")
|
|
|
|
|
2017-10-10 19:16:19 +00:00
|
|
|
if not async_check_same_func(entity, from_state, to_state):
|
|
|
|
clear_listener()
|
2017-09-05 00:01:01 +00:00
|
|
|
|
|
|
|
async_remove_state_for_listener = async_track_point_in_utc_time(
|
2019-07-31 19:25:30 +00:00
|
|
|
hass, state_for_listener, dt_util.utcnow() + period
|
|
|
|
)
|
2017-09-05 00:01:01 +00:00
|
|
|
|
2020-07-17 00:50:06 +00:00
|
|
|
if entity_ids == MATCH_ALL:
|
|
|
|
async_remove_state_for_cancel = hass.bus.async_listen(
|
|
|
|
EVENT_STATE_CHANGED, state_for_cancel_listener
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
async_remove_state_for_cancel = async_track_state_change_event(
|
|
|
|
hass,
|
|
|
|
[entity_ids] if isinstance(entity_ids, str) else entity_ids,
|
|
|
|
state_for_cancel_listener,
|
|
|
|
)
|
2017-09-05 00:01:01 +00:00
|
|
|
|
|
|
|
return clear_listener
|
|
|
|
|
|
|
|
|
|
|
|
track_same_state = threaded_listener_factory(async_track_same_state)
|
|
|
|
|
|
|
|
|
2017-02-18 22:17:18 +00:00
|
|
|
@callback
|
2017-10-08 15:17:54 +00:00
|
|
|
@bind_hass
|
2019-09-07 06:48:58 +00:00
|
|
|
def async_track_point_in_time(
|
|
|
|
hass: HomeAssistant, action: Callable[..., None], point_in_time: datetime
|
|
|
|
) -> CALLBACK_TYPE:
|
2017-01-05 22:05:16 +00:00
|
|
|
"""Add a listener that fires once after a specific point in time."""
|
2015-07-26 08:45:49 +00:00
|
|
|
|
2016-10-05 03:44:32 +00:00
|
|
|
@callback
|
2019-10-28 20:36:26 +00:00
|
|
|
def utc_converter(utc_now: datetime) -> None:
|
2016-03-07 22:39:52 +00:00
|
|
|
"""Convert passed in UTC now to local now."""
|
2016-10-05 03:44:32 +00:00
|
|
|
hass.async_run_job(action, dt_util.as_local(utc_now))
|
2015-07-26 08:45:49 +00:00
|
|
|
|
2020-07-17 04:47:53 +00:00
|
|
|
return async_track_point_in_utc_time(hass, utc_converter, point_in_time)
|
2016-09-30 19:57:24 +00:00
|
|
|
|
|
|
|
|
2016-10-04 05:39:27 +00:00
|
|
|
track_point_in_time = threaded_listener_factory(async_track_point_in_time)
|
2016-09-30 19:57:24 +00:00
|
|
|
|
|
|
|
|
2017-02-18 22:17:18 +00:00
|
|
|
@callback
|
2017-10-08 15:17:54 +00:00
|
|
|
@bind_hass
|
2019-09-07 06:48:58 +00:00
|
|
|
def async_track_point_in_utc_time(
|
2020-01-31 22:47:40 +00:00
|
|
|
hass: HomeAssistant, action: Callable[..., Any], point_in_time: datetime
|
2019-09-07 06:48:58 +00:00
|
|
|
) -> CALLBACK_TYPE:
|
2016-03-07 22:39:52 +00:00
|
|
|
"""Add a listener that fires once after a specific point in UTC time."""
|
2015-08-04 16:13:55 +00:00
|
|
|
# Ensure point_in_time is UTC
|
2020-07-17 04:47:53 +00:00
|
|
|
utc_point_in_time = dt_util.as_utc(point_in_time)
|
2015-07-26 08:45:49 +00:00
|
|
|
|
2020-06-29 16:39:24 +00:00
|
|
|
cancel_callback = hass.loop.call_at(
|
|
|
|
hass.loop.time() + point_in_time.timestamp() - time.time(),
|
2020-07-15 03:24:36 +00:00
|
|
|
hass.async_run_job,
|
|
|
|
action,
|
2020-07-17 04:47:53 +00:00
|
|
|
utc_point_in_time,
|
2020-06-29 16:39:24 +00:00
|
|
|
)
|
2016-09-18 01:28:01 +00:00
|
|
|
|
2020-06-29 16:39:24 +00:00
|
|
|
@callback
|
|
|
|
def unsub_point_in_time_listener() -> None:
|
|
|
|
"""Cancel the call_later."""
|
|
|
|
cancel_callback.cancel()
|
2016-09-18 01:28:01 +00:00
|
|
|
|
2020-06-29 16:39:24 +00:00
|
|
|
return unsub_point_in_time_listener
|
2015-07-26 08:45:49 +00:00
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
track_point_in_utc_time = threaded_listener_factory(async_track_point_in_utc_time)
|
2016-10-01 08:22:13 +00:00
|
|
|
|
|
|
|
|
2018-02-10 10:40:24 +00:00
|
|
|
@callback
|
|
|
|
@bind_hass
|
2019-10-21 14:54:59 +00:00
|
|
|
def async_call_later(
|
|
|
|
hass: HomeAssistant, delay: float, action: Callable[..., None]
|
|
|
|
) -> CALLBACK_TYPE:
|
2018-02-10 10:40:24 +00:00
|
|
|
"""Add a listener that is called in <delay>."""
|
|
|
|
return async_track_point_in_utc_time(
|
2019-07-31 19:25:30 +00:00
|
|
|
hass, action, dt_util.utcnow() + timedelta(seconds=delay)
|
|
|
|
)
|
2018-02-10 10:40:24 +00:00
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
call_later = threaded_listener_factory(async_call_later)
|
2018-10-04 07:25:05 +00:00
|
|
|
|
|
|
|
|
2017-02-18 22:17:18 +00:00
|
|
|
@callback
|
2017-10-08 15:17:54 +00:00
|
|
|
@bind_hass
|
2019-10-21 14:54:59 +00:00
|
|
|
def async_track_time_interval(
|
2020-04-14 00:41:01 +00:00
|
|
|
hass: HomeAssistant,
|
|
|
|
action: Callable[..., Union[None, Awaitable]],
|
|
|
|
interval: timedelta,
|
2019-10-21 14:54:59 +00:00
|
|
|
) -> CALLBACK_TYPE:
|
2017-01-05 22:05:16 +00:00
|
|
|
"""Add a listener that fires repetitively at every timedelta interval."""
|
2017-01-08 13:06:15 +00:00
|
|
|
remove = None
|
|
|
|
|
2019-10-28 20:36:26 +00:00
|
|
|
def next_interval() -> datetime:
|
2017-01-05 22:05:16 +00:00
|
|
|
"""Return the next interval."""
|
|
|
|
return dt_util.utcnow() + interval
|
|
|
|
|
|
|
|
@callback
|
2019-10-28 20:36:26 +00:00
|
|
|
def interval_listener(now: datetime) -> None:
|
2018-01-29 22:37:19 +00:00
|
|
|
"""Handle elapsed intervals."""
|
2017-01-05 22:05:16 +00:00
|
|
|
nonlocal remove
|
2019-07-31 19:25:30 +00:00
|
|
|
remove = async_track_point_in_utc_time(hass, interval_listener, next_interval())
|
2017-01-05 22:05:16 +00:00
|
|
|
hass.async_run_job(action, now)
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
remove = async_track_point_in_utc_time(hass, interval_listener, next_interval())
|
2017-01-05 22:05:16 +00:00
|
|
|
|
2019-10-28 20:36:26 +00:00
|
|
|
def remove_listener() -> None:
|
2017-01-05 22:05:16 +00:00
|
|
|
"""Remove interval listener."""
|
|
|
|
remove()
|
|
|
|
|
|
|
|
return remove_listener
|
|
|
|
|
|
|
|
|
|
|
|
track_time_interval = threaded_listener_factory(async_track_time_interval)
|
|
|
|
|
|
|
|
|
2019-06-10 23:05:32 +00:00
|
|
|
@attr.s
|
|
|
|
class SunListener:
|
|
|
|
"""Helper class to help listen to sun events."""
|
|
|
|
|
2020-07-14 17:30:30 +00:00
|
|
|
hass: HomeAssistant = attr.ib()
|
2019-10-21 14:54:59 +00:00
|
|
|
action: Callable[..., None] = attr.ib()
|
|
|
|
event: str = attr.ib()
|
|
|
|
offset: Optional[timedelta] = attr.ib()
|
|
|
|
_unsub_sun: Optional[CALLBACK_TYPE] = attr.ib(default=None)
|
|
|
|
_unsub_config: Optional[CALLBACK_TYPE] = attr.ib(default=None)
|
2016-01-24 20:07:09 +00:00
|
|
|
|
2016-10-05 03:44:32 +00:00
|
|
|
@callback
|
2019-10-21 14:54:59 +00:00
|
|
|
def async_attach(self) -> None:
|
2019-06-10 23:05:32 +00:00
|
|
|
"""Attach a sun listener."""
|
|
|
|
assert self._unsub_config is None
|
2016-01-24 20:07:09 +00:00
|
|
|
|
2019-06-10 23:05:32 +00:00
|
|
|
self._unsub_config = self.hass.bus.async_listen(
|
2019-07-31 19:25:30 +00:00
|
|
|
EVENT_CORE_CONFIG_UPDATE, self._handle_config_event
|
|
|
|
)
|
2016-08-26 06:25:35 +00:00
|
|
|
|
2019-06-10 23:05:32 +00:00
|
|
|
self._listen_next_sun_event()
|
2016-08-26 06:25:35 +00:00
|
|
|
|
2019-06-10 23:05:32 +00:00
|
|
|
@callback
|
2019-10-21 14:54:59 +00:00
|
|
|
def async_detach(self) -> None:
|
2019-06-10 23:05:32 +00:00
|
|
|
"""Detach the sun listener."""
|
|
|
|
assert self._unsub_sun is not None
|
|
|
|
assert self._unsub_config is not None
|
2016-01-24 20:07:09 +00:00
|
|
|
|
2019-06-10 23:05:32 +00:00
|
|
|
self._unsub_sun()
|
|
|
|
self._unsub_sun = None
|
|
|
|
self._unsub_config()
|
|
|
|
self._unsub_config = None
|
2016-01-24 20:07:09 +00:00
|
|
|
|
2019-06-10 23:05:32 +00:00
|
|
|
@callback
|
2019-10-21 14:54:59 +00:00
|
|
|
def _listen_next_sun_event(self) -> None:
|
2019-06-10 23:05:32 +00:00
|
|
|
"""Set up the sun event listener."""
|
|
|
|
assert self._unsub_sun is None
|
|
|
|
|
|
|
|
self._unsub_sun = async_track_point_in_utc_time(
|
2019-07-31 19:25:30 +00:00
|
|
|
self.hass,
|
|
|
|
self._handle_sun_event,
|
|
|
|
get_astral_event_next(self.hass, self.event, offset=self.offset),
|
2019-06-10 23:05:32 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
@callback
|
2019-10-21 14:54:59 +00:00
|
|
|
def _handle_sun_event(self, _now: Any) -> None:
|
2019-06-10 23:05:32 +00:00
|
|
|
"""Handle solar event."""
|
|
|
|
self._unsub_sun = None
|
|
|
|
self._listen_next_sun_event()
|
|
|
|
self.hass.async_run_job(self.action)
|
|
|
|
|
|
|
|
@callback
|
2019-10-21 14:54:59 +00:00
|
|
|
def _handle_config_event(self, _event: Any) -> None:
|
2019-06-10 23:05:32 +00:00
|
|
|
"""Handle core config update."""
|
|
|
|
assert self._unsub_sun is not None
|
|
|
|
self._unsub_sun()
|
|
|
|
self._unsub_sun = None
|
|
|
|
self._listen_next_sun_event()
|
2016-10-01 08:22:13 +00:00
|
|
|
|
|
|
|
|
2017-02-18 22:17:18 +00:00
|
|
|
@callback
|
2017-10-08 15:17:54 +00:00
|
|
|
@bind_hass
|
2019-10-21 14:54:59 +00:00
|
|
|
def async_track_sunrise(
|
|
|
|
hass: HomeAssistant, action: Callable[..., None], offset: Optional[timedelta] = None
|
|
|
|
) -> CALLBACK_TYPE:
|
2019-06-10 23:05:32 +00:00
|
|
|
"""Add a listener that will fire a specified offset from sunrise daily."""
|
|
|
|
listener = SunListener(hass, action, SUN_EVENT_SUNRISE, offset)
|
|
|
|
listener.async_attach()
|
|
|
|
return listener.async_detach
|
2016-01-24 20:07:09 +00:00
|
|
|
|
|
|
|
|
2019-06-10 23:05:32 +00:00
|
|
|
track_sunrise = threaded_listener_factory(async_track_sunrise)
|
2016-08-26 06:25:35 +00:00
|
|
|
|
|
|
|
|
2019-06-10 23:05:32 +00:00
|
|
|
@callback
|
|
|
|
@bind_hass
|
2019-10-21 14:54:59 +00:00
|
|
|
def async_track_sunset(
|
|
|
|
hass: HomeAssistant, action: Callable[..., None], offset: Optional[timedelta] = None
|
|
|
|
) -> CALLBACK_TYPE:
|
2019-06-10 23:05:32 +00:00
|
|
|
"""Add a listener that will fire a specified offset from sunset daily."""
|
|
|
|
listener = SunListener(hass, action, SUN_EVENT_SUNSET, offset)
|
|
|
|
listener.async_attach()
|
|
|
|
return listener.async_detach
|
2016-01-24 20:07:09 +00:00
|
|
|
|
|
|
|
|
2016-10-04 05:39:27 +00:00
|
|
|
track_sunset = threaded_listener_factory(async_track_sunset)
|
2016-10-01 08:22:13 +00:00
|
|
|
|
|
|
|
|
2017-02-18 22:17:18 +00:00
|
|
|
@callback
|
2017-10-08 15:17:54 +00:00
|
|
|
@bind_hass
|
2019-07-31 19:25:30 +00:00
|
|
|
def async_track_utc_time_change(
|
2019-10-21 14:54:59 +00:00
|
|
|
hass: HomeAssistant,
|
|
|
|
action: Callable[..., None],
|
|
|
|
hour: Optional[Any] = None,
|
|
|
|
minute: Optional[Any] = None,
|
|
|
|
second: Optional[Any] = None,
|
|
|
|
local: bool = False,
|
|
|
|
) -> CALLBACK_TYPE:
|
2016-03-07 22:39:52 +00:00
|
|
|
"""Add a listener that will fire if time matches a pattern."""
|
2015-07-26 08:45:49 +00:00
|
|
|
# We do not have to wrap the function with time pattern matching logic
|
|
|
|
# if no pattern given
|
2018-10-09 08:14:18 +00:00
|
|
|
if all(val is None for val in (hour, minute, second)):
|
2019-07-31 19:25:30 +00:00
|
|
|
|
2016-10-05 03:44:32 +00:00
|
|
|
@callback
|
2019-10-28 20:36:26 +00:00
|
|
|
def time_change_listener(event: Event) -> None:
|
2016-03-07 22:39:52 +00:00
|
|
|
"""Fire every time event that comes in."""
|
2016-10-05 03:44:32 +00:00
|
|
|
hass.async_run_job(action, event.data[ATTR_NOW])
|
2015-07-26 08:45:49 +00:00
|
|
|
|
2016-10-01 08:22:13 +00:00
|
|
|
return hass.bus.async_listen(EVENT_TIME_CHANGED, time_change_listener)
|
2015-07-26 08:45:49 +00:00
|
|
|
|
2018-10-09 08:14:18 +00:00
|
|
|
matching_seconds = dt_util.parse_time_expression(second, 0, 59)
|
|
|
|
matching_minutes = dt_util.parse_time_expression(minute, 0, 59)
|
|
|
|
matching_hours = dt_util.parse_time_expression(hour, 0, 23)
|
|
|
|
|
|
|
|
next_time = None
|
|
|
|
|
2019-10-21 14:54:59 +00:00
|
|
|
def calculate_next(now: datetime) -> None:
|
2018-10-09 08:14:18 +00:00
|
|
|
"""Calculate and set the next time the trigger should fire."""
|
|
|
|
nonlocal next_time
|
|
|
|
|
|
|
|
localized_now = dt_util.as_local(now) if local else now
|
|
|
|
next_time = dt_util.find_next_time_expression_time(
|
2019-07-31 19:25:30 +00:00
|
|
|
localized_now, matching_seconds, matching_minutes, matching_hours
|
|
|
|
)
|
2018-10-09 08:14:18 +00:00
|
|
|
|
|
|
|
# Make sure rolling back the clock doesn't prevent the timer from
|
|
|
|
# triggering.
|
2019-10-21 14:54:59 +00:00
|
|
|
last_now: Optional[datetime] = None
|
2015-07-26 08:45:49 +00:00
|
|
|
|
2016-10-05 03:44:32 +00:00
|
|
|
@callback
|
2019-10-21 14:54:59 +00:00
|
|
|
def pattern_time_change_listener(event: Event) -> None:
|
2016-03-07 22:39:52 +00:00
|
|
|
"""Listen for matching time_changed events."""
|
2018-10-09 08:14:18 +00:00
|
|
|
nonlocal next_time, last_now
|
|
|
|
|
2015-07-26 08:45:49 +00:00
|
|
|
now = event.data[ATTR_NOW]
|
|
|
|
|
2018-10-09 08:14:18 +00:00
|
|
|
if last_now is None or now < last_now:
|
|
|
|
# Time rolled back or next time not yet calculated
|
|
|
|
calculate_next(now)
|
2015-07-26 08:45:49 +00:00
|
|
|
|
2018-10-09 08:14:18 +00:00
|
|
|
last_now = now
|
2015-07-26 08:45:49 +00:00
|
|
|
|
2018-10-09 08:14:18 +00:00
|
|
|
if next_time <= now:
|
2019-03-04 04:36:13 +00:00
|
|
|
hass.async_run_job(action, dt_util.as_local(now) if local else now)
|
2018-10-09 08:14:18 +00:00
|
|
|
calculate_next(now + timedelta(seconds=1))
|
2015-07-26 08:45:49 +00:00
|
|
|
|
2018-10-09 08:14:18 +00:00
|
|
|
# We can't use async_track_point_in_utc_time here because it would
|
|
|
|
# break in the case that the system time abruptly jumps backwards.
|
|
|
|
# Our custom last_now logic takes care of resolving that scenario.
|
2019-07-31 19:25:30 +00:00
|
|
|
return hass.bus.async_listen(EVENT_TIME_CHANGED, pattern_time_change_listener)
|
2016-10-01 08:22:13 +00:00
|
|
|
|
|
|
|
|
2016-10-04 05:39:27 +00:00
|
|
|
track_utc_time_change = threaded_listener_factory(async_track_utc_time_change)
|
2015-07-26 08:45:49 +00:00
|
|
|
|
|
|
|
|
2017-02-18 22:17:18 +00:00
|
|
|
@callback
|
2017-10-08 15:17:54 +00:00
|
|
|
@bind_hass
|
2019-10-21 14:54:59 +00:00
|
|
|
def async_track_time_change(
|
|
|
|
hass: HomeAssistant,
|
|
|
|
action: Callable[..., None],
|
|
|
|
hour: Optional[Any] = None,
|
|
|
|
minute: Optional[Any] = None,
|
|
|
|
second: Optional[Any] = None,
|
|
|
|
) -> CALLBACK_TYPE:
|
2016-03-07 22:39:52 +00:00
|
|
|
"""Add a listener that will fire if UTC time matches a pattern."""
|
2019-07-31 19:25:30 +00:00
|
|
|
return async_track_utc_time_change(hass, action, hour, minute, second, local=True)
|
2016-10-01 08:22:13 +00:00
|
|
|
|
|
|
|
|
2016-10-04 05:39:27 +00:00
|
|
|
track_time_change = threaded_listener_factory(async_track_time_change)
|
2015-07-26 08:45:49 +00:00
|
|
|
|
|
|
|
|
2020-03-23 23:05:21 +00:00
|
|
|
def process_state_match(
|
2019-10-21 14:54:59 +00:00
|
|
|
parameter: Union[None, str, Iterable[str]]
|
|
|
|
) -> Callable[[str], bool]:
|
2017-10-10 20:26:03 +00:00
|
|
|
"""Convert parameter to function that matches input against parameter."""
|
2016-06-13 03:37:33 +00:00
|
|
|
if parameter is None or parameter == MATCH_ALL:
|
2017-10-10 20:26:03 +00:00
|
|
|
return lambda _: True
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
if isinstance(parameter, str) or not hasattr(parameter, "__iter__"):
|
2017-10-10 20:26:03 +00:00
|
|
|
return lambda state: state == parameter
|
|
|
|
|
2020-07-05 22:31:33 +00:00
|
|
|
parameter_set = set(parameter)
|
|
|
|
return lambda state: state in parameter_set
|