From 9a723607eb4ca340f13fbd269ef2fe273cca0ca2 Mon Sep 17 00:00:00 2001 From: jbouwh Date: Mon, 10 Apr 2023 14:45:50 +0000 Subject: [PATCH 1/2] Support unknown state for template cover --- homeassistant/components/template/cover.py | 43 ++++--- tests/components/template/test_cover.py | 124 ++++++++++++++++++--- 2 files changed, 136 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index 1e0fdfacc8e6dc..930069305c5984 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -52,6 +52,7 @@ STATE_CLOSING, "true", "false", + "none", ] CONF_POSITION_TEMPLATE = "position_template" @@ -138,11 +139,11 @@ class CoverTemplate(TemplateEntity, CoverEntity): def __init__( self, - hass, + hass: HomeAssistant, object_id, config, unique_id, - ): + ) -> None: """Initialize the Template cover.""" super().__init__( hass, config=config, fallback_name=object_id, unique_id=unique_id @@ -150,24 +151,24 @@ def __init__( self.entity_id = async_generate_entity_id( ENTITY_ID_FORMAT, object_id, hass=hass ) - friendly_name = self._attr_name + friendly_name = self._attr_name or "" self._template = config.get(CONF_VALUE_TEMPLATE) self._position_template = config.get(CONF_POSITION_TEMPLATE) self._tilt_template = config.get(CONF_TILT_TEMPLATE) self._device_class: CoverDeviceClass | None = config.get(CONF_DEVICE_CLASS) - self._open_script = None + self._open_script: Script | None = None if (open_action := config.get(OPEN_ACTION)) is not None: self._open_script = Script(hass, open_action, friendly_name, DOMAIN) - self._close_script = None + self._close_script: Script | None = None if (close_action := config.get(CLOSE_ACTION)) is not None: self._close_script = Script(hass, close_action, friendly_name, DOMAIN) - self._stop_script = None + self._stop_script: Script | None = None if (stop_action := config.get(STOP_ACTION)) is not None: self._stop_script = Script(hass, stop_action, friendly_name, DOMAIN) - self._position_script = None + self._position_script: Script | None = None if (position_action := config.get(POSITION_ACTION)) is not None: self._position_script = Script(hass, position_action, friendly_name, DOMAIN) - self._tilt_script = None + self._tilt_script: Script | None = None if (tilt_action := config.get(TILT_ACTION)) is not None: self._tilt_script = Script(hass, tilt_action, friendly_name, DOMAIN) optimistic = config.get(CONF_OPTIMISTIC) @@ -176,10 +177,10 @@ def __init__( ) tilt_optimistic = config.get(CONF_TILT_OPTIMISTIC) self._tilt_optimistic = tilt_optimistic or not self._tilt_template - self._position = None + self._position: int | None = None self._is_opening = False self._is_closing = False - self._tilt_value = None + self._tilt_value: int | None = None async def async_added_to_hass(self) -> None: """Register callbacks.""" @@ -206,7 +207,7 @@ async def async_added_to_hass(self) -> None: await super().async_added_to_hass() @callback - def _update_state(self, result): + def _update_state(self, result: Any | TemplateError) -> None: super()._update_state(result) if isinstance(result, TemplateError): self._position = None @@ -237,7 +238,11 @@ def _update_state(self, result): self._is_closing = False @callback - def _update_position(self, result): + def _update_position(self, result: Any) -> None: + if result is None: + self._position = None + return + try: state = float(result) except ValueError as err: @@ -252,10 +257,14 @@ def _update_position(self, result): state, ) else: - self._position = state + self._position = round(state) @callback - def _update_tilt(self, result): + def _update_tilt(self, result: Any) -> None: + if result is None: + self._tilt_value = None + return + try: state = float(result) except ValueError as err: @@ -270,7 +279,7 @@ def _update_tilt(self, result): state, ) else: - self._tilt_value = state + self._tilt_value = round(state) @property def is_closed(self) -> bool | None: @@ -365,6 +374,7 @@ async def async_stop_cover(self, **kwargs: Any) -> None: async def async_set_cover_position(self, **kwargs: Any) -> None: """Set cover position.""" self._position = kwargs[ATTR_POSITION] + assert self._position_script is not None await self.async_run_script( self._position_script, run_variables={"position": self._position}, @@ -376,6 +386,7 @@ async def async_set_cover_position(self, **kwargs: Any) -> None: async def async_open_cover_tilt(self, **kwargs: Any) -> None: """Tilt the cover open.""" self._tilt_value = 100 + assert self._tilt_script is not None await self.async_run_script( self._tilt_script, run_variables={"tilt": self._tilt_value}, @@ -387,6 +398,7 @@ async def async_open_cover_tilt(self, **kwargs: Any) -> None: async def async_close_cover_tilt(self, **kwargs: Any) -> None: """Tilt the cover closed.""" self._tilt_value = 0 + assert self._tilt_script is not None await self.async_run_script( self._tilt_script, run_variables={"tilt": self._tilt_value}, @@ -398,6 +410,7 @@ async def async_close_cover_tilt(self, **kwargs: Any) -> None: async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: """Move the cover tilt to a specific position.""" self._tilt_value = kwargs[ATTR_TILT_POSITION] + assert self._tilt_script is not None await self.async_run_script( self._tilt_script, run_variables={"tilt": self._tilt_value}, diff --git a/tests/components/template/test_cover.py b/tests/components/template/test_cover.py index adc41fe717b700..fefad59aa08735 100644 --- a/tests/components/template/test_cover.py +++ b/tests/components/template/test_cover.py @@ -1,4 +1,6 @@ """The tests for the Template cover platform.""" +from typing import Any + import pytest from homeassistant import setup @@ -149,6 +151,72 @@ async def test_template_state_text( assert text in caplog.text +@pytest.mark.parametrize(("count", "domain"), [(1, DOMAIN)]) +@pytest.mark.parametrize( + ("config", "entity", "set_state", "test_state", "attr"), + [ + ( + { + DOMAIN: { + "platform": "template", + "covers": { + "test_template_cover": { + **OPEN_CLOSE_COVER_CONFIG, + "position_template": ( + "{{ states.cover.test.attributes.position }}" + ), + "value_template": "{{ states.cover.test_state.state }}", + } + }, + } + }, + "cover.test_state", + "", + STATE_UNKNOWN, + {}, + ), + ( + { + DOMAIN: { + "platform": "template", + "covers": { + "test_template_cover": { + **OPEN_CLOSE_COVER_CONFIG, + "position_template": ( + "{{ states.cover.test.attributes.position }}" + ), + "value_template": "{{ states.cover.test_state.state }}", + } + }, + } + }, + "cover.test_state", + None, + STATE_UNKNOWN, + {}, + ), + ], +) +async def test_template_state_text_ignored_if_none_or_empty( + hass: HomeAssistant, + entity: str, + set_state: str, + test_state: str, + attr: dict[str, Any], + start_ha, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test ignoring an empty state text of a template.""" + state = hass.states.get("cover.test_template_cover") + assert state.state == STATE_UNKNOWN + + hass.states.async_set(entity, set_state, attributes=attr) + await hass.async_block_till_done() + state = hass.states.get("cover.test_template_cover") + assert state.state == test_state + assert "ERROR" not in caplog.text + + @pytest.mark.parametrize(("count", "domain"), [(1, DOMAIN)]) @pytest.mark.parametrize( "config", @@ -191,7 +259,9 @@ async def test_template_state_boolean(hass: HomeAssistant, start_ha) -> None: }, ], ) -async def test_template_position(hass: HomeAssistant, start_ha) -> None: +async def test_template_position( + hass: HomeAssistant, start_ha, caplog: pytest.LogCaptureFixture +) -> None: """Test the position_template attribute.""" hass.states.async_set("cover.test", STATE_OPEN) attrs = {} @@ -199,6 +269,7 @@ async def test_template_position(hass: HomeAssistant, start_ha) -> None: for set_state, pos, test_state in [ (STATE_CLOSED, 42, STATE_OPEN), (STATE_OPEN, 0.0, STATE_CLOSED), + (STATE_CLOSED, None, STATE_UNKNOWN), ]: attrs["position"] = pos hass.states.async_set("cover.test", set_state, attributes=attrs) @@ -206,6 +277,7 @@ async def test_template_position(hass: HomeAssistant, start_ha) -> None: state = hass.states.get("cover.test_template_cover") assert state.attributes.get("current_position") == pos assert state.state == test_state + assert "ValueError" not in caplog.text @pytest.mark.parametrize(("count", "domain"), [(1, DOMAIN)]) @@ -233,26 +305,46 @@ async def test_template_not_optimistic(hass: HomeAssistant, start_ha) -> None: @pytest.mark.parametrize(("count", "domain"), [(1, DOMAIN)]) @pytest.mark.parametrize( - "config", + ("config", "tilt_position"), [ - { - DOMAIN: { - "platform": "template", - "covers": { - "test_template_cover": { - **OPEN_CLOSE_COVER_CONFIG, - "value_template": "{{ 1 == 1 }}", - "tilt_template": "{{ 42 }}", - } - }, - } - }, + ( + { + DOMAIN: { + "platform": "template", + "covers": { + "test_template_cover": { + **OPEN_CLOSE_COVER_CONFIG, + "value_template": "{{ 1 == 1 }}", + "tilt_template": "{{ 42 }}", + } + }, + } + }, + 42.0, + ), + ( + { + DOMAIN: { + "platform": "template", + "covers": { + "test_template_cover": { + **OPEN_CLOSE_COVER_CONFIG, + "value_template": "{{ 1 == 1 }}", + "tilt_template": "{{ None }}", + } + }, + } + }, + None, + ), ], ) -async def test_template_tilt(hass: HomeAssistant, start_ha) -> None: +async def test_template_tilt( + hass: HomeAssistant, tilt_position: float | None, start_ha +) -> None: """Test the tilt_template attribute.""" state = hass.states.get("cover.test_template_cover") - assert state.attributes.get("current_tilt_position") == 42.0 + assert state.attributes.get("current_tilt_position") == tilt_position @pytest.mark.parametrize(("count", "domain"), [(1, DOMAIN)]) From a2531660712d52ad80473dac275fd6b912434d0e Mon Sep 17 00:00:00 2001 From: jbouwh Date: Wed, 12 Apr 2023 18:04:51 +0000 Subject: [PATCH 2/2] Remove not related changes --- homeassistant/components/template/cover.py | 34 ++++++++++------------ 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index 930069305c5984..256773b714ba89 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -139,11 +139,11 @@ class CoverTemplate(TemplateEntity, CoverEntity): def __init__( self, - hass: HomeAssistant, + hass, object_id, config, unique_id, - ) -> None: + ): """Initialize the Template cover.""" super().__init__( hass, config=config, fallback_name=object_id, unique_id=unique_id @@ -151,24 +151,24 @@ def __init__( self.entity_id = async_generate_entity_id( ENTITY_ID_FORMAT, object_id, hass=hass ) - friendly_name = self._attr_name or "" + friendly_name = self._attr_name self._template = config.get(CONF_VALUE_TEMPLATE) self._position_template = config.get(CONF_POSITION_TEMPLATE) self._tilt_template = config.get(CONF_TILT_TEMPLATE) self._device_class: CoverDeviceClass | None = config.get(CONF_DEVICE_CLASS) - self._open_script: Script | None = None + self._open_script = None if (open_action := config.get(OPEN_ACTION)) is not None: self._open_script = Script(hass, open_action, friendly_name, DOMAIN) - self._close_script: Script | None = None + self._close_script = None if (close_action := config.get(CLOSE_ACTION)) is not None: self._close_script = Script(hass, close_action, friendly_name, DOMAIN) - self._stop_script: Script | None = None + self._stop_script = None if (stop_action := config.get(STOP_ACTION)) is not None: self._stop_script = Script(hass, stop_action, friendly_name, DOMAIN) - self._position_script: Script | None = None + self._position_script = None if (position_action := config.get(POSITION_ACTION)) is not None: self._position_script = Script(hass, position_action, friendly_name, DOMAIN) - self._tilt_script: Script | None = None + self._tilt_script = None if (tilt_action := config.get(TILT_ACTION)) is not None: self._tilt_script = Script(hass, tilt_action, friendly_name, DOMAIN) optimistic = config.get(CONF_OPTIMISTIC) @@ -177,10 +177,10 @@ def __init__( ) tilt_optimistic = config.get(CONF_TILT_OPTIMISTIC) self._tilt_optimistic = tilt_optimistic or not self._tilt_template - self._position: int | None = None + self._position = None self._is_opening = False self._is_closing = False - self._tilt_value: int | None = None + self._tilt_value = None async def async_added_to_hass(self) -> None: """Register callbacks.""" @@ -207,7 +207,7 @@ async def async_added_to_hass(self) -> None: await super().async_added_to_hass() @callback - def _update_state(self, result: Any | TemplateError) -> None: + def _update_state(self, result): super()._update_state(result) if isinstance(result, TemplateError): self._position = None @@ -238,7 +238,7 @@ def _update_state(self, result: Any | TemplateError) -> None: self._is_closing = False @callback - def _update_position(self, result: Any) -> None: + def _update_position(self, result): if result is None: self._position = None return @@ -257,10 +257,10 @@ def _update_position(self, result: Any) -> None: state, ) else: - self._position = round(state) + self._position = state @callback - def _update_tilt(self, result: Any) -> None: + def _update_tilt(self, result): if result is None: self._tilt_value = None return @@ -279,7 +279,7 @@ def _update_tilt(self, result: Any) -> None: state, ) else: - self._tilt_value = round(state) + self._tilt_value = state @property def is_closed(self) -> bool | None: @@ -374,7 +374,6 @@ async def async_stop_cover(self, **kwargs: Any) -> None: async def async_set_cover_position(self, **kwargs: Any) -> None: """Set cover position.""" self._position = kwargs[ATTR_POSITION] - assert self._position_script is not None await self.async_run_script( self._position_script, run_variables={"position": self._position}, @@ -386,7 +385,6 @@ async def async_set_cover_position(self, **kwargs: Any) -> None: async def async_open_cover_tilt(self, **kwargs: Any) -> None: """Tilt the cover open.""" self._tilt_value = 100 - assert self._tilt_script is not None await self.async_run_script( self._tilt_script, run_variables={"tilt": self._tilt_value}, @@ -398,7 +396,6 @@ async def async_open_cover_tilt(self, **kwargs: Any) -> None: async def async_close_cover_tilt(self, **kwargs: Any) -> None: """Tilt the cover closed.""" self._tilt_value = 0 - assert self._tilt_script is not None await self.async_run_script( self._tilt_script, run_variables={"tilt": self._tilt_value}, @@ -410,7 +407,6 @@ async def async_close_cover_tilt(self, **kwargs: Any) -> None: async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: """Move the cover tilt to a specific position.""" self._tilt_value = kwargs[ATTR_TILT_POSITION] - assert self._tilt_script is not None await self.async_run_script( self._tilt_script, run_variables={"tilt": self._tilt_value},