From a955f3db088acdd021782a892c315d412cb4e1f5 Mon Sep 17 00:00:00 2001 From: pavoni Date: Tue, 2 Feb 2016 19:25:17 +0000 Subject: [PATCH 1/7] WIP commit - template state working, on / off still to do. --- homeassistant/components/switch/template.py | 156 ++++++++++++++++ tests/components/switch/test_template.py | 188 ++++++++++++++++++++ 2 files changed, 344 insertions(+) create mode 100644 homeassistant/components/switch/template.py create mode 100644 tests/components/switch/test_template.py diff --git a/homeassistant/components/switch/template.py b/homeassistant/components/switch/template.py new file mode 100644 index 00000000000..170df067e57 --- /dev/null +++ b/homeassistant/components/switch/template.py @@ -0,0 +1,156 @@ +""" +homeassistant.components.switch.template +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Allows the creation of a switch that integrates other components together + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.template/ +""" +import logging + +from homeassistant.helpers.entity import generate_entity_id + +from homeassistant.components.switch import SwitchDevice + +from homeassistant.core import EVENT_STATE_CHANGED +from homeassistant.const import ( + ATTR_FRIENDLY_NAME, + CONF_VALUE_TEMPLATE) + +from homeassistant.util import template, slugify +from homeassistant.exceptions import TemplateError + +from homeassistant.components.switch import DOMAIN + +ENTITY_ID_FORMAT = DOMAIN + '.{}' + +_LOGGER = logging.getLogger(__name__) + +CONF_SWITCHES = 'switches' + +STATE_ERROR = 'error' + +ON_ACTION = 'turn_on' +OFF_ACTION = 'turn_off' + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up the switches. """ + + switches = [] + if config.get(CONF_SWITCHES) is None: + _LOGGER.error("Missing configuration data for switch platform") + return False + + for device, device_config in config[CONF_SWITCHES].items(): + + if device != slugify(device): + _LOGGER.error("Found invalid key for switch.template: %s. " + "Use %s instead", device, slugify(device)) + continue + + if not isinstance(device_config, dict): + _LOGGER.error("Missing configuration data for switch %s", device) + continue + + friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device) + state_template = device_config.get(CONF_VALUE_TEMPLATE) + on_action = device_config.get(ON_ACTION) + off_action = device_config.get(OFF_ACTION) + if state_template is None: + _LOGGER.error( + "Missing %s for switch %s", CONF_VALUE_TEMPLATE, device) + continue + + if on_action is None or off_action is None: + _LOGGER.error( + "Missing action for switch %s", device) + continue + + switches.append( + SwitchTemplate( + hass, + device, + friendly_name, + state_template, + on_action, + off_action) + ) + if not switches: + _LOGGER.error("No switches added") + return False + add_devices(switches) + return True + + +class SwitchTemplate(SwitchDevice): + """ Represents a Template Switch. """ + + # pylint: disable=too-many-arguments + def __init__(self, + hass, + device_id, + friendly_name, + state_template, + on_action, + off_action): + + self.entity_id = generate_entity_id( + ENTITY_ID_FORMAT, device_id, + hass=hass) + + self.hass = hass + self._name = friendly_name + self._template = state_template + self._on_action = on_action + self._off_action = off_action + self.update() + + def _update_callback(_event): + """ Called when the target device changes state. """ + # This can be called before the entity is properly + # initialised, so check before updating state, + if self.entity_id: + self.update_ha_state(True) + + self.hass.bus.listen(EVENT_STATE_CHANGED, _update_callback) + + @property + def name(self): + """ Returns the name of the device. """ + return self._name + + @property + def state(self): + """ Returns the state of the device. """ + return self._state + + @property + def should_poll(self): + """ Tells Home Assistant not to poll this entity. """ + return False + + def turn_on(self, **kwargs): + _LOGGER.error("TURN ON not implemented yet") + + def turn_off(self, **kwargs): + _LOGGER.error("TURN OFF not implemented yet") + + @property + def should_poll(self): + """ Tells Home Assistant not to poll this entity. """ + return False + + @property + def is_on(self): + """ True if device is on. """ + _LOGGER.error("IS ON CALLED %s", self._state) + return self._state == STATE_ON + + def update(self): + try: + self._state = template.render(self.hass, self._template) + except TemplateError as ex: + self._state = STATE_ERROR + _LOGGER.error(ex) diff --git a/tests/components/switch/test_template.py b/tests/components/switch/test_template.py new file mode 100644 index 00000000000..3dc93c41bf2 --- /dev/null +++ b/tests/components/switch/test_template.py @@ -0,0 +1,188 @@ +""" +tests.components.switch.template +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests template switch. +""" + +import homeassistant.core as ha +import homeassistant.components.switch as switch + + +class TestTemplateSwitch: + """ Test the Template switch. """ + + def setup_method(self, method): + self.hass = ha.HomeAssistant() + + def teardown_method(self, method): + """ Stop down stuff we started. """ + self.hass.stop() + + def test_template_state(self): + assert switch.setup(self.hass, { + 'switch': { + 'platform': 'template', + 'switches': { + 'test_template_switch': { + 'value_template': + "{{ states.switch.test_state.state }}", + 'turn_on': { + 'service': 'switch.turn_on', + 'entity_id': 'switch.test_state' + }, + 'turn_off': { + 'service': 'switch.turn_off', + 'entity_id': 'switch.test_state' + }, + } + } + } + }) + + + state = self.hass.states.set('switch.test_state', 'On') + self.hass.pool.block_till_done() + + state = self.hass.states.get('switch.test_template_switch') + assert state.state == 'On' + + state = self.hass.states.set('switch.test_state', 'Off') + self.hass.pool.block_till_done() + + state = self.hass.states.get('switch.test_template_switch') + assert state.state == 'Off' + + + def test_template_syntax_error(self): + assert switch.setup(self.hass, { + 'switch': { + 'platform': 'template', + 'switches': { + 'test_template_switch': { + 'value_template': + "{% if rubbish %}", + 'turn_on': { + 'service': 'switch.turn_on', + 'entity_id': 'switch.test_state' + }, + 'turn_off': { + 'service': 'switch.turn_off', + 'entity_id': 'switch.test_state' + }, + } + } + } + }) + + state = self.hass.states.set('switch.test_state', 'On') + self.hass.pool.block_till_done() + state = self.hass.states.get('switch.test_template_switch') + assert state.state == 'error' + + def test_invalid_name_does_not_create(self): + assert switch.setup(self.hass, { + 'switch': { + 'platform': 'template', + 'switches': { + 'test INVALID switch': { + 'value_template': + "{{ rubbish }", + 'turn_on': { + 'service': 'switch.turn_on', + 'entity_id': 'switch.test_state' + }, + 'turn_off': { + 'service': 'switch.turn_off', + 'entity_id': 'switch.test_state' + }, + } + } + } + }) + assert self.hass.states.all() == [] + + def test_invalid_switch_does_not_create(self): + assert switch.setup(self.hass, { + 'switch': { + 'platform': 'template', + 'switches': { + 'test_template_switch': 'Invalid' + } + } + }) + assert self.hass.states.all() == [] + + def test_no_switches_does_not_create(self): + assert switch.setup(self.hass, { + 'switch': { + 'platform': 'template' + } + }) + assert self.hass.states.all() == [] + + def test_missing_template_does_not_create(self): + assert switch.setup(self.hass, { + 'switch': { + 'platform': 'template', + 'switches': { + 'test_template_switch': { + 'not_value_template': + "{{ states.switch.test_state.state }}", + 'turn_on': { + 'service': 'switch.turn_on', + 'entity_id': 'switch.test_state' + }, + 'turn_off': { + 'service': 'switch.turn_off', + 'entity_id': 'switch.test_state' + }, + } + } + } + }) + assert self.hass.states.all() == [] + + def test_missing_on_does_not_create(self): + assert switch.setup(self.hass, { + 'switch': { + 'platform': 'template', + 'switches': { + 'test_template_switch': { + 'value_template': + "{{ states.switch.test_state.state }}", + 'not_on': { + 'service': 'switch.turn_on', + 'entity_id': 'switch.test_state' + }, + 'turn_off': { + 'service': 'switch.turn_off', + 'entity_id': 'switch.test_state' + }, + } + } + } + }) + assert self.hass.states.all() == [] + + def test_missing_off_does_not_create(self): + assert switch.setup(self.hass, { + 'switch': { + 'platform': 'template', + 'switches': { + 'test_template_switch': { + 'value_template': + "{{ states.switch.test_state.state }}", + 'turn_on': { + 'service': 'switch.turn_on', + 'entity_id': 'switch.test_state' + }, + 'not_off': { + 'service': 'switch.turn_off', + 'entity_id': 'switch.test_state' + }, + } + } + } + }) + assert self.hass.states.all() == [] From 9a9dbcfaeaf5d77b7ef33531d165e90eac1b6abb Mon Sep 17 00:00:00 2001 From: pavoni Date: Wed, 3 Feb 2016 13:16:13 +0000 Subject: [PATCH 2/7] Refactor, support template logic values, add tests. --- homeassistant/components/switch/template.py | 32 +++++++---- tests/components/switch/test_template.py | 64 +++++++++++++++++++-- 2 files changed, 78 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/switch/template.py b/homeassistant/components/switch/template.py index 170df067e57..1747df37d56 100644 --- a/homeassistant/components/switch/template.py +++ b/homeassistant/components/switch/template.py @@ -14,6 +14,8 @@ from homeassistant.components.switch import SwitchDevice from homeassistant.core import EVENT_STATE_CHANGED from homeassistant.const import ( + STATE_ON, + STATE_OFF, ATTR_FRIENDLY_NAME, CONF_VALUE_TEMPLATE) @@ -33,6 +35,8 @@ STATE_ERROR = 'error' ON_ACTION = 'turn_on' OFF_ACTION = 'turn_off' +STATE_TRUE = 'True' +STATE_FALSE = 'False' # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): @@ -116,16 +120,12 @@ class SwitchTemplate(SwitchDevice): self.hass.bus.listen(EVENT_STATE_CHANGED, _update_callback) + @property def name(self): """ Returns the name of the device. """ return self._name - @property - def state(self): - """ Returns the state of the device. """ - return self._state - @property def should_poll(self): """ Tells Home Assistant not to poll this entity. """ @@ -137,16 +137,24 @@ class SwitchTemplate(SwitchDevice): def turn_off(self, **kwargs): _LOGGER.error("TURN OFF not implemented yet") - @property - def should_poll(self): - """ Tells Home Assistant not to poll this entity. """ - return False - @property def is_on(self): """ True if device is on. """ - _LOGGER.error("IS ON CALLED %s", self._state) - return self._state == STATE_ON + return self._state == STATE_TRUE or self._state == STATE_ON + + @property + def is_off(self): + """ True if device is on. """ + return self._state == STATE_FALSE or self._state == STATE_OFF + + @property + def state(self): + """ Returns the state. """ + if self.is_on: + return STATE_ON + if self.is_off: + return STATE_OFF + return self._state def update(self): try: diff --git a/tests/components/switch/test_template.py b/tests/components/switch/test_template.py index 3dc93c41bf2..1f9de0fdd3a 100644 --- a/tests/components/switch/test_template.py +++ b/tests/components/switch/test_template.py @@ -8,6 +8,10 @@ Tests template switch. import homeassistant.core as ha import homeassistant.components.switch as switch +from homeassistant.const import ( + STATE_ON, + STATE_OFF) + class TestTemplateSwitch: """ Test the Template switch. """ @@ -19,7 +23,7 @@ class TestTemplateSwitch: """ Stop down stuff we started. """ self.hass.stop() - def test_template_state(self): + def test_template_state_text(self): assert switch.setup(self.hass, { 'switch': { 'platform': 'template', @@ -41,19 +45,67 @@ class TestTemplateSwitch: }) - state = self.hass.states.set('switch.test_state', 'On') + state = self.hass.states.set('switch.test_state', STATE_ON) self.hass.pool.block_till_done() state = self.hass.states.get('switch.test_template_switch') - assert state.state == 'On' + assert state.state == STATE_ON - state = self.hass.states.set('switch.test_state', 'Off') + state = self.hass.states.set('switch.test_state', STATE_OFF) self.hass.pool.block_till_done() state = self.hass.states.get('switch.test_template_switch') - assert state.state == 'Off' + assert state.state == STATE_OFF + def test_template_state_boolean_on(self): + assert switch.setup(self.hass, { + 'switch': { + 'platform': 'template', + 'switches': { + 'test_template_switch': { + 'value_template': + "{{ 1 == 1 }}", + 'turn_on': { + 'service': 'switch.turn_on', + 'entity_id': 'switch.test_state' + }, + 'turn_off': { + 'service': 'switch.turn_off', + 'entity_id': 'switch.test_state' + }, + } + } + } + }) + + state = self.hass.states.get('switch.test_template_switch') + assert state.state == STATE_ON + + def test_template_state_boolean_off(self): + assert switch.setup(self.hass, { + 'switch': { + 'platform': 'template', + 'switches': { + 'test_template_switch': { + 'value_template': + "{{ 1 == 2 }}", + 'turn_on': { + 'service': 'switch.turn_on', + 'entity_id': 'switch.test_state' + }, + 'turn_off': { + 'service': 'switch.turn_off', + 'entity_id': 'switch.test_state' + }, + } + } + } + }) + + state = self.hass.states.get('switch.test_template_switch') + assert state.state == STATE_OFF + def test_template_syntax_error(self): assert switch.setup(self.hass, { 'switch': { @@ -75,7 +127,7 @@ class TestTemplateSwitch: } }) - state = self.hass.states.set('switch.test_state', 'On') + state = self.hass.states.set('switch.test_state', STATE_ON) self.hass.pool.block_till_done() state = self.hass.states.get('switch.test_template_switch') assert state.state == 'error' From 5521096c027aac63737d7c366584dd6c8dbb4149 Mon Sep 17 00:00:00 2001 From: pavoni Date: Wed, 3 Feb 2016 14:29:25 +0000 Subject: [PATCH 3/7] Add actions. --- homeassistant/components/switch/template.py | 7 +- tests/components/switch/test_template.py | 71 +++++++++++++++++++++ 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switch/template.py b/homeassistant/components/switch/template.py index 1747df37d56..53b4e36055c 100644 --- a/homeassistant/components/switch/template.py +++ b/homeassistant/components/switch/template.py @@ -19,7 +19,10 @@ from homeassistant.const import ( ATTR_FRIENDLY_NAME, CONF_VALUE_TEMPLATE) +from homeassistant.helpers.service import call_from_config + from homeassistant.util import template, slugify + from homeassistant.exceptions import TemplateError from homeassistant.components.switch import DOMAIN @@ -132,10 +135,10 @@ class SwitchTemplate(SwitchDevice): return False def turn_on(self, **kwargs): - _LOGGER.error("TURN ON not implemented yet") + call_from_config(self.hass, self._on_action, True) def turn_off(self, **kwargs): - _LOGGER.error("TURN OFF not implemented yet") + call_from_config(self.hass, self._off_action, True) @property def is_on(self): diff --git a/tests/components/switch/test_template.py b/tests/components/switch/test_template.py index 1f9de0fdd3a..108f9a24f85 100644 --- a/tests/components/switch/test_template.py +++ b/tests/components/switch/test_template.py @@ -6,6 +6,7 @@ Tests template switch. """ import homeassistant.core as ha +import homeassistant.components as core import homeassistant.components.switch as switch from homeassistant.const import ( @@ -19,6 +20,14 @@ class TestTemplateSwitch: def setup_method(self, method): self.hass = ha.HomeAssistant() + self.calls = [] + + def record_call(service): + self.calls.append(service) + + self.hass.services.register('test', 'automation', record_call) + + def teardown_method(self, method): """ Stop down stuff we started. """ self.hass.stop() @@ -238,3 +247,65 @@ class TestTemplateSwitch: } }) assert self.hass.states.all() == [] + + def test_on_action(self): + assert switch.setup(self.hass, { + 'switch': { + 'platform': 'template', + 'switches': { + 'test_template_switch': { + 'value_template': + "{{ states.switch.test_state.state }}", + 'turn_on': { + 'service': 'test.automation' + }, + 'turn_off': { + 'service': 'switch.turn_off', + 'entity_id': 'switch.test_state' + }, + } + } + } + }) + self.hass.states.set('switch.test_state', STATE_OFF) + self.hass.pool.block_till_done() + + state = self.hass.states.get('switch.test_template_switch') + assert state.state == STATE_OFF + + core.switch.turn_on(self.hass, 'switch.test_template_switch') + self.hass.pool.block_till_done() + + assert 1 == len(self.calls) + + + def test_off_action(self): + assert switch.setup(self.hass, { + 'switch': { + 'platform': 'template', + 'switches': { + 'test_template_switch': { + 'value_template': + "{{ states.switch.test_state.state }}", + 'turn_on': { + 'service': 'switch.turn_on', + 'entity_id': 'switch.test_state' + + }, + 'turn_off': { + 'service': 'test.automation' + }, + } + } + } + }) + self.hass.states.set('switch.test_state', STATE_ON) + self.hass.pool.block_till_done() + + state = self.hass.states.get('switch.test_template_switch') + assert state.state == STATE_ON + + core.switch.turn_off(self.hass, 'switch.test_template_switch') + self.hass.pool.block_till_done() + + assert 1 == len(self.calls) From 6e6c3c5cd56cd3a75f054b52db0b8b2c57410a82 Mon Sep 17 00:00:00 2001 From: pavoni Date: Wed, 3 Feb 2016 14:30:58 +0000 Subject: [PATCH 4/7] Tidy. --- homeassistant/components/switch/template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/switch/template.py b/homeassistant/components/switch/template.py index 53b4e36055c..9e231de0299 100644 --- a/homeassistant/components/switch/template.py +++ b/homeassistant/components/switch/template.py @@ -41,6 +41,7 @@ OFF_ACTION = 'turn_off' STATE_TRUE = 'True' STATE_FALSE = 'False' + # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the switches. """ @@ -123,7 +124,6 @@ class SwitchTemplate(SwitchDevice): self.hass.bus.listen(EVENT_STATE_CHANGED, _update_callback) - @property def name(self): """ Returns the name of the device. """ From b20d3f8b3af41a233262175fbdc593be0fb61d9c Mon Sep 17 00:00:00 2001 From: pavoni Date: Wed, 3 Feb 2016 23:23:19 +0000 Subject: [PATCH 5/7] Update docstrings. --- homeassistant/components/switch/template.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/switch/template.py b/homeassistant/components/switch/template.py index 9e231de0299..34524978c84 100644 --- a/homeassistant/components/switch/template.py +++ b/homeassistant/components/switch/template.py @@ -135,9 +135,11 @@ class SwitchTemplate(SwitchDevice): return False def turn_on(self, **kwargs): + """ Fires the on action. """ call_from_config(self.hass, self._on_action, True) def turn_off(self, **kwargs): + """ Fires the off action. """ call_from_config(self.hass, self._off_action, True) @property @@ -147,7 +149,7 @@ class SwitchTemplate(SwitchDevice): @property def is_off(self): - """ True if device is on. """ + """ True if device is off. """ return self._state == STATE_FALSE or self._state == STATE_OFF @property @@ -160,6 +162,7 @@ class SwitchTemplate(SwitchDevice): return self._state def update(self): + """ Updates the state from the template. """ try: self._state = template.render(self.hass, self._template) except TemplateError as ex: From ced380f0cda0566110c6e89eeeba84dcff71dbea Mon Sep 17 00:00:00 2001 From: pavoni Date: Thu, 4 Feb 2016 17:24:38 +0000 Subject: [PATCH 6/7] Remove unneeded entity_id check and blank lines. --- homeassistant/components/sensor/template.py | 5 +---- homeassistant/components/switch/template.py | 8 +------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/sensor/template.py b/homeassistant/components/sensor/template.py index b87e26aa415..6ed4a0256e5 100644 --- a/homeassistant/components/sensor/template.py +++ b/homeassistant/components/sensor/template.py @@ -94,10 +94,7 @@ class SensorTemplate(Entity): def _update_callback(_event): """ Called when the target device changes state. """ - # This can be called before the entity is properly - # initialised, so check before updating state, - if self.entity_id: - self.update_ha_state(True) + self.update_ha_state(True) self.hass.bus.listen(EVENT_STATE_CHANGED, _update_callback) diff --git a/homeassistant/components/switch/template.py b/homeassistant/components/switch/template.py index 34524978c84..12a53ccbcc2 100644 --- a/homeassistant/components/switch/template.py +++ b/homeassistant/components/switch/template.py @@ -20,11 +20,8 @@ from homeassistant.const import ( CONF_VALUE_TEMPLATE) from homeassistant.helpers.service import call_from_config - from homeassistant.util import template, slugify - from homeassistant.exceptions import TemplateError - from homeassistant.components.switch import DOMAIN ENTITY_ID_FORMAT = DOMAIN + '.{}' @@ -117,10 +114,7 @@ class SwitchTemplate(SwitchDevice): def _update_callback(_event): """ Called when the target device changes state. """ - # This can be called before the entity is properly - # initialised, so check before updating state, - if self.entity_id: - self.update_ha_state(True) + self.update_ha_state(True) self.hass.bus.listen(EVENT_STATE_CHANGED, _update_callback) From 2622cf2e5352c52085bd68a4cdb39ab9086d5d10 Mon Sep 17 00:00:00 2001 From: pavoni Date: Fri, 5 Feb 2016 11:18:50 +0000 Subject: [PATCH 7/7] Use available, remove state, improve true,false tests. --- homeassistant/components/switch/template.py | 26 ++++++++++----------- tests/components/switch/test_template.py | 2 +- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/switch/template.py b/homeassistant/components/switch/template.py index 12a53ccbcc2..589768db9bd 100644 --- a/homeassistant/components/switch/template.py +++ b/homeassistant/components/switch/template.py @@ -35,9 +35,6 @@ STATE_ERROR = 'error' ON_ACTION = 'turn_on' OFF_ACTION = 'turn_off' -STATE_TRUE = 'True' -STATE_FALSE = 'False' - # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): @@ -139,26 +136,27 @@ class SwitchTemplate(SwitchDevice): @property def is_on(self): """ True if device is on. """ - return self._state == STATE_TRUE or self._state == STATE_ON + return self._value.lower() == 'true' or self._value == STATE_ON @property def is_off(self): """ True if device is off. """ - return self._state == STATE_FALSE or self._state == STATE_OFF + return self._value.lower() == 'false' or self._value == STATE_OFF @property - def state(self): - """ Returns the state. """ - if self.is_on: - return STATE_ON - if self.is_off: - return STATE_OFF - return self._state + def available(self): + """Return True if entity is available.""" + return self.is_on or self.is_off def update(self): """ Updates the state from the template. """ try: - self._state = template.render(self.hass, self._template) + self._value = template.render(self.hass, self._template) + if not self.available: + _LOGGER.error( + "`%s` is not a switch state, setting %s to unavailable", + self._value, self.entity_id) + except TemplateError as ex: - self._state = STATE_ERROR + self._value = STATE_ERROR _LOGGER.error(ex) diff --git a/tests/components/switch/test_template.py b/tests/components/switch/test_template.py index 108f9a24f85..aeffe9ff194 100644 --- a/tests/components/switch/test_template.py +++ b/tests/components/switch/test_template.py @@ -139,7 +139,7 @@ class TestTemplateSwitch: state = self.hass.states.set('switch.test_state', STATE_ON) self.hass.pool.block_till_done() state = self.hass.states.get('switch.test_template_switch') - assert state.state == 'error' + assert state.state == 'unavailable' def test_invalid_name_does_not_create(self): assert switch.setup(self.hass, {