mirror of
https://github.com/Electric-Special/ha-core.git
synced 2026-03-21 03:03:17 +01:00
add upper and lower shutter of Velux dualrollershutters as entities (#162998)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -2,11 +2,13 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import StrEnum
|
||||
from typing import Any
|
||||
|
||||
from pyvlx import (
|
||||
from pyvlx.opening_device import (
|
||||
Awning,
|
||||
Blind,
|
||||
DualRollerShutter,
|
||||
GarageDoor,
|
||||
Gate,
|
||||
OpeningDevice,
|
||||
@@ -43,6 +45,23 @@ async def async_setup_entry(
|
||||
for node in pyvlx.nodes:
|
||||
if isinstance(node, Blind):
|
||||
entities.append(VeluxBlind(node, config_entry.entry_id))
|
||||
elif isinstance(node, DualRollerShutter):
|
||||
# add three entities, one for each part and the "dual" control
|
||||
entities.append(
|
||||
VeluxDualRollerShutter(
|
||||
node, config_entry.entry_id, VeluxDualRollerPart.DUAL
|
||||
)
|
||||
)
|
||||
entities.append(
|
||||
VeluxDualRollerShutter(
|
||||
node, config_entry.entry_id, VeluxDualRollerPart.UPPER
|
||||
)
|
||||
)
|
||||
entities.append(
|
||||
VeluxDualRollerShutter(
|
||||
node, config_entry.entry_id, VeluxDualRollerPart.LOWER
|
||||
)
|
||||
)
|
||||
elif isinstance(node, OpeningDevice):
|
||||
entities.append(VeluxCover(node, config_entry.entry_id))
|
||||
|
||||
@@ -54,9 +73,6 @@ class VeluxCover(VeluxEntity, CoverEntity):
|
||||
|
||||
node: OpeningDevice
|
||||
|
||||
# Do not name the "main" feature of the device (position control)
|
||||
_attr_name = None
|
||||
|
||||
# Features common to all covers
|
||||
_attr_supported_features = (
|
||||
CoverEntityFeature.OPEN
|
||||
@@ -125,6 +141,72 @@ class VeluxCover(VeluxEntity, CoverEntity):
|
||||
await self.node.stop(wait_for_completion=False)
|
||||
|
||||
|
||||
class VeluxDualRollerPart(StrEnum):
|
||||
"""Enum for the parts of a dual roller shutter."""
|
||||
|
||||
UPPER = "upper"
|
||||
LOWER = "lower"
|
||||
DUAL = "dual"
|
||||
|
||||
|
||||
class VeluxDualRollerShutter(VeluxCover):
|
||||
"""Representation of a Velux dual roller shutter cover."""
|
||||
|
||||
node: DualRollerShutter
|
||||
_attr_device_class = CoverDeviceClass.SHUTTER
|
||||
|
||||
def __init__(
|
||||
self, node: DualRollerShutter, config_entry_id: str, part: VeluxDualRollerPart
|
||||
) -> None:
|
||||
"""Initialize VeluxDualRollerShutter."""
|
||||
super().__init__(node, config_entry_id)
|
||||
if part == VeluxDualRollerPart.DUAL:
|
||||
self._attr_name = None
|
||||
else:
|
||||
self._attr_unique_id = f"{self._attr_unique_id}_{part}"
|
||||
self._attr_translation_key = f"dual_roller_shutter_{part}"
|
||||
self.part = part
|
||||
|
||||
@property
|
||||
def current_cover_position(self) -> int:
|
||||
"""Return the current position of the cover."""
|
||||
if self.part == VeluxDualRollerPart.UPPER:
|
||||
return 100 - self.node.position_upper_curtain.position_percent
|
||||
if self.part == VeluxDualRollerPart.LOWER:
|
||||
return 100 - self.node.position_lower_curtain.position_percent
|
||||
return 100 - self.node.position.position_percent
|
||||
|
||||
@property
|
||||
def is_closed(self) -> bool:
|
||||
"""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
|
||||
|
||||
@wrap_pyvlx_call_exceptions
|
||||
async def async_close_cover(self, **kwargs: Any) -> None:
|
||||
"""Close the cover."""
|
||||
await self.node.close(curtain=self.part, wait_for_completion=False)
|
||||
|
||||
@wrap_pyvlx_call_exceptions
|
||||
async def async_open_cover(self, **kwargs: Any) -> None:
|
||||
"""Open the cover."""
|
||||
await self.node.open(curtain=self.part, wait_for_completion=False)
|
||||
|
||||
@wrap_pyvlx_call_exceptions
|
||||
async def async_set_cover_position(self, **kwargs: Any) -> None:
|
||||
"""Move the cover to a specific position."""
|
||||
position_percent = 100 - kwargs[ATTR_POSITION]
|
||||
|
||||
await self.node.set_position(
|
||||
Position(position_percent=position_percent),
|
||||
curtain=self.part,
|
||||
wait_for_completion=False,
|
||||
)
|
||||
|
||||
|
||||
class VeluxBlind(VeluxCover):
|
||||
"""Representation of a Velux blind cover."""
|
||||
|
||||
|
||||
@@ -45,6 +45,14 @@
|
||||
"rain_sensor": {
|
||||
"name": "Rain sensor"
|
||||
}
|
||||
},
|
||||
"cover": {
|
||||
"dual_roller_shutter_lower": {
|
||||
"name": "Lower shutter"
|
||||
},
|
||||
"dual_roller_shutter_upper": {
|
||||
"name": "Upper shutter"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
|
||||
@@ -15,7 +15,8 @@ async def update_callback_entity(
|
||||
) -> None:
|
||||
"""Simulate an update triggered by the pyvlx lib for a Velux node."""
|
||||
|
||||
callback = mock_velux_node.register_device_updated_cb.call_args[0][0]
|
||||
for c in mock_velux_node.register_device_updated_cb.call_args_list:
|
||||
callback = c[0][0]
|
||||
await callback(mock_velux_node)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@ from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from pyvlx import Blind, Light, OnOffLight, OnOffSwitch, Scene, Window
|
||||
from pyvlx import Light, OnOffLight, OnOffSwitch, Scene
|
||||
from pyvlx.opening_device import Blind, DualRollerShutter, Window
|
||||
|
||||
from homeassistant.components.velux import DOMAIN
|
||||
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD, Platform
|
||||
@@ -69,6 +70,22 @@ def mock_window() -> AsyncMock:
|
||||
return window
|
||||
|
||||
|
||||
# a dual roller shutter
|
||||
@pytest.fixture
|
||||
def mock_dual_roller_shutter() -> AsyncMock:
|
||||
"""Create a mock Velux dual roller shutter."""
|
||||
cover = AsyncMock(spec=DualRollerShutter, autospec=True)
|
||||
cover.name = "Test Dual Roller Shutter"
|
||||
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.pyvlx = MagicMock()
|
||||
return cover
|
||||
|
||||
|
||||
# a blind
|
||||
@pytest.fixture
|
||||
def mock_blind() -> AsyncMock:
|
||||
@@ -137,6 +154,8 @@ def mock_cover_type(request: pytest.FixtureRequest) -> AsyncMock:
|
||||
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.pyvlx = MagicMock()
|
||||
return cover
|
||||
|
||||
@@ -149,6 +168,7 @@ def mock_pyvlx(
|
||||
mock_onoff_switch: AsyncMock,
|
||||
mock_window: AsyncMock,
|
||||
mock_blind: AsyncMock,
|
||||
mock_dual_roller_shutter: AsyncMock,
|
||||
request: pytest.FixtureRequest,
|
||||
) -> Generator[MagicMock]:
|
||||
"""Create the library mock and patch PyVLX in both component and config_flow.
|
||||
@@ -164,6 +184,7 @@ def mock_pyvlx(
|
||||
pyvlx.nodes = [request.getfixturevalue(request.param)]
|
||||
else:
|
||||
pyvlx.nodes = [
|
||||
mock_dual_roller_shutter,
|
||||
mock_light,
|
||||
mock_onoff_light,
|
||||
mock_onoff_switch,
|
||||
|
||||
@@ -104,6 +104,162 @@
|
||||
'state': 'open',
|
||||
})
|
||||
# ---
|
||||
# name: test_cover_entity_setup[mock_cover_type-DualRollerShutter][cover.test_dualrollershutter-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'cover',
|
||||
'entity_category': None,
|
||||
'entity_id': 'cover.test_dualrollershutter',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <CoverDeviceClass.SHUTTER: 'shutter'>,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'velux',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <CoverEntityFeature: 15>,
|
||||
'translation_key': None,
|
||||
'unique_id': 'serial_DualRollerShutter',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_cover_entity_setup[mock_cover_type-DualRollerShutter][cover.test_dualrollershutter-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'current_position': 70,
|
||||
'device_class': 'shutter',
|
||||
'friendly_name': 'Test DualRollerShutter',
|
||||
'supported_features': <CoverEntityFeature: 15>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'cover.test_dualrollershutter',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'open',
|
||||
})
|
||||
# ---
|
||||
# name: test_cover_entity_setup[mock_cover_type-DualRollerShutter][cover.test_dualrollershutter_lower_shutter-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'cover',
|
||||
'entity_category': None,
|
||||
'entity_id': 'cover.test_dualrollershutter_lower_shutter',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Lower shutter',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <CoverDeviceClass.SHUTTER: 'shutter'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Lower shutter',
|
||||
'platform': 'velux',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <CoverEntityFeature: 15>,
|
||||
'translation_key': 'dual_roller_shutter_lower',
|
||||
'unique_id': 'serial_DualRollerShutter_lower',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_cover_entity_setup[mock_cover_type-DualRollerShutter][cover.test_dualrollershutter_lower_shutter-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'current_position': 70,
|
||||
'device_class': 'shutter',
|
||||
'friendly_name': 'Test DualRollerShutter Lower shutter',
|
||||
'supported_features': <CoverEntityFeature: 15>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'cover.test_dualrollershutter_lower_shutter',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'open',
|
||||
})
|
||||
# ---
|
||||
# name: test_cover_entity_setup[mock_cover_type-DualRollerShutter][cover.test_dualrollershutter_upper_shutter-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'cover',
|
||||
'entity_category': None,
|
||||
'entity_id': 'cover.test_dualrollershutter_upper_shutter',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Upper shutter',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <CoverDeviceClass.SHUTTER: 'shutter'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Upper shutter',
|
||||
'platform': 'velux',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <CoverEntityFeature: 15>,
|
||||
'translation_key': 'dual_roller_shutter_upper',
|
||||
'unique_id': 'serial_DualRollerShutter_upper',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_cover_entity_setup[mock_cover_type-DualRollerShutter][cover.test_dualrollershutter_upper_shutter-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'current_position': 70,
|
||||
'device_class': 'shutter',
|
||||
'friendly_name': 'Test DualRollerShutter Upper shutter',
|
||||
'supported_features': <CoverEntityFeature: 15>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'cover.test_dualrollershutter_upper_shutter',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'open',
|
||||
})
|
||||
# ---
|
||||
# name: test_cover_entity_setup[mock_cover_type-GarageDoor][cover.test_garagedoor-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
|
||||
@@ -4,7 +4,14 @@ from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
from pyvlx.exception import PyVLXException
|
||||
from pyvlx.opening_device import Awning, GarageDoor, Gate, RollerShutter, Window
|
||||
from pyvlx.opening_device import (
|
||||
Awning,
|
||||
DualRollerShutter,
|
||||
GarageDoor,
|
||||
Gate,
|
||||
RollerShutter,
|
||||
Window,
|
||||
)
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
ATTR_POSITION,
|
||||
@@ -64,7 +71,9 @@ async def test_blind_entity_setup(
|
||||
|
||||
@pytest.mark.usefixtures("mock_cover_type")
|
||||
@pytest.mark.parametrize(
|
||||
"mock_cover_type", [Awning, GarageDoor, Gate, RollerShutter, Window], indirect=True
|
||||
"mock_cover_type",
|
||||
[Awning, DualRollerShutter, GarageDoor, Gate, RollerShutter, Window],
|
||||
indirect=True,
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"mock_pyvlx",
|
||||
@@ -103,7 +112,13 @@ async def test_cover_device_association(
|
||||
assert entry.device_id is not None
|
||||
device_entry = device_registry.async_get(entry.device_id)
|
||||
assert device_entry is not None
|
||||
assert (DOMAIN, entry.unique_id) in device_entry.identifiers
|
||||
|
||||
# For dual roller shutters, the unique_id is suffixed with "_upper" or "_lower",
|
||||
# so remove that suffix to get the domain_id for device registry lookup
|
||||
domain_id = entry.unique_id
|
||||
if entry.unique_id.endswith("_upper") or entry.unique_id.endswith("_lower"):
|
||||
domain_id = entry.unique_id.rsplit("_", 1)[0]
|
||||
assert (DOMAIN, domain_id) in device_entry.identifiers
|
||||
assert device_entry.via_device_id is not None
|
||||
via_device_entry = device_registry.async_get(device_entry.via_device_id)
|
||||
assert via_device_entry is not None
|
||||
@@ -220,6 +235,165 @@ async def test_window_current_position_and_opening_closing_states(
|
||||
assert state.state == STATE_CLOSING
|
||||
|
||||
|
||||
# Dual roller shutter command tests
|
||||
async def test_dual_roller_shutter_open_close_services(
|
||||
hass: HomeAssistant, mock_dual_roller_shutter: AsyncMock
|
||||
) -> None:
|
||||
"""Verify open/close services map to device calls with correct part."""
|
||||
|
||||
dual_entity_id = "cover.test_dual_roller_shutter"
|
||||
upper_entity_id = "cover.test_dual_roller_shutter_upper_shutter"
|
||||
lower_entity_id = "cover.test_dual_roller_shutter_lower_shutter"
|
||||
|
||||
# Open upper part
|
||||
await hass.services.async_call(
|
||||
COVER_DOMAIN, SERVICE_OPEN_COVER, {"entity_id": upper_entity_id}, blocking=True
|
||||
)
|
||||
mock_dual_roller_shutter.open.assert_awaited_with(
|
||||
curtain="upper", wait_for_completion=False
|
||||
)
|
||||
|
||||
# Open lower part
|
||||
await hass.services.async_call(
|
||||
COVER_DOMAIN, SERVICE_OPEN_COVER, {"entity_id": lower_entity_id}, blocking=True
|
||||
)
|
||||
mock_dual_roller_shutter.open.assert_awaited_with(
|
||||
curtain="lower", wait_for_completion=False
|
||||
)
|
||||
|
||||
# Open dual
|
||||
await hass.services.async_call(
|
||||
COVER_DOMAIN, SERVICE_OPEN_COVER, {"entity_id": dual_entity_id}, blocking=True
|
||||
)
|
||||
mock_dual_roller_shutter.open.assert_awaited_with(
|
||||
curtain="dual", wait_for_completion=False
|
||||
)
|
||||
|
||||
# Close upper part
|
||||
await hass.services.async_call(
|
||||
COVER_DOMAIN, SERVICE_CLOSE_COVER, {"entity_id": upper_entity_id}, blocking=True
|
||||
)
|
||||
mock_dual_roller_shutter.close.assert_awaited_with(
|
||||
curtain="upper", wait_for_completion=False
|
||||
)
|
||||
|
||||
# Close lower part
|
||||
await hass.services.async_call(
|
||||
COVER_DOMAIN, SERVICE_CLOSE_COVER, {"entity_id": lower_entity_id}, blocking=True
|
||||
)
|
||||
mock_dual_roller_shutter.close.assert_awaited_with(
|
||||
curtain="lower", wait_for_completion=False
|
||||
)
|
||||
|
||||
# Close dual
|
||||
await hass.services.async_call(
|
||||
COVER_DOMAIN, SERVICE_CLOSE_COVER, {"entity_id": dual_entity_id}, blocking=True
|
||||
)
|
||||
mock_dual_roller_shutter.close.assert_awaited_with(
|
||||
curtain="dual", wait_for_completion=False
|
||||
)
|
||||
|
||||
|
||||
async def test_dual_shutter_set_cover_position_inversion(
|
||||
hass: HomeAssistant, mock_dual_roller_shutter: AsyncMock
|
||||
) -> None:
|
||||
"""HA position is inverted for device's Position."""
|
||||
|
||||
entity_id = "cover.test_dual_roller_shutter"
|
||||
# Call with position 30 (=70% for device)
|
||||
await hass.services.async_call(
|
||||
COVER_DOMAIN,
|
||||
SERVICE_SET_COVER_POSITION,
|
||||
{"entity_id": entity_id, ATTR_POSITION: 30},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# Expect device Position 70%
|
||||
args, kwargs = mock_dual_roller_shutter.set_position.await_args
|
||||
position_obj = args[0]
|
||||
assert position_obj.position_percent == 70
|
||||
assert kwargs.get("wait_for_completion") is False
|
||||
assert kwargs.get("curtain") == "dual"
|
||||
|
||||
entity_id = "cover.test_dual_roller_shutter_upper_shutter"
|
||||
# Call with position 30 (=70% for device)
|
||||
await hass.services.async_call(
|
||||
COVER_DOMAIN,
|
||||
SERVICE_SET_COVER_POSITION,
|
||||
{"entity_id": entity_id, ATTR_POSITION: 30},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# Expect device Position 70%
|
||||
args, kwargs = mock_dual_roller_shutter.set_position.await_args
|
||||
position_obj = args[0]
|
||||
assert position_obj.position_percent == 70
|
||||
assert kwargs.get("wait_for_completion") is False
|
||||
assert kwargs.get("curtain") == "upper"
|
||||
|
||||
entity_id = "cover.test_dual_roller_shutter_lower_shutter"
|
||||
# Call with position 30 (=70% for device)
|
||||
await hass.services.async_call(
|
||||
COVER_DOMAIN,
|
||||
SERVICE_SET_COVER_POSITION,
|
||||
{"entity_id": entity_id, ATTR_POSITION: 30},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# Expect device Position 70%
|
||||
args, kwargs = mock_dual_roller_shutter.set_position.await_args
|
||||
position_obj = args[0]
|
||||
assert position_obj.position_percent == 70
|
||||
assert kwargs.get("wait_for_completion") is False
|
||||
assert kwargs.get("curtain") == "lower"
|
||||
|
||||
|
||||
async def test_dual_roller_shutter_position_tests(
|
||||
hass: HomeAssistant, mock_dual_roller_shutter: AsyncMock
|
||||
) -> None:
|
||||
"""Validate current_position and open/closed state."""
|
||||
|
||||
entity_id_dual = "cover.test_dual_roller_shutter"
|
||||
entity_id_lower = "cover.test_dual_roller_shutter_lower_shutter"
|
||||
entity_id_upper = "cover.test_dual_roller_shutter_upper_shutter"
|
||||
|
||||
# device position is inverted (100 - x)
|
||||
mock_dual_roller_shutter.position.position_percent = 29
|
||||
mock_dual_roller_shutter.position_upper_curtain.position_percent = 28
|
||||
mock_dual_roller_shutter.position_lower_curtain.position_percent = 27
|
||||
await update_callback_entity(hass, mock_dual_roller_shutter)
|
||||
state = hass.states.get(entity_id_dual)
|
||||
assert state is not None
|
||||
assert state.attributes.get("current_position") == 71
|
||||
assert state.state == STATE_OPEN
|
||||
|
||||
state = hass.states.get(entity_id_upper)
|
||||
assert state is not None
|
||||
assert state.attributes.get("current_position") == 72
|
||||
assert state.state == STATE_OPEN
|
||||
|
||||
state = hass.states.get(entity_id_lower)
|
||||
assert state is not None
|
||||
assert state.attributes.get("current_position") == 73
|
||||
assert state.state == STATE_OPEN
|
||||
|
||||
mock_dual_roller_shutter.position.closed = True
|
||||
mock_dual_roller_shutter.position_upper_curtain.closed = True
|
||||
mock_dual_roller_shutter.position_lower_curtain.closed = True
|
||||
await update_callback_entity(hass, mock_dual_roller_shutter)
|
||||
state = hass.states.get(entity_id_dual)
|
||||
assert state is not None
|
||||
assert state.state == STATE_CLOSED
|
||||
|
||||
state = hass.states.get(entity_id_upper)
|
||||
assert state is not None
|
||||
assert state.state == STATE_CLOSED
|
||||
|
||||
state = hass.states.get(entity_id_lower)
|
||||
assert state is not None
|
||||
assert state.state == STATE_CLOSED
|
||||
|
||||
|
||||
# Blind command tests
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user