Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Trigger variables in automation actions #1882

Merged
merged 11 commits into from Apr 23, 2016
14 changes: 10 additions & 4 deletions homeassistant/components/alexa.py
Expand Up @@ -8,8 +8,7 @@
import logging

from homeassistant.const import HTTP_OK, HTTP_UNPROCESSABLE_ENTITY
from homeassistant.helpers.service import call_from_config
from homeassistant.helpers import template
from homeassistant.helpers import template, script

DOMAIN = 'alexa'
DEPENDENCIES = ['http']
Expand All @@ -27,7 +26,14 @@

def setup(hass, config):
"""Activate Alexa component."""
_CONFIG.update(config[DOMAIN].get(CONF_INTENTS, {}))
intents = config[DOMAIN].get(CONF_INTENTS, {})

for name, intent in intents.items():
if CONF_ACTION in intent:
intent[CONF_ACTION] = script.Script(hass, intent[CONF_ACTION],
"Alexa intent {}".format(name))

_CONFIG.update(intents)

hass.http.register_path('POST', API_ENDPOINT, _handle_alexa, True)

Expand Down Expand Up @@ -91,7 +97,7 @@ def _handle_alexa(handler, path_match, data):
card['content'])

if action is not None:
call_from_config(handler.server.hass, action, True)
action.run(response.variables)

handler.write_json(response.as_dict())

Expand Down
27 changes: 12 additions & 15 deletions homeassistant/components/automation/__init__.py
Expand Up @@ -11,8 +11,7 @@
from homeassistant.bootstrap import prepare_setup_platform
from homeassistant.const import CONF_PLATFORM
from homeassistant.components import logbook
from homeassistant.helpers import extract_domain_configs
from homeassistant.helpers.service import call_from_config
from homeassistant.helpers import extract_domain_configs, script
from homeassistant.loader import get_platform
import homeassistant.helpers.config_validation as cv

Expand Down Expand Up @@ -88,7 +87,7 @@ def validator(config):
vol.Required(CONF_CONDITION_TYPE, default=DEFAULT_CONDITION_TYPE):
vol.All(vol.Lower, vol.Any(CONDITION_TYPE_AND, CONDITION_TYPE_OR)),
CONF_CONDITION: _CONDITION_SCHEMA,
vol.Required(CONF_ACTION): cv.SERVICE_SCHEMA,
vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA,
})


Expand Down Expand Up @@ -122,12 +121,13 @@ def _setup_automation(hass, config_block, name, config):

def _get_action(hass, config, name):
"""Return an action based on a configuration."""
def action():
script_obj = script.Script(hass, config, name)

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)
script_obj.run(variables)

return action

Expand Down Expand Up @@ -159,24 +159,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))
Expand Down
7 changes: 6 additions & 1 deletion homeassistant/components/automation/event.py
Expand Up @@ -26,7 +26,12 @@ def handle_event(event):
"""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
9 changes: 8 additions & 1 deletion homeassistant/components/automation/mqtt.py
Expand Up @@ -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)

Expand Down
33 changes: 27 additions & 6 deletions homeassistant/components/automation/numeric_state.py
Expand Up @@ -18,12 +18,15 @@
_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):
Expand All @@ -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)
Expand Down Expand Up @@ -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))
Expand Down
45 changes: 29 additions & 16 deletions homeassistant/components/automation/state.py
Expand Up @@ -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)
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)

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()
hass.bus.remove_listener(EVENT_TIME_CHANGED,
attached_state_for_listener)
hass.bus.remove_listener(EVENT_STATE_CHANGED,
attached_state_for_cancel)

attached_state_for_listener = track_point_in_time(
hass, state_for_listener, dt_util.utcnow() + time_delta)

attached_state_for_cancel = track_state_change(
hass, entity_id, state_for_cancel_listener)

track_state_change(
hass, entity_id, state_automation_listener, from_state, to_state)
Expand All @@ -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
Expand Down
22 changes: 16 additions & 6 deletions homeassistant/components/automation/sun.py
Expand Up @@ -35,16 +35,16 @@
TRIGGER_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'sun',
vol.Required(CONF_EVENT): _SUN_EVENT,
vol.Required(CONF_OFFSET, default=timedelta(0)): cv.time_offset,
vol.Required(CONF_OFFSET, default=timedelta(0)): cv.time_period,
})

IF_ACTION_SCHEMA = vol.All(
vol.Schema({
vol.Required(CONF_PLATFORM): 'sun',
CONF_BEFORE: _SUN_EVENT,
CONF_AFTER: _SUN_EVENT,
vol.Required(CONF_BEFORE_OFFSET, default=timedelta(0)): cv.time_offset,
vol.Required(CONF_AFTER_OFFSET, default=timedelta(0)): cv.time_offset,
vol.Required(CONF_BEFORE_OFFSET, default=timedelta(0)): cv.time_period,
vol.Required(CONF_AFTER_OFFSET, default=timedelta(0)): cv.time_period,
}),
cv.has_at_least_one_key(CONF_BEFORE, CONF_AFTER),
)
Expand All @@ -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

Expand Down Expand Up @@ -97,7 +107,7 @@ def after_func():
"""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()
Expand Down
23 changes: 16 additions & 7 deletions homeassistant/components/automation/template.py
Expand Up @@ -9,9 +9,10 @@
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


Expand All @@ -30,33 +31,41 @@ 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)

# 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


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"):
Expand Down
9 changes: 7 additions & 2 deletions homeassistant/components/automation/time.py
Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down