From ab8ff42cdd10db7d1ec39c28e59f3808d1831b7e Mon Sep 17 00:00:00 2001 From: Philip Lundrigan Date: Wed, 16 Dec 2015 10:52:33 -0700 Subject: [PATCH 1/4] Create template automation --- .../components/automation/template.py | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 homeassistant/components/automation/template.py diff --git a/homeassistant/components/automation/template.py b/homeassistant/components/automation/template.py new file mode 100644 index 00000000000..1f594dad914 --- /dev/null +++ b/homeassistant/components/automation/template.py @@ -0,0 +1,63 @@ +""" +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 +from homeassistant.exceptions import TemplateError +from homeassistant.helpers.event import track_state_change +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 + + # Get all entity ids + all_entity_ids = hass.states.entity_ids() + + # pylint: disable=unused-argument + def state_automation_listener(entity, from_s, to_s): + """ Listens for state changes and calls action. """ + + # Check to see if template returns true + if _check_template(hass, value_template): + action() + + track_state_change(hass, all_entity_ids, state_automation_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' From fe2ae162108392605d55a7132864b5ad92713d54 Mon Sep 17 00:00:00 2001 From: Philip Lundrigan Date: Wed, 16 Dec 2015 14:27:43 -0700 Subject: [PATCH 2/4] Add tests for template automation --- tests/components/automation/test_template.py | 351 +++++++++++++++++++ 1 file changed, 351 insertions(+) create mode 100644 tests/components/automation/test_template.py diff --git a/tests/components/automation/test_template.py b/tests/components/automation/test_template.py new file mode 100644 index 00000000000..763b0194ded --- /dev/null +++ b/tests/components/automation/test_template.py @@ -0,0 +1,351 @@ +""" +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(2, len(self.calls)) + + self.hass.states.set('test.entity', 'world') + self.hass.pool.block_till_done() + self.assertEqual(2, len(self.calls)) + + self.hass.states.set('test.entity', 'home') + self.hass.pool.block_till_done() + self.assertEqual(3, 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)) From 4c33eba378eb11cd54c6896235becff90439abfc Mon Sep 17 00:00:00 2001 From: Philip Lundrigan Date: Wed, 16 Dec 2015 15:07:14 -0700 Subject: [PATCH 3/4] Prevent triggering twice --- homeassistant/components/automation/template.py | 11 +++++++++-- tests/components/automation/test_template.py | 10 +++++++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/automation/template.py b/homeassistant/components/automation/template.py index 1f594dad914..d2005ced8ea 100644 --- a/homeassistant/components/automation/template.py +++ b/homeassistant/components/automation/template.py @@ -27,13 +27,20 @@ def trigger(hass, config, action): # Get all entity ids all_entity_ids = hass.states.entity_ids() - # pylint: disable=unused-argument + # Local variable to keep track of if the action has already been triggered + already_triggered = False + def state_automation_listener(entity, from_s, to_s): """ 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 _check_template(hass, value_template): + if template_result and not already_triggered: + already_triggered = True action() + elif not template_result: + already_triggered = False track_state_change(hass, all_entity_ids, state_automation_listener) diff --git a/tests/components/automation/test_template.py b/tests/components/automation/test_template.py index 763b0194ded..0c63d03d6c3 100644 --- a/tests/components/automation/test_template.py +++ b/tests/components/automation/test_template.py @@ -274,15 +274,19 @@ class TestAutomationTemplate(unittest.TestCase): self.hass.states.set('test.entity', 'work') self.hass.pool.block_till_done() - self.assertEqual(2, len(self.calls)) + 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(2, len(self.calls)) + self.assertEqual(1, len(self.calls)) self.hass.states.set('test.entity', 'home') self.hass.pool.block_till_done() - self.assertEqual(3, len(self.calls)) + self.assertEqual(2, len(self.calls)) def test_if_action(self): automation.setup(self.hass, { From 56b38e64aeea12269b36d11849e0952377510c16 Mon Sep 17 00:00:00 2001 From: Philip Lundrigan Date: Wed, 16 Dec 2015 23:53:10 -0700 Subject: [PATCH 4/4] Change method of listening to state changes --- homeassistant/components/automation/template.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/automation/template.py b/homeassistant/components/automation/template.py index d2005ced8ea..8615538c42a 100644 --- a/homeassistant/components/automation/template.py +++ b/homeassistant/components/automation/template.py @@ -8,9 +8,8 @@ at https://home-assistant.io/components/automation/#template-trigger """ import logging -from homeassistant.const import CONF_VALUE_TEMPLATE +from homeassistant.const import CONF_VALUE_TEMPLATE, EVENT_STATE_CHANGED from homeassistant.exceptions import TemplateError -from homeassistant.helpers.event import track_state_change from homeassistant.util import template _LOGGER = logging.getLogger(__name__) @@ -24,13 +23,10 @@ def trigger(hass, config, action): _LOGGER.error("Missing configuration key %s", CONF_VALUE_TEMPLATE) return False - # Get all entity ids - all_entity_ids = hass.states.entity_ids() - # Local variable to keep track of if the action has already been triggered already_triggered = False - def state_automation_listener(entity, from_s, to_s): + def event_listener(event): """ Listens for state changes and calls action. """ nonlocal already_triggered template_result = _check_template(hass, value_template) @@ -42,8 +38,7 @@ def trigger(hass, config, action): elif not template_result: already_triggered = False - track_state_change(hass, all_entity_ids, state_automation_listener) - + hass.bus.listen(EVENT_STATE_CHANGED, event_listener) return True