Skip to content

Commit

Permalink
Prevent toggle from calling stop on covers which do not support it
Browse files Browse the repository at this point in the history
  • Loading branch information
vexofp committed Jan 7, 2024
1 parent 50fbcaf commit bf52c69
Show file tree
Hide file tree
Showing 3 changed files with 52 additions and 16 deletions.
2 changes: 1 addition & 1 deletion homeassistant/components/cover/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
22 changes: 20 additions & 2 deletions tests/components/cover/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,28 +34,32 @@ 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)
assert is_open(hass, ent2)
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)
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)
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)
Expand All @@ -65,27 +69,36 @@ 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)
assert is_closed(hass, ent2)
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)
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)
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)
assert is_opening(hass, ent2)
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):
Expand All @@ -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)
Expand Down
44 changes: 31 additions & 13 deletions tests/testing_config/custom_components/test/cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
),
]
)

Expand All @@ -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
Expand Down

0 comments on commit bf52c69

Please sign in to comment.