Skip to content
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
39 changes: 26 additions & 13 deletions homeassistant/components/velux/cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,17 @@ def __init__(self, node: OpeningDevice, config_entry_id: str) -> None:
self._attr_device_class = CoverDeviceClass.SHUTTER

@property
def current_cover_position(self) -> int:
def current_cover_position(self) -> int | None:
"""Return the current position of the cover."""
if not self.node.position.known:
return None
return 100 - self.node.position.position_percent

@property
def is_closed(self) -> bool:
def is_closed(self) -> bool | None:
"""Return if the cover is closed."""
if not self.node.position.known:
return None
return self.node.position.closed

@property
Expand Down Expand Up @@ -168,22 +172,29 @@ def __init__(
self.part = part

@property
def current_cover_position(self) -> int:
"""Return the current position of the cover."""
def _part_position(self) -> Position:
"""Return the pyvlx Position for this part of the shutter."""
if self.part == VeluxDualRollerPart.UPPER:
return 100 - self.node.position_upper_curtain.position_percent
return self.node.position_upper_curtain
if self.part == VeluxDualRollerPart.LOWER:
return 100 - self.node.position_lower_curtain.position_percent
return 100 - self.node.position.position_percent
return self.node.position_lower_curtain
return self.node.position

@property
def current_cover_position(self) -> int | None:
"""Return the current position of the cover."""
position = self._part_position
if not position.known:
return None
return 100 - position.position_percent

@property
def is_closed(self) -> bool:
def is_closed(self) -> bool | None:
"""Return if the cover is closed."""
if self.part == VeluxDualRollerPart.UPPER:
return self.node.position_upper_curtain.closed
if self.part == VeluxDualRollerPart.LOWER:
return self.node.position_lower_curtain.closed
return self.node.position.closed
position = self._part_position
if not position.known:
return None
return position.closed

@wrap_pyvlx_call_exceptions
async def async_close_cover(self, **kwargs: Any) -> None:
Expand Down Expand Up @@ -227,6 +238,8 @@ def __init__(self, node: Blind, config_entry_id: str) -> None:
@property
def current_cover_tilt_position(self) -> int | None:
"""Return the current tilt position of the cover."""
if not self.node.orientation.known:
return None
return 100 - self.node.orientation.position_percent

@wrap_pyvlx_call_exceptions
Expand Down
26 changes: 17 additions & 9 deletions tests/components/velux/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def mock_window() -> AsyncMock:
window.device_updated_cbs = []
window.is_opening = False
window.is_closing = False
window.position = MagicMock(position_percent=30, closed=False)
window.position = MagicMock(position_percent=30, closed=False, known=True)
window.wink = AsyncMock()
window.pyvlx = MagicMock()
return window
Expand All @@ -89,9 +89,13 @@ def mock_dual_roller_shutter() -> AsyncMock:
cover.serial_number = "987654321"
cover.is_opening = False
cover.is_closing = False
cover.position_upper_curtain = MagicMock(position_percent=30, closed=False)
cover.position_lower_curtain = MagicMock(position_percent=30, closed=False)
cover.position = MagicMock(position_percent=30, closed=False)
cover.position_upper_curtain = MagicMock(
position_percent=30, closed=False, known=True
)
cover.position_lower_curtain = MagicMock(
position_percent=30, closed=False, known=True
)
cover.position = MagicMock(position_percent=30, closed=False, known=True)
cover.pyvlx = MagicMock()
return cover

Expand All @@ -104,11 +108,11 @@ def mock_blind() -> AsyncMock:
blind.name = "Test Blind"
blind.serial_number = "4711"
# Standard cover position (used by current_cover_position)
blind.position = MagicMock(position_percent=40, closed=False)
blind.position = MagicMock(position_percent=40, closed=False, known=True)
blind.is_opening = False
blind.is_closing = False
# Orientation/tilt-related attributes and methods
blind.orientation = MagicMock(position_percent=25)
blind.orientation = MagicMock(position_percent=25, known=True)
blind.open_orientation = AsyncMock()
blind.close_orientation = AsyncMock()
blind.stop_orientation = AsyncMock()
Expand Down Expand Up @@ -175,9 +179,13 @@ def mock_cover_type(request: pytest.FixtureRequest) -> AsyncMock:
cover.serial_number = f"serial_{request.param.__name__}"
cover.is_opening = False
cover.is_closing = False
cover.position = MagicMock(position_percent=30, closed=False)
cover.position_upper_curtain = MagicMock(position_percent=30, closed=False)
cover.position_lower_curtain = MagicMock(position_percent=30, closed=False)
cover.position = MagicMock(position_percent=30, closed=False, known=True)
cover.position_upper_curtain = MagicMock(
position_percent=30, closed=False, known=True
)
cover.position_lower_curtain = MagicMock(
position_percent=30, closed=False, known=True
)
cover.pyvlx = MagicMock()
return cover

Expand Down
72 changes: 72 additions & 0 deletions tests/components/velux/test_cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
STATE_CLOSING,
STATE_OPEN,
STATE_OPENING,
STATE_UNKNOWN,
Platform,
)
from homeassistant.core import HomeAssistant
Expand Down Expand Up @@ -475,6 +476,77 @@ async def test_non_blind_has_no_tilt_position(
assert "current_tilt_position" not in state.attributes


# Unknown position tests


async def test_window_unknown_position(
hass: HomeAssistant, mock_window: AsyncMock
) -> None:
"""When the device position is not known, state and position must be unknown."""

entity_id = "cover.test_window"

mock_window.position.known = False
await update_callback_entity(hass, mock_window)

state = hass.states.get(entity_id)
assert state is not None
assert state.state == STATE_UNKNOWN
assert state.attributes.get("current_position") is None


@pytest.mark.parametrize(
("unknown_attr", "unknown_entity_id"),
[
("position", "cover.test_dual_roller_shutter"),
("position_upper_curtain", "cover.test_dual_roller_shutter_upper_shutter"),
("position_lower_curtain", "cover.test_dual_roller_shutter_lower_shutter"),
],
)
async def test_dual_roller_shutter_unknown_position(
hass: HomeAssistant,
mock_dual_roller_shutter: AsyncMock,
unknown_attr: str,
unknown_entity_id: str,
) -> None:
"""Each part falls back to unknown independently when only its position is unknown."""

all_entity_ids = {
"cover.test_dual_roller_shutter",
"cover.test_dual_roller_shutter_upper_shutter",
"cover.test_dual_roller_shutter_lower_shutter",
}

getattr(mock_dual_roller_shutter, unknown_attr).known = False
await update_callback_entity(hass, mock_dual_roller_shutter)

state = hass.states.get(unknown_entity_id)
assert state is not None
assert state.state == STATE_UNKNOWN
assert state.attributes.get("current_position") is None

for entity_id in all_entity_ids - {unknown_entity_id}:
state = hass.states.get(entity_id)
assert state is not None
assert state.state != STATE_UNKNOWN
assert state.attributes.get("current_position") == 70


async def test_blind_unknown_tilt_position(
hass: HomeAssistant, mock_blind: AsyncMock
) -> None:
"""Tilt position must be None when the orientation is not known."""

entity_id = "cover.test_blind"

mock_blind.orientation.known = False
await update_callback_entity(hass, mock_blind)

state = hass.states.get(entity_id)
assert state is not None
assert state.attributes.get("current_tilt_position") is None


# Exception handling tests


Expand Down
Loading