Adds script component.

A script is composed of a sequence of actions (currently service calls)
that are executed in order.  Individual actions can also be delayed by a
given timedelta.
This commit is contained in:
andythigpen 2015-03-07 21:14:21 -06:00
parent 948a5c97ec
commit 046efe3acb
10 changed files with 223 additions and 7 deletions

View file

@ -121,3 +121,21 @@ sensor:
- type: 'processor_use'
- type: 'process'
arg: 'octave-cli'
script:
# Turns on the bedroom lights and then the living room lights 1 minute later
wakeup:
alias: Wake Up
sequence:
# alias is optional
- alias: Bedroom lights on
execute_service: light.turn_on
service_data:
entity_id: group.bedroom
- delay:
# supports seconds, milliseconds, minutes, hours, etc.
minutes: 1
- alias: Living room lights on
execute_service: light.turn_on
service_data:
entity_id: group.living_room

View file

@ -115,6 +115,7 @@ class HomeAssistant(object):
action(now)
self.bus.listen(EVENT_TIME_CHANGED, point_in_time_listener)
return point_in_time_listener
# pylint: disable=too-many-arguments
def track_time_change(self, action,
@ -154,6 +155,7 @@ class HomeAssistant(object):
action(event.data[ATTR_NOW])
self.bus.listen(EVENT_TIME_CHANGED, time_listener)
return time_listener
def stop(self):
""" Stops Home Assistant and shuts down all threads. """

View file

@ -93,6 +93,13 @@
</paper-item>
</template>
<template if="{{hasScriptComponent}}">
<paper-item data-panel="script">
<core-icon icon="description"></core-icon>
Scripts
</paper-item>
</template>
<div flex></div>
<paper-item on-click="{{handleLogOutClick}}">
@ -124,10 +131,10 @@
This is the main partial, never remove it from the DOM but hide it
to speed up when people click on states.
-->
<partial-states hidden?="{{selected != 'states' && selected != 'group'}}"
<partial-states hidden?="{{hideStates}}"
main narrow="{{narrow}}"
togglePanel="{{togglePanel}}"
filter="{{selected == 'group' ? 'group' : null}}">
filter="{{selected}}">
</partial-states>
<template if="{{selected == 'history'}}">
@ -153,10 +160,13 @@ Polymer(Polymer.mixin({
selected: "states",
narrow: false,
hasHistoryComponent: false,
hasScriptComponent: false,
isStreaming: false,
hasStreamError: false,
hideStates: false,
attached: function() {
this.togglePanel = this.togglePanel.bind(this);
@ -169,6 +179,7 @@ Polymer(Polymer.mixin({
componentStoreChanged: function(componentStore) {
this.hasHistoryComponent = componentStore.isLoaded('history');
this.hasScriptComponent = componentStore.isLoaded('script');
},
streamStoreChanged: function(streamStore) {
@ -194,6 +205,15 @@ Polymer(Polymer.mixin({
this.togglePanel();
this.selected = newChoice;
}
switch(this.selected) {
case 'states':
case 'group':
case 'script':
hideStates = false;
break;
default:
hideStates = true;
}
},
responsiveChanged: function(ev, detail, sender) {

View file

@ -45,7 +45,7 @@
<div class='sidebar'>
<b>Available services:</b>
<services-list cbServiceClicked={{serviceSelected}}></event-list>
<services-list cbServiceClicked={{serviceSelected}}></services-list>
</div>
</div>
</div>

View file

@ -70,6 +70,10 @@
var stateStore = window.hass.stateStore;
var stateGroupFilter = function(state) { return state.domain === 'group'; };
var stateScriptFilter = function(state) { return state.domain === 'script'; };
var stateFilter = function(state) {
return !stateGroupFilter(state) && !stateScriptFilter(state);
};
Polymer(Polymer.mixin({
headerTitle: "States",
@ -130,6 +134,10 @@
this.headerTitle = "Groups";
break;
case "script":
this.headerTitle = "Scripts";
break;
default:
this.headerTitle = "States";
break;
@ -139,10 +147,12 @@
refreshStates: function() {
var states = stateStore.all;
if (this.filter === 'group') {
if (this.filter == 'group') {
states = states.filter(stateGroupFilter);
} else if (this.filter == 'script') {
states = states.filter(stateScriptFilter);
} else {
states = states.filterNot(stateGroupFilter);
states = states.filter(stateFilter);
}
this.states = states.toArray();

View file

@ -6,6 +6,7 @@
<link rel="import" href="more-info-sun.html">
<link rel="import" href="more-info-configurator.html">
<link rel="import" href="more-info-thermostat.html">
<link rel="import" href="more-info-script.html">
<polymer-element name="more-info-content" attributes="stateObj">
<template>

View file

@ -0,0 +1,22 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/core-style/core-style.html">
<polymer-element name="more-info-script" attributes="stateObj" noscript>
<template>
<core-style ref='ha-key-value-table'></core-style>
<style>
.data-entry .value {
max-width: 200px;
}
</style>
<div layout vertical>
<div layout justified horizontal class='data-entry'>
<div class='key'>Last Action</div>
<div class='value'>
{{stateObj.attributes.last_action}}
</div>
</div>
</div>
</template>
</polymer-element>

View file

@ -81,6 +81,9 @@ window.hass.uiUtil.domainIcon = function(domain, state) {
case "conversation":
return "av:hearing";
case "script":
return "description";
default:
return "bookmark-outline";
}

View file

@ -4,7 +4,7 @@
(function() {
var DOMAINS_WITH_CARD = ['thermostat', 'configurator'];
var DOMAINS_WITH_MORE_INFO = [
'light', 'group', 'sun', 'configurator', 'thermostat'
'light', 'group', 'sun', 'configurator', 'thermostat', 'script'
];
// Register some polymer filters

View file

@ -0,0 +1,140 @@
"""
homeassistant.components.script
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Scripts are a sequence of actions that can be triggered manually
by the user or automatically based upon automation events, etc.
"""
import logging
from datetime import datetime, timedelta
import threading
from homeassistant.util import split_entity_id
from homeassistant.const import (
STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF, EVENT_TIME_CHANGED)
DOMAIN = "script"
DEPENDENCIES = ["group"]
CONF_ALIAS = "alias"
CONF_SERVICE = "execute_service"
CONF_SERVICE_DATA = "service_data"
CONF_SEQUENCE = "sequence"
CONF_DELAY = "delay"
_LOGGER = logging.getLogger(__name__)
def setup(hass, config):
""" Load the scripts from the configuration. """
scripts = []
for name, cfg in config[DOMAIN].items():
if CONF_SEQUENCE not in cfg:
_LOGGER.warn("Missing key 'sequence' for script %s", name)
continue
alias = cfg.get(CONF_ALIAS, name)
entity_id = "{}.{}".format(DOMAIN, name)
script = Script(hass, entity_id, alias, cfg[CONF_SEQUENCE])
hass.services.register(DOMAIN, name, script)
scripts.append(script)
def turn_on(service):
""" Calls a script. """
for entity_id in service.data['entity_id']:
domain, service = split_entity_id(entity_id)
hass.services.call(domain, service, {})
def turn_off(service):
""" Cancels a script. """
for entity_id in service.data['entity_id']:
for script in scripts:
if script.entity_id == entity_id:
script.cancel()
hass.services.register(DOMAIN, SERVICE_TURN_ON, turn_on)
hass.services.register(DOMAIN, SERVICE_TURN_OFF, turn_off)
return True
class Script(object):
# pylint: disable=attribute-defined-outside-init
# pylint: disable=too-many-instance-attributes
# pylint: disable=too-few-public-methods
"""
A script contains a sequence of service calls or configured delays
that are executed in order.
Each script also has a state (on/off) indicating whether the script is
running or not.
"""
def __init__(self, hass, entity_id, alias, sequence):
self.hass = hass
self.alias = alias
self.sequence = sequence
self.entity_id = entity_id
self._lock = threading.Lock()
self._reset()
def cancel(self):
""" Cancels a running script and resets the state back to off. """
_LOGGER.info("Cancelled script %s", self.alias)
with self._lock:
if self.listener:
self.hass.bus.remove_listener(EVENT_TIME_CHANGED,
self.listener)
self.listener = None
self._reset()
def _reset(self):
""" Resets a script back to default state so that it is ready to
run from the start again. """
self.actions = None
self.listener = None
self.last_action = "Not Running"
self.hass.states.set(self.entity_id, STATE_OFF, {
"friendly_name": self.alias,
"last_action": self.last_action
})
def _execute_until_done(self):
""" Executes a sequence of actions until finished or until a delay
is encountered. If a delay action is encountered, the script
registers itself to be called again in the future, when
_execute_until_done will resume.
Returns True if finished, False otherwise. """
for action in self.actions:
if CONF_SERVICE in action:
self._call_service(action)
elif CONF_DELAY in action:
delay = timedelta(**action[CONF_DELAY])
point_in_time = datetime.now() + delay
self.listener = self.hass.track_point_in_time(
self, point_in_time)
return False
return True
def __call__(self, *args, **kwargs):
""" Executes the script. """
_LOGGER.info("Executing script %s", self.alias)
with self._lock:
if self.actions is None:
self.actions = (action for action in self.sequence)
if not self._execute_until_done():
state = self.hass.states.get(self.entity_id)
state.attributes['last_action'] = self.last_action
self.hass.states.set(self.entity_id, STATE_ON,
state.attributes)
else:
self._reset()
def _call_service(self, action):
""" Calls the service specified in the action. """
self.last_action = action.get(CONF_ALIAS, action[CONF_SERVICE])
_LOGGER.info("Executing script %s step %s", self.alias,
self.last_action)
domain, service = split_entity_id(action[CONF_SERVICE])
data = action.get(CONF_SERVICE_DATA, {})
self.hass.services.call(domain, service, data)