2016-04-21 22:52:20 +00:00
|
|
|
"""Helpers to execute scripts."""
|
2018-02-25 11:38:46 +00:00
|
|
|
|
2016-04-21 22:52:20 +00:00
|
|
|
import logging
|
|
|
|
from itertools import islice
|
2016-08-07 23:26:35 +00:00
|
|
|
from typing import Optional, Sequence
|
|
|
|
|
2016-07-20 18:26:17 +00:00
|
|
|
import voluptuous as vol
|
|
|
|
|
2017-02-12 21:27:53 +00:00
|
|
|
from homeassistant.core import HomeAssistant, callback
|
|
|
|
from homeassistant.const import CONF_CONDITION, CONF_TIMEOUT
|
2018-01-19 06:13:14 +00:00
|
|
|
from homeassistant.exceptions import TemplateError
|
2016-08-09 03:42:25 +00:00
|
|
|
from homeassistant.helpers import (
|
2018-01-19 06:13:14 +00:00
|
|
|
service, condition, template as template,
|
|
|
|
config_validation as cv)
|
2017-02-12 21:27:53 +00:00
|
|
|
from homeassistant.helpers.event import (
|
|
|
|
async_track_point_in_utc_time, async_track_template)
|
2016-08-09 03:42:25 +00:00
|
|
|
from homeassistant.helpers.typing import ConfigType
|
|
|
|
import homeassistant.util.dt as date_util
|
2016-10-01 06:26:01 +00:00
|
|
|
from homeassistant.util.async import (
|
|
|
|
run_coroutine_threadsafe, run_callback_threadsafe)
|
2016-04-21 22:52:20 +00:00
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2017-05-02 16:18:47 +00:00
|
|
|
CONF_ALIAS = 'alias'
|
|
|
|
CONF_SERVICE = 'service'
|
|
|
|
CONF_SERVICE_DATA = 'data'
|
|
|
|
CONF_SEQUENCE = 'sequence'
|
|
|
|
CONF_EVENT = 'event'
|
|
|
|
CONF_EVENT_DATA = 'event_data'
|
2018-01-19 06:13:14 +00:00
|
|
|
CONF_EVENT_DATA_TEMPLATE = 'event_data_template'
|
2017-05-02 16:18:47 +00:00
|
|
|
CONF_DELAY = 'delay'
|
|
|
|
CONF_WAIT_TEMPLATE = 'wait_template'
|
2016-04-21 22:52:20 +00:00
|
|
|
|
|
|
|
|
2016-08-09 03:42:25 +00:00
|
|
|
def call_from_config(hass: HomeAssistant, config: ConfigType,
|
2018-02-11 17:20:28 +00:00
|
|
|
variables: Optional[Sequence] = None) -> None:
|
2016-04-21 22:52:20 +00:00
|
|
|
"""Call a script based on a config entry."""
|
2016-09-28 04:29:55 +00:00
|
|
|
Script(hass, cv.SCRIPT_SCHEMA(config)).run(variables)
|
2016-04-21 22:52:20 +00:00
|
|
|
|
|
|
|
|
|
|
|
class Script():
|
|
|
|
"""Representation of a script."""
|
|
|
|
|
2018-02-11 17:20:28 +00:00
|
|
|
def __init__(self, hass: HomeAssistant, sequence, name: str = None,
|
2016-08-07 23:26:35 +00:00
|
|
|
change_listener=None) -> None:
|
2016-04-21 22:52:20 +00:00
|
|
|
"""Initialize the script."""
|
|
|
|
self.hass = hass
|
2016-09-28 04:29:55 +00:00
|
|
|
self.sequence = sequence
|
|
|
|
template.attach(hass, self.sequence)
|
2016-04-21 22:52:20 +00:00
|
|
|
self.name = name
|
|
|
|
self._change_listener = change_listener
|
|
|
|
self._cur = -1
|
|
|
|
self.last_action = None
|
2017-01-11 15:23:05 +00:00
|
|
|
self.last_triggered = None
|
2017-02-12 21:27:53 +00:00
|
|
|
self.can_cancel = any(CONF_DELAY in action or CONF_WAIT_TEMPLATE
|
|
|
|
in action for action in self.sequence)
|
|
|
|
self._async_listener = []
|
2016-09-28 04:29:55 +00:00
|
|
|
self._template_cache = {}
|
2016-10-11 06:36:38 +00:00
|
|
|
self._config_cache = {}
|
2016-04-21 22:52:20 +00:00
|
|
|
|
|
|
|
@property
|
2016-08-07 23:26:35 +00:00
|
|
|
def is_running(self) -> bool:
|
2016-04-21 22:52:20 +00:00
|
|
|
"""Return true if script is on."""
|
|
|
|
return self._cur != -1
|
|
|
|
|
2016-10-01 06:26:01 +00:00
|
|
|
def run(self, variables=None):
|
2016-04-21 22:52:20 +00:00
|
|
|
"""Run script."""
|
2016-10-01 06:26:01 +00:00
|
|
|
run_coroutine_threadsafe(
|
|
|
|
self.async_run(variables), self.hass.loop).result()
|
|
|
|
|
2018-02-25 11:38:46 +00:00
|
|
|
async def async_run(self, variables: Optional[Sequence] = None) -> None:
|
2016-10-01 06:26:01 +00:00
|
|
|
"""Run script.
|
|
|
|
|
2016-10-04 05:39:27 +00:00
|
|
|
This method is a coroutine.
|
2016-10-01 06:26:01 +00:00
|
|
|
"""
|
2017-01-11 15:23:05 +00:00
|
|
|
self.last_triggered = date_util.utcnow()
|
2016-10-01 06:26:01 +00:00
|
|
|
if self._cur == -1:
|
|
|
|
self._log('Running script')
|
|
|
|
self._cur = 0
|
|
|
|
|
2017-02-12 21:27:53 +00:00
|
|
|
# Unregister callback if we were in a delay or wait but turn on is
|
|
|
|
# called again. In that case we just continue execution.
|
2016-10-01 06:26:01 +00:00
|
|
|
self._async_remove_listener()
|
|
|
|
|
2017-02-12 21:27:53 +00:00
|
|
|
for cur, action in islice(enumerate(self.sequence), self._cur, None):
|
2016-10-01 06:26:01 +00:00
|
|
|
|
|
|
|
if CONF_DELAY in action:
|
|
|
|
# Call ourselves in the future to continue work
|
2017-02-12 21:27:53 +00:00
|
|
|
unsub = None
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def async_script_delay(now):
|
2017-05-02 16:18:47 +00:00
|
|
|
"""Handle delay."""
|
2017-02-12 21:27:53 +00:00
|
|
|
# pylint: disable=cell-var-from-loop
|
|
|
|
self._async_listener.remove(unsub)
|
2016-11-06 17:26:40 +00:00
|
|
|
self.hass.async_add_job(self.async_run(variables))
|
2016-10-01 06:26:01 +00:00
|
|
|
|
|
|
|
delay = action[CONF_DELAY]
|
|
|
|
|
|
|
|
if isinstance(delay, template.Template):
|
|
|
|
delay = vol.All(
|
|
|
|
cv.time_period,
|
|
|
|
cv.positive_timedelta)(
|
2016-10-16 23:08:12 +00:00
|
|
|
delay.async_render(variables))
|
2016-10-01 06:26:01 +00:00
|
|
|
|
2017-02-12 21:27:53 +00:00
|
|
|
unsub = async_track_point_in_utc_time(
|
|
|
|
self.hass, async_script_delay,
|
|
|
|
date_util.utcnow() + delay
|
|
|
|
)
|
|
|
|
self._async_listener.append(unsub)
|
|
|
|
|
|
|
|
self._cur = cur + 1
|
|
|
|
if self._change_listener:
|
|
|
|
self.hass.async_add_job(self._change_listener)
|
|
|
|
return
|
|
|
|
|
|
|
|
elif CONF_WAIT_TEMPLATE in action:
|
|
|
|
# Call ourselves in the future to continue work
|
|
|
|
wait_template = action[CONF_WAIT_TEMPLATE]
|
|
|
|
wait_template.hass = self.hass
|
|
|
|
|
2017-09-23 15:15:46 +00:00
|
|
|
# check if condition already okay
|
2017-02-12 21:27:53 +00:00
|
|
|
if condition.async_template(
|
|
|
|
self.hass, wait_template, variables):
|
|
|
|
continue
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def async_script_wait(entity_id, from_s, to_s):
|
2017-05-02 16:18:47 +00:00
|
|
|
"""Handle script after template condition is true."""
|
2017-02-12 21:27:53 +00:00
|
|
|
self._async_remove_listener()
|
|
|
|
self.hass.async_add_job(self.async_run(variables))
|
|
|
|
|
|
|
|
self._async_listener.append(async_track_template(
|
2017-10-12 14:57:18 +00:00
|
|
|
self.hass, wait_template, async_script_wait, variables))
|
2017-02-12 21:27:53 +00:00
|
|
|
|
2016-10-01 06:26:01 +00:00
|
|
|
self._cur = cur + 1
|
2016-10-04 05:39:27 +00:00
|
|
|
if self._change_listener:
|
|
|
|
self.hass.async_add_job(self._change_listener)
|
2017-02-12 21:27:53 +00:00
|
|
|
|
|
|
|
if CONF_TIMEOUT in action:
|
|
|
|
self._async_set_timeout(action, variables)
|
|
|
|
|
2016-10-01 06:26:01 +00:00
|
|
|
return
|
2016-04-21 22:52:20 +00:00
|
|
|
|
2016-10-01 06:26:01 +00:00
|
|
|
elif CONF_CONDITION in action:
|
|
|
|
if not self._async_check_condition(action, variables):
|
|
|
|
break
|
2016-04-28 10:03:57 +00:00
|
|
|
|
2016-10-01 06:26:01 +00:00
|
|
|
elif CONF_EVENT in action:
|
2018-01-19 06:13:14 +00:00
|
|
|
self._async_fire_event(action, variables)
|
2016-04-21 22:52:20 +00:00
|
|
|
|
2016-10-01 06:26:01 +00:00
|
|
|
else:
|
2018-02-25 11:38:46 +00:00
|
|
|
await self._async_call_service(action, variables)
|
2016-04-23 05:11:21 +00:00
|
|
|
|
2016-10-01 06:26:01 +00:00
|
|
|
self._cur = -1
|
|
|
|
self.last_action = None
|
2016-10-04 05:39:27 +00:00
|
|
|
if self._change_listener:
|
|
|
|
self.hass.async_add_job(self._change_listener)
|
2016-04-21 22:52:20 +00:00
|
|
|
|
2016-08-07 23:26:35 +00:00
|
|
|
def stop(self) -> None:
|
2016-04-21 22:52:20 +00:00
|
|
|
"""Stop running script."""
|
2016-10-01 06:26:01 +00:00
|
|
|
run_callback_threadsafe(self.hass.loop, self.async_stop).result()
|
2016-04-21 22:52:20 +00:00
|
|
|
|
2016-10-01 06:26:01 +00:00
|
|
|
def async_stop(self) -> None:
|
|
|
|
"""Stop running script."""
|
|
|
|
if self._cur == -1:
|
|
|
|
return
|
2016-04-21 22:52:20 +00:00
|
|
|
|
2016-10-01 06:26:01 +00:00
|
|
|
self._cur = -1
|
|
|
|
self._async_remove_listener()
|
2016-10-04 05:39:27 +00:00
|
|
|
if self._change_listener:
|
|
|
|
self.hass.async_add_job(self._change_listener)
|
2016-10-01 06:26:01 +00:00
|
|
|
|
2018-02-25 11:38:46 +00:00
|
|
|
async def _async_call_service(self, action, variables):
|
2016-10-04 05:39:27 +00:00
|
|
|
"""Call the service specified in the action.
|
|
|
|
|
|
|
|
This method is a coroutine.
|
|
|
|
"""
|
2016-04-21 22:52:20 +00:00
|
|
|
self.last_action = action.get(CONF_ALIAS, 'call service')
|
2016-04-28 11:19:38 +00:00
|
|
|
self._log("Executing step %s" % self.last_action)
|
2018-02-25 11:38:46 +00:00
|
|
|
await service.async_call_from_config(
|
2016-10-01 06:26:01 +00:00
|
|
|
self.hass, action, True, variables, validate_config=False)
|
2016-04-21 22:52:20 +00:00
|
|
|
|
2018-01-19 06:13:14 +00:00
|
|
|
def _async_fire_event(self, action, variables):
|
2016-04-21 22:52:20 +00:00
|
|
|
"""Fire an event."""
|
|
|
|
self.last_action = action.get(CONF_ALIAS, action[CONF_EVENT])
|
2016-04-28 11:19:38 +00:00
|
|
|
self._log("Executing step %s" % self.last_action)
|
2018-01-19 06:13:14 +00:00
|
|
|
event_data = dict(action.get(CONF_EVENT_DATA, {}))
|
|
|
|
if CONF_EVENT_DATA_TEMPLATE in action:
|
|
|
|
try:
|
|
|
|
event_data.update(template.render_complex(
|
|
|
|
action[CONF_EVENT_DATA_TEMPLATE], variables))
|
|
|
|
except TemplateError as ex:
|
|
|
|
_LOGGER.error('Error rendering event data template: %s', ex)
|
|
|
|
|
2016-10-01 06:26:01 +00:00
|
|
|
self.hass.bus.async_fire(action[CONF_EVENT],
|
2018-01-19 06:13:14 +00:00
|
|
|
event_data)
|
2016-04-21 22:52:20 +00:00
|
|
|
|
2016-10-01 06:26:01 +00:00
|
|
|
def _async_check_condition(self, action, variables):
|
2016-04-28 10:03:57 +00:00
|
|
|
"""Test if condition is matching."""
|
2016-10-11 06:36:38 +00:00
|
|
|
config_cache_key = frozenset((k, str(v)) for k, v in action.items())
|
|
|
|
config = self._config_cache.get(config_cache_key)
|
|
|
|
if not config:
|
|
|
|
config = condition.async_from_config(action, False)
|
|
|
|
self._config_cache[config_cache_key] = config
|
|
|
|
|
2016-04-28 10:03:57 +00:00
|
|
|
self.last_action = action.get(CONF_ALIAS, action[CONF_CONDITION])
|
2016-10-11 06:36:38 +00:00
|
|
|
check = config(self.hass, variables)
|
2016-04-28 11:39:44 +00:00
|
|
|
self._log("Test condition {}: {}".format(self.last_action, check))
|
2016-04-28 10:03:57 +00:00
|
|
|
return check
|
|
|
|
|
2017-02-12 21:27:53 +00:00
|
|
|
def _async_set_timeout(self, action, variables):
|
|
|
|
"""Schedule a timeout to abort script."""
|
|
|
|
timeout = action[CONF_TIMEOUT]
|
|
|
|
unsub = None
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def async_script_timeout(now):
|
|
|
|
"""Call after timeout is retrieve stop script."""
|
|
|
|
self._async_listener.remove(unsub)
|
2018-01-29 22:37:19 +00:00
|
|
|
self._log("Timeout reached, abort script.")
|
2017-02-12 21:27:53 +00:00
|
|
|
self.async_stop()
|
|
|
|
|
|
|
|
unsub = async_track_point_in_utc_time(
|
|
|
|
self.hass, async_script_timeout,
|
|
|
|
date_util.utcnow() + timeout
|
|
|
|
)
|
|
|
|
self._async_listener.append(unsub)
|
|
|
|
|
2016-10-01 06:26:01 +00:00
|
|
|
def _async_remove_listener(self):
|
2016-04-21 22:52:20 +00:00
|
|
|
"""Remove point in time listener, if any."""
|
2017-02-12 21:27:53 +00:00
|
|
|
for unsub in self._async_listener:
|
|
|
|
unsub()
|
|
|
|
self._async_listener.clear()
|
2016-04-21 22:52:20 +00:00
|
|
|
|
2016-04-28 11:19:38 +00:00
|
|
|
def _log(self, msg):
|
2016-04-21 22:52:20 +00:00
|
|
|
"""Logger helper."""
|
|
|
|
if self.name is not None:
|
2016-04-28 11:19:38 +00:00
|
|
|
msg = "Script {}: {}".format(self.name, msg)
|
2016-04-21 22:52:20 +00:00
|
|
|
|
|
|
|
_LOGGER.info(msg)
|