From 3eaa005c7e9a7bf5c0937c8b9be84dceb9beba4f Mon Sep 17 00:00:00 2001 From: AlCalzone Date: Fri, 13 Sep 2024 12:41:13 +0200 Subject: [PATCH] Use start/stop level change to open/close Z-Wave JS Window Covering CC covers (#125827) * Z-Wave JS: Use start/stop level change to open/close Window Covering CC covers * fix: import * Update tests/components/zwave_js/test_cover.py Co-authored-by: Martin Hjelmare * assert that up_value and down_value exist * fix: forgot one --------- Co-authored-by: Martin Hjelmare --- homeassistant/components/zwave_js/cover.py | 27 ++ tests/components/zwave_js/conftest.py | 16 + .../window_covering_outbound_bottom.json | 282 ++++++++++++++++++ tests/components/zwave_js/test_cover.py | 103 +++++++ 4 files changed, 428 insertions(+) create mode 100644 tests/components/zwave_js/fixtures/window_covering_outbound_bottom.json diff --git a/homeassistant/components/zwave_js/cover.py b/homeassistant/components/zwave_js/cover.py index 363b32cedda..218c5cc82fe 100644 --- a/homeassistant/components/zwave_js/cover.py +++ b/homeassistant/components/zwave_js/cover.py @@ -19,6 +19,7 @@ from zwave_js_server.const.command_class.multilevel_switch import ( from zwave_js_server.const.command_class.window_covering import ( NO_POSITION_PROPERTY_KEYS, NO_POSITION_SUFFIX, + WINDOW_COVERING_LEVEL_CHANGE_DOWN_PROPERTY, WINDOW_COVERING_LEVEL_CHANGE_UP_PROPERTY, SlatStates, ) @@ -341,6 +342,20 @@ class ZWaveWindowCovering(CoverPositionMixin, CoverTiltMixin): super().__init__(config_entry, driver, info) pos_value: ZwaveValue | None = None tilt_value: ZwaveValue | None = None + self._up_value = cast( + ZwaveValue, + self.get_zwave_value( + WINDOW_COVERING_LEVEL_CHANGE_UP_PROPERTY, + value_property_key=info.primary_value.property_key, + ), + ) + self._down_value = cast( + ZwaveValue, + self.get_zwave_value( + WINDOW_COVERING_LEVEL_CHANGE_DOWN_PROPERTY, + value_property_key=info.primary_value.property_key, + ), + ) # If primary value is for position, we have to search for a tilt value if info.primary_value.property_key in COVER_POSITION_PROPERTY_KEYS: @@ -402,6 +417,18 @@ class ZWaveWindowCovering(CoverPositionMixin, CoverTiltMixin): """Return range of valid tilt positions.""" return abs(SlatStates.CLOSED_2 - SlatStates.CLOSED_1) + async def async_open_cover(self, **kwargs: Any) -> None: + """Open the cover.""" + await self._async_set_value(self._up_value, True) + + async def async_close_cover(self, **kwargs: Any) -> None: + """Close the cover.""" + await self._async_set_value(self._down_value, True) + + async def async_stop_cover(self, **kwargs: Any) -> None: + """Stop the cover.""" + await self._async_set_value(self._up_value, False) + class ZwaveMotorizedBarrier(ZWaveBaseEntity, CoverEntity): """Representation of a Z-Wave motorized barrier device.""" diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index a6bbe554f9a..489c2ee4b01 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -477,6 +477,12 @@ def basic_cc_sensor_state_fixture(): return json.loads(load_fixture("zwave_js/basic_cc_sensor_state.json")) +@pytest.fixture(name="window_covering_outbound_bottom_state", scope="package") +def window_covering_outbound_bottom_state_fixture(): + """Load node with Window Covering CC fixture data, with only the outbound bottom position supported.""" + return json.loads(load_fixture("zwave_js/window_covering_outbound_bottom.json")) + + # model fixtures @@ -1161,3 +1167,13 @@ def basic_cc_sensor_fixture(client, basic_cc_sensor_state): node = Node(client, copy.deepcopy(basic_cc_sensor_state)) client.driver.controller.nodes[node.node_id] = node return node + + +@pytest.fixture(name="window_covering_outbound_bottom") +def window_covering_outbound_bottom_fixture( + client, window_covering_outbound_bottom_state +): + """Load node with Window Covering CC fixture data, with only the outbound bottom position supported.""" + node = Node(client, copy.deepcopy(window_covering_outbound_bottom_state)) + client.driver.controller.nodes[node.node_id] = node + return node diff --git a/tests/components/zwave_js/fixtures/window_covering_outbound_bottom.json b/tests/components/zwave_js/fixtures/window_covering_outbound_bottom.json new file mode 100644 index 00000000000..4791e0d9486 --- /dev/null +++ b/tests/components/zwave_js/fixtures/window_covering_outbound_bottom.json @@ -0,0 +1,282 @@ +{ + "nodeId": 2, + "index": 0, + "status": 4, + "ready": true, + "isListening": true, + "isRouting": true, + "isSecure": false, + "interviewAttempts": 1, + "isFrequentListening": false, + "maxDataRate": 100000, + "supportedDataRates": [40000, 9600, 100000], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing End Node" + }, + "generic": { + "key": 6, + "label": "Appliance" + }, + "specific": { + "key": 1, + "label": "General Appliance" + } + }, + "interviewStage": "Complete", + "statistics": { + "commandsTX": 8, + "commandsRX": 5, + "commandsDroppedRX": 0, + "commandsDroppedTX": 0, + "timeoutResponse": 2, + "rtt": 96.3, + "lastSeen": "2024-09-12T11:46:43.065Z" + }, + "highestSecurityClass": -1, + "isControllerNode": false, + "keepAwake": false, + "lastSeen": "2024-09-12T11:46:43.065Z", + "protocol": 0, + "values": [ + { + "endpoint": 0, + "commandClass": 106, + "commandClassName": "Window Covering", + "property": "levelChangeUp", + "propertyKey": 13, + "propertyName": "levelChangeUp", + "propertyKeyName": "Outbound Bottom", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Open - Outbound Bottom", + "ccSpecific": { + "parameter": 13 + }, + "valueChangeOptions": ["transitionDuration"], + "states": { + "true": "Start", + "false": "Stop" + }, + "stateful": true, + "secret": false + }, + "value": true + }, + { + "endpoint": 0, + "commandClass": 106, + "commandClassName": "Window Covering", + "property": "levelChangeDown", + "propertyKey": 13, + "propertyName": "levelChangeDown", + "propertyKeyName": "Outbound Bottom", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Close - Outbound Bottom", + "ccSpecific": { + "parameter": 13 + }, + "valueChangeOptions": ["transitionDuration"], + "states": { + "true": "Start", + "false": "Stop" + }, + "stateful": true, + "secret": false + }, + "value": true + }, + { + "endpoint": 0, + "commandClass": 106, + "commandClassName": "Window Covering", + "property": "targetValue", + "propertyKey": 13, + "propertyName": "targetValue", + "propertyKeyName": "Outbound Bottom", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target value - Outbound Bottom", + "ccSpecific": { + "parameter": 13 + }, + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 99, + "states": { + "0": "Closed", + "99": "Open" + }, + "stateful": true, + "secret": false + }, + "value": 52 + }, + { + "endpoint": 0, + "commandClass": 106, + "commandClassName": "Window Covering", + "property": "currentValue", + "propertyKey": 13, + "propertyName": "currentValue", + "propertyKeyName": "Outbound Bottom", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current value - Outbound Bottom", + "ccSpecific": { + "parameter": 13 + }, + "min": 0, + "max": 99, + "states": { + "0": "Closed", + "99": "Open" + }, + "stateful": true, + "secret": false + }, + "value": 52 + }, + { + "endpoint": 0, + "commandClass": 106, + "commandClassName": "Window Covering", + "property": "duration", + "propertyKey": 13, + "propertyName": "duration", + "propertyKeyName": "Outbound Bottom", + "ccVersion": 1, + "metadata": { + "type": "duration", + "readable": true, + "writeable": false, + "label": "Remaining duration - Outbound Bottom", + "ccSpecific": { + "parameter": 13 + }, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 1, + "metadata": { + "type": "string[]", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Library type", + "states": { + "0": "Unknown", + "1": "Static Controller", + "2": "Controller", + "3": "Enhanced Slave", + "4": "Slave", + "5": "Installer", + "6": "Routing Slave", + "7": "Bridge Controller", + "8": "Device under Test", + "9": "N/A", + "10": "AV Remote", + "11": "AV Device" + }, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 1, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version", + "stateful": true, + "secret": false + } + } + ], + "endpoints": [ + { + "nodeId": 2, + "index": 0, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing End Node" + }, + "generic": { + "key": 6, + "label": "Appliance" + }, + "specific": { + "key": 1, + "label": "General Appliance" + } + }, + "commandClasses": [ + { + "id": 134, + "name": "Version", + "version": 1, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 106, + "name": "Window Covering", + "version": 1, + "isSecure": false + } + ] + } + ] +} diff --git a/tests/components/zwave_js/test_cover.py b/tests/components/zwave_js/test_cover.py index 07edb68f1da..ce394cb9067 100644 --- a/tests/components/zwave_js/test_cover.py +++ b/tests/components/zwave_js/test_cover.py @@ -994,3 +994,106 @@ async def test_nice_ibt4zwave_cover( assert args["value"] == 99 client.async_send_command.reset_mock() + + +async def test_window_covering_open_close( + hass: HomeAssistant, client, window_covering_outbound_bottom, integration +) -> None: + """Test Window Covering device open and close commands. + + A Window Covering device with position support + should be able to open/close with the start/stop level change properties. + """ + entity_id = "cover.node_2_outbound_bottom" + state = hass.states.get(entity_id) + + # The entity has position support, but not tilt + assert state + assert ATTR_CURRENT_POSITION in state.attributes + assert ATTR_CURRENT_TILT_POSITION not in state.attributes + + # Test opening + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 2 + assert args["valueId"] == { + "commandClass": 106, + "endpoint": 0, + "property": "levelChangeUp", + "propertyKey": 13, + } + assert args["value"] is True + + client.async_send_command.reset_mock() + + # Test stop after opening + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_STOP_COVER, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 2 + assert args["valueId"] == { + "commandClass": 106, + "endpoint": 0, + "property": "levelChangeUp", + "propertyKey": 13, + } + assert args["value"] is False + + client.async_send_command.reset_mock() + + # Test closing + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 2 + assert args["valueId"] == { + "commandClass": 106, + "endpoint": 0, + "property": "levelChangeDown", + "propertyKey": 13, + } + assert args["value"] is True + + client.async_send_command.reset_mock() + + # Test stop after closing + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_STOP_COVER, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 2 + assert args["valueId"] == { + "commandClass": 106, + "endpoint": 0, + "property": "levelChangeUp", + "propertyKey": 13, + } + assert args["value"] is False + + client.async_send_command.reset_mock()