Prevent toggle from calling stop on covers which do not support it (#106848)

* Prevent toggle from calling stop on covers which do not support it

* Update homeassistant/components/cover/__init__.py

---------

Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
vexofp 2024-01-09 06:32:27 -05:00 committed by GitHub
parent 3a36117c08
commit 3c53693fe3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 52 additions and 16 deletions

View File

@ -481,7 +481,7 @@ class CoverEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
def _get_toggle_function( def _get_toggle_function(
self, fns: dict[str, Callable[_P, _R]] self, fns: dict[str, Callable[_P, _R]]
) -> Callable[_P, _R]: ) -> Callable[_P, _R]:
if CoverEntityFeature.STOP | self.supported_features and ( if self.supported_features & CoverEntityFeature.STOP and (
self.is_closing or self.is_opening self.is_closing or self.is_opening
): ):
return fns["stop"] return fns["stop"]

View File

@ -34,7 +34,8 @@ async def test_services(hass: HomeAssistant, enable_custom_integrations: None) -
# ent3 = cover with simple tilt functions and no position # ent3 = cover with simple tilt functions and no position
# ent4 = cover with all tilt functions but no position # ent4 = cover with all tilt functions but no position
# ent5 = cover with all functions # 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 # Test init all covers should be open
assert is_open(hass, ent1) 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, ent3)
assert is_open(hass, ent4) assert is_open(hass, ent4)
assert is_open(hass, ent5) assert is_open(hass, ent5)
assert is_open(hass, ent6)
# call basic toggle services # call basic toggle services
await call_service(hass, SERVICE_TOGGLE, ent1) 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, ent3)
await call_service(hass, SERVICE_TOGGLE, ent4) await call_service(hass, SERVICE_TOGGLE, ent4)
await call_service(hass, SERVICE_TOGGLE, ent5) 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_closed(hass, ent1)
assert is_closing(hass, ent2) assert is_closing(hass, ent2)
assert is_closed(hass, ent3) assert is_closed(hass, ent3)
assert is_closed(hass, ent4) assert is_closed(hass, ent4)
assert is_closing(hass, ent5) assert is_closing(hass, ent5)
assert is_closing(hass, ent6)
# call basic toggle services and set different cover position states # call basic toggle services and set different cover position states
await call_service(hass, SERVICE_TOGGLE, ent1) 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) await call_service(hass, SERVICE_TOGGLE, ent4)
set_cover_position(ent5, 15) set_cover_position(ent5, 15)
await call_service(hass, SERVICE_TOGGLE, ent5) 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 # entities should be in correct state depending on the SUPPORT_STOP feature and cover position
assert is_open(hass, ent1) 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, ent3)
assert is_open(hass, ent4) assert is_open(hass, ent4)
assert is_open(hass, ent5) assert is_open(hass, ent5)
assert is_opening(hass, ent6)
# call basic toggle services # call basic toggle services
await call_service(hass, SERVICE_TOGGLE, ent1) 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, ent3)
await call_service(hass, SERVICE_TOGGLE, ent4) await call_service(hass, SERVICE_TOGGLE, ent4)
await call_service(hass, SERVICE_TOGGLE, ent5) 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 # entities should be in correct state depending on the SUPPORT_STOP feature and cover position
assert is_closed(hass, ent1) 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, ent3)
assert is_closed(hass, ent4) assert is_closed(hass, ent4)
assert is_opening(hass, ent5) 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): def call_service(hass, service, ent):
@ -100,6 +113,11 @@ def set_cover_position(ent, position) -> None:
ent._values["current_cover_position"] = position 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): def is_open(hass, ent):
"""Return if the cover is closed based on the statemachine.""" """Return if the cover is closed based on the statemachine."""
return hass.states.is_state(ent.entity_id, STATE_OPEN) return hass.states.is_state(ent.entity_id, STATE_OPEN)

View File

@ -2,6 +2,8 @@
Call init before using it in your tests to ensure clean test data. 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.components.cover import CoverEntity, CoverEntityFeature
from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING
@ -70,6 +72,13 @@ def init(empty=False):
| CoverEntityFeature.STOP_TILT | CoverEntityFeature.STOP_TILT
| CoverEntityFeature.SET_TILT_POSITION, | 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): class MockCover(MockEntity, CoverEntity):
"""Mock Cover class.""" """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 @property
def is_closed(self): def is_closed(self):
"""Return if the cover is closed or not.""" """Return if the cover is closed or not."""
if self.supported_features & CoverEntityFeature.STOP: if "state" in self._values and self._values["state"] == STATE_CLOSED:
return self.current_cover_position == 0 return True
if "state" in self._values: return self.current_cover_position == 0
return self._values["state"] == STATE_CLOSED
return False
@property @property
def is_opening(self): def is_opening(self):
"""Return if the cover is opening or not.""" """Return if the cover is opening or not."""
if self.supported_features & CoverEntityFeature.STOP: if "state" in self._values:
if "state" in self._values: return self._values["state"] == STATE_OPENING
return self._values["state"] == STATE_OPENING
return False return False
@property @property
def is_closing(self): def is_closing(self):
"""Return if the cover is closing or not.""" """Return if the cover is closing or not."""
if self.supported_features & CoverEntityFeature.STOP: if "state" in self._values:
if "state" in self._values: return self._values["state"] == STATE_CLOSING
return self._values["state"] == STATE_CLOSING
return False return False
def open_cover(self, **kwargs) -> None: def open_cover(self, **kwargs) -> None:
"""Open cover.""" """Open cover."""
if self.supported_features & CoverEntityFeature.STOP: if self._reports_opening_closing:
self._values["state"] = STATE_OPENING self._values["state"] = STATE_OPENING
else: else:
self._values["state"] = STATE_OPEN self._values["state"] = STATE_OPEN
def close_cover(self, **kwargs) -> None: def close_cover(self, **kwargs) -> None:
"""Close cover.""" """Close cover."""
if self.supported_features & CoverEntityFeature.STOP: if self._reports_opening_closing:
self._values["state"] = STATE_CLOSING self._values["state"] = STATE_CLOSING
else: else:
self._values["state"] = STATE_CLOSED self._values["state"] = STATE_CLOSED
def stop_cover(self, **kwargs) -> None: def stop_cover(self, **kwargs) -> None:
"""Stop cover.""" """Stop cover."""
assert CoverEntityFeature.STOP in self.supported_features
self._values["state"] = STATE_CLOSED if self.is_closed else STATE_OPEN self._values["state"] = STATE_CLOSED if self.is_closed else STATE_OPEN
@property @property