Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 2 commits into from
Jan 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 self.supported_features & CoverEntityFeature.STOP 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