From bb6087cf875cc8594dc923c647e138ce67e259c7 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Fri, 8 May 2026 11:57:37 +0200 Subject: [PATCH] Fix tilt controls for TiltOnlyVenetianBlind in Overkiz (#170055) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- homeassistant/components/overkiz/cover.py | 9 +- homeassistant/components/overkiz/executor.py | 2 + .../setup/cloud_somfy_connexoon_rts_asia.json | 57 +++++++++++ .../overkiz/snapshots/test_cover.ambr | 54 ++++++++++ tests/components/overkiz/test_cover.py | 98 ++++++++++++++----- 5 files changed, 197 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/overkiz/cover.py b/homeassistant/components/overkiz/cover.py index ca8057fd389d..5b08e5714ec7 100644 --- a/homeassistant/components/overkiz/cover.py +++ b/homeassistant/components/overkiz/cover.py @@ -100,14 +100,21 @@ COVER_DESCRIPTIONS: list[OverkizCoverDescription] = [ close_tilt_command=OverkizCommand.LOWER_CLOSE, stop_tilt_command=OverkizCommand.STOP, ), - # Needs override to remove open/close commands + # Needs override to add support for very specific tilt commands # uiClass is VenetianBlind OverkizCoverDescription( key=UIWidget.TILT_ONLY_VENETIAN_BLIND, device_class=CoverDeviceClass.BLIND, is_closed_state=OverkizState.CORE_OPEN_CLOSED, + # Position commands fully open/close the tilt + open_command=OverkizCommand.OPEN, + close_command=OverkizCommand.CLOSE, + stop_command=OverkizCommand.STOP, + # Tilt commands move the tilt with a few degrees open_tilt_command=OverkizCommand.TILT_POSITIVE, + open_tilt_command_args=(1, 0), close_tilt_command=OverkizCommand.TILT_NEGATIVE, + close_tilt_command_args=(1, 0), stop_tilt_command=OverkizCommand.STOP, ), # Needs override to support very specific tilt commands (rts:ExteriorVenetianBlindRTSComponent) diff --git a/homeassistant/components/overkiz/executor.py b/homeassistant/components/overkiz/executor.py index 111a6f318ec7..2f2b2f7f0999 100644 --- a/homeassistant/components/overkiz/executor.py +++ b/homeassistant/components/overkiz/executor.py @@ -20,6 +20,8 @@ COMMANDS_WITHOUT_DELAY = [ OverkizCommand.ON, OverkizCommand.ON_WITH_TIMER, OverkizCommand.TEST, + OverkizCommand.TILT_POSITIVE, + OverkizCommand.TILT_NEGATIVE, ] diff --git a/tests/components/overkiz/fixtures/setup/cloud_somfy_connexoon_rts_asia.json b/tests/components/overkiz/fixtures/setup/cloud_somfy_connexoon_rts_asia.json index d2fad670bd2b..8ea89c840ca9 100644 --- a/tests/components/overkiz/fixtures/setup/cloud_somfy_connexoon_rts_asia.json +++ b/tests/components/overkiz/fixtures/setup/cloud_somfy_connexoon_rts_asia.json @@ -323,6 +323,63 @@ "type": 1, "oid": "ef870fe7-daf3-4f33-b91e-62b2a809ef4e", "uiClass": "RollerShutter" + }, + { + "creationTime": 1613676700000, + "lastUpdateTime": 1613676700000, + "label": "Jaloezie", + "deviceURL": "rts://1234-1234-6362/16730044", + "shortcut": false, + "controllableName": "rts:TiltOnlyVenetianBlindRTSComponent", + "definition": { + "commands": [ + { + "commandName": "open", + "nparams": 0 + }, + { + "commandName": "close", + "nparams": 0 + }, + { + "commandName": "stop", + "nparams": 0 + }, + { + "commandName": "tiltPositive", + "nparams": 2 + }, + { + "commandName": "tiltNegative", + "nparams": 2 + }, + { + "commandName": "my", + "nparams": 1 + }, + { + "commandName": "identify", + "nparams": 0 + } + ], + "states": [], + "attributes": [], + "dataProperties": [], + "widgetName": "TiltOnlyVenetianBlind", + "uiProfiles": ["OpenClose"], + "uiClass": "VenetianBlind", + "qualifiedName": "rts:TiltOnlyVenetianBlindRTSComponent", + "type": "ACTUATOR" + }, + "states": [], + "attributes": [], + "available": true, + "enabled": true, + "placeOID": "6133b4a0-f514-4553-b635-d1b7beb7e7b2", + "widget": "TiltOnlyVenetianBlind", + "type": 1, + "oid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "uiClass": "VenetianBlind" } ], "zones": [], diff --git a/tests/components/overkiz/snapshots/test_cover.ambr b/tests/components/overkiz/snapshots/test_cover.ambr index 4683840c5f80..4fc0d4c9abc1 100644 --- a/tests/components/overkiz/snapshots/test_cover.ambr +++ b/tests/components/overkiz/snapshots/test_cover.ambr @@ -323,6 +323,60 @@ 'state': 'open', }) # --- +# name: test_cover_entities_snapshot[cloud_somfy_connexoon_rts_asia.json][cover.jaloezie-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'cover', + 'entity_category': None, + 'entity_id': 'cover.jaloezie', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'overkiz', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'rts://1234-1234-6362/16730044', + 'unit_of_measurement': None, + }) +# --- +# name: test_cover_entities_snapshot[cloud_somfy_connexoon_rts_asia.json][cover.jaloezie-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'assumed_state': True, + 'device_class': 'blind', + 'friendly_name': 'Jaloezie', + 'is_closed': None, + 'supported_features': , + }), + 'context': , + 'entity_id': 'cover.jaloezie', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_cover_entities_snapshot[cloud_somfy_connexoon_rts_asia.json][cover.kitchen_shutter-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ diff --git a/tests/components/overkiz/test_cover.py b/tests/components/overkiz/test_cover.py index 3ec0e2b7ef37..ec0e561fb5ca 100644 --- a/tests/components/overkiz/test_cover.py +++ b/tests/components/overkiz/test_cover.py @@ -27,7 +27,12 @@ from homeassistant.components.cover import ( CoverEntityFeature, CoverState, ) -from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + Platform, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import entity_registry as er @@ -89,6 +94,11 @@ POSITIONABLE_DUAL_ROLLER_SHUTTER = FixtureDevice( "io://1234-5678-5010/12931361", "cover.basement_roller_shutter", ) +TILT_ONLY_VENETIAN_BLIND = FixtureDevice( + "setup/cloud_somfy_connexoon_rts_asia.json", + "rts://1234-1234-6362/16730044", + "cover.jaloezie", +) DYNAMIC_GARAGE_DOOR = FixtureDevice( "setup/cloud_somfy_tahoma_v2_europe.json", "io://1234-1234-6233/16730050", @@ -144,26 +154,62 @@ async def test_cover_entities_snapshot( @pytest.mark.parametrize( - ("device", "service", "command_name", "expected_state"), + ("device", "service", "command_name", "parameters", "expected_state"), [ - (SHUTTER, SERVICE_OPEN_COVER, "open", CoverState.OPENING), - (AWNING, SERVICE_OPEN_COVER, "deploy", CoverState.OPENING), - (GARAGE, SERVICE_OPEN_COVER, "open", CoverState.OPENING), - (DYNAMIC_GARAGE_DOOR, SERVICE_OPEN_COVER, "open", CoverState.OPENING), - (DYNAMIC_GARAGE_DOOR_OGP, SERVICE_OPEN_COVER, "open", CoverState.OPENING), - (PARTIAL_GARAGE_DOOR, SERVICE_OPEN_COVER, "open", CoverState.OPENING), - (SHUTTER, SERVICE_CLOSE_COVER, "close", CoverState.CLOSING), - (AWNING, SERVICE_CLOSE_COVER, "undeploy", CoverState.CLOSING), - (GARAGE, SERVICE_CLOSE_COVER, "close", CoverState.CLOSING), - (DYNAMIC_GARAGE_DOOR, SERVICE_CLOSE_COVER, "close", CoverState.CLOSING), - (DYNAMIC_GARAGE_DOOR_OGP, SERVICE_CLOSE_COVER, "close", CoverState.CLOSING), - (PARTIAL_GARAGE_DOOR, SERVICE_CLOSE_COVER, "close", CoverState.CLOSING), - (SHUTTER, SERVICE_STOP_COVER, "stop", CoverState.CLOSED), - (AWNING, SERVICE_STOP_COVER, "stop", CoverState.CLOSED), - (GARAGE, SERVICE_STOP_COVER, "stop", CoverState.CLOSED), - (DYNAMIC_GARAGE_DOOR, SERVICE_STOP_COVER, "stop", CoverState.CLOSED), - (DYNAMIC_GARAGE_DOOR_OGP, SERVICE_STOP_COVER, "stop", CoverState.CLOSED), - (PARTIAL_GARAGE_DOOR, SERVICE_STOP_COVER, "stop", CoverState.CLOSED), + (SHUTTER, SERVICE_OPEN_COVER, "open", None, CoverState.OPENING), + (AWNING, SERVICE_OPEN_COVER, "deploy", None, CoverState.OPENING), + (GARAGE, SERVICE_OPEN_COVER, "open", None, CoverState.OPENING), + (DYNAMIC_GARAGE_DOOR, SERVICE_OPEN_COVER, "open", None, CoverState.OPENING), + (DYNAMIC_GARAGE_DOOR_OGP, SERVICE_OPEN_COVER, "open", None, CoverState.OPENING), + (PARTIAL_GARAGE_DOOR, SERVICE_OPEN_COVER, "open", None, CoverState.OPENING), + (TILT_ONLY_VENETIAN_BLIND, SERVICE_OPEN_COVER, "open", [0], CoverState.OPENING), + (SHUTTER, SERVICE_CLOSE_COVER, "close", None, CoverState.CLOSING), + (AWNING, SERVICE_CLOSE_COVER, "undeploy", None, CoverState.CLOSING), + (GARAGE, SERVICE_CLOSE_COVER, "close", None, CoverState.CLOSING), + (DYNAMIC_GARAGE_DOOR, SERVICE_CLOSE_COVER, "close", None, CoverState.CLOSING), + ( + DYNAMIC_GARAGE_DOOR_OGP, + SERVICE_CLOSE_COVER, + "close", + None, + CoverState.CLOSING, + ), + (PARTIAL_GARAGE_DOOR, SERVICE_CLOSE_COVER, "close", None, CoverState.CLOSING), + ( + TILT_ONLY_VENETIAN_BLIND, + SERVICE_CLOSE_COVER, + "close", + [0], + CoverState.CLOSING, + ), + (SHUTTER, SERVICE_STOP_COVER, "stop", None, CoverState.CLOSED), + (AWNING, SERVICE_STOP_COVER, "stop", None, CoverState.CLOSED), + (GARAGE, SERVICE_STOP_COVER, "stop", None, CoverState.CLOSED), + (DYNAMIC_GARAGE_DOOR, SERVICE_STOP_COVER, "stop", None, CoverState.CLOSED), + (DYNAMIC_GARAGE_DOOR_OGP, SERVICE_STOP_COVER, "stop", None, CoverState.CLOSED), + (PARTIAL_GARAGE_DOOR, SERVICE_STOP_COVER, "stop", None, CoverState.CLOSED), + (TILT_ONLY_VENETIAN_BLIND, SERVICE_STOP_COVER, "stop", [0], STATE_UNKNOWN), + ( + TILT_ONLY_VENETIAN_BLIND, + SERVICE_OPEN_COVER_TILT, + "tiltPositive", + [1, 0], + CoverState.OPENING, + ), + ( + TILT_ONLY_VENETIAN_BLIND, + SERVICE_CLOSE_COVER_TILT, + "tiltNegative", + [1, 0], + CoverState.CLOSING, + ), + ( + TILT_ONLY_VENETIAN_BLIND, + SERVICE_STOP_COVER_TILT, + "stop", + [0], + STATE_UNKNOWN, + ), ], ids=[ "open-roller-shutter", @@ -172,18 +218,24 @@ async def test_cover_entities_snapshot( "open-dynamic-garage-door", "open-dynamic-garage-door-ogp", "open-partial-garage-door", + "open-tilt-only-venetian-blind", "close-roller-shutter", "close-awning", "close-garage-door", "close-dynamic-garage-door", "close-dynamic-garage-door-ogp", "close-partial-garage-door", + "close-tilt-only-venetian-blind", "stop-roller-shutter", "stop-awning", "stop-garage-door", "stop-dynamic-garage-door", "stop-dynamic-garage-door-ogp", "stop-partial-garage-door", + "stop-tilt-only-venetian-blind", + "open-tilt-tilt-only-venetian-blind", + "close-tilt-tilt-only-venetian-blind", + "stop-tilt-tilt-only-venetian-blind", ], ) async def test_cover_service_actions( @@ -193,9 +245,10 @@ async def test_cover_service_actions( device: FixtureDevice, service: str, command_name: str, - expected_state: CoverState, + parameters: list[Any] | None, + expected_state: CoverState | str, ) -> None: - """Test open, close, and stop cover services.""" + """Test open, close, and stop cover and tilt services.""" await setup_overkiz_integration(fixture=device.fixture) await hass.services.async_call( @@ -212,6 +265,7 @@ async def test_cover_service_actions( mock_client, device_url=device.device_url, command_name=command_name, + parameters=parameters, )