2016-03-07 22:39:52 +00:00
|
|
|
"""Helpers for listening to events."""
|
2015-07-26 08:45:49 +00:00
|
|
|
import functools as ft
|
|
|
|
|
2017-05-09 07:03:34 +00:00
|
|
|
from homeassistant.helpers.sun import get_astral_event_next
|
2016-10-05 03:44:32 +00:00
|
|
|
from ..core import HomeAssistant, callback
|
2015-07-26 08:45:49 +00:00
|
|
|
from ..const import (
|
|
|
|
ATTR_NOW, EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL)
|
2016-02-19 05:27:50 +00:00
|
|
|
from ..util import dt as dt_util
|
2016-09-18 02:51:18 +00:00
|
|
|
from ..util.async import run_callback_threadsafe
|
2015-07-26 08:45:49 +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
|
|
|
|
2016-10-04 05:39:27 +00:00
|
|
|
def threaded_listener_factory(async_factory):
|
2016-10-01 08:22:13 +00:00
|
|
|
"""Convert an async event helper to a threaded one."""
|
|
|
|
@ft.wraps(async_factory)
|
|
|
|
def factory(*args, **kwargs):
|
|
|
|
"""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):
|
|
|
|
raise TypeError('First parameter needs to be a hass instance')
|
|
|
|
|
|
|
|
async_remove = run_callback_threadsafe(
|
|
|
|
hass.loop, ft.partial(async_factory, *args, **kwargs)).result()
|
2016-09-30 19:57:24 +00:00
|
|
|
|
2016-10-01 08:22:13 +00:00
|
|
|
def remove():
|
|
|
|
"""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
|
2016-09-30 19:57:24 +00:00
|
|
|
def async_track_state_change(hass, entity_ids, action, from_state=None,
|
|
|
|
to_state=None):
|
|
|
|
"""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.
|
|
|
|
"""
|
2016-06-13 03:37:33 +00:00
|
|
|
from_state = _process_state_match(from_state)
|
|
|
|
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
|
2015-07-26 08:45:49 +00:00
|
|
|
def state_change_listener(event):
|
2017-05-02 16:18:47 +00:00
|
|
|
"""Handle specific state changes."""
|
2016-04-21 20:59:42 +00:00
|
|
|
if entity_ids != MATCH_ALL and \
|
2016-05-30 17:19:12 +00:00
|
|
|
event.data.get('entity_id') not in entity_ids:
|
2015-07-26 08:45:49 +00:00
|
|
|
return
|
|
|
|
|
2016-05-30 17:19:12 +00:00
|
|
|
if event.data.get('old_state') is not None:
|
2016-02-14 06:57:40 +00:00
|
|
|
old_state = event.data['old_state'].state
|
|
|
|
else:
|
2016-05-30 17:19:12 +00:00
|
|
|
old_state = None
|
|
|
|
|
|
|
|
if event.data.get('new_state') is not None:
|
2016-02-14 06:57:40 +00:00
|
|
|
new_state = event.data['new_state'].state
|
2016-05-30 17:19:12 +00:00
|
|
|
else:
|
|
|
|
new_state = None
|
2015-07-26 08:45:49 +00:00
|
|
|
|
2016-02-14 06:57:40 +00:00
|
|
|
if _matcher(old_state, from_state) and _matcher(new_state, to_state):
|
2016-10-05 03:44:32 +00:00
|
|
|
hass.async_run_job(action, event.data.get('entity_id'),
|
2016-09-18 01:28:01 +00:00
|
|
|
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-02-12 21:27:53 +00:00
|
|
|
def async_track_template(hass, template, action, variables=None):
|
|
|
|
"""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
|
|
|
|
def template_condition_listener(entity_id, from_s, to_s):
|
|
|
|
"""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(
|
|
|
|
hass, template.extract_entities(), template_condition_listener)
|
|
|
|
|
|
|
|
|
|
|
|
track_template = threaded_listener_factory(async_track_template)
|
|
|
|
|
|
|
|
|
2017-02-18 22:17:18 +00:00
|
|
|
@callback
|
2016-10-01 08:22:13 +00:00
|
|
|
def async_track_point_in_time(hass, action, point_in_time):
|
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
|
2015-07-26 08:45:49 +00:00
|
|
|
def utc_converter(utc_now):
|
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
|
|
|
|
2016-10-01 08:22:13 +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
|
2016-09-30 19:57:24 +00:00
|
|
|
def async_track_point_in_utc_time(hass, action, point_in_time):
|
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
|
2015-07-26 08:45:49 +00:00
|
|
|
def point_in_time_listener(event):
|
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]
|
|
|
|
|
2016-08-26 06:25:35 +00:00
|
|
|
if now < point_in_time or hasattr(point_in_time_listener, 'run'):
|
|
|
|
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.
|
|
|
|
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
|
|
|
|
2016-09-30 19:57:24 +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
|
|
|
|
|
|
|
|
2016-10-04 05:39:27 +00:00
|
|
|
track_point_in_utc_time = threaded_listener_factory(
|
|
|
|
async_track_point_in_utc_time)
|
2016-10-01 08:22:13 +00:00
|
|
|
|
|
|
|
|
2017-02-18 22:17:18 +00:00
|
|
|
@callback
|
2017-01-05 22:05:16 +00:00
|
|
|
def async_track_time_interval(hass, action, interval):
|
|
|
|
"""Add a listener that fires repetitively at every timedelta interval."""
|
2017-01-08 13:06:15 +00:00
|
|
|
remove = None
|
|
|
|
|
2017-01-05 22:05:16 +00:00
|
|
|
def next_interval():
|
|
|
|
"""Return the next interval."""
|
|
|
|
return dt_util.utcnow() + interval
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def interval_listener(now):
|
2017-05-02 16:18:47 +00:00
|
|
|
"""Handle elaspsed intervals."""
|
2017-01-05 22:05:16 +00:00
|
|
|
nonlocal remove
|
|
|
|
remove = async_track_point_in_utc_time(
|
|
|
|
hass, interval_listener, next_interval())
|
|
|
|
hass.async_run_job(action, now)
|
|
|
|
|
|
|
|
remove = async_track_point_in_utc_time(
|
|
|
|
hass, interval_listener, next_interval())
|
|
|
|
|
|
|
|
def remove_listener():
|
|
|
|
"""Remove interval listener."""
|
|
|
|
remove()
|
|
|
|
|
|
|
|
return remove_listener
|
|
|
|
|
|
|
|
|
|
|
|
track_time_interval = threaded_listener_factory(async_track_time_interval)
|
|
|
|
|
|
|
|
|
2017-02-18 22:17:18 +00:00
|
|
|
@callback
|
2016-10-01 08:22:13 +00:00
|
|
|
def async_track_sunrise(hass, action, offset=None):
|
2016-03-07 22:39:52 +00:00
|
|
|
"""Add a listener that will fire a specified offset from sunrise daily."""
|
2017-01-08 13:06:15 +00:00
|
|
|
remove = None
|
2016-01-24 20:07:09 +00:00
|
|
|
|
2016-10-05 03:44:32 +00:00
|
|
|
@callback
|
2016-01-24 20:07:09 +00:00
|
|
|
def sunrise_automation_listener(now):
|
2017-05-02 16:18:47 +00:00
|
|
|
"""Handle points in time to execute actions."""
|
2016-08-26 06:25:35 +00:00
|
|
|
nonlocal remove
|
2016-09-30 19:57:24 +00:00
|
|
|
remove = async_track_point_in_utc_time(
|
2017-05-09 07:03:34 +00:00
|
|
|
hass, sunrise_automation_listener, get_astral_event_next(
|
|
|
|
hass, 'sunrise', offset=offset))
|
2016-10-05 03:44:32 +00:00
|
|
|
hass.async_run_job(action)
|
2016-01-24 20:07:09 +00:00
|
|
|
|
2016-10-01 08:22:13 +00:00
|
|
|
remove = async_track_point_in_utc_time(
|
2017-05-09 07:03:34 +00:00
|
|
|
hass, sunrise_automation_listener, get_astral_event_next(
|
|
|
|
hass, 'sunrise', offset=offset))
|
2016-08-26 06:25:35 +00:00
|
|
|
|
|
|
|
def remove_listener():
|
2016-09-30 19:57:24 +00:00
|
|
|
"""Remove sunset listener."""
|
2016-10-01 08:22:13 +00:00
|
|
|
remove()
|
2016-08-26 06:25:35 +00:00
|
|
|
|
|
|
|
return remove_listener
|
2016-01-24 20:07:09 +00:00
|
|
|
|
|
|
|
|
2016-10-04 05:39:27 +00:00
|
|
|
track_sunrise = threaded_listener_factory(async_track_sunrise)
|
2016-10-01 08:22:13 +00:00
|
|
|
|
|
|
|
|
2017-02-18 22:17:18 +00:00
|
|
|
@callback
|
2016-10-01 08:22:13 +00:00
|
|
|
def async_track_sunset(hass, action, offset=None):
|
2016-03-07 22:39:52 +00:00
|
|
|
"""Add a listener that will fire a specified offset from sunset daily."""
|
2017-01-08 13:06:15 +00:00
|
|
|
remove = None
|
2016-01-24 20:07:09 +00:00
|
|
|
|
2016-10-05 03:44:32 +00:00
|
|
|
@callback
|
2016-01-24 20:07:09 +00:00
|
|
|
def sunset_automation_listener(now):
|
2017-05-02 16:18:47 +00:00
|
|
|
"""Handle points in time to execute actions."""
|
2016-08-26 06:25:35 +00:00
|
|
|
nonlocal remove
|
2016-09-30 19:57:24 +00:00
|
|
|
remove = async_track_point_in_utc_time(
|
2017-05-09 07:03:34 +00:00
|
|
|
hass, sunset_automation_listener, get_astral_event_next(
|
|
|
|
hass, 'sunset', offset=offset))
|
2016-10-05 03:44:32 +00:00
|
|
|
hass.async_run_job(action)
|
2016-01-24 20:07:09 +00:00
|
|
|
|
2016-10-01 08:22:13 +00:00
|
|
|
remove = async_track_point_in_utc_time(
|
2017-05-09 07:03:34 +00:00
|
|
|
hass, sunset_automation_listener, get_astral_event_next(
|
|
|
|
hass, 'sunset', offset=offset))
|
2016-08-26 06:25:35 +00:00
|
|
|
|
|
|
|
def remove_listener():
|
|
|
|
"""Remove sunset listener."""
|
2016-10-01 08:22:13 +00:00
|
|
|
remove()
|
2016-08-26 06:25:35 +00:00
|
|
|
|
|
|
|
return remove_listener
|
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
|
2016-10-01 08:22:13 +00:00
|
|
|
def async_track_utc_time_change(hass, action, year=None, month=None, day=None,
|
|
|
|
hour=None, minute=None, second=None,
|
|
|
|
local=False):
|
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
|
|
|
|
if all(val is None for val in (year, month, day, hour, minute, second)):
|
2016-10-05 03:44:32 +00:00
|
|
|
@callback
|
2015-07-26 08:45:49 +00:00
|
|
|
def time_change_listener(event):
|
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
|
|
|
|
2016-06-13 03:37:33 +00:00
|
|
|
pmp = _process_time_match
|
2015-07-26 08:45:49 +00:00
|
|
|
year, month, day = pmp(year), pmp(month), pmp(day)
|
2016-01-26 17:37:19 +00:00
|
|
|
hour, minute, second = pmp(hour), pmp(minute), pmp(second)
|
2015-07-26 08:45:49 +00:00
|
|
|
|
2016-10-05 03:44:32 +00:00
|
|
|
@callback
|
2015-07-26 08:45:49 +00:00
|
|
|
def pattern_time_change_listener(event):
|
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]
|
|
|
|
|
|
|
|
if local:
|
|
|
|
now = dt_util.as_local(now)
|
|
|
|
mat = _matcher
|
|
|
|
|
2015-11-29 21:49:05 +00:00
|
|
|
# pylint: disable=too-many-boolean-expressions
|
2015-07-26 08:45:49 +00:00
|
|
|
if mat(now.year, year) and \
|
|
|
|
mat(now.month, month) and \
|
|
|
|
mat(now.day, day) and \
|
|
|
|
mat(now.hour, hour) and \
|
|
|
|
mat(now.minute, minute) and \
|
|
|
|
mat(now.second, second):
|
|
|
|
|
2016-10-05 03:44:32 +00:00
|
|
|
hass.async_run_job(action, 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,
|
|
|
|
pattern_time_change_listener)
|
|
|
|
|
|
|
|
|
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
|
2016-10-01 08:22:13 +00:00
|
|
|
def async_track_time_change(hass, action, year=None, month=None, day=None,
|
|
|
|
hour=None, minute=None, second=None):
|
2016-03-07 22:39:52 +00:00
|
|
|
"""Add a listener that will fire if UTC time matches a pattern."""
|
2016-10-01 08:22:13 +00:00
|
|
|
return async_track_utc_time_change(hass, action, year, month, day, hour,
|
|
|
|
minute, second, local=True)
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
2016-06-13 03:37:33 +00:00
|
|
|
def _process_state_match(parameter):
|
|
|
|
"""Wrap parameter in a tuple if it is not one and returns it."""
|
|
|
|
if parameter is None or parameter == MATCH_ALL:
|
|
|
|
return MATCH_ALL
|
|
|
|
elif isinstance(parameter, str) or not hasattr(parameter, '__iter__'):
|
|
|
|
return (parameter,)
|
|
|
|
else:
|
|
|
|
return tuple(parameter)
|
|
|
|
|
|
|
|
|
|
|
|
def _process_time_match(parameter):
|
2016-03-07 22:39:52 +00:00
|
|
|
"""Wrap parameter in a tuple if it is not one and returns it."""
|
2015-07-26 08:45:49 +00:00
|
|
|
if parameter is None or parameter == MATCH_ALL:
|
|
|
|
return MATCH_ALL
|
2016-01-26 20:38:07 +00:00
|
|
|
elif isinstance(parameter, str) and parameter.startswith('/'):
|
|
|
|
return parameter
|
2015-07-26 08:45:49 +00:00
|
|
|
elif isinstance(parameter, str) or not hasattr(parameter, '__iter__'):
|
|
|
|
return (parameter,)
|
|
|
|
else:
|
|
|
|
return tuple(parameter)
|
|
|
|
|
|
|
|
|
|
|
|
def _matcher(subject, pattern):
|
2016-03-07 22:39:52 +00:00
|
|
|
"""Return True if subject matches the pattern.
|
2015-07-26 08:45:49 +00:00
|
|
|
|
|
|
|
Pattern is either a tuple of allowed subjects or a `MATCH_ALL`.
|
|
|
|
"""
|
2016-01-26 20:38:07 +00:00
|
|
|
if isinstance(pattern, str) and pattern.startswith('/'):
|
2016-01-26 17:37:19 +00:00
|
|
|
try:
|
2016-01-26 20:38:07 +00:00
|
|
|
return subject % float(pattern.lstrip('/')) == 0
|
2016-01-26 17:37:19 +00:00
|
|
|
except ValueError:
|
|
|
|
return False
|
2016-01-26 19:43:29 +00:00
|
|
|
|
2015-07-26 08:45:49 +00:00
|
|
|
return MATCH_ALL == pattern or subject in pattern
|