core/homeassistant/components/script.py

224 lines
7.4 KiB
Python
Raw Normal View History

"""
homeassistant.components.script
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Scripts are a sequence of actions that can be triggered manually
by the user or automatically based upon automation events, etc.
2015-10-25 14:14:56 +00:00
For more details about this component, please refer to the documentation at
2015-11-09 12:12:18 +00:00
https://home-assistant.io/components/script/
"""
import logging
2015-05-14 23:49:17 +00:00
from datetime import timedelta
2015-10-15 06:09:52 +00:00
from itertools import islice
import threading
2015-10-15 06:09:52 +00:00
from homeassistant.helpers.entity_component import EntityComponent
2016-01-24 06:49:49 +00:00
from homeassistant.helpers.entity import ToggleEntity, split_entity_id
2015-10-15 06:09:52 +00:00
from homeassistant.helpers.event import track_point_in_utc_time
2016-01-22 06:47:25 +00:00
from homeassistant.helpers.service import call_from_config
2016-01-24 06:49:49 +00:00
from homeassistant.util import slugify
2015-11-29 21:49:05 +00:00
import homeassistant.util.dt as date_util
from homeassistant.const import (
2015-10-15 06:09:52 +00:00
ATTR_ENTITY_ID, EVENT_TIME_CHANGED, STATE_ON, SERVICE_TURN_ON,
SERVICE_TURN_OFF)
DOMAIN = "script"
2015-10-15 06:09:52 +00:00
ENTITY_ID_FORMAT = DOMAIN + '.{}'
DEPENDENCIES = ["group"]
2015-10-15 06:09:52 +00:00
STATE_NOT_RUNNING = 'Not Running'
CONF_ALIAS = "alias"
2015-10-15 06:09:52 +00:00
CONF_SERVICE = "service"
CONF_SERVICE_OLD = "execute_service"
2016-01-22 06:47:25 +00:00
CONF_SERVICE_DATA = "data"
CONF_SERVICE_DATA_OLD = "service_data"
CONF_SEQUENCE = "sequence"
2015-09-14 06:54:48 +00:00
CONF_EVENT = "event"
CONF_EVENT_DATA = "event_data"
CONF_DELAY = "delay"
2015-10-15 06:09:52 +00:00
ATTR_LAST_ACTION = 'last_action'
2015-11-14 23:36:27 +00:00
ATTR_CAN_CANCEL = 'can_cancel'
_LOGGER = logging.getLogger(__name__)
2015-10-15 06:09:52 +00:00
def is_on(hass, entity_id):
""" Returns if the switch is on based on the statemachine. """
return hass.states.is_state(entity_id, STATE_ON)
def turn_on(hass, entity_id):
""" Turn script on. """
_, object_id = split_entity_id(entity_id)
hass.services.call(DOMAIN, object_id)
def turn_off(hass, entity_id):
""" Turn script on. """
hass.services.call(DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id})
def setup(hass, config):
""" Load the scripts from the configuration. """
2015-10-15 06:09:52 +00:00
component = EntityComponent(_LOGGER, DOMAIN, hass)
def service_handler(service):
""" Execute a service call to script.<script name>. """
entity_id = ENTITY_ID_FORMAT.format(service.service)
script = component.entities.get(entity_id)
if script:
script.turn_on()
2015-10-28 19:24:33 +00:00
for object_id, cfg in config[DOMAIN].items():
if object_id != slugify(object_id):
2015-11-29 21:49:05 +00:00
_LOGGER.warning("Found invalid key for script: %s. Use %s instead",
object_id, slugify(object_id))
2015-10-28 19:24:33 +00:00
continue
if not isinstance(cfg.get(CONF_SEQUENCE), list):
2015-11-29 21:49:05 +00:00
_LOGGER.warning("Key 'sequence' for script %s should be a list",
object_id)
continue
2015-10-28 19:24:33 +00:00
alias = cfg.get(CONF_ALIAS, object_id)
2016-01-15 07:19:08 +00:00
script = Script(object_id, alias, cfg[CONF_SEQUENCE])
2015-10-15 06:09:52 +00:00
component.add_entities((script,))
hass.services.register(DOMAIN, object_id, service_handler)
def turn_on_service(service):
""" Calls a service to turn script on. """
# We could turn on script directly here, but we only want to offer
# one way to do it. Otherwise no easy way to call invocations.
for script in component.extract_from_service(service):
turn_on(hass, script.entity_id)
def turn_off_service(service):
""" Cancels a script. """
2015-10-15 06:09:52 +00:00
for script in component.extract_from_service(service):
script.turn_off()
2015-10-15 06:09:52 +00:00
hass.services.register(DOMAIN, SERVICE_TURN_ON, turn_on_service)
hass.services.register(DOMAIN, SERVICE_TURN_OFF, turn_off_service)
return True
2015-10-15 06:09:52 +00:00
class Script(ToggleEntity):
""" Represents a script. """
2015-10-28 19:24:33 +00:00
# pylint: disable=too-many-instance-attributes
2016-01-15 07:19:08 +00:00
def __init__(self, object_id, name, sequence):
2015-10-28 19:24:33 +00:00
self.entity_id = ENTITY_ID_FORMAT.format(object_id)
2015-10-15 06:09:52 +00:00
self._name = name
self.sequence = sequence
self._lock = threading.Lock()
2015-10-15 06:09:52 +00:00
self._cur = -1
self._last_action = None
self._listener = None
2015-11-14 23:36:27 +00:00
self._can_cancel = not any(CONF_DELAY in action for action
in self.sequence)
2015-10-15 06:09:52 +00:00
@property
def should_poll(self):
return False
@property
def name(self):
""" Returns the name of the entity. """
return self._name
@property
def state_attributes(self):
""" Returns the state attributes. """
2015-11-14 23:36:27 +00:00
attrs = {
ATTR_CAN_CANCEL: self._can_cancel
}
2015-10-15 06:09:52 +00:00
if self._last_action:
attrs[ATTR_LAST_ACTION] = self._last_action
return attrs
@property
def is_on(self):
""" True if entity is on. """
return self._cur != -1
def turn_on(self, **kwargs):
""" Turn the entity on. """
_LOGGER.info("Executing script %s", self._name)
with self._lock:
2015-10-15 06:09:52 +00:00
if self._cur == -1:
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._remove_listener()
for cur, action in islice(enumerate(self.sequence), self._cur,
None):
if CONF_SERVICE in action or CONF_SERVICE_OLD in action:
self._call_service(action)
elif CONF_EVENT in action:
self._fire_event(action)
elif CONF_DELAY in action:
# Call ourselves in the future to continue work
def script_delay(now):
""" Called after delay is done. """
self._listener = None
self.turn_on()
delay = timedelta(**action[CONF_DELAY])
self._listener = track_point_in_utc_time(
self.hass, script_delay, date_util.utcnow() + delay)
self._cur = cur + 1
self.update_ha_state()
return
self._cur = -1
self._last_action = None
self.update_ha_state()
def turn_off(self, **kwargs):
""" Turn script off. """
_LOGGER.info("Cancelled script %s", self._name)
with self._lock:
2015-10-15 06:09:52 +00:00
if self._cur == -1:
return
2015-10-15 06:09:52 +00:00
self._cur = -1
self.update_ha_state()
self._remove_listener()
def _call_service(self, action):
""" Calls the service specified in the action. """
2016-01-22 06:47:25 +00:00
# Backwards compatibility
if CONF_SERVICE not in action and CONF_SERVICE_OLD in action:
action[CONF_SERVICE] = action[CONF_SERVICE_OLD]
if CONF_SERVICE_DATA not in action and CONF_SERVICE_DATA_OLD in action:
action[CONF_SERVICE_DATA] = action[CONF_SERVICE_DATA_OLD]
self._last_action = action.get(CONF_ALIAS, action[CONF_SERVICE])
2015-10-15 06:09:52 +00:00
_LOGGER.info("Executing script %s step %s", self._name,
self._last_action)
2016-01-22 06:47:25 +00:00
call_from_config(self.hass, action, True)
2015-09-14 06:54:48 +00:00
def _fire_event(self, action):
""" Fires an event. """
2015-10-15 06:09:52 +00:00
self._last_action = action.get(CONF_ALIAS, action[CONF_EVENT])
_LOGGER.info("Executing script %s step %s", self._name,
self._last_action)
2015-09-14 06:54:48 +00:00
self.hass.bus.fire(action[CONF_EVENT], action.get(CONF_EVENT_DATA))
2015-10-15 06:09:52 +00:00
def _remove_listener(self):
""" Remove point in time listener, if any. """
if self._listener:
self.hass.bus.remove_listener(EVENT_TIME_CHANGED,
self._listener)
self._listener = None