Automation: Add trigger context and expose to action

This commit is contained in:
Paulus Schoutsen 2016-04-21 13:59:42 -07:00
parent c4913a87e4
commit 4e568f8b99
20 changed files with 232 additions and 69 deletions

View file

@ -122,12 +122,11 @@ def _setup_automation(hass, config_block, name, config):
def _get_action(hass, config, name):
"""Return an action based on a configuration."""
def action():
def action(variables=None):
"""Action to be executed."""
_LOGGER.info('Executing %s', name)
logbook.log_entry(hass, name, 'has been triggered', DOMAIN)
call_from_config(hass, config)
call_from_config(hass, config, variables=variables)
return action
@ -159,24 +158,21 @@ def _process_if(hass, config, p_config, action):
checks.append(check)
if cond_type == CONDITION_TYPE_AND:
def if_action():
def if_action(variables=None):
"""AND all conditions."""
if all(check() for check in checks):
action()
if all(check(variables) for check in checks):
action(variables)
else:
def if_action():
def if_action(variables=None):
"""OR all conditions."""
if any(check() for check in checks):
action()
if any(check(variables) for check in checks):
action(variables)
return if_action
def _process_trigger(hass, config, trigger_configs, name, action):
"""Setup the triggers."""
if isinstance(trigger_configs, dict):
trigger_configs = [trigger_configs]
for conf in trigger_configs:
platform = _resolve_platform(METHOD_TRIGGER, hass, config,
conf.get(CONF_PLATFORM))

View file

@ -26,7 +26,12 @@ def trigger(hass, config, action):
"""Listen for events and calls the action when data matches."""
if not event_data or all(val == event.data.get(key) for key, val
in event_data.items()):
action()
action({
'trigger': {
'platform': 'event',
'event': event,
},
})
hass.bus.listen(event_type, handle_event)
return True

View file

@ -30,7 +30,14 @@ def trigger(hass, config, action):
def mqtt_automation_listener(msg_topic, msg_payload, qos):
"""Listen for MQTT messages."""
if payload is None or payload == msg_payload:
action()
action({
'trigger': {
'platform': 'mqtt',
'topic': msg_topic,
'payload': msg_payload,
'qos': qos,
}
})
mqtt.subscribe(hass, topic, mqtt_automation_listener)

View file

@ -18,12 +18,15 @@ CONF_ABOVE = "above"
_LOGGER = logging.getLogger(__name__)
def _renderer(hass, value_template, state):
def _renderer(hass, value_template, state, variables=None):
"""Render the state value."""
if value_template is None:
return state.state
return template.render(hass, value_template, {'state': state})
variables = dict(variables or {})
variables['state'] = state
return template.render(hass, value_template, variables)
def trigger(hass, config, action):
@ -50,9 +53,27 @@ def trigger(hass, config, action):
def state_automation_listener(entity, from_s, to_s):
"""Listen for state changes and calls action."""
# Fire action if we go from outside range into range
if _in_range(above, below, renderer(to_s)) and \
(from_s is None or not _in_range(above, below, renderer(from_s))):
action()
if to_s is None:
return
variables = {
'trigger': {
'platform': 'numeric_state',
'entity_id': entity_id,
'below': below,
'above': above,
}
}
to_s_value = renderer(to_s, variables)
from_s_value = None if from_s is None else renderer(from_s, variables)
if _in_range(above, below, to_s_value) and \
(from_s is None or not _in_range(above, below, from_s_value)):
variables['trigger']['from_state'] = from_s
variables['trigger']['from_value'] = from_s_value
variables['trigger']['to_state'] = to_s
variables['trigger']['to_value'] = to_s_value
action(variables)
track_state_change(
hass, entity_id, state_automation_listener)
@ -80,7 +101,7 @@ def if_action(hass, config):
renderer = partial(_renderer, hass, value_template)
def if_numeric_state():
def if_numeric_state(variables):
"""Test numeric state condition."""
state = hass.states.get(entity_id)
return state is not None and _in_range(above, below, renderer(state))

View file

@ -73,29 +73,42 @@ def trigger(hass, config, action):
def state_automation_listener(entity, from_s, to_s):
"""Listen for state changes and calls action."""
def call_action():
"""Call action with right context."""
action({
'trigger': {
'platform': 'state',
'entity_id': entity,
'from_state': from_s,
'to_state': to_s,
'for': time_delta,
}
})
if time_delta is None:
call_action()
return
def state_for_listener(now):
"""Fire on state changes after a delay and calls action."""
hass.bus.remove_listener(
EVENT_STATE_CHANGED, for_state_listener)
action()
EVENT_STATE_CHANGED, attached_state_for_cancel_listener)
call_action()
def state_for_cancel_listener(entity, inner_from_s, inner_to_s):
"""Fire on changes and cancel for listener if changed."""
if inner_to_s == to_s:
return
hass.bus.remove_listener(EVENT_TIME_CHANGED, for_time_listener)
hass.bus.remove_listener(
EVENT_STATE_CHANGED, for_state_listener)
hass.bus.remove_listener(EVENT_TIME_CHANGED,
attached_state_for_listener)
hass.bus.remove_listener(EVENT_STATE_CHANGED,
attached_state_for_cancel_listener)
if time_delta is not None:
target_tm = dt_util.utcnow() + time_delta
for_time_listener = track_point_in_time(
hass, state_for_listener, target_tm)
for_state_listener = track_state_change(
hass, entity_id, state_for_cancel_listener,
MATCH_ALL, MATCH_ALL)
else:
action()
attached_state_for_listener = track_point_in_time(
hass, state_for_listener, dt_util.utcnow() + time_delta)
attached_state_for_cancel_listener = track_state_change(
hass, entity_id, state_for_cancel_listener)
track_state_change(
hass, entity_id, state_automation_listener, from_state, to_state)
@ -109,7 +122,7 @@ def if_action(hass, config):
state = config.get(CONF_STATE)
time_delta = get_time_config(config)
def if_state():
def if_state(variables):
"""Test if condition."""
is_state = hass.states.is_state(entity_id, state)
return (time_delta is None and is_state or

View file

@ -55,11 +55,21 @@ def trigger(hass, config, action):
event = config.get(CONF_EVENT)
offset = config.get(CONF_OFFSET)
def call_action():
"""Call action with right context."""
action({
'trigger': {
'platform': 'sun',
'event': event,
'offset': offset,
},
})
# Do something to call action
if event == EVENT_SUNRISE:
track_sunrise(hass, action, offset)
track_sunrise(hass, call_action, offset)
else:
track_sunset(hass, action, offset)
track_sunset(hass, call_action, offset)
return True
@ -97,7 +107,7 @@ def if_action(hass, config):
"""Return time after sunset."""
return sun.next_setting(hass) + after_offset
def time_if():
def time_if(variables):
"""Validate time based if-condition."""
now = dt_util.now()
before = before_func()

View file

@ -9,9 +9,10 @@ import logging
import voluptuous as vol
from homeassistant.const import (
CONF_VALUE_TEMPLATE, EVENT_STATE_CHANGED, CONF_PLATFORM)
CONF_VALUE_TEMPLATE, CONF_PLATFORM, MATCH_ALL)
from homeassistant.exceptions import TemplateError
from homeassistant.helpers import template
from homeassistant.helpers.event import track_state_change
import homeassistant.helpers.config_validation as cv
@ -30,7 +31,7 @@ def trigger(hass, config, action):
# Local variable to keep track of if the action has already been triggered
already_triggered = False
def event_listener(event):
def state_changed_listener(entity_id, from_s, to_s):
"""Listen for state changes and calls action."""
nonlocal already_triggered
template_result = _check_template(hass, value_template)
@ -38,11 +39,18 @@ def trigger(hass, config, action):
# Check to see if template returns true
if template_result and not already_triggered:
already_triggered = True
action()
action({
'trigger': {
'platform': 'template',
'entity_id': entity_id,
'from_state': from_s,
'to_state': to_s,
},
})
elif not template_result:
already_triggered = False
hass.bus.listen(EVENT_STATE_CHANGED, event_listener)
track_state_change(hass, MATCH_ALL, state_changed_listener)
return True
@ -50,13 +58,14 @@ def if_action(hass, config):
"""Wrap action method with state based condition."""
value_template = config.get(CONF_VALUE_TEMPLATE)
return lambda: _check_template(hass, value_template)
return lambda variables: _check_template(hass, value_template,
variables=variables)
def _check_template(hass, value_template):
def _check_template(hass, value_template, variables=None):
"""Check if result of template is true."""
try:
value = template.render(hass, value_template, {})
value = template.render(hass, value_template, variables)
except TemplateError as ex:
if ex.args and ex.args[0].startswith(
"UndefinedError: 'None' has no attribute"):

View file

@ -41,7 +41,12 @@ def trigger(hass, config, action):
def time_automation_listener(now):
"""Listen for time changes and calls action."""
action()
action({
'trigger': {
'platform': 'time',
'now': now,
},
})
track_time_change(hass, time_automation_listener,
hour=hours, minute=minutes, second=seconds)
@ -73,7 +78,7 @@ def if_action(hass, config):
_error_time(after, CONF_AFTER)
return None
def time_if():
def time_if(variables):
"""Validate time based if-condition."""
now = dt_util.now()
if before is not None and now > now.replace(hour=before.hour,

View file

@ -48,13 +48,22 @@ def trigger(hass, config, action):
to_s.attributes.get(ATTR_LONGITUDE)):
return
from_match = _in_zone(hass, zone_entity_id, from_s) if from_s else None
to_match = _in_zone(hass, zone_entity_id, to_s)
zone_state = hass.states.get(zone_entity_id)
from_match = _in_zone(hass, zone_state, from_s) if from_s else None
to_match = _in_zone(hass, zone_state, to_s)
# pylint: disable=too-many-boolean-expressions
if event == EVENT_ENTER and not from_match and to_match or \
event == EVENT_LEAVE and from_match and not to_match:
action()
action({
'trigger': {
'platform': 'zone',
'entity_id': entity,
'from_state': from_s,
'to_state': to_s,
'zone': zone_state,
},
})
track_state_change(
hass, entity_id, zone_automation_listener, MATCH_ALL, MATCH_ALL)
@ -67,20 +76,20 @@ def if_action(hass, config):
entity_id = config.get(CONF_ENTITY_ID)
zone_entity_id = config.get(CONF_ZONE)
def if_in_zone():
def if_in_zone(variables):
"""Test if condition."""
return _in_zone(hass, zone_entity_id, hass.states.get(entity_id))
zone_state = hass.states.get(zone_entity_id)
return _in_zone(hass, zone_state, hass.states.get(entity_id))
return if_in_zone
def _in_zone(hass, zone_entity_id, state):
def _in_zone(hass, zone_state, state):
"""Check if state is in zone."""
if not state or None in (state.attributes.get(ATTR_LATITUDE),
state.attributes.get(ATTR_LONGITUDE)):
return False
zone_state = hass.states.get(zone_entity_id)
return zone_state and zone.in_zone(
zone_state, state.attributes.get(ATTR_LATITUDE),
state.attributes.get(ATTR_LONGITUDE),