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
|
2019-10-28 20:36:26 +00:00
|
|
|
from typing import Any, Callable, Dict, Iterable, Optional, Union, cast
|
2019-06-10 23:05:32 +00:00
|
|
|
|
|
|
|
import attr
|
2015-07-26 08:45:49 +00:00
|
|
|
|
2017-10-08 15:17:54 +00:00
|
|
|
from homeassistant.loader import bind_hass
|
2017-05-09 07:03:34 +00:00
|
|
|
from homeassistant.helpers.sun import get_astral_event_next
|
2019-10-28 20:36:26 +00:00
|
|
|
from homeassistant.helpers.template import Template
|
|
|
|
from homeassistant.core import HomeAssistant, callback, CALLBACK_TYPE, Event, State
|
2019-06-10 23:05:32 +00:00
|
|
|
from homeassistant.const import (
|
2019-07-31 19:25:30 +00:00
|
|
|
ATTR_NOW,
|
|
|
|
EVENT_STATE_CHANGED,
|
|
|
|
EVENT_TIME_CHANGED,
|
|
|
|
MATCH_ALL,
|
|
|
|
SUN_EVENT_SUNRISE,
|
|
|
|
SUN_EVENT_SUNSET,
|
|
|
|
EVENT_CORE_CONFIG_UPDATE,
|
|
|
|
)
|
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
|
|
|
|
2019-07-21 16:59:02 +00:00
|
|
|
|
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.
|
|
|
|
|
|
|
|
Must be run within the event loop.
|
|
|
|
"""
|
2017-10-10 20:26:03 +00:00
|
|
|
match_from_state = _process_state_match(from_state)
|
|
|
|
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."""
|
2019-10-28 20:36:26 +00:00
|
|
|
if (
|
|
|
|
entity_ids != MATCH_ALL
|
|
|
|
and cast(str, event.data.get("entity_id")) not in entity_ids
|
|
|
|
):
|
2015-07-26 08:45:49 +00:00
|
|
|
return
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
old_state = event.data.get("old_state")
|
2017-10-10 20:26:03 +00:00
|
|
|
if old_state is not None:
|
|
|
|
old_state = old_state.state
|
2016-05-30 17:19:12 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
new_state = event.data.get("new_state")
|
2017-10-10 20:26:03 +00:00
|
|
|
if new_state is not None:
|
|
|
|
new_state = new_state.state
|
2015-07-26 08:45:49 +00:00
|
|
|
|
2017-10-10 20:26:03 +00:00
|
|
|
if match_from_state(old_state) and match_to_state(new_state):
|
2019-07-31 19:25:30 +00:00
|
|
|
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
|
|
|
|
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
|
|
|
|
|
|
|
|
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."""
|
|
|
|
from . import condition
|
|
|
|
|
|
|
|
# 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],
|
|
|
|
async_check_same_func: Callable[[str, State, State], bool],
|
|
|
|
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
|
2019-10-28 20:36:26 +00:00
|
|
|
def state_for_cancel_listener(
|
|
|
|
entity: str, from_state: State, to_state: State
|
|
|
|
) -> None:
|
2017-09-05 00:01:01 +00:00
|
|
|
"""Fire on changes and cancel for listener if changed."""
|
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
|
|
|
|
|
|
|
async_remove_state_for_cancel = async_track_state_change(
|
2019-07-31 19:25:30 +00:00
|
|
|
hass, 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
|
|
|
utc_point_in_time = dt_util.as_utc(point_in_time)
|
|
|
|
|
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
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
return async_track_point_in_utc_time(hass, utc_converter, utc_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(
|
|
|
|
hass: HomeAssistant, action: Callable[..., None], point_in_time: datetime
|
|
|
|
) -> 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
|
|
|
|
point_in_time = dt_util.as_utc(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 point_in_time_listener(event: Event) -> None:
|
2016-03-07 22:39:52 +00:00
|
|
|
"""Listen for matching time_changed events."""
|
2015-07-26 08:45:49 +00:00
|
|
|
now = event.data[ATTR_NOW]
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
if now < point_in_time or hasattr(point_in_time_listener, "run"):
|
2016-08-26 06:25:35 +00:00
|
|
|
return
|
2015-07-26 08:45:49 +00:00
|
|
|
|
2016-08-26 06:25:35 +00:00
|
|
|
# Set variable so that we will never run twice.
|
|
|
|
# Because the event bus might have to wait till a thread comes
|
|
|
|
# available to execute this listener it might occur that the
|
|
|
|
# listener gets lined up twice to be executed. This will make
|
|
|
|
# sure the second time it does nothing.
|
2019-10-28 20:36:26 +00:00
|
|
|
setattr(point_in_time_listener, "run", True)
|
2016-09-30 19:57:24 +00:00
|
|
|
async_unsub()
|
2016-09-18 02:51:18 +00:00
|
|
|
|
2016-10-05 03:44:32 +00:00
|
|
|
hass.async_run_job(action, now)
|
2016-09-18 01:28:01 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
async_unsub = hass.bus.async_listen(EVENT_TIME_CHANGED, point_in_time_listener)
|
2016-09-18 01:28:01 +00:00
|
|
|
|
2016-09-30 19:57:24 +00:00
|
|
|
return async_unsub
|
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(
|
|
|
|
hass: HomeAssistant, action: Callable[..., None], interval: timedelta
|
|
|
|
) -> 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."""
|
|
|
|
|
|
|
|
hass = attr.ib(type=HomeAssistant)
|
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
|
|
|
|
|
|
|
|
2019-10-21 14:54:59 +00:00
|
|
|
def _process_state_match(
|
|
|
|
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
|
|
|
|
|
2019-10-21 14:54:59 +00:00
|
|
|
parameter_tuple = tuple(parameter)
|
|
|
|
return lambda state: state in parameter_tuple
|