From bf52c69b812632c7169b2e6416db2e2dee2118eb Mon Sep 17 00:00:00 2001 From: Reagan Sanders Date: Sat, 6 Jan 2024 23:02:14 -0500 Subject: [PATCH] Prevent toggle from calling stop on covers which do not support it --- homeassistant/components/cover/__init__.py | 2 +- tests/components/cover/test_init.py | 22 +++++++++- .../custom_components/test/cover.py | 44 +++++++++++++------ 3 files changed, 52 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index 945585de522990..ba773ca9335e63 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -481,7 +481,7 @@ async def async_toggle_tilt(self, **kwargs: Any) -> None: def _get_toggle_function( self, fns: dict[str, Callable[_P, _R]] ) -> Callable[_P, _R]: - if CoverEntityFeature.STOP | self.supported_features and ( + if CoverEntityFeature.STOP in self.supported_features and ( self.is_closing or self.is_opening ): return fns["stop"] diff --git a/tests/components/cover/test_init.py b/tests/components/cover/test_init.py index 480d1ef83aa571..0503017f634f32 100644 --- a/tests/components/cover/test_init.py +++ b/tests/components/cover/test_init.py @@ -34,7 +34,8 @@ async def test_services(hass: HomeAssistant, enable_custom_integrations: None) - # ent3 = cover with simple tilt functions and no position # ent4 = cover with all tilt functions but no position # ent5 = cover with all functions - ent1, ent2, ent3, ent4, ent5 = platform.ENTITIES + # ent6 = cover with only open/close, but also reports opening/closing + ent1, ent2, ent3, ent4, ent5, ent6 = platform.ENTITIES # Test init all covers should be open assert is_open(hass, ent1) @@ -42,6 +43,7 @@ async def test_services(hass: HomeAssistant, enable_custom_integrations: None) - assert is_open(hass, ent3) assert is_open(hass, ent4) assert is_open(hass, ent5) + assert is_open(hass, ent6) # call basic toggle services await call_service(hass, SERVICE_TOGGLE, ent1) @@ -49,13 +51,15 @@ async def test_services(hass: HomeAssistant, enable_custom_integrations: None) - await call_service(hass, SERVICE_TOGGLE, ent3) await call_service(hass, SERVICE_TOGGLE, ent4) await call_service(hass, SERVICE_TOGGLE, ent5) + await call_service(hass, SERVICE_TOGGLE, ent6) - # entities without stop should be closed and with stop should be closing + # entities should be either closed or closing, depending on if they report transitional states assert is_closed(hass, ent1) assert is_closing(hass, ent2) assert is_closed(hass, ent3) assert is_closed(hass, ent4) assert is_closing(hass, ent5) + assert is_closing(hass, ent6) # call basic toggle services and set different cover position states await call_service(hass, SERVICE_TOGGLE, ent1) @@ -65,6 +69,7 @@ async def test_services(hass: HomeAssistant, enable_custom_integrations: None) - await call_service(hass, SERVICE_TOGGLE, ent4) set_cover_position(ent5, 15) await call_service(hass, SERVICE_TOGGLE, ent5) + await call_service(hass, SERVICE_TOGGLE, ent6) # entities should be in correct state depending on the SUPPORT_STOP feature and cover position assert is_open(hass, ent1) @@ -72,6 +77,7 @@ async def test_services(hass: HomeAssistant, enable_custom_integrations: None) - assert is_open(hass, ent3) assert is_open(hass, ent4) assert is_open(hass, ent5) + assert is_opening(hass, ent6) # call basic toggle services await call_service(hass, SERVICE_TOGGLE, ent1) @@ -79,6 +85,7 @@ async def test_services(hass: HomeAssistant, enable_custom_integrations: None) - await call_service(hass, SERVICE_TOGGLE, ent3) await call_service(hass, SERVICE_TOGGLE, ent4) await call_service(hass, SERVICE_TOGGLE, ent5) + await call_service(hass, SERVICE_TOGGLE, ent6) # entities should be in correct state depending on the SUPPORT_STOP feature and cover position assert is_closed(hass, ent1) @@ -86,6 +93,12 @@ async def test_services(hass: HomeAssistant, enable_custom_integrations: None) - assert is_closed(hass, ent3) assert is_closed(hass, ent4) assert is_opening(hass, ent5) + assert is_closing(hass, ent6) + + # Without STOP but still reports opening/closing has a 4th possible toggle state + set_state(ent6, STATE_CLOSED) + await call_service(hass, SERVICE_TOGGLE, ent6) + assert is_opening(hass, ent6) def call_service(hass, service, ent): @@ -100,6 +113,11 @@ def set_cover_position(ent, position) -> None: ent._values["current_cover_position"] = position +def set_state(ent, state) -> None: + """Set the state of a cover.""" + ent._values["state"] = state + + def is_open(hass, ent): """Return if the cover is closed based on the statemachine.""" return hass.states.is_state(ent.entity_id, STATE_OPEN) diff --git a/tests/testing_config/custom_components/test/cover.py b/tests/testing_config/custom_components/test/cover.py index 2a57412ea9e595..dc89b95981b1ab 100644 --- a/tests/testing_config/custom_components/test/cover.py +++ b/tests/testing_config/custom_components/test/cover.py @@ -2,6 +2,8 @@ Call init before using it in your tests to ensure clean test data. """ +from typing import Any + from homeassistant.components.cover import CoverEntity, CoverEntityFeature from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING @@ -70,6 +72,13 @@ def init(empty=False): | CoverEntityFeature.STOP_TILT | CoverEntityFeature.SET_TILT_POSITION, ), + MockCover( + name="Simple with opening/closing cover", + is_on=True, + unique_id="unique_opening_closing_cover", + supported_features=CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE, + reports_opening_closing=True, + ), ] ) @@ -84,50 +93,59 @@ async def async_setup_platform( class MockCover(MockEntity, CoverEntity): """Mock Cover class.""" + def __init__( + self, reports_opening_closing: bool | None = None, **values: Any + ) -> None: + """Initialize a mock cover entity.""" + + super().__init__(**values) + self._reports_opening_closing = ( + reports_opening_closing + if reports_opening_closing is not None + else CoverEntityFeature.STOP in self.supported_features + ) + @property def is_closed(self): """Return if the cover is closed or not.""" - if self.supported_features & CoverEntityFeature.STOP: - return self.current_cover_position == 0 + if "state" in self._values and self._values["state"] == STATE_CLOSED: + return True - if "state" in self._values: - return self._values["state"] == STATE_CLOSED - return False + return self.current_cover_position == 0 @property def is_opening(self): """Return if the cover is opening or not.""" - if self.supported_features & CoverEntityFeature.STOP: - if "state" in self._values: - return self._values["state"] == STATE_OPENING + if "state" in self._values: + return self._values["state"] == STATE_OPENING return False @property def is_closing(self): """Return if the cover is closing or not.""" - if self.supported_features & CoverEntityFeature.STOP: - if "state" in self._values: - return self._values["state"] == STATE_CLOSING + if "state" in self._values: + return self._values["state"] == STATE_CLOSING return False def open_cover(self, **kwargs) -> None: """Open cover.""" - if self.supported_features & CoverEntityFeature.STOP: + if self._reports_opening_closing: self._values["state"] = STATE_OPENING else: self._values["state"] = STATE_OPEN def close_cover(self, **kwargs) -> None: """Close cover.""" - if self.supported_features & CoverEntityFeature.STOP: + if self._reports_opening_closing: self._values["state"] = STATE_CLOSING else: self._values["state"] = STATE_CLOSED def stop_cover(self, **kwargs) -> None: """Stop cover.""" + assert CoverEntityFeature.STOP in self.supported_features self._values["state"] = STATE_CLOSED if self.is_closed else STATE_OPEN @property