diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index 7a3061d24c8..2f4abff749e 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -187,6 +187,8 @@ class CoverEntity(Entity): _attr_is_opening: bool | None = None _attr_state: None = None + _cover_is_last_toggle_direction_open = True + @property def current_cover_position(self) -> int | None: """Return current position of cover. @@ -208,8 +210,10 @@ class CoverEntity(Entity): def state(self) -> str | None: """Return the state of the cover.""" if self.is_opening: + self._cover_is_last_toggle_direction_open = True return STATE_OPENING if self.is_closing: + self._cover_is_last_toggle_direction_open = False return STATE_CLOSING if (closed := self.is_closed) is None: @@ -285,17 +289,23 @@ class CoverEntity(Entity): def toggle(self, **kwargs: Any) -> None: """Toggle the entity.""" - if self.is_closed: - self.open_cover(**kwargs) - else: - self.close_cover(**kwargs) + fns = { + "open": self.open_cover, + "close": self.close_cover, + "stop": self.stop_cover, + } + function = self._get_toggle_function(fns) + function(**kwargs) async def async_toggle(self, **kwargs): """Toggle the entity.""" - if self.is_closed: - await self.async_open_cover(**kwargs) - else: - await self.async_close_cover(**kwargs) + fns = { + "open": self.async_open_cover, + "close": self.async_close_cover, + "stop": self.async_stop_cover, + } + function = self._get_toggle_function(fns) + await function(**kwargs) def set_cover_position(self, **kwargs): """Move the cover to a specific position.""" @@ -363,6 +373,17 @@ class CoverEntity(Entity): else: await self.async_close_cover_tilt(**kwargs) + def _get_toggle_function(self, fns): + if SUPPORT_STOP | self.supported_features and ( + self.is_closing or self.is_opening + ): + return fns["stop"] + if self.is_closed: + return fns["open"] + if self._cover_is_last_toggle_direction_open: + return fns["close"] + return fns["open"] + class CoverDevice(CoverEntity): """Representation of a cover (for backwards compatibility).""" diff --git a/tests/components/cover/test_device_action.py b/tests/components/cover/test_device_action.py index 0dc76aa7e61..ab7b6b91699 100644 --- a/tests/components/cover/test_device_action.py +++ b/tests/components/cover/test_device_action.py @@ -110,7 +110,7 @@ async def test_get_action_capabilities( """Test we get the expected capabilities from a cover action.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() - ent = platform.ENTITIES[0] + ent = platform.ENTITIES[2] config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -126,7 +126,7 @@ async def test_get_action_capabilities( await hass.async_block_till_done() actions = await async_get_device_automations(hass, "action", device_entry.id) - assert len(actions) == 3 # open, close, stop + assert len(actions) == 4 # open, close, stop, set_position for action in actions: capabilities = await async_get_device_automation_capabilities( hass, "action", action diff --git a/tests/components/cover/test_device_condition.py b/tests/components/cover/test_device_condition.py index 0812a7fefa0..efa8e8b3383 100644 --- a/tests/components/cover/test_device_condition.py +++ b/tests/components/cover/test_device_condition.py @@ -196,7 +196,7 @@ async def test_get_condition_capabilities_set_tilt_pos( """Test we get the expected capabilities from a cover condition.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() - ent = platform.ENTITIES[2] + ent = platform.ENTITIES[3] config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -487,7 +487,7 @@ async def test_if_tilt_position(hass, calls, caplog, enable_custom_integrations) """Test for tilt position conditions.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() - ent = platform.ENTITIES[2] + ent = platform.ENTITIES[3] assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() diff --git a/tests/components/cover/test_device_trigger.py b/tests/components/cover/test_device_trigger.py index b7f15de1e3c..0c7c99bc521 100644 --- a/tests/components/cover/test_device_trigger.py +++ b/tests/components/cover/test_device_trigger.py @@ -228,7 +228,7 @@ async def test_get_trigger_capabilities_set_tilt_pos( """Test we get the expected capabilities from a cover trigger.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() - ent = platform.ENTITIES[2] + ent = platform.ENTITIES[3] config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) diff --git a/tests/components/cover/test_init.py b/tests/components/cover/test_init.py index df8df2c4bf1..b46c0417cd2 100644 --- a/tests/components/cover/test_init.py +++ b/tests/components/cover/test_init.py @@ -1,5 +1,116 @@ """The tests for Cover.""" import homeassistant.components.cover as cover +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_PLATFORM, + SERVICE_TOGGLE, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, +) +from homeassistant.setup import async_setup_component + + +async def test_services(hass, enable_custom_integrations): + """Test the provided services.""" + platform = getattr(hass.components, "test.cover") + + platform.init() + assert await async_setup_component( + hass, cover.DOMAIN, {cover.DOMAIN: {CONF_PLATFORM: "test"}} + ) + await hass.async_block_till_done() + + # ent1 = cover without tilt and position + # ent2 = cover with position but no tilt + # 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 + + # Test init all covers should be open + assert is_open(hass, ent1) + assert is_open(hass, ent2) + assert is_open(hass, ent3) + assert is_open(hass, ent4) + assert is_open(hass, ent5) + + # call basic toggle services + await call_service(hass, SERVICE_TOGGLE, ent1) + await call_service(hass, SERVICE_TOGGLE, ent2) + await call_service(hass, SERVICE_TOGGLE, ent3) + await call_service(hass, SERVICE_TOGGLE, ent4) + await call_service(hass, SERVICE_TOGGLE, ent5) + + # entities without stop should be closed and with stop should be closing + 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) + + # call basic toggle services and set different cover position states + await call_service(hass, SERVICE_TOGGLE, ent1) + set_cover_position(ent2, 0) + await call_service(hass, SERVICE_TOGGLE, ent2) + await call_service(hass, SERVICE_TOGGLE, ent3) + await call_service(hass, SERVICE_TOGGLE, ent4) + set_cover_position(ent5, 15) + await call_service(hass, SERVICE_TOGGLE, ent5) + + # entities should be in correct state depending on the SUPPORT_STOP feature and cover position + assert is_open(hass, ent1) + assert is_closed(hass, ent2) + assert is_open(hass, ent3) + assert is_open(hass, ent4) + assert is_open(hass, ent5) + + # call basic toggle services + await call_service(hass, SERVICE_TOGGLE, ent1) + await call_service(hass, SERVICE_TOGGLE, ent2) + await call_service(hass, SERVICE_TOGGLE, ent3) + await call_service(hass, SERVICE_TOGGLE, ent4) + await call_service(hass, SERVICE_TOGGLE, ent5) + + # entities should be in correct state depending on the SUPPORT_STOP feature and cover position + assert is_closed(hass, ent1) + assert is_opening(hass, ent2) + assert is_closed(hass, ent3) + assert is_closed(hass, ent4) + assert is_opening(hass, ent5) + + +def call_service(hass, service, ent): + """Call any service on entity.""" + return hass.services.async_call( + cover.DOMAIN, service, {ATTR_ENTITY_ID: ent.entity_id}, blocking=True + ) + + +def set_cover_position(ent, position) -> None: + """Set a position value to a cover.""" + ent._values["current_cover_position"] = position + + +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) + + +def is_opening(hass, ent): + """Return if the cover is closed based on the statemachine.""" + return hass.states.is_state(ent.entity_id, STATE_OPENING) + + +def is_closed(hass, ent): + """Return if the cover is closed based on the statemachine.""" + return hass.states.is_state(ent.entity_id, STATE_CLOSED) + + +def is_closing(hass, ent): + """Return if the cover is closed based on the statemachine.""" + return hass.states.is_state(ent.entity_id, STATE_CLOSING) def test_deprecated_base_class(caplog): diff --git a/tests/testing_config/custom_components/test/cover.py b/tests/testing_config/custom_components/test/cover.py index 095489ce7b4..edd8965e4e9 100644 --- a/tests/testing_config/custom_components/test/cover.py +++ b/tests/testing_config/custom_components/test/cover.py @@ -14,6 +14,7 @@ from homeassistant.components.cover import ( SUPPORT_STOP_TILT, CoverEntity, ) +from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING from tests.common import MockEntity @@ -32,27 +33,53 @@ def init(empty=False): name="Simple cover", is_on=True, unique_id="unique_cover", - supports_tilt=False, + supported_features=SUPPORT_OPEN | SUPPORT_CLOSE, ), MockCover( name="Set position cover", is_on=True, unique_id="unique_set_pos_cover", current_cover_position=50, - supports_tilt=False, + supported_features=SUPPORT_OPEN + | SUPPORT_CLOSE + | SUPPORT_STOP + | SUPPORT_SET_POSITION, + ), + MockCover( + name="Simple tilt cover", + is_on=True, + unique_id="unique_tilt_cover", + supported_features=SUPPORT_OPEN + | SUPPORT_CLOSE + | SUPPORT_OPEN_TILT + | SUPPORT_CLOSE_TILT, ), MockCover( name="Set tilt position cover", is_on=True, unique_id="unique_set_pos_tilt_cover", current_cover_tilt_position=50, - supports_tilt=True, + supported_features=SUPPORT_OPEN + | SUPPORT_CLOSE + | SUPPORT_OPEN_TILT + | SUPPORT_CLOSE_TILT + | SUPPORT_STOP_TILT + | SUPPORT_SET_TILT_POSITION, ), MockCover( - name="Tilt cover", + name="All functions cover", is_on=True, - unique_id="unique_tilt_cover", - supports_tilt=True, + unique_id="unique_all_functions_cover", + current_cover_position=50, + current_cover_tilt_position=50, + supported_features=SUPPORT_OPEN + | SUPPORT_CLOSE + | SUPPORT_STOP + | SUPPORT_SET_POSITION + | SUPPORT_OPEN_TILT + | SUPPORT_CLOSE_TILT + | SUPPORT_STOP_TILT + | SUPPORT_SET_TILT_POSITION, ), ] ) @@ -71,8 +98,54 @@ class MockCover(MockEntity, CoverEntity): @property def is_closed(self): """Return if the cover is closed or not.""" + if self.supported_features & SUPPORT_STOP: + return self.current_cover_position == 0 + + if "state" in self._values: + return self._values["state"] == STATE_CLOSED return False + @property + def is_opening(self): + """Return if the cover is opening or not.""" + if self.supported_features & SUPPORT_STOP: + 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 & SUPPORT_STOP: + 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 & SUPPORT_STOP: + self._values["state"] = STATE_OPENING + else: + self._values["state"] = STATE_OPEN + + def close_cover(self, **kwargs) -> None: + """Close cover.""" + if self.supported_features & SUPPORT_STOP: + self._values["state"] = STATE_CLOSING + else: + self._values["state"] = STATE_CLOSED + + def stop_cover(self, **kwargs) -> None: + """Stop cover.""" + self._values["state"] = STATE_CLOSED if self.is_closed else STATE_OPEN + + @property + def state(self): + """Fake State.""" + return CoverEntity.state.fget(self) + @property def current_cover_position(self): """Return current position of cover.""" @@ -82,26 +155,3 @@ class MockCover(MockEntity, CoverEntity): def current_cover_tilt_position(self): """Return current position of cover tilt.""" return self._handle("current_cover_tilt_position") - - @property - def supported_features(self): - """Flag supported features.""" - supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP - - if self._handle("supports_tilt"): - supported_features |= ( - SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT | SUPPORT_STOP_TILT - ) - - if self.current_cover_position is not None: - supported_features |= SUPPORT_SET_POSITION - - if self.current_cover_tilt_position is not None: - supported_features |= ( - SUPPORT_OPEN_TILT - | SUPPORT_CLOSE_TILT - | SUPPORT_STOP_TILT - | SUPPORT_SET_TILT_POSITION - ) - - return supported_features