diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py index 08841465321..ac3f233ba49 100644 --- a/homeassistant/components/zwave_js/services.py +++ b/homeassistant/components/zwave_js/services.py @@ -408,7 +408,7 @@ class ZWaveServices: async def async_set_value(self, service: ServiceCall) -> None: """Set a value on a node.""" # pylint: disable=no-self-use - nodes = service.data[const.ATTR_NODES] + nodes: set[ZwaveNode] = service.data[const.ATTR_NODES] command_class = service.data[const.ATTR_COMMAND_CLASS] property_ = service.data[const.ATTR_PROPERTY] property_key = service.data.get(const.ATTR_PROPERTY_KEY) @@ -418,15 +418,27 @@ class ZWaveServices: options = service.data.get(const.ATTR_OPTIONS) for node in nodes: + value_id = get_value_id( + node, + command_class, + property_, + endpoint=endpoint, + property_key=property_key, + ) + # If value has a string type but the new value is not a string, we need to + # convert it to one. We use new variable `new_value_` to convert the data + # so we can preserve the original `new_value` for every node. + if ( + value_id in node.values + and node.values[value_id].metadata.type == "string" + and not isinstance(new_value, str) + ): + new_value_ = str(new_value) + else: + new_value_ = new_value success = await node.async_set_value( - get_value_id( - node, - command_class, - property_, - endpoint=endpoint, - property_key=property_key, - ), - new_value, + value_id, + new_value_, options=options, wait_for_result=wait_for_result, ) @@ -452,11 +464,16 @@ class ZWaveServices: await self.async_set_value(service) return + command_class = service.data[const.ATTR_COMMAND_CLASS] + property_ = service.data[const.ATTR_PROPERTY] + property_key = service.data.get(const.ATTR_PROPERTY_KEY) + endpoint = service.data.get(const.ATTR_ENDPOINT) + value = { - "commandClass": service.data[const.ATTR_COMMAND_CLASS], - "property": service.data[const.ATTR_PROPERTY], - "propertyKey": service.data.get(const.ATTR_PROPERTY_KEY), - "endpoint": service.data.get(const.ATTR_ENDPOINT), + "commandClass": command_class, + "property": property_, + "propertyKey": property_key, + "endpoint": endpoint, } new_value = service.data[const.ATTR_VALUE] @@ -464,12 +481,30 @@ class ZWaveServices: # schema validation and can use that to get the client, otherwise we can just # get the client from the node. client: ZwaveClient = None - first_node = next((node for node in nodes), None) + first_node: ZwaveNode = next((node for node in nodes), None) if first_node: client = first_node.client else: entry_id = self._hass.config_entries.async_entries(const.DOMAIN)[0].entry_id client = self._hass.data[const.DOMAIN][entry_id][const.DATA_CLIENT] + first_node = next( + node + for node in client.driver.controller.nodes.values() + if get_value_id(node, command_class, property_, endpoint, property_key) + in node.values + ) + + # If value has a string type but the new value is not a string, we need to + # convert it to one + value_id = get_value_id( + first_node, command_class, property_, endpoint, property_key + ) + if ( + value_id in first_node.values + and first_node.values[value_id].metadata.type == "string" + and not isinstance(new_value, str) + ): + new_value = str(new_value) success = await async_multicast_set_value( client=client, diff --git a/tests/components/zwave_js/test_services.py b/tests/components/zwave_js/test_services.py index 0831d08b216..571190bd35c 100644 --- a/tests/components/zwave_js/test_services.py +++ b/tests/components/zwave_js/test_services.py @@ -43,6 +43,7 @@ from .common import ( CLIMATE_DANFOSS_LC13_ENTITY, CLIMATE_EUROTRONICS_SPIRIT_Z_ENTITY, CLIMATE_RADIO_THERMOSTAT_ENTITY, + SCHLAGE_BE469_LOCK_ENTITY, ) from tests.common import MockConfigEntry @@ -1021,6 +1022,51 @@ async def test_set_value(hass, client, climate_danfoss_lc_13, integration): ) +async def test_set_value_string( + hass, client, climate_danfoss_lc_13, lock_schlage_be469, integration +): + """Test set_value service converts number to string when needed.""" + client.async_send_command.return_value = {"success": True} + + # Test that number gets converted to a string when needed + await hass.services.async_call( + DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: SCHLAGE_BE469_LOCK_ENTITY, + ATTR_COMMAND_CLASS: 99, + ATTR_PROPERTY: "userCode", + ATTR_PROPERTY_KEY: 1, + ATTR_VALUE: 12345, + }, + 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"] == lock_schlage_be469.node_id + assert args["valueId"] == { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userCode", + "propertyName": "userCode", + "propertyKey": 1, + "propertyKeyName": "1", + "metadata": { + "type": "string", + "readable": True, + "writeable": True, + "minLength": 4, + "maxLength": 10, + "label": "User Code (1)", + }, + "value": "**********", + } + assert args["value"] == "12345" + + async def test_set_value_options(hass, client, aeon_smart_switch_6, integration): """Test set_value service with options.""" await hass.services.async_call( @@ -1381,6 +1427,41 @@ async def test_multicast_set_value_options( client.async_send_command.reset_mock() +async def test_multicast_set_value_string( + hass, + client, + lock_id_lock_as_id150, + lock_schlage_be469, + integration, +): + """Test multicast_set_value service converts number to string when needed.""" + client.async_send_command.return_value = {"success": True} + + # Test that number gets converted to a string when needed + await hass.services.async_call( + DOMAIN, + SERVICE_MULTICAST_SET_VALUE, + { + ATTR_BROADCAST: True, + ATTR_COMMAND_CLASS: 99, + ATTR_PROPERTY: "userCode", + ATTR_PROPERTY_KEY: 1, + ATTR_VALUE: 12345, + }, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "broadcast_node.set_value" + assert args["valueId"] == { + "commandClass": 99, + "property": "userCode", + "propertyKey": 1, + } + assert args["value"] == "12345" + + async def test_ping( hass, client,