diff --git a/homeassistant/components/sensor/template.py b/homeassistant/components/sensor/template.py index 18701c19435..17b739f88cc 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 new file mode 100644 index 00000000000..589768db9bd --- /dev/null +++ b/homeassistant/components/switch/template.py @@ -0,0 +1,162 @@ +""" +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 ( + STATE_ON, + STATE_OFF, + 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 + +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. """ + 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 should_poll(self): + """ Tells Home Assistant not to poll this entity. """ + 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 + def is_on(self): + """ True if device is on. """ + return self._value.lower() == 'true' or self._value == STATE_ON + + @property + def is_off(self): + """ True if device is off. """ + return self._value.lower() == 'false' or self._value == STATE_OFF + + @property + 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._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._value = 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..aeffe9ff194 --- /dev/null +++ b/tests/components/switch/test_template.py @@ -0,0 +1,311 @@ +""" +tests.components.switch.template +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests template switch. +""" + +import homeassistant.core as ha +import homeassistant.components as core +import homeassistant.components.switch as switch + +from homeassistant.const import ( + STATE_ON, + STATE_OFF) + + +class TestTemplateSwitch: + """ Test the Template switch. """ + + 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() + + def test_template_state_text(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', STATE_ON) + self.hass.pool.block_till_done() + + state = self.hass.states.get('switch.test_template_switch') + assert state.state == STATE_ON + + 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 + + + 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': { + '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', STATE_ON) + self.hass.pool.block_till_done() + state = self.hass.states.get('switch.test_template_switch') + assert state.state == 'unavailable' + + 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() == [] + + 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)