diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 5cf3ba32411..754fdfc25ab 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -51,6 +51,7 @@ from .const import ( EVENT_DEVICE_ADDED_TO_REGISTRY, ) from .helpers import async_enable_statistics, update_data_collection_preference +from .services import BITMASK_SCHEMA # general API constants ID = "id" @@ -925,7 +926,7 @@ async def websocket_refresh_node_cc_values( vol.Required(NODE_ID): int, vol.Required(PROPERTY): int, vol.Optional(PROPERTY_KEY): int, - vol.Required(VALUE): int, + vol.Required(VALUE): vol.Any(int, BITMASK_SCHEMA), } ) @websocket_api.async_response diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py index 930f002cf1e..47709a908ed 100644 --- a/homeassistant/components/zwave_js/services.py +++ b/homeassistant/components/zwave_js/services.py @@ -66,6 +66,14 @@ BITMASK_SCHEMA = vol.All( lambda value: int(value, 16), ) +VALUE_SCHEMA = vol.Any( + bool, + vol.Coerce(int), + vol.Coerce(float), + BITMASK_SCHEMA, + cv.string, +) + class ZWaveServices: """Class that holds our services (Zwave Commands) that should be published to hass.""" @@ -177,7 +185,7 @@ class ZWaveServices: vol.Coerce(int), BITMASK_SCHEMA ), vol.Required(const.ATTR_CONFIG_VALUE): vol.Any( - vol.Coerce(int), cv.string + vol.Coerce(int), BITMASK_SCHEMA, cv.string ), }, cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), @@ -204,7 +212,7 @@ class ZWaveServices: { vol.Any( vol.Coerce(int), BITMASK_SCHEMA, cv.string - ): vol.Any(vol.Coerce(int), cv.string) + ): vol.Any(vol.Coerce(int), BITMASK_SCHEMA, cv.string) }, ), }, @@ -224,7 +232,7 @@ class ZWaveServices: vol.Required(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional( const.ATTR_REFRESH_ALL_VALUES, default=False - ): bool, + ): cv.boolean, }, validate_entities, ) @@ -250,10 +258,8 @@ class ZWaveServices: vol.Coerce(int), str ), vol.Optional(const.ATTR_ENDPOINT): vol.Coerce(int), - vol.Required(const.ATTR_VALUE): vol.Any( - bool, vol.Coerce(int), vol.Coerce(float), cv.string - ), - vol.Optional(const.ATTR_WAIT_FOR_RESULT): vol.Coerce(bool), + vol.Required(const.ATTR_VALUE): VALUE_SCHEMA, + vol.Optional(const.ATTR_WAIT_FOR_RESULT): cv.boolean, }, cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), get_nodes_from_service_data, @@ -281,9 +287,7 @@ class ZWaveServices: vol.Coerce(int), str ), vol.Optional(const.ATTR_ENDPOINT): vol.Coerce(int), - vol.Required(const.ATTR_VALUE): vol.Any( - bool, vol.Coerce(int), vol.Coerce(float), cv.string - ), + vol.Required(const.ATTR_VALUE): VALUE_SCHEMA, }, vol.Any( cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index bb34193e2d1..f0b5a470df0 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -1179,13 +1179,62 @@ async def test_set_config_parameter( 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, + TYPE: "zwave_js/set_config_parameter", + ENTRY_ID: entry.entry_id, + NODE_ID: 52, + PROPERTY: 102, + PROPERTY_KEY: 1, + VALUE: "0x1", + } + ) + + 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"] == { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 102, + "propertyName": "Group 2: Send battery reports", + "propertyKey": 1, + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": True, + "label": "Group 2: Send battery reports", + "description": "Include battery information in periodic reports to Group 2", + "isFromConfig": True, + }, + "value": 0, + } + assert args["value"] == 1 + + client.async_send_command_no_wait.reset_mock() + with patch( "homeassistant.components.zwave_js.api.async_set_config_parameter", ) as set_param_mock: set_param_mock.side_effect = InvalidNewValue("test") await ws_client.send_json( { - ID: 2, + ID: 3, TYPE: "zwave_js/set_config_parameter", ENTRY_ID: entry.entry_id, NODE_ID: 52, @@ -1205,7 +1254,7 @@ async def test_set_config_parameter( set_param_mock.side_effect = NotFoundError("test") await ws_client.send_json( { - ID: 3, + ID: 4, TYPE: "zwave_js/set_config_parameter", ENTRY_ID: entry.entry_id, NODE_ID: 52, @@ -1225,7 +1274,7 @@ async def test_set_config_parameter( set_param_mock.side_effect = SetValueFailed("test") await ws_client.send_json( { - ID: 4, + ID: 5, TYPE: "zwave_js/set_config_parameter", ENTRY_ID: entry.entry_id, NODE_ID: 52, @@ -1245,7 +1294,7 @@ async def test_set_config_parameter( # Test getting non-existent node fails await ws_client.send_json( { - ID: 5, + ID: 6, TYPE: "zwave_js/set_config_parameter", ENTRY_ID: entry.entry_id, NODE_ID: 9999, @@ -1264,7 +1313,7 @@ async def test_set_config_parameter( await ws_client.send_json( { - ID: 6, + ID: 7, TYPE: "zwave_js/set_config_parameter", ENTRY_ID: entry.entry_id, NODE_ID: 52, diff --git a/tests/components/zwave_js/test_services.py b/tests/components/zwave_js/test_services.py index a7aea70f6a7..dfc7ddaa85d 100644 --- a/tests/components/zwave_js/test_services.py +++ b/tests/components/zwave_js/test_services.py @@ -89,6 +89,50 @@ async def test_set_config_parameter(hass, client, multisensor_6, integration): client.async_send_command_no_wait.reset_mock() + # Test setting config parameter value in hex + await hass.services.async_call( + DOMAIN, + SERVICE_SET_CONFIG_PARAMETER, + { + ATTR_ENTITY_ID: AIR_TEMPERATURE_SENSOR, + ATTR_CONFIG_PARAMETER: 102, + ATTR_CONFIG_PARAMETER_BITMASK: 1, + ATTR_CONFIG_VALUE: "0x1", + }, + 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"] == { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 102, + "propertyName": "Group 2: Send battery reports", + "propertyKey": 1, + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": True, + "label": "Group 2: Send battery reports", + "description": "Include battery information in periodic reports to Group 2", + "isFromConfig": True, + }, + "value": 0, + } + assert args["value"] == 1 + + client.async_send_command_no_wait.reset_mock() + # Test setting parameter by property name await hass.services.async_call( DOMAIN, @@ -419,6 +463,36 @@ async def test_bulk_set_config_parameters(hass, client, multisensor_6, integrati client.async_send_command_no_wait.reset_mock() + # Test using hex values for config parameter values + 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: "0x1", + 16: "0x1", + 32: "0x1", + 64: "0x1", + 128: "0x1", + }, + }, + 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, @@ -535,6 +609,21 @@ async def test_poll_value( ) assert len(client.async_send_command.call_args_list) == 8 + client.async_send_command.reset_mock() + + # Test polling all watched values using string for boolean + client.async_send_command.return_value = {"result": 2} + await hass.services.async_call( + DOMAIN, + SERVICE_REFRESH_VALUE, + { + ATTR_ENTITY_ID: CLIMATE_RADIO_THERMOSTAT_ENTITY, + ATTR_REFRESH_ALL_VALUES: "true", + }, + blocking=True, + ) + assert len(client.async_send_command.call_args_list) == 8 + # Test polling against an invalid entity raises MultipleInvalid with pytest.raises(vol.MultipleInvalid): await hass.services.async_call( @@ -586,6 +675,44 @@ async def test_set_value(hass, client, climate_danfoss_lc_13, integration): client.async_send_command_no_wait.reset_mock() + # Test bitmask as value and non bool as bool + await hass.services.async_call( + DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: CLIMATE_DANFOSS_LC13_ENTITY, + ATTR_COMMAND_CLASS: 117, + ATTR_PROPERTY: "local", + ATTR_VALUE: "0x2", + ATTR_WAIT_FOR_RESULT: 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"] == 5 + assert args["valueId"] == { + "commandClassName": "Protection", + "commandClass": 117, + "endpoint": 0, + "property": "local", + "propertyName": "local", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "label": "Local protection state", + "states": {"0": "Unprotected", "2": "NoOperationPossible"}, + }, + "value": 0, + } + assert args["value"] == 2 + + client.async_send_command.reset_mock() + # Test that when a command fails we raise an exception client.async_send_command.return_value = {"success": False} @@ -680,6 +807,37 @@ async def test_multicast_set_value( client.async_send_command.reset_mock() + # Test successful multicast call with hex value + await hass.services.async_call( + DOMAIN, + SERVICE_MULTICAST_SET_VALUE, + { + ATTR_ENTITY_ID: [ + CLIMATE_DANFOSS_LC13_ENTITY, + CLIMATE_RADIO_THERMOSTAT_ENTITY, + ], + ATTR_COMMAND_CLASS: 117, + ATTR_PROPERTY: "local", + ATTR_VALUE: "0x2", + }, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "multicast_group.set_value" + assert args["nodeIDs"] == [ + climate_radio_thermostat_ct100_plus_different_endpoints.node_id, + climate_danfoss_lc_13.node_id, + ] + assert args["valueId"] == { + "commandClass": 117, + "property": "local", + } + assert args["value"] == 2 + + client.async_send_command.reset_mock() + # Test successful broadcast call await hass.services.async_call( DOMAIN,