core/homeassistant/helpers/script.py

180 lines
6.1 KiB
Python
Raw Normal View History

2016-04-21 22:52:20 +00:00
"""Helpers to execute scripts."""
2016-10-01 06:26:01 +00:00
import asyncio
2016-04-21 22:52:20 +00:00
import logging
from itertools import islice
from typing import Optional, Sequence
import voluptuous as vol
2016-08-09 03:42:25 +00:00
from homeassistant.core import HomeAssistant
2016-08-26 06:25:35 +00:00
from homeassistant.const import CONF_CONDITION
2016-08-09 03:42:25 +00:00
from homeassistant.helpers import (
service, condition, template, config_validation as cv)
2016-10-01 06:26:01 +00:00
from homeassistant.helpers.event import async_track_point_in_utc_time
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__)
CONF_ALIAS = "alias"
CONF_SERVICE = "service"
CONF_SERVICE_DATA = "data"
CONF_SEQUENCE = "sequence"
CONF_EVENT = "event"
CONF_EVENT_DATA = "event_data"
CONF_DELAY = "delay"
2016-08-09 03:42:25 +00:00
def call_from_config(hass: HomeAssistant, config: ConfigType,
variables: Optional[Sequence]=None) -> None:
2016-04-21 22:52:20 +00:00
"""Call a script based on a config entry."""
Script(hass, cv.SCRIPT_SCHEMA(config)).run(variables)
2016-04-21 22:52:20 +00:00
class Script():
"""Representation of a script."""
# pylint: disable=too-many-instance-attributes
2016-08-09 03:42:25 +00:00
def __init__(self, hass: HomeAssistant, sequence, name: str=None,
change_listener=None) -> None:
2016-04-21 22:52:20 +00:00
"""Initialize the script."""
self.hass = hass
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
self.can_cancel = any(CONF_DELAY in action for action
in self.sequence)
2016-10-01 06:26:01 +00:00
self._async_unsub_delay_listener = None
self._template_cache = {}
self._config_cache = {}
2016-04-21 22:52:20 +00:00
@property
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()
@asyncio.coroutine
def async_run(self, variables: Optional[Sequence]=None) -> None:
"""Run script.
This method is a coroutine.
2016-10-01 06:26:01 +00:00
"""
if self._cur == -1:
self._log('Running script')
self._cur = 0
# Unregister callback if we were in a delay but turn on is called
# again. In that case we just continue execution.
self._async_remove_listener()
for cur, action in islice(enumerate(self.sequence), self._cur,
None):
if CONF_DELAY in action:
# Call ourselves in the future to continue work
@asyncio.coroutine
def script_delay(now):
"""Called after delay is done."""
self._async_unsub_delay_listener = None
self.hass.loop.create_task(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)(
delay.async_render())
self._async_unsub_delay_listener = \
async_track_point_in_utc_time(
2016-04-21 22:52:20 +00:00
self.hass, script_delay,
date_util.utcnow() + delay)
2016-10-01 06:26:01 +00:00
self._cur = cur + 1
if self._change_listener:
self.hass.async_add_job(self._change_listener)
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-10-01 06:26:01 +00:00
elif CONF_EVENT in action:
self._async_fire_event(action)
2016-04-21 22:52:20 +00:00
2016-10-01 06:26:01 +00:00
else:
yield from 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
if self._change_listener:
self.hass.async_add_job(self._change_listener)
2016-04-21 22:52:20 +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()
if self._change_listener:
self.hass.async_add_job(self._change_listener)
2016-10-01 06:26:01 +00:00
@asyncio.coroutine
def _async_call_service(self, action, variables):
"""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')
self._log("Executing step %s" % self.last_action)
2016-10-01 06:26:01 +00:00
yield from service.async_call_from_config(
self.hass, action, True, variables, validate_config=False)
2016-04-21 22:52:20 +00:00
2016-10-01 06:26:01 +00:00
def _async_fire_event(self, action):
2016-04-21 22:52:20 +00:00
"""Fire an event."""
self.last_action = action.get(CONF_ALIAS, action[CONF_EVENT])
self._log("Executing step %s" % self.last_action)
2016-10-01 06:26:01 +00:00
self.hass.bus.async_fire(action[CONF_EVENT],
action.get(CONF_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):
"""Test if condition is matching."""
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
self.last_action = action.get(CONF_ALIAS, action[CONF_CONDITION])
check = config(self.hass, variables)
2016-04-28 11:39:44 +00:00
self._log("Test condition {}: {}".format(self.last_action, check))
return check
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."""
2016-10-01 06:26:01 +00:00
if self._async_unsub_delay_listener:
self._async_unsub_delay_listener()
self._async_unsub_delay_listener = None
2016-04-21 22:52:20 +00:00
def _log(self, msg):
2016-04-21 22:52:20 +00:00
"""Logger helper."""
if self.name is not None:
msg = "Script {}: {}".format(self.name, msg)
2016-04-21 22:52:20 +00:00
_LOGGER.info(msg)