diff --git a/homeassistant/components/automation/template.py b/homeassistant/components/automation/template.py new file mode 100644 index 00000000000..8615538c42a --- /dev/null +++ b/homeassistant/components/automation/template.py @@ -0,0 +1,65 @@ +""" +homeassistant.components.automation.template +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Offers template automation rules. + +For more details about this automation rule, please refer to the documentation +at https://home-assistant.io/components/automation/#template-trigger +""" +import logging + +from homeassistant.const import CONF_VALUE_TEMPLATE, EVENT_STATE_CHANGED +from homeassistant.exceptions import TemplateError +from homeassistant.util import template + +_LOGGER = logging.getLogger(__name__) + + +def trigger(hass, config, action): + """ Listen for state changes based on `config`. """ + value_template = config.get(CONF_VALUE_TEMPLATE) + + if value_template is None: + _LOGGER.error("Missing configuration key %s", CONF_VALUE_TEMPLATE) + return False + + # Local variable to keep track of if the action has already been triggered + already_triggered = False + + def event_listener(event): + """ Listens 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() + elif not template_result: + already_triggered = False + + hass.bus.listen(EVENT_STATE_CHANGED, event_listener) + return True + + +def if_action(hass, config): + """ Wraps action method with state based condition. """ + + value_template = config.get(CONF_VALUE_TEMPLATE) + + if value_template is None: + _LOGGER.error("Missing configuration key %s", CONF_VALUE_TEMPLATE) + return False + + return lambda: _check_template(hass, value_template) + + +def _check_template(hass, value_template): + """ Checks if result of template is true """ + try: + value = template.render(hass, value_template, {}) + except TemplateError: + _LOGGER.exception('Error parsing template') + return False + + return value.lower() == 'true' diff --git a/tests/components/automation/test_template.py b/tests/components/automation/test_template.py new file mode 100644 index 00000000000..0c63d03d6c3 --- /dev/null +++ b/tests/components/automation/test_template.py @@ -0,0 +1,355 @@ +""" +tests.components.automation.test_template +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests template automation. +""" +import unittest + +import homeassistant.core as ha +import homeassistant.components.automation as automation + + +class TestAutomationTemplate(unittest.TestCase): + """ Test the event automation. """ + + def setUp(self): # pylint: disable=invalid-name + self.hass = ha.HomeAssistant() + self.hass.states.set('test.entity', 'hello') + self.calls = [] + + def record_call(service): + self.calls.append(service) + + self.hass.services.register('test', 'automation', record_call) + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def test_if_fires_on_change_bool(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'template', + 'value_template': '{{ true }}', + }, + 'action': { + 'service': 'test.automation' + } + } + })) + + self.hass.states.set('test.entity', 'world') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_fires_on_change_str(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'template', + 'value_template': 'true', + }, + 'action': { + 'service': 'test.automation' + } + } + })) + + self.hass.states.set('test.entity', 'world') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_fires_on_change_str_crazy(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'template', + 'value_template': 'TrUE', + }, + 'action': { + 'service': 'test.automation' + } + } + })) + + self.hass.states.set('test.entity', 'world') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_not_fires_on_change_bool(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'template', + 'value_template': '{{ false }}', + }, + 'action': { + 'service': 'test.automation' + } + } + })) + + self.hass.states.set('test.entity', 'world') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + def test_if_not_fires_on_change_str(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'template', + 'value_template': 'False', + }, + 'action': { + 'service': 'test.automation' + } + } + })) + + self.hass.states.set('test.entity', 'world') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + def test_if_not_fires_on_change_str_crazy(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'template', + 'value_template': 'Anything other than "true" is false.', + }, + 'action': { + 'service': 'test.automation' + } + } + })) + + self.hass.states.set('test.entity', 'world') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + def test_if_fires_on_no_change(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'template', + 'value_template': '{{ true }}', + }, + 'action': { + 'service': 'test.automation' + } + } + })) + + self.hass.states.set('test.entity', 'hello') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + def test_if_fires_on_two_change(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'template', + 'value_template': '{{ true }}', + }, + 'action': { + 'service': 'test.automation' + } + } + })) + + # Trigger once + self.hass.states.set('test.entity', 'world') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + # Trigger again + self.hass.states.set('test.entity', 'world') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_fires_on_change_with_template(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'template', + 'value_template': '{{ is_state("test.entity", "world") }}', + }, + 'action': { + 'service': 'test.automation' + } + } + })) + + self.hass.states.set('test.entity', 'world') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_not_fires_on_change_with_template(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'template', + 'value_template': '{{ is_state("test.entity", "hello") }}', + }, + 'action': { + 'service': 'test.automation' + } + } + })) + + self.hass.states.set('test.entity', 'world') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + def test_if_fires_on_change_with_template_advanced(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'template', + 'value_template': '''{%- if is_state("test.entity", "world") -%} + true + {%- else -%} + false + {%- endif -%}''', + }, + 'action': { + 'service': 'test.automation' + } + } + })) + + self.hass.states.set('test.entity', 'world') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_fires_on_no_change_with_template_advanced(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'template', + 'value_template': '''{%- if is_state("test.entity", "world") -%} + true + {%- else -%} + false + {%- endif -%}''', + }, + 'action': { + 'service': 'test.automation' + } + } + })) + + # Different state + self.hass.states.set('test.entity', 'worldz') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + # Different state + self.hass.states.set('test.entity', 'hello') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + def test_if_fires_on_change_with_template_2(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'template', + 'value_template': '{{ not is_state("test.entity", "world") }}', + }, + 'action': { + 'service': 'test.automation' + } + } + })) + + self.hass.states.set('test.entity', 'world') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + self.hass.states.set('test.entity', 'home') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + self.hass.states.set('test.entity', 'work') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + self.hass.states.set('test.entity', 'not_home') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + self.hass.states.set('test.entity', 'world') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + self.hass.states.set('test.entity', 'home') + self.hass.pool.block_till_done() + self.assertEqual(2, len(self.calls)) + + def test_if_action(self): + automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + }, + 'condition': [{ + 'platform': 'template', + 'value_template': '{{ is_state("test.entity", "world") }}' + }], + 'action': { + 'service': 'test.automation' + } + } + }) + + # Condition is not true yet + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + # Change condition to true, but it shouldn't be triggered yet + self.hass.states.set('test.entity', 'world') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + # Condition is true and event is triggered + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_fires_on_change_with_bad_template(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'template', + 'value_template': '{{ ', + }, + 'action': { + 'service': 'test.automation' + } + } + })) + + self.hass.states.set('test.entity', 'world') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + def test_if_fires_on_change_with_bad_template_2(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'template', + 'value_template': '{{ xyz | round(0) }}', + }, + 'action': { + 'service': 'test.automation' + } + } + })) + + self.hass.states.set('test.entity', 'world') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls))