From 17237e9d3f2e7985aa78b9a28cce62857f7c33dc Mon Sep 17 00:00:00 2001 From: PhracturedBlue Date: Fri, 30 Jun 2017 08:24:29 -0700 Subject: [PATCH] Implement templates for covers (#8100) * Implement templates for covers * Fix a few remaining pylint warnings * Fix hound line-length warnings * Fix one more hound line-length warning * Fix quadruple-quotes an line length code-quality issues * Irrelevant change to retrigger travis due to timeout * Use volutuous Exclusive to check for mutex condition * Fix incorrect state check --- homeassistant/components/cover/template.py | 354 ++++++++++++ tests/components/cover/test_template.py | 630 +++++++++++++++++++++ 2 files changed, 984 insertions(+) create mode 100644 homeassistant/components/cover/template.py create mode 100644 tests/components/cover/test_template.py diff --git a/homeassistant/components/cover/template.py b/homeassistant/components/cover/template.py new file mode 100644 index 00000000000..fd746131288 --- /dev/null +++ b/homeassistant/components/cover/template.py @@ -0,0 +1,354 @@ +""" +Support for covers which integrate with other components. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/cover.template/ +""" +import asyncio +import logging + +import voluptuous as vol + +from homeassistant.core import callback +from homeassistant.components.cover import ( + ENTITY_ID_FORMAT, CoverDevice, PLATFORM_SCHEMA, + SUPPORT_OPEN_TILT, SUPPORT_CLOSE_TILT, SUPPORT_STOP_TILT, + SUPPORT_SET_TILT_POSITION, SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_STOP, + SUPPORT_SET_POSITION, ATTR_POSITION, ATTR_TILT_POSITION) +from homeassistant.const import ( + CONF_FRIENDLY_NAME, CONF_ENTITY_ID, + EVENT_HOMEASSISTANT_START, MATCH_ALL, + CONF_VALUE_TEMPLATE, CONF_ICON_TEMPLATE, + STATE_OPEN, STATE_CLOSED) +from homeassistant.exceptions import TemplateError +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import async_generate_entity_id +from homeassistant.helpers.event import async_track_state_change +from homeassistant.helpers.restore_state import async_get_last_state +from homeassistant.helpers.script import Script + +_LOGGER = logging.getLogger(__name__) +_VALID_STATES = [STATE_OPEN, STATE_CLOSED, 'true', 'false'] + +CONF_COVERS = 'covers' + +CONF_POSITION_TEMPLATE = 'position_template' +CONF_TILT_TEMPLATE = 'tilt_template' +OPEN_ACTION = 'open_cover' +CLOSE_ACTION = 'close_cover' +STOP_ACTION = 'stop_cover' +POSITION_ACTION = 'set_cover_position' +TILT_ACTION = 'set_cover_tilt_position' +CONF_VALUE_OR_POSITION_TEMPLATE = 'value_or_position' + +TILT_FEATURES = (SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT | SUPPORT_STOP_TILT | + SUPPORT_SET_TILT_POSITION) + +COVER_SCHEMA = vol.Schema({ + vol.Required(OPEN_ACTION): cv.SCRIPT_SCHEMA, + vol.Required(CLOSE_ACTION): cv.SCRIPT_SCHEMA, + vol.Required(STOP_ACTION): cv.SCRIPT_SCHEMA, + vol.Exclusive(CONF_POSITION_TEMPLATE, + CONF_VALUE_OR_POSITION_TEMPLATE): cv.template, + vol.Exclusive(CONF_VALUE_TEMPLATE, + CONF_VALUE_OR_POSITION_TEMPLATE): cv.template, + vol.Optional(CONF_POSITION_TEMPLATE): cv.template, + vol.Optional(CONF_TILT_TEMPLATE): cv.template, + vol.Optional(CONF_ICON_TEMPLATE): cv.template, + vol.Optional(POSITION_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(TILT_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_FRIENDLY_NAME, default=None): cv.string, + vol.Optional(CONF_ENTITY_ID): cv.entity_ids +}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}), +}) + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set up the Template cover.""" + covers = [] + + for device, device_config in config[CONF_COVERS].items(): + friendly_name = device_config.get(CONF_FRIENDLY_NAME, device) + state_template = device_config.get(CONF_VALUE_TEMPLATE) + position_template = device_config.get(CONF_POSITION_TEMPLATE) + tilt_template = device_config.get(CONF_TILT_TEMPLATE) + icon_template = device_config.get(CONF_ICON_TEMPLATE) + open_action = device_config[OPEN_ACTION] + close_action = device_config[CLOSE_ACTION] + stop_action = device_config[STOP_ACTION] + position_action = device_config.get(POSITION_ACTION) + tilt_action = device_config.get(TILT_ACTION) + + if position_template is None and state_template is None: + _LOGGER.error('Must specify either %s' or '%s', + CONF_VALUE_TEMPLATE, CONF_VALUE_TEMPLATE) + continue + + template_entity_ids = set() + if state_template is not None: + temp_ids = state_template.extract_entities() + if str(temp_ids) != MATCH_ALL: + template_entity_ids |= set(temp_ids) + + if position_template is not None: + temp_ids = position_template.extract_entities() + if str(temp_ids) != MATCH_ALL: + template_entity_ids |= set(temp_ids) + + if tilt_template is not None: + temp_ids = tilt_template.extract_entities() + if str(temp_ids) != MATCH_ALL: + template_entity_ids |= set(temp_ids) + + if icon_template is not None: + temp_ids = icon_template.extract_entities() + if str(temp_ids) != MATCH_ALL: + template_entity_ids |= set(temp_ids) + + if not template_entity_ids: + template_entity_ids = MATCH_ALL + + entity_ids = device_config.get(CONF_ENTITY_ID, template_entity_ids) + + covers.append( + CoverTemplate( + hass, + device, friendly_name, state_template, + position_template, tilt_template, icon_template, + open_action, close_action, stop_action, + position_action, tilt_action, entity_ids + ) + ) + if not covers: + _LOGGER.error("No covers added") + return False + + async_add_devices(covers, True) + return True + + +class CoverTemplate(CoverDevice): + """Representation of a Template cover.""" + + def __init__(self, hass, device_id, friendly_name, state_template, + position_template, tilt_template, icon_template, + open_action, close_action, stop_action, + position_action, tilt_action, entity_ids): + """Initialize the Template cover.""" + self.hass = hass + self.entity_id = async_generate_entity_id( + ENTITY_ID_FORMAT, device_id, hass=hass) + self._name = friendly_name + self._template = state_template + self._position_template = position_template + self._tilt_template = tilt_template + self._icon_template = icon_template + self._open_script = Script(hass, open_action) + self._close_script = Script(hass, close_action) + self._stop_script = Script(hass, stop_action) + self._position_script = None + if position_action is not None: + self._position_script = Script(hass, position_action) + self._tilt_script = None + if tilt_action is not None: + self._tilt_script = Script(hass, tilt_action) + self._icon = None + self._position = None + self._tilt_value = None + self._entities = entity_ids + + if self._template is not None: + self._template.hass = self.hass + if self._position_template is not None: + self._position_template.hass = self.hass + if self._tilt_template is not None: + self._tilt_template.hass = self.hass + if self._icon_template is not None: + self._icon_template.hass = self.hass + + @asyncio.coroutine + def async_added_to_hass(self): + """Register callbacks.""" + state = yield from async_get_last_state(self.hass, self.entity_id) + if state: + self._position = 100 if state.state == STATE_OPEN else 0 + + @callback + def template_cover_state_listener(entity, old_state, new_state): + """Handle target device state changes.""" + self.hass.async_add_job(self.async_update_ha_state(True)) + + @callback + def template_cover_startup(event): + """Update template on startup.""" + async_track_state_change( + self.hass, self._entities, template_cover_state_listener) + + self.hass.async_add_job(self.async_update_ha_state(True)) + + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, template_cover_startup) + + @property + def name(self): + """Return the name of the cover.""" + return self._name + + @property + def is_closed(self): + """Return if the cover is closed.""" + return self._position == 0 + + @property + def current_cover_position(self): + """Return current position of cover. + + None is unknown, 0 is closed, 100 is fully open. + """ + return self._position + + @property + def current_cover_tilt_position(self): + """Return current position of cover tilt. + + None is unknown, 0 is closed, 100 is fully open. + """ + return self._tilt_value + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return self._icon + + @property + def supported_features(self): + """Flag supported features.""" + supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP + + if self.current_cover_position is not None: + supported_features |= SUPPORT_SET_POSITION + + if self.current_cover_tilt_position is not None: + supported_features |= TILT_FEATURES + + return supported_features + + @property + def should_poll(self): + """Return the polling state.""" + return False + + @asyncio.coroutine + def async_open_cover(self, **kwargs): + """Move the cover up.""" + self.hass.async_add_job(self._open_script.async_run()) + + @asyncio.coroutine + def async_close_cover(self, **kwargs): + """Move the cover down.""" + self.hass.async_add_job(self._close_script.async_run()) + + @asyncio.coroutine + def async_stop_cover(self, **kwargs): + """Fire the stop action.""" + self.hass.async_add_job(self._stop_script.async_run()) + + @asyncio.coroutine + def async_set_cover_position(self, **kwargs): + """Set cover position.""" + if ATTR_POSITION not in kwargs: + return + self._position = kwargs[ATTR_POSITION] + self.hass.async_add_job(self._position_script.async_run( + {"position": self._position})) + + @asyncio.coroutine + def async_open_cover_tilt(self, **kwargs): + """Tilt the cover open.""" + self._tilt_value = 100 + self.hass.async_add_job(self._tilt_script.async_run( + {"tilt": self._tilt_value})) + + @asyncio.coroutine + def async_close_cover_tilt(self, **kwargs): + """Tilt the cover closed.""" + self._tilt_value = 0 + self.hass.async_add_job(self._tilt_script.async_run( + {"tilt": self._tilt_value})) + + @asyncio.coroutine + def async_set_cover_tilt_position(self, **kwargs): + """Move the cover tilt to a specific position.""" + if ATTR_TILT_POSITION not in kwargs: + return + self._tilt_value = kwargs[ATTR_TILT_POSITION] + self.hass.async_add_job(self._tilt_script.async_run( + {"tilt": self._tilt_value})) + + @asyncio.coroutine + def async_update(self): + """Update the state from the template.""" + if self._template is not None: + try: + state = self._template.async_render().lower() + if state in _VALID_STATES: + if state in ('true', STATE_OPEN): + self._position = 100 + else: + self._position = 0 + else: + _LOGGER.error( + 'Received invalid cover is_on state: %s. Expected: %s', + state, ', '.join(_VALID_STATES)) + self._position = None + except TemplateError as ex: + _LOGGER.error(ex) + self._position = None + if self._position_template is not None: + try: + state = float(self._position_template.async_render()) + if state < 0 or state > 100: + self._position = None + _LOGGER.error("Cover position value must be" + " between 0 and 100." + " Value was: %.2f", state) + else: + self._position = state + except TemplateError as ex: + _LOGGER.error(ex) + self._position = None + except ValueError as ex: + _LOGGER.error(ex) + self._position = None + if self._tilt_template is not None: + try: + state = float(self._tilt_template.async_render()) + if state < 0 or state > 100: + self._tilt_value = None + _LOGGER.error("Tilt value must be between 0 and 100." + " Value was: %.2f", state) + else: + self._tilt_value = state + except TemplateError as ex: + _LOGGER.error(ex) + self._tilt_value = None + except ValueError as ex: + _LOGGER.error(ex) + self._tilt_value = None + if self._icon_template is not None: + try: + self._icon = self._icon_template.async_render() + except TemplateError as ex: + if ex.args and ex.args[0].startswith( + "UndefinedError: 'None' has no attribute"): + # Common during HA startup - so just a warning + _LOGGER.warning('Could not render icon template %s,' + ' the state is unknown.', self._name) + return + self._icon = super().icon + _LOGGER.error('Could not render icon template %s: %s', + self._name, ex) diff --git a/tests/components/cover/test_template.py b/tests/components/cover/test_template.py new file mode 100644 index 00000000000..35ec21bfbdf --- /dev/null +++ b/tests/components/cover/test_template.py @@ -0,0 +1,630 @@ +"""The tests the cover command line platform.""" + +import logging +import unittest + +from homeassistant.core import callback +from homeassistant import setup +import homeassistant.components.cover as cover +from homeassistant.const import STATE_OPEN, STATE_CLOSED + +from tests.common import ( + get_test_home_assistant, assert_setup_component) +_LOGGER = logging.getLogger(__name__) + + +class TestTemplateCover(unittest.TestCase): + """Test the cover command line platform.""" + + hass = None + calls = None + # pylint: disable=invalid-name + + def setup_method(self, method): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.calls = [] + + @callback + def record_call(service): + """Track function calls..""" + self.calls.append(service) + + self.hass.services.register('test', 'automation', record_call) + + def teardown_method(self, method): + """Stop everything that was started.""" + self.hass.stop() + + def test_template_state_text(self): + """Test the state text of a template.""" + with assert_setup_component(1, 'cover'): + assert setup.setup_component(self.hass, 'cover', { + 'cover': { + 'platform': 'template', + 'covers': { + 'test_template_cover': { + 'value_template': + "{{ states.cover.test_state.state }}", + 'open_cover': { + 'service': 'cover.open_cover', + 'entity_id': 'cover.test_state' + }, + 'close_cover': { + 'service': 'cover.close_cover', + 'entity_id': 'cover.test_state' + }, + 'stop_cover': { + 'service': 'cover.close_cover', + 'entity_id': 'cover.test_state' + }, + } + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + state = self.hass.states.set('cover.test_state', STATE_OPEN) + self.hass.block_till_done() + + state = self.hass.states.get('cover.test_template_cover') + assert state.state == STATE_OPEN + + state = self.hass.states.set('cover.test_state', STATE_CLOSED) + self.hass.block_till_done() + + state = self.hass.states.get('cover.test_template_cover') + assert state.state == STATE_CLOSED + + def test_template_state_boolean(self): + """Test the state text of a template.""" + with assert_setup_component(1, 'cover'): + assert setup.setup_component(self.hass, 'cover', { + 'cover': { + 'platform': 'template', + 'covers': { + 'test_template_cover': { + 'value_template': + "{{ 1 == 1 }}", + 'open_cover': { + 'service': 'cover.open_cover', + 'entity_id': 'cover.test_state' + }, + 'close_cover': { + 'service': 'cover.close_cover', + 'entity_id': 'cover.test_state' + }, + 'stop_cover': { + 'service': 'cover.close_cover', + 'entity_id': 'cover.test_state' + }, + } + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + state = self.hass.states.get('cover.test_template_cover') + assert state.state == STATE_OPEN + + def test_template_position(self): + """Test the state text of a template.""" + with assert_setup_component(1, 'cover'): + assert setup.setup_component(self.hass, 'cover', { + 'cover': { + 'platform': 'template', + 'covers': { + 'test_template_cover': { + 'position_template': + "{{ states.cover.test.attributes.position }}", + 'open_cover': { + 'service': 'cover.open_cover', + 'entity_id': 'cover.test' + }, + 'close_cover': { + 'service': 'cover.close_cover', + 'entity_id': 'cover.test' + }, + 'stop_cover': { + 'service': 'cover.close_cover', + 'entity_id': 'cover.test' + }, + } + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + state = self.hass.states.set('cover.test', STATE_CLOSED) + self.hass.block_till_done() + + entity = self.hass.states.get('cover.test') + attrs = dict() + attrs['position'] = 42 + self.hass.states.async_set( + entity.entity_id, entity.state, + attributes=attrs) + self.hass.block_till_done() + + state = self.hass.states.get('cover.test_template_cover') + assert state.attributes.get('current_position') == 42.0 + assert state.state == STATE_OPEN + + state = self.hass.states.set('cover.test', STATE_OPEN) + self.hass.block_till_done() + entity = self.hass.states.get('cover.test') + attrs['position'] = 0.0 + self.hass.states.async_set( + entity.entity_id, entity.state, + attributes=attrs) + self.hass.block_till_done() + + state = self.hass.states.get('cover.test_template_cover') + assert state.attributes.get('current_position') == 0.0 + assert state.state == STATE_CLOSED + + def test_template_tilt(self): + """Test the state text of a template.""" + with assert_setup_component(1, 'cover'): + assert setup.setup_component(self.hass, 'cover', { + 'cover': { + 'platform': 'template', + 'covers': { + 'test_template_cover': { + 'value_template': + "{{ 1 == 1 }}", + 'tilt_template': + "{{ 42 }}", + 'open_cover': { + 'service': 'cover.open_cover', + 'entity_id': 'cover.test_state' + }, + 'close_cover': { + 'service': 'cover.close_cover', + 'entity_id': 'cover.test_state' + }, + 'stop_cover': { + 'service': 'cover.close_cover', + 'entity_id': 'cover.test_state' + }, + } + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + state = self.hass.states.get('cover.test_template_cover') + assert state.attributes.get('current_tilt_position') == 42.0 + + def test_template_out_of_bounds(self): + """Test the state text of a template.""" + with assert_setup_component(1, 'cover'): + assert setup.setup_component(self.hass, 'cover', { + 'cover': { + 'platform': 'template', + 'covers': { + 'test_template_cover': { + 'position_template': + "{{ -1 }}", + 'tilt_template': + "{{ 110 }}", + 'open_cover': { + 'service': 'cover.open_cover', + 'entity_id': 'cover.test_state' + }, + 'close_cover': { + 'service': 'cover.close_cover', + 'entity_id': 'cover.test_state' + }, + 'stop_cover': { + 'service': 'cover.close_cover', + 'entity_id': 'cover.test_state' + }, + } + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + state = self.hass.states.get('cover.test_template_cover') + assert state.attributes.get('current_tilt_position') is None + assert state.attributes.get('current_position') is None + + def test_template_mutex(self): + """Test that only value or position template can be used.""" + with assert_setup_component(0, 'cover'): + assert setup.setup_component(self.hass, 'cover', { + 'cover': { + 'platform': 'template', + 'covers': { + 'test_template_cover': { + 'value_template': + "{{ 1 == 1 }}", + 'position_template': + "{{ 42 }}", + 'open_cover': { + 'service': 'cover.open_cover', + 'entity_id': 'cover.test_state' + }, + 'close_cover': { + 'service': 'cover.close_cover', + 'entity_id': 'cover.test_state' + }, + 'stop_cover': { + 'service': 'cover.close_cover', + 'entity_id': 'cover.test_state' + }, + 'icon_template': + "{% if states.cover.test_state.state %}" + "mdi:check" + "{% endif %}" + } + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + assert self.hass.states.all() == [] + + def test_template_position_or_value(self): + """Test that at least one of value or position template is used.""" + with assert_setup_component(1, 'cover'): + assert setup.setup_component(self.hass, 'cover', { + 'cover': { + 'platform': 'template', + 'covers': { + 'test_template_cover': { + 'open_cover': { + 'service': 'cover.open_cover', + 'entity_id': 'cover.test_state' + }, + 'close_cover': { + 'service': 'cover.close_cover', + 'entity_id': 'cover.test_state' + }, + 'stop_cover': { + 'service': 'cover.close_cover', + 'entity_id': 'cover.test_state' + }, + 'icon_template': + "{% if states.cover.test_state.state %}" + "mdi:check" + "{% endif %}" + } + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + assert self.hass.states.all() == [] + + def test_template_non_numeric(self): + """Test the state text of a template.""" + with assert_setup_component(1, 'cover'): + assert setup.setup_component(self.hass, 'cover', { + 'cover': { + 'platform': 'template', + 'covers': { + 'test_template_cover': { + 'position_template': + "{{ on }}", + 'tilt_template': + "{% if states.cover.test_state.state %}" + "on" + "{% else %}" + "off" + "{% endif %}", + 'open_cover': { + 'service': 'cover.open_cover', + 'entity_id': 'cover.test_state' + }, + 'close_cover': { + 'service': 'cover.close_cover', + 'entity_id': 'cover.test_state' + }, + 'stop_cover': { + 'service': 'cover.close_cover', + 'entity_id': 'cover.test_state' + }, + } + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + state = self.hass.states.get('cover.test_template_cover') + assert state.attributes.get('current_tilt_position') is None + assert state.attributes.get('current_position') is None + + def test_open_action(self): + """Test the state text of a template.""" + with assert_setup_component(1, 'cover'): + assert setup.setup_component(self.hass, 'cover', { + 'cover': { + 'platform': 'template', + 'covers': { + 'test_template_cover': { + 'position_template': + "{{ 0 }}", + 'open_cover': { + 'service': 'test.automation', + }, + 'close_cover': { + 'service': 'cover.close_cover', + 'entity_id': 'cover.test_state' + }, + 'stop_cover': { + 'service': 'cover.close_cover', + 'entity_id': 'cover.test_state' + }, + } + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + state = self.hass.states.get('cover.test_template_cover') + assert state.state == STATE_CLOSED + + cover.open_cover(self.hass, 'cover.test_template_cover') + self.hass.block_till_done() + + assert len(self.calls) == 1 + + def test_close_stop_action(self): + """Test the state text of a template.""" + with assert_setup_component(1, 'cover'): + assert setup.setup_component(self.hass, 'cover', { + 'cover': { + 'platform': 'template', + 'covers': { + 'test_template_cover': { + 'position_template': + "{{ 100 }}", + 'open_cover': { + 'service': 'cover.open_cover', + 'entity_id': 'cover.test_state' + }, + 'close_cover': { + 'service': 'test.automation', + }, + 'stop_cover': { + 'service': 'test.automation', + }, + } + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + state = self.hass.states.get('cover.test_template_cover') + assert state.state == STATE_OPEN + + cover.close_cover(self.hass, 'cover.test_template_cover') + self.hass.block_till_done() + + cover.stop_cover(self.hass, 'cover.test_template_cover') + self.hass.block_till_done() + + assert len(self.calls) == 2 + + def test_set_position(self): + """Test the state text of a template.""" + with assert_setup_component(1, 'cover'): + assert setup.setup_component(self.hass, 'cover', { + 'cover': { + 'platform': 'template', + 'covers': { + 'test_template_cover': { + 'position_template': + "{{ 100 }}", + 'open_cover': { + 'service': 'cover.open_cover', + 'entity_id': 'cover.test_state' + }, + 'close_cover': { + 'service': 'cover.close_cover', + 'entity_id': 'cover.test_state' + }, + 'stop_cover': { + 'service': 'cover.stop_cover', + 'entity_id': 'cover.test_state' + }, + 'set_cover_position': { + 'service': 'test.automation', + }, + } + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + state = self.hass.states.get('cover.test_template_cover') + assert state.state == STATE_OPEN + + cover.set_cover_position(self.hass, 42, + 'cover.test_template_cover') + self.hass.block_till_done() + + assert len(self.calls) == 1 + + def test_set_tilt_position(self): + """Test the state text of a template.""" + with assert_setup_component(1, 'cover'): + assert setup.setup_component(self.hass, 'cover', { + 'cover': { + 'platform': 'template', + 'covers': { + 'test_template_cover': { + 'position_template': + "{{ 100 }}", + 'open_cover': { + 'service': 'cover.open_cover', + 'entity_id': 'cover.test_state' + }, + 'close_cover': { + 'service': 'cover.close_cover', + 'entity_id': 'cover.test_state' + }, + 'stop_cover': { + 'service': 'cover.stop_cover', + 'entity_id': 'cover.test_state' + }, + 'set_cover_tilt_position': { + 'service': 'test.automation', + }, + } + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + cover.set_cover_tilt_position(self.hass, 42, + 'cover.test_template_cover') + self.hass.block_till_done() + + assert len(self.calls) == 1 + + def test_open_tilt_action(self): + """Test the state text of a template.""" + with assert_setup_component(1, 'cover'): + assert setup.setup_component(self.hass, 'cover', { + 'cover': { + 'platform': 'template', + 'covers': { + 'test_template_cover': { + 'position_template': + "{{ 100 }}", + 'open_cover': { + 'service': 'cover.open_cover', + 'entity_id': 'cover.test_state' + }, + 'close_cover': { + 'service': 'cover.close_cover', + 'entity_id': 'cover.test_state' + }, + 'stop_cover': { + 'service': 'cover.stop_cover', + 'entity_id': 'cover.test_state' + }, + 'set_cover_tilt_position': { + 'service': 'test.automation', + }, + } + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + cover.open_cover_tilt(self.hass, 'cover.test_template_cover') + self.hass.block_till_done() + + assert len(self.calls) == 1 + + def test_close_tilt_action(self): + """Test the state text of a template.""" + with assert_setup_component(1, 'cover'): + assert setup.setup_component(self.hass, 'cover', { + 'cover': { + 'platform': 'template', + 'covers': { + 'test_template_cover': { + 'position_template': + "{{ 100 }}", + 'open_cover': { + 'service': 'cover.open_cover', + 'entity_id': 'cover.test_state' + }, + 'close_cover': { + 'service': 'cover.close_cover', + 'entity_id': 'cover.test_state' + }, + 'stop_cover': { + 'service': 'cover.stop_cover', + 'entity_id': 'cover.test_state' + }, + 'set_cover_tilt_position': { + 'service': 'test.automation', + }, + } + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + cover.close_cover_tilt(self.hass, 'cover.test_template_cover') + self.hass.block_till_done() + + assert len(self.calls) == 1 + + def test_icon_template(self): + """Test icon template.""" + with assert_setup_component(1, 'cover'): + assert setup.setup_component(self.hass, 'cover', { + 'cover': { + 'platform': 'template', + 'covers': { + 'test_template_cover': { + 'value_template': + "{{ states.cover.test_state.state }}", + 'open_cover': { + 'service': 'cover.open_cover', + 'entity_id': 'cover.test_state' + }, + 'close_cover': { + 'service': 'cover.close_cover', + 'entity_id': 'cover.test_state' + }, + 'stop_cover': { + 'service': 'cover.close_cover', + 'entity_id': 'cover.test_state' + }, + 'icon_template': + "{% if states.cover.test_state.state %}" + "mdi:check" + "{% endif %}" + } + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + state = self.hass.states.get('cover.test_template_cover') + assert state.attributes.get('icon') == '' + + state = self.hass.states.set('cover.test_state', STATE_OPEN) + self.hass.block_till_done() + + state = self.hass.states.get('cover.test_template_cover') + + assert state.attributes['icon'] == 'mdi:check'