From 4119d3198ade4c950d9c0f1de0e38a5e0751bd38 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 30 May 2023 23:52:12 -0400 Subject: [PATCH] Support zwave config parameters not on endpoint 0 (#93383) * Support zwave config parameters not on endpoint 0 * Update device automation logic * Make endpoint required * Update homeassistant/components/zwave_js/services.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/zwave_js/services.py Co-authored-by: Martin Hjelmare --------- Co-authored-by: Martin Hjelmare --- homeassistant/components/zwave_js/api.py | 6 +- .../components/zwave_js/device_action.py | 3 + .../zwave_js/device_automation_helpers.py | 10 +++- homeassistant/components/zwave_js/services.py | 6 ++ .../components/zwave_js/services.yaml | 28 ++++++++-- tests/components/zwave_js/test_api.py | 55 ++++++++++++++++--- .../components/zwave_js/test_device_action.py | 7 ++- .../zwave_js/test_device_condition.py | 4 +- .../zwave_js/test_device_trigger.py | 4 +- 9 files changed, 102 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 29e0dcf9e06..867405530ab 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -98,6 +98,7 @@ COMMAND_CLASS_ID = "command_class_id" TYPE = "type" PROPERTY = "property" PROPERTY_KEY = "property_key" +ENDPOINT = "endpoint" VALUE = "value" # constants for log config commands @@ -1608,6 +1609,7 @@ async def websocket_refresh_node_cc_values( vol.Required(TYPE): "zwave_js/set_config_parameter", vol.Required(DEVICE_ID): str, vol.Required(PROPERTY): int, + vol.Optional(ENDPOINT, default=0): int, vol.Optional(PROPERTY_KEY): int, vol.Required(VALUE): vol.Any(int, BITMASK_SCHEMA), } @@ -1623,12 +1625,13 @@ async def websocket_set_config_parameter( ) -> None: """Set a config parameter value for a Z-Wave node.""" property_ = msg[PROPERTY] + endpoint = msg[ENDPOINT] property_key = msg.get(PROPERTY_KEY) value = msg[VALUE] try: zwave_value, cmd_status = await async_set_config_parameter( - node, value, property_, property_key=property_key + node, value, property_, property_key=property_key, endpoint=endpoint ) except (InvalidNewValue, NotFoundError, NotImplementedError, SetValueFailed) as err: code = ERR_UNKNOWN_ERROR @@ -1673,6 +1676,7 @@ async def websocket_get_config_parameters( result[value_id] = { "property": zwave_value.property_, "property_key": zwave_value.property_key, + "endpoint": zwave_value.endpoint, "configuration_value_type": zwave_value.configuration_value_type.value, "metadata": { "description": metadata.description, diff --git a/homeassistant/components/zwave_js/device_action.py b/homeassistant/components/zwave_js/device_action.py index bfb7956432b..20c37b5cbb6 100644 --- a/homeassistant/components/zwave_js/device_action.py +++ b/homeassistant/components/zwave_js/device_action.py @@ -101,6 +101,7 @@ RESET_METER_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( SET_CONFIG_PARAMETER_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( { vol.Required(CONF_TYPE): SERVICE_SET_CONFIG_PARAMETER, + vol.Required(ATTR_ENDPOINT): vol.Coerce(int), vol.Required(ATTR_CONFIG_PARAMETER): vol.Any(int, str), vol.Required(ATTR_CONFIG_PARAMETER_BITMASK): vol.Any(None, int, str), vol.Required(ATTR_VALUE): vol.Coerce(int), @@ -168,6 +169,7 @@ async def async_get_actions( { **base_action, CONF_TYPE: SERVICE_SET_CONFIG_PARAMETER, + ATTR_ENDPOINT: config_value.endpoint, ATTR_CONFIG_PARAMETER: config_value.property_, ATTR_CONFIG_PARAMETER_BITMASK: config_value.property_key, CONF_SUBTYPE: generate_config_parameter_subtype(config_value), @@ -347,6 +349,7 @@ async def async_get_action_capabilities( CommandClass.CONFIGURATION, config[ATTR_CONFIG_PARAMETER], property_key=config[ATTR_CONFIG_PARAMETER_BITMASK], + endpoint=config[ATTR_ENDPOINT], ) value_schema = get_config_parameter_value_schema(node, value_id) if value_schema is None: diff --git a/homeassistant/components/zwave_js/device_automation_helpers.py b/homeassistant/components/zwave_js/device_automation_helpers.py index 11c4fde3137..7a60d491b3c 100644 --- a/homeassistant/components/zwave_js/device_automation_helpers.py +++ b/homeassistant/components/zwave_js/device_automation_helpers.py @@ -47,9 +47,15 @@ def generate_config_parameter_subtype(config_value: ConfigurationValue) -> str: if config_value.property_key: # Property keys for config values are always an int assert isinstance(config_value.property_key, int) - parameter = f"{parameter}[{hex(config_value.property_key)}]" + parameter = ( + f"{parameter}[{hex(config_value.property_key)}] on endpoint " + f"{config_value.endpoint}" + ) - return f"{parameter} ({config_value.property_name})" + return ( + f"{parameter} ({config_value.property_name}) on endpoint " + f"{config_value.endpoint}" + ) @callback diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py index 47a16ee1273..6f4f9f8e330 100644 --- a/homeassistant/components/zwave_js/services.py +++ b/homeassistant/components/zwave_js/services.py @@ -213,6 +213,7 @@ class ZWaveServices: cv.ensure_list, [cv.string] ), vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(const.ATTR_ENDPOINT, default=0): vol.Coerce(int), vol.Required(const.ATTR_CONFIG_PARAMETER): vol.Any( vol.Coerce(int), cv.string ), @@ -247,6 +248,7 @@ class ZWaveServices: cv.ensure_list, [cv.string] ), vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(const.ATTR_ENDPOINT, default=0): vol.Coerce(int), vol.Required(const.ATTR_CONFIG_PARAMETER): vol.Coerce(int), vol.Required(const.ATTR_CONFIG_VALUE): vol.Any( vol.Coerce(int), @@ -413,6 +415,7 @@ class ZWaveServices: async def async_set_config_parameter(self, service: ServiceCall) -> None: """Set a config value on a node.""" nodes: set[ZwaveNode] = service.data[const.ATTR_NODES] + endpoint = service.data[const.ATTR_ENDPOINT] property_or_property_name = service.data[const.ATTR_CONFIG_PARAMETER] property_key = service.data.get(const.ATTR_CONFIG_PARAMETER_BITMASK) new_value = service.data[const.ATTR_CONFIG_VALUE] @@ -424,6 +427,7 @@ class ZWaveServices: new_value, property_or_property_name, property_key=property_key, + endpoint=endpoint, ) for node in nodes ), @@ -448,6 +452,7 @@ class ZWaveServices: ) -> None: """Bulk set multiple partial config values on a node.""" nodes: set[ZwaveNode] = service.data[const.ATTR_NODES] + endpoint = service.data[const.ATTR_ENDPOINT] property_ = service.data[const.ATTR_CONFIG_PARAMETER] new_value = service.data[const.ATTR_CONFIG_VALUE] @@ -457,6 +462,7 @@ class ZWaveServices: node, property_, new_value, + endpoint=endpoint, ) for node in nodes ), diff --git a/homeassistant/components/zwave_js/services.yaml b/homeassistant/components/zwave_js/services.yaml index b9209c6904f..05e2f8bd9fb 100644 --- a/homeassistant/components/zwave_js/services.yaml +++ b/homeassistant/components/zwave_js/services.yaml @@ -46,6 +46,14 @@ set_config_parameter: entity: integration: zwave_js fields: + endpoint: + name: Endpoint + description: The configuration parameter's endpoint. + example: 1 + default: 0 + required: false + selector: + text: parameter: name: Parameter description: The (name or id of the) configuration parameter you want to configure. @@ -53,6 +61,12 @@ set_config_parameter: required: true selector: text: + bitmask: + name: Bitmask + description: Target a specific bitmask (see the documentation for more information). + advanced: true + selector: + text: value: name: Value description: The new value to set for this configuration parameter. @@ -60,12 +74,6 @@ set_config_parameter: required: true selector: text: - bitmask: - name: Bitmask - description: Target a specific bitmask (see the documentation for more information). - advanced: true - selector: - text: bulk_set_partial_config_parameters: name: Bulk set partial configuration parameters for a Z-Wave device (Advanced). @@ -74,6 +82,14 @@ bulk_set_partial_config_parameters: entity: integration: zwave_js fields: + endpoint: + name: Endpoint + description: The configuration parameter's endpoint. + example: 1 + default: 0 + required: false + selector: + text: parameter: name: Parameter description: The id of the configuration parameter you want to configure. diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index b3dd4e34ee8..c6a0f7a845d 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -32,6 +32,7 @@ from zwave_js_server.model.controller import ( from zwave_js_server.model.controller.firmware import ControllerFirmwareUpdateData from zwave_js_server.model.node import Node from zwave_js_server.model.node.firmware import NodeFirmwareUpdateData +from zwave_js_server.model.value import ConfigurationValue, get_value_id_str from homeassistant.components.websocket_api import ERR_INVALID_FORMAT, ERR_NOT_FOUND from homeassistant.components.zwave_js.api import ( @@ -43,6 +44,7 @@ from homeassistant.components.zwave_js.api import ( DEVICE_ID, DSK, ENABLED, + ENDPOINT, ENTRY_ID, ERR_NOT_LOADED, FEATURE, @@ -2756,6 +2758,12 @@ async def test_set_config_parameter( entry = integration ws_client = await hass_ws_client(hass) device = get_device(hass, multisensor_6) + new_value_data = multisensor_6.values[ + get_value_id_str(multisensor_6, 112, 102, 0, 1) + ].data.copy() + new_value_data["endpoint"] = 1 + new_value = ConfigurationValue(multisensor_6, new_value_data) + multisensor_6.values[get_value_id_str(multisensor_6, 112, 102, 1, 1)] = new_value client.async_send_command_no_wait.return_value = None @@ -2787,12 +2795,44 @@ async def test_set_config_parameter( client.async_send_command_no_wait.reset_mock() + client.async_send_command_no_wait.return_value = None + + # Test using a different endpoint + await ws_client.send_json( + { + ID: 2, + TYPE: "zwave_js/set_config_parameter", + DEVICE_ID: device.id, + ENDPOINT: 1, + PROPERTY: 102, + PROPERTY_KEY: 1, + VALUE: 1, + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + + assert len(client.async_send_command_no_wait.call_args_list) == 1 + args = client.async_send_command_no_wait.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 52 + assert args["valueId"] == { + "commandClass": 112, + "endpoint": 1, + "property": 102, + "propertyKey": 1, + } + assert args["value"] == 1 + + client.async_send_command_no_wait.reset_mock() + # Test that hex strings are accepted and converted as expected client.async_send_command_no_wait.return_value = None await ws_client.send_json( { - ID: 2, + ID: 3, TYPE: "zwave_js/set_config_parameter", DEVICE_ID: device.id, PROPERTY: 102, @@ -2824,7 +2864,7 @@ async def test_set_config_parameter( set_param_mock.side_effect = InvalidNewValue("test") await ws_client.send_json( { - ID: 3, + ID: 4, TYPE: "zwave_js/set_config_parameter", DEVICE_ID: device.id, PROPERTY: 102, @@ -2843,7 +2883,7 @@ async def test_set_config_parameter( set_param_mock.side_effect = NotFoundError("test") await ws_client.send_json( { - ID: 4, + ID: 5, TYPE: "zwave_js/set_config_parameter", DEVICE_ID: device.id, PROPERTY: 102, @@ -2862,7 +2902,7 @@ async def test_set_config_parameter( set_param_mock.side_effect = SetValueFailed("test") await ws_client.send_json( { - ID: 5, + ID: 6, TYPE: "zwave_js/set_config_parameter", DEVICE_ID: device.id, PROPERTY: 102, @@ -2881,7 +2921,7 @@ async def test_set_config_parameter( # Test getting non-existent node fails await ws_client.send_json( { - ID: 6, + ID: 7, TYPE: "zwave_js/set_config_parameter", DEVICE_ID: "fake_device", PROPERTY: 102, @@ -2900,7 +2940,7 @@ async def test_set_config_parameter( ): await ws_client.send_json( { - ID: 7, + ID: 8, TYPE: "zwave_js/set_config_parameter", DEVICE_ID: device.id, PROPERTY: 102, @@ -2920,7 +2960,7 @@ async def test_set_config_parameter( await ws_client.send_json( { - ID: 8, + ID: 9, TYPE: "zwave_js/set_config_parameter", DEVICE_ID: device.id, PROPERTY: 102, @@ -2959,6 +2999,7 @@ async def test_get_config_parameters( key = "52-112-0-2" assert result[key]["property"] == 2 assert result[key]["property_key"] is None + assert result[key]["endpoint"] == 0 assert result[key]["metadata"]["type"] == "number" assert result[key]["configuration_value_type"] == "enumerated" assert result[key]["metadata"]["states"] diff --git a/tests/components/zwave_js/test_device_action.py b/tests/components/zwave_js/test_device_action.py index 8672e886ab5..97631c94501 100644 --- a/tests/components/zwave_js/test_device_action.py +++ b/tests/components/zwave_js/test_device_action.py @@ -79,9 +79,10 @@ async def test_get_actions( "domain": DOMAIN, "type": "set_config_parameter", "device_id": device.id, + "endpoint": 0, "parameter": 3, "bitmask": None, - "subtype": "3 (Beeper)", + "subtype": "3 (Beeper) on endpoint 0", "metadata": {}, }, ] @@ -188,6 +189,7 @@ async def test_actions( "domain": DOMAIN, "type": "set_config_parameter", "device_id": device.id, + "endpoint": 0, "parameter": 1, "bitmask": None, "subtype": "3 (Beeper)", @@ -510,6 +512,7 @@ async def test_get_action_capabilities( "domain": DOMAIN, "device_id": device.id, "type": "set_config_parameter", + "endpoint": 0, "parameter": 1, "bitmask": None, "subtype": "1 (Temperature Reporting Threshold)", @@ -542,6 +545,7 @@ async def test_get_action_capabilities( "domain": DOMAIN, "device_id": device.id, "type": "set_config_parameter", + "endpoint": 0, "parameter": 10, "bitmask": None, "subtype": "10 (Temperature Reporting Filter)", @@ -569,6 +573,7 @@ async def test_get_action_capabilities( "domain": DOMAIN, "device_id": device.id, "type": "set_config_parameter", + "endpoint": 0, "parameter": 2, "bitmask": None, "subtype": "2 (HVAC Settings)", diff --git a/tests/components/zwave_js/test_device_condition.py b/tests/components/zwave_js/test_device_condition.py index b66e804eb80..11213d9c375 100644 --- a/tests/components/zwave_js/test_device_condition.py +++ b/tests/components/zwave_js/test_device_condition.py @@ -28,7 +28,7 @@ from tests.common import async_get_device_automations, async_mock_service @pytest.fixture -def calls(hass): +def calls(hass: HomeAssistant): """Track calls to a mock service.""" return async_mock_service(hass, "test", "automation") @@ -63,7 +63,7 @@ async def test_get_conditions( "type": "config_parameter", "device_id": device.id, "value_id": value_id, - "subtype": f"{config_value.property_} ({name})", + "subtype": f"{config_value.property_} ({name}) on endpoint 0", "metadata": {}, }, { diff --git a/tests/components/zwave_js/test_device_trigger.py b/tests/components/zwave_js/test_device_trigger.py index 7f324098324..a8f5ff98fdf 100644 --- a/tests/components/zwave_js/test_device_trigger.py +++ b/tests/components/zwave_js/test_device_trigger.py @@ -32,7 +32,7 @@ from tests.common import ( @pytest.fixture -def calls(hass): +def calls(hass: HomeAssistant): """Track calls to a mock service.""" return async_mock_service(hass, "test", "automation") @@ -1292,7 +1292,7 @@ async def test_get_value_updated_config_parameter_triggers( "property_key": None, "endpoint": 0, "command_class": CommandClass.CONFIGURATION.value, - "subtype": "3 (Beeper)", + "subtype": "3 (Beeper) on endpoint 0", "metadata": {}, } triggers = await async_get_device_automations(