From 6d773198a12f399cb9d89795d27fa4c5023c8734 Mon Sep 17 00:00:00 2001 From: Gil Peeters Date: Sat, 28 Sep 2019 21:53:16 +1000 Subject: [PATCH] Add availability_template to Template Cover platform (#26509) * Added availability_template to Template Cover platform * Added to test for invalid values in availability_template * Updated AVAILABILITY_TEMPLATE Rendering error * Moved const to package Const.py * Fix import order (pylint) * Moved availability_template rendering to common loop * Removed 'Magic' string and removed duplicate code * Cleaned up const and compare lowercase result to 'true' * reverted _available back to boolean * Fixed tests (async, magic values and state checks) --- homeassistant/components/template/cover.py | 39 +++++--- tests/components/template/test_cover.py | 109 +++++++++++++++++++++ 2 files changed, 137 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index 51b9a523b3b..483ee1ae872 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -38,6 +38,7 @@ 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.script import Script +from .const import CONF_AVAILABILITY_TEMPLATE _LOGGER = logging.getLogger(__name__) _VALID_STATES = [STATE_OPEN, STATE_CLOSED, "true", "false"] @@ -74,6 +75,7 @@ COVER_SCHEMA = vol.Schema( vol.Exclusive( CONF_VALUE_TEMPLATE, CONF_VALUE_OR_POSITION_TEMPLATE ): cv.template, + vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, vol.Optional(CONF_POSITION_TEMPLATE): cv.template, vol.Optional(CONF_TILT_TEMPLATE): cv.template, vol.Optional(CONF_ICON_TEMPLATE): cv.template, @@ -103,6 +105,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= position_template = device_config.get(CONF_POSITION_TEMPLATE) tilt_template = device_config.get(CONF_TILT_TEMPLATE) icon_template = device_config.get(CONF_ICON_TEMPLATE) + availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE) entity_picture_template = device_config.get(CONF_ENTITY_PICTURE_TEMPLATE) device_class = device_config.get(CONF_DEVICE_CLASS) open_action = device_config.get(OPEN_ACTION) @@ -144,6 +147,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if str(temp_ids) != MATCH_ALL: template_entity_ids |= set(temp_ids) + if availability_template is not None: + temp_ids = availability_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 @@ -160,6 +168,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= tilt_template, icon_template, entity_picture_template, + availability_template, open_action, close_action, stop_action, @@ -192,6 +201,7 @@ class CoverTemplate(CoverDevice): tilt_template, icon_template, entity_picture_template, + availability_template, open_action, close_action, stop_action, @@ -213,6 +223,7 @@ class CoverTemplate(CoverDevice): self._icon_template = icon_template self._device_class = device_class self._entity_picture_template = entity_picture_template + self._availability_template = availability_template self._open_script = None if open_action is not None: self._open_script = Script(hass, open_action) @@ -235,6 +246,7 @@ class CoverTemplate(CoverDevice): self._position = None self._tilt_value = None self._entities = entity_ids + self._available = True if self._template is not None: self._template.hass = self.hass @@ -246,6 +258,8 @@ class CoverTemplate(CoverDevice): self._icon_template.hass = self.hass if self._entity_picture_template is not None: self._entity_picture_template.hass = self.hass + if self._availability_template is not None: + self._availability_template.hass = self.hass async def async_added_to_hass(self): """Register callbacks.""" @@ -332,6 +346,11 @@ class CoverTemplate(CoverDevice): """Return the polling state.""" return False + @property + def available(self) -> bool: + """Return if the device is available.""" + return self._available + async def async_open_cover(self, **kwargs): """Move the cover up.""" if self._open_script: @@ -430,11 +449,8 @@ class CoverTemplate(CoverDevice): ) else: self._position = state - except TemplateError as ex: - _LOGGER.error(ex) - self._position = None - except ValueError as ex: - _LOGGER.error(ex) + except (TemplateError, ValueError) as err: + _LOGGER.error(err) self._position = None if self._tilt_template is not None: try: @@ -447,22 +463,23 @@ class CoverTemplate(CoverDevice): ) else: self._tilt_value = state - except TemplateError as ex: - _LOGGER.error(ex) - self._tilt_value = None - except ValueError as ex: - _LOGGER.error(ex) + except (TemplateError, ValueError) as err: + _LOGGER.error(err) self._tilt_value = None for property_name, template in ( ("_icon", self._icon_template), ("_entity_picture", self._entity_picture_template), + ("_available", self._availability_template), ): if template is None: continue try: - setattr(self, property_name, template.async_render()) + value = template.async_render() + if property_name == "_available": + value = value.lower() == "true" + setattr(self, property_name, value) except TemplateError as ex: friendly_property_name = property_name[1:].replace("_", " ") if ex.args and ex.args[0].startswith( diff --git a/tests/components/template/test_cover.py b/tests/components/template/test_cover.py index 247ee25027c..d3be01cbdc3 100644 --- a/tests/components/template/test_cover.py +++ b/tests/components/template/test_cover.py @@ -16,7 +16,10 @@ from homeassistant.const import ( SERVICE_SET_COVER_TILT_POSITION, SERVICE_STOP_COVER, STATE_CLOSED, + STATE_UNAVAILABLE, STATE_OPEN, + STATE_ON, + STATE_OFF, ) from tests.common import assert_setup_component, async_mock_service @@ -839,6 +842,112 @@ async def test_entity_picture_template(hass, calls): assert state.attributes["entity_picture"] == "/local/cover.png" +async def test_availability_template(hass, calls): + """Test availability template.""" + with assert_setup_component(1, "cover"): + assert await setup.async_setup_component( + hass, + "cover", + { + "cover": { + "platform": "template", + "covers": { + "test_template_cover": { + "value_template": "open", + "open_cover": { + "service": "cover.open_cover", + "entity_id": "cover.test_state", + }, + "close_cover": { + "service": "cover.close_cover", + "entity_id": "cover.test_state", + }, + "availability_template": "{{ is_state('availability_state.state','on') }}", + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + hass.states.async_set("availability_state.state", STATE_OFF) + await hass.async_block_till_done() + + assert hass.states.get("cover.test_template_cover").state == STATE_UNAVAILABLE + + hass.states.async_set("availability_state.state", STATE_ON) + await hass.async_block_till_done() + + assert hass.states.get("cover.test_template_cover").state != STATE_UNAVAILABLE + + +async def test_availability_without_availability_template(hass, calls): + """Test that component is availble if there is no.""" + assert await setup.async_setup_component( + hass, + "cover", + { + "cover": { + "platform": "template", + "covers": { + "test_template_cover": { + "value_template": "open", + "open_cover": { + "service": "cover.open_cover", + "entity_id": "cover.test_state", + }, + "close_cover": { + "service": "cover.close_cover", + "entity_id": "cover.test_state", + }, + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("cover.test_template_cover") + assert state.state != STATE_UNAVAILABLE + + +async def test_invalid_availability_template_keeps_component_available(hass, caplog): + """Test that an invalid availability keeps the device available.""" + assert await setup.async_setup_component( + hass, + "cover", + { + "cover": { + "platform": "template", + "covers": { + "test_template_cover": { + "availability_template": "{{ x - 12 }}", + "value_template": "open", + "open_cover": { + "service": "cover.open_cover", + "entity_id": "cover.test_state", + }, + "close_cover": { + "service": "cover.close_cover", + "entity_id": "cover.test_state", + }, + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + assert hass.states.get("cover.test_template_cover") != STATE_UNAVAILABLE + assert ("UndefinedError: 'x' is undefined") in caplog.text + + async def test_device_class(hass, calls): """Test device class.""" with assert_setup_component(1, "cover"):