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() == []