From 7fecbfa2f09fa846c3c77d748bb00856cf0f6bef Mon Sep 17 00:00:00 2001 From: raman325 <7243222+raman325@users.noreply.github.com> Date: Wed, 24 Mar 2021 22:34:56 -0400 Subject: [PATCH 1/4] Add zwave_js.bulk_set_partial_config_parameters service --- homeassistant/components/zwave_js/const.py | 1 + homeassistant/components/zwave_js/services.py | 67 ++++++++++++++- .../components/zwave_js/services.yaml | 28 ++++++- tests/components/zwave_js/test_services.py | 84 +++++++++++++++++++ 4 files changed, 177 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index d9c3a3ebb9b4a..2166261190542 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -52,6 +52,7 @@ # service constants SERVICE_SET_CONFIG_PARAMETER = "set_config_parameter" +SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS = "bulk_set_partial_config_parameters" ATTR_CONFIG_PARAMETER = "parameter" ATTR_CONFIG_PARAMETER_BITMASK = "bitmask" diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py index 6b0d99f4920d7..ce01e895a9911 100644 --- a/homeassistant/components/zwave_js/services.py +++ b/homeassistant/components/zwave_js/services.py @@ -6,7 +6,10 @@ import voluptuous as vol from zwave_js_server.const import CommandStatus from zwave_js_server.model.node import Node as ZwaveNode -from zwave_js_server.util.node import async_set_config_parameter +from zwave_js_server.util.node import ( + async_bulk_set_partial_config_parameters, + async_set_config_parameter, +) from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID from homeassistant.core import HomeAssistant, ServiceCall, callback @@ -37,7 +40,13 @@ def parameter_name_does_not_need_bitmask( # Validates that a bitmask is provided in hex form and converts it to decimal # int equivalent since that's what the library uses BITMASK_SCHEMA = vol.All( - cv.string, vol.Lower, vol.Match(r"^(0x)?[0-9a-f]+$"), lambda value: int(value, 16) + cv.string, + vol.Lower, + vol.Match( + r"^(0x)?[0-9a-f]+$", + msg="Must provide an integer (e.g. 255) or a bitmask in hex form (e.g. 0xff)", + ), + lambda value: int(value, 16), ) @@ -75,6 +84,30 @@ def async_register(self) -> None: ), ) + self._hass.services.async_register( + const.DOMAIN, + const.SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS, + self.async_bulk_set_partial_config_parameters, + schema=vol.All( + { + vol.Optional(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(const.ATTR_CONFIG_PARAMETER): vol.Any( + vol.Coerce(int), cv.string + ), + vol.Required(const.ATTR_CONFIG_VALUE): vol.Any( + vol.Coerce(int), + { + vol.Any(vol.Coerce(int), BITMASK_SCHEMA): vol.Any( + vol.Coerce(int), cv.string + ) + }, + ), + }, + cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), + ), + ) + self._hass.services.async_register( const.DOMAIN, const.SERVICE_REFRESH_VALUE, @@ -122,6 +155,36 @@ async def async_set_config_parameter(self, service: ServiceCall) -> None: _LOGGER.info(msg, zwave_value, node, new_value) + async def async_bulk_set_partial_config_parameters( + self, service: ServiceCall + ) -> None: + """Bulk set multiple partial config values on a node.""" + nodes: set[ZwaveNode] = set() + if ATTR_ENTITY_ID in service.data: + nodes |= { + async_get_node_from_entity_id(self._hass, entity_id) + for entity_id in service.data[ATTR_ENTITY_ID] + } + if ATTR_DEVICE_ID in service.data: + nodes |= { + async_get_node_from_device_id(self._hass, device_id) + for device_id in service.data[ATTR_DEVICE_ID] + } + property_ = service.data[const.ATTR_CONFIG_PARAMETER] + new_value = service.data[const.ATTR_CONFIG_VALUE] + + for node in nodes: + await async_bulk_set_partial_config_parameters( + node, + property_, + new_value, + ) + _LOGGER.info( + "Bulk set partials for configuration parameter %s on Node %s", + property_, + node, + ) + async def async_poll_value(self, service: ServiceCall) -> None: """Poll value on a node.""" for entity_id in service.data[ATTR_ENTITY_ID]: diff --git a/homeassistant/components/zwave_js/services.yaml b/homeassistant/components/zwave_js/services.yaml index 7277f540d7682..c9a1483fd2ec2 100644 --- a/homeassistant/components/zwave_js/services.yaml +++ b/homeassistant/components/zwave_js/services.yaml @@ -59,11 +59,37 @@ set_config_parameter: example: 5 required: true selector: - object: + 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). + description: Allow for bulk setting partial parameters. Useful when multiple partial parameters have to be set at the same time. + target: + entity: + integration: zwave_js + fields: + parameter: + name: Parameter + description: The id of the configuration parameter you want to configure. + example: 9 + required: true + selector: + text: + value: + name: Value + description: The new value(s) to set for this configuration parameter. Can either be a raw integer value to represent the bulk change or a mapping where the key is the bitmask (either in hex or integer form) and the value is the new value you want to set for that partial parameter. + example: + "0x1": 1 + "0x10": 1 + "0x20": 1 + "0x40": 1 + required: true selector: object: diff --git a/tests/components/zwave_js/test_services.py b/tests/components/zwave_js/test_services.py index a03d0b4544b83..5dd34cc2ea109 100644 --- a/tests/components/zwave_js/test_services.py +++ b/tests/components/zwave_js/test_services.py @@ -8,6 +8,7 @@ ATTR_CONFIG_VALUE, ATTR_REFRESH_ALL_VALUES, DOMAIN, + SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS, SERVICE_REFRESH_VALUE, SERVICE_SET_CONFIG_PARAMETER, ) @@ -343,6 +344,89 @@ async def test_set_config_parameter(hass, client, multisensor_6, integration): client.async_send_command.reset_mock() +async def test_bulk_set_config_parameters(hass, client, multisensor_6, integration): + """Test the bulk_set_partial_config_parameters service.""" + # Test setting config parameter by property and property_key + await hass.services.async_call( + DOMAIN, + SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS, + { + ATTR_ENTITY_ID: AIR_TEMPERATURE_SENSOR, + ATTR_CONFIG_PARAMETER: 102, + ATTR_CONFIG_VALUE: 241, + }, + blocking=True, + ) + + 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, + "property": 102, + } + assert args["value"] == 241 + + client.async_send_command_no_wait.reset_mock() + + await hass.services.async_call( + DOMAIN, + SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS, + { + ATTR_ENTITY_ID: AIR_TEMPERATURE_SENSOR, + ATTR_CONFIG_PARAMETER: 102, + ATTR_CONFIG_VALUE: { + 1: 1, + 16: 1, + 32: 1, + 64: 1, + 128: 1, + }, + }, + blocking=True, + ) + + 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, + "property": 102, + } + assert args["value"] == 241 + + client.async_send_command_no_wait.reset_mock() + + await hass.services.async_call( + DOMAIN, + SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS, + { + ATTR_ENTITY_ID: AIR_TEMPERATURE_SENSOR, + ATTR_CONFIG_PARAMETER: 102, + ATTR_CONFIG_VALUE: { + "0x1": 1, + "0x10": 1, + "0x20": 1, + "0x40": 1, + "0x80": 1, + }, + }, + blocking=True, + ) + + 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, + "property": 102, + } + assert args["value"] == 241 + + async def test_poll_value( hass, client, climate_radio_thermostat_ct100_plus_different_endpoints, integration ): From 7dfc482eb03bf856b1278aad3f8e5cd43f0a38d9 Mon Sep 17 00:00:00 2001 From: raman325 <7243222+raman325@users.noreply.github.com> Date: Thu, 25 Mar 2021 12:23:08 -0400 Subject: [PATCH 2/4] update to handle command status --- homeassistant/components/zwave_js/services.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py index ce01e895a9911..66b0b09db177f 100644 --- a/homeassistant/components/zwave_js/services.py +++ b/homeassistant/components/zwave_js/services.py @@ -174,16 +174,21 @@ async def async_bulk_set_partial_config_parameters( new_value = service.data[const.ATTR_CONFIG_VALUE] for node in nodes: - await async_bulk_set_partial_config_parameters( + cmd_status = await async_bulk_set_partial_config_parameters( node, property_, new_value, ) - _LOGGER.info( - "Bulk set partials for configuration parameter %s on Node %s", - property_, - node, - ) + + if cmd_status == CommandStatus.ACCEPTED: + msg = "Bulk set partials for configuration parameter %s on Node %s" + else: + msg = ( + "Added command to queue to bulk set partials for configuration " + "parameter %s on Node %s" + ) + + _LOGGER.info(msg, property_, node) async def async_poll_value(self, service: ServiceCall) -> None: """Poll value on a node.""" From 1d5712900fbd4bc8f12f9ffb0c8e28f36dd565c4 Mon Sep 17 00:00:00 2001 From: raman325 <7243222+raman325@users.noreply.github.com> Date: Fri, 26 Mar 2021 10:47:18 -0400 Subject: [PATCH 3/4] add test for awake node --- tests/components/zwave_js/test_services.py | 34 ++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/components/zwave_js/test_services.py b/tests/components/zwave_js/test_services.py index 5dd34cc2ea109..d459e40376eac 100644 --- a/tests/components/zwave_js/test_services.py +++ b/tests/components/zwave_js/test_services.py @@ -426,6 +426,40 @@ async def test_bulk_set_config_parameters(hass, client, multisensor_6, integrati } assert args["value"] == 241 + client.async_send_command_no_wait.reset_mock() + + # Test that when a device is awake, we call async_send_command instead of + # async_send_command_no_wait + multisensor_6.handle_wake_up(None) + await hass.services.async_call( + DOMAIN, + SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS, + { + ATTR_ENTITY_ID: AIR_TEMPERATURE_SENSOR, + ATTR_CONFIG_PARAMETER: 102, + ATTR_CONFIG_VALUE: { + 1: 1, + 16: 1, + 32: 1, + 64: 1, + 128: 1, + }, + }, + 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"] == 52 + assert args["valueId"] == { + "commandClass": 112, + "property": 102, + } + assert args["value"] == 241 + + client.async_send_command.reset_mock() + async def test_poll_value( hass, client, climate_radio_thermostat_ct100_plus_different_endpoints, integration From f59635403822c79b7a7c910241fd75ed6cf8236b Mon Sep 17 00:00:00 2001 From: raman325 <7243222+raman325@users.noreply.github.com> Date: Mon, 29 Mar 2021 17:35:05 -0400 Subject: [PATCH 4/4] test using a device in service call --- tests/components/zwave_js/test_services.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/components/zwave_js/test_services.py b/tests/components/zwave_js/test_services.py index d459e40376eac..c6bd69abadbd4 100644 --- a/tests/components/zwave_js/test_services.py +++ b/tests/components/zwave_js/test_services.py @@ -13,7 +13,10 @@ SERVICE_SET_CONFIG_PARAMETER, ) from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID -from homeassistant.helpers.device_registry import async_get as async_get_dev_reg +from homeassistant.helpers.device_registry import ( + async_entries_for_config_entry, + async_get as async_get_dev_reg, +) from homeassistant.helpers.entity_registry import async_get as async_get_ent_reg from .common import AIR_TEMPERATURE_SENSOR, CLIMATE_RADIO_THERMOSTAT_ENTITY @@ -346,12 +349,14 @@ async def test_set_config_parameter(hass, client, multisensor_6, integration): async def test_bulk_set_config_parameters(hass, client, multisensor_6, integration): """Test the bulk_set_partial_config_parameters service.""" + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] # Test setting config parameter by property and property_key await hass.services.async_call( DOMAIN, SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS, { - ATTR_ENTITY_ID: AIR_TEMPERATURE_SENSOR, + ATTR_DEVICE_ID: device.id, ATTR_CONFIG_PARAMETER: 102, ATTR_CONFIG_VALUE: 241, },