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:
parent
948a5c97ec
commit
046efe3acb
10 changed files with 223 additions and 7 deletions
|
@ -121,3 +121,21 @@ sensor:
|
||||||
- type: 'processor_use'
|
- type: 'processor_use'
|
||||||
- type: 'process'
|
- type: 'process'
|
||||||
arg: 'octave-cli'
|
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
|
||||||
|
|
|
@ -115,6 +115,7 @@ class HomeAssistant(object):
|
||||||
action(now)
|
action(now)
|
||||||
|
|
||||||
self.bus.listen(EVENT_TIME_CHANGED, point_in_time_listener)
|
self.bus.listen(EVENT_TIME_CHANGED, point_in_time_listener)
|
||||||
|
return point_in_time_listener
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments
|
# pylint: disable=too-many-arguments
|
||||||
def track_time_change(self, action,
|
def track_time_change(self, action,
|
||||||
|
@ -154,6 +155,7 @@ class HomeAssistant(object):
|
||||||
action(event.data[ATTR_NOW])
|
action(event.data[ATTR_NOW])
|
||||||
|
|
||||||
self.bus.listen(EVENT_TIME_CHANGED, time_listener)
|
self.bus.listen(EVENT_TIME_CHANGED, time_listener)
|
||||||
|
return time_listener
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
""" Stops Home Assistant and shuts down all threads. """
|
""" Stops Home Assistant and shuts down all threads. """
|
||||||
|
|
|
@ -93,6 +93,13 @@
|
||||||
</paper-item>
|
</paper-item>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template if="{{hasScriptComponent}}">
|
||||||
|
<paper-item data-panel="script">
|
||||||
|
<core-icon icon="description"></core-icon>
|
||||||
|
Scripts
|
||||||
|
</paper-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div flex></div>
|
<div flex></div>
|
||||||
|
|
||||||
<paper-item on-click="{{handleLogOutClick}}">
|
<paper-item on-click="{{handleLogOutClick}}">
|
||||||
|
@ -124,10 +131,10 @@
|
||||||
This is the main partial, never remove it from the DOM but hide it
|
This is the main partial, never remove it from the DOM but hide it
|
||||||
to speed up when people click on states.
|
to speed up when people click on states.
|
||||||
-->
|
-->
|
||||||
<partial-states hidden?="{{selected != 'states' && selected != 'group'}}"
|
<partial-states hidden?="{{hideStates}}"
|
||||||
main narrow="{{narrow}}"
|
main narrow="{{narrow}}"
|
||||||
togglePanel="{{togglePanel}}"
|
togglePanel="{{togglePanel}}"
|
||||||
filter="{{selected == 'group' ? 'group' : null}}">
|
filter="{{selected}}">
|
||||||
</partial-states>
|
</partial-states>
|
||||||
|
|
||||||
<template if="{{selected == 'history'}}">
|
<template if="{{selected == 'history'}}">
|
||||||
|
@ -153,10 +160,13 @@ Polymer(Polymer.mixin({
|
||||||
selected: "states",
|
selected: "states",
|
||||||
narrow: false,
|
narrow: false,
|
||||||
hasHistoryComponent: false,
|
hasHistoryComponent: false,
|
||||||
|
hasScriptComponent: false,
|
||||||
|
|
||||||
isStreaming: false,
|
isStreaming: false,
|
||||||
hasStreamError: false,
|
hasStreamError: false,
|
||||||
|
|
||||||
|
hideStates: false,
|
||||||
|
|
||||||
attached: function() {
|
attached: function() {
|
||||||
this.togglePanel = this.togglePanel.bind(this);
|
this.togglePanel = this.togglePanel.bind(this);
|
||||||
|
|
||||||
|
@ -169,6 +179,7 @@ Polymer(Polymer.mixin({
|
||||||
|
|
||||||
componentStoreChanged: function(componentStore) {
|
componentStoreChanged: function(componentStore) {
|
||||||
this.hasHistoryComponent = componentStore.isLoaded('history');
|
this.hasHistoryComponent = componentStore.isLoaded('history');
|
||||||
|
this.hasScriptComponent = componentStore.isLoaded('script');
|
||||||
},
|
},
|
||||||
|
|
||||||
streamStoreChanged: function(streamStore) {
|
streamStoreChanged: function(streamStore) {
|
||||||
|
@ -194,6 +205,15 @@ Polymer(Polymer.mixin({
|
||||||
this.togglePanel();
|
this.togglePanel();
|
||||||
this.selected = newChoice;
|
this.selected = newChoice;
|
||||||
}
|
}
|
||||||
|
switch(this.selected) {
|
||||||
|
case 'states':
|
||||||
|
case 'group':
|
||||||
|
case 'script':
|
||||||
|
hideStates = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
hideStates = true;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
responsiveChanged: function(ev, detail, sender) {
|
responsiveChanged: function(ev, detail, sender) {
|
||||||
|
|
|
@ -45,7 +45,7 @@
|
||||||
|
|
||||||
<div class='sidebar'>
|
<div class='sidebar'>
|
||||||
<b>Available services:</b>
|
<b>Available services:</b>
|
||||||
<services-list cbServiceClicked={{serviceSelected}}></event-list>
|
<services-list cbServiceClicked={{serviceSelected}}></services-list>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -70,6 +70,10 @@
|
||||||
var stateStore = window.hass.stateStore;
|
var stateStore = window.hass.stateStore;
|
||||||
|
|
||||||
var stateGroupFilter = function(state) { return state.domain === 'group'; };
|
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({
|
Polymer(Polymer.mixin({
|
||||||
headerTitle: "States",
|
headerTitle: "States",
|
||||||
|
@ -130,6 +134,10 @@
|
||||||
this.headerTitle = "Groups";
|
this.headerTitle = "Groups";
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "script":
|
||||||
|
this.headerTitle = "Scripts";
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
this.headerTitle = "States";
|
this.headerTitle = "States";
|
||||||
break;
|
break;
|
||||||
|
@ -139,10 +147,12 @@
|
||||||
refreshStates: function() {
|
refreshStates: function() {
|
||||||
var states = stateStore.all;
|
var states = stateStore.all;
|
||||||
|
|
||||||
if (this.filter === 'group') {
|
if (this.filter == 'group') {
|
||||||
states = states.filter(stateGroupFilter);
|
states = states.filter(stateGroupFilter);
|
||||||
|
} else if (this.filter == 'script') {
|
||||||
|
states = states.filter(stateScriptFilter);
|
||||||
} else {
|
} else {
|
||||||
states = states.filterNot(stateGroupFilter);
|
states = states.filter(stateFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.states = states.toArray();
|
this.states = states.toArray();
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
<link rel="import" href="more-info-sun.html">
|
<link rel="import" href="more-info-sun.html">
|
||||||
<link rel="import" href="more-info-configurator.html">
|
<link rel="import" href="more-info-configurator.html">
|
||||||
<link rel="import" href="more-info-thermostat.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">
|
<polymer-element name="more-info-content" attributes="stateObj">
|
||||||
<template>
|
<template>
|
||||||
|
|
|
@ -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>
|
|
@ -81,6 +81,9 @@ window.hass.uiUtil.domainIcon = function(domain, state) {
|
||||||
case "conversation":
|
case "conversation":
|
||||||
return "av:hearing";
|
return "av:hearing";
|
||||||
|
|
||||||
|
case "script":
|
||||||
|
return "description";
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return "bookmark-outline";
|
return "bookmark-outline";
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
(function() {
|
(function() {
|
||||||
var DOMAINS_WITH_CARD = ['thermostat', 'configurator'];
|
var DOMAINS_WITH_CARD = ['thermostat', 'configurator'];
|
||||||
var DOMAINS_WITH_MORE_INFO = [
|
var DOMAINS_WITH_MORE_INFO = [
|
||||||
'light', 'group', 'sun', 'configurator', 'thermostat'
|
'light', 'group', 'sun', 'configurator', 'thermostat', 'script'
|
||||||
];
|
];
|
||||||
|
|
||||||
// Register some polymer filters
|
// Register some polymer filters
|
||||||
|
|
140
homeassistant/components/script.py
Normal file
140
homeassistant/components/script.py
Normal 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)
|
Loading…
Add table
Reference in a new issue