From 3a2afb8bde2b8be06c8e10b224122ff087141d1b Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 20 Aug 2021 05:18:19 -0400 Subject: [PATCH] Support group entities in zwave_js service calls (#54903) --- homeassistant/components/zwave_js/services.py | 4 +- tests/components/zwave_js/test_services.py | 213 +++++++++++++++++- 2 files changed, 213 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py index fa0e93a72aa..a24f8461873 100644 --- a/homeassistant/components/zwave_js/services.py +++ b/homeassistant/components/zwave_js/services.py @@ -17,6 +17,7 @@ from zwave_js_server.util.node import ( async_set_config_parameter, ) +from homeassistant.components.group import expand_entity_ids from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID from homeassistant.core import HomeAssistant, ServiceCall, callback import homeassistant.helpers.config_validation as cv @@ -95,7 +96,7 @@ class ZWaveServices: def get_nodes_from_service_data(val: dict[str, Any]) -> dict[str, Any]: """Get nodes set from service data.""" nodes: set[ZwaveNode] = set() - for entity_id in val.pop(ATTR_ENTITY_ID, []): + for entity_id in expand_entity_ids(self._hass, val.pop(ATTR_ENTITY_ID, [])): try: nodes.add( async_get_node_from_entity_id( @@ -152,6 +153,7 @@ class ZWaveServices: @callback def validate_entities(val: dict[str, Any]) -> dict[str, Any]: """Validate entities exist and are from the zwave_js platform.""" + val[ATTR_ENTITY_ID] = expand_entity_ids(self._hass, val[ATTR_ENTITY_ID]) for entity_id in val[ATTR_ENTITY_ID]: entry = self._ent_reg.async_get(entity_id) if entry is None or entry.platform != const.DOMAIN: diff --git a/tests/components/zwave_js/test_services.py b/tests/components/zwave_js/test_services.py index 4cc5b599f19..275a2dbb403 100644 --- a/tests/components/zwave_js/test_services.py +++ b/tests/components/zwave_js/test_services.py @@ -5,6 +5,7 @@ import pytest import voluptuous as vol from zwave_js_server.exceptions import SetValueFailed +from homeassistant.components.group import Group from homeassistant.components.zwave_js.const import ( ATTR_BROADCAST, ATTR_COMMAND_CLASS, @@ -31,6 +32,7 @@ from homeassistant.helpers.device_registry import ( async_get as async_get_dev_reg, ) from homeassistant.helpers.entity_registry import async_get as async_get_ent_reg +from homeassistant.setup import async_setup_component from .common import ( AEON_SMART_SWITCH_LIGHT_ENTITY, @@ -268,6 +270,52 @@ async def test_set_config_parameter(hass, client, multisensor_6, integration): client.async_send_command_no_wait.reset_mock() + # Test groups get expanded + assert await async_setup_component(hass, "group", {}) + await Group.async_create_group(hass, "test", [AIR_TEMPERATURE_SENSOR]) + await hass.services.async_call( + DOMAIN, + SERVICE_SET_CONFIG_PARAMETER, + { + ATTR_ENTITY_ID: "group.test", + ATTR_CONFIG_PARAMETER: 102, + ATTR_CONFIG_PARAMETER_BITMASK: "0x01", + ATTR_CONFIG_VALUE: 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"] == { + "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 that we can't include a bitmask value if parameter is a string with pytest.raises(vol.Invalid): await hass.services.async_call( @@ -550,11 +598,43 @@ async def test_bulk_set_config_parameters(hass, client, multisensor_6, integrati client.async_send_command.reset_mock() + # Test groups get expanded + assert await async_setup_component(hass, "group", {}) + await Group.async_create_group(hass, "test", [AIR_TEMPERATURE_SENSOR]) + await hass.services.async_call( + DOMAIN, + SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS, + { + ATTR_ENTITY_ID: "group.test", + ATTR_CONFIG_PARAMETER: 102, + ATTR_CONFIG_VALUE: { + 1: 1, + 16: 1, + 32: 1, + 64: 1, + 128: 1, + }, + }, + blocking=True, + ) -async def test_poll_value( + 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_refresh_value( hass, client, climate_radio_thermostat_ct100_plus_different_endpoints, integration ): - """Test the poll_value service.""" + """Test the refresh_value service.""" # Test polling the primary value client.async_send_command.return_value = {"result": 2} await hass.services.async_call( @@ -620,6 +700,25 @@ async def test_poll_value( ) assert len(client.async_send_command.call_args_list) == 8 + client.async_send_command.reset_mock() + + # Test groups get expanded + assert await async_setup_component(hass, "group", {}) + await Group.async_create_group(hass, "test", [CLIMATE_RADIO_THERMOSTAT_ENTITY]) + client.async_send_command.return_value = {"result": 2} + await hass.services.async_call( + DOMAIN, + SERVICE_REFRESH_VALUE, + { + ATTR_ENTITY_ID: "group.test", + ATTR_REFRESH_ALL_VALUES: "true", + }, + blocking=True, + ) + assert len(client.async_send_command.call_args_list) == 8 + + client.async_send_command.reset_mock() + # Test polling against an invalid entity raises MultipleInvalid with pytest.raises(vol.MultipleInvalid): await hass.services.async_call( @@ -709,6 +808,46 @@ async def test_set_value(hass, client, climate_danfoss_lc_13, integration): client.async_send_command.reset_mock() + # Test groups get expanded + assert await async_setup_component(hass, "group", {}) + await Group.async_create_group(hass, "test", [CLIMATE_DANFOSS_LC13_ENTITY]) + await hass.services.async_call( + DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: "group.test", + 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} @@ -878,6 +1017,40 @@ async def test_multicast_set_value( client.async_send_command.reset_mock() + # Test groups get expanded for multicast call + assert await async_setup_component(hass, "group", {}) + await Group.async_create_group( + hass, "test", [CLIMATE_DANFOSS_LC13_ENTITY, CLIMATE_EUROTRONICS_SPIRIT_Z_ENTITY] + ) + await hass.services.async_call( + DOMAIN, + SERVICE_MULTICAST_SET_VALUE, + { + ATTR_ENTITY_ID: "group.test", + ATTR_COMMAND_CLASS: 67, + ATTR_PROPERTY: "setpoint", + ATTR_PROPERTY_KEY: 1, + 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_eurotronic_spirit_z.node_id, + climate_danfoss_lc_13.node_id, + ] + assert args["valueId"] == { + "commandClass": 67, + "property": "setpoint", + "propertyKey": 1, + } + assert args["value"] == 2 + + client.async_send_command.reset_mock() + # Test successful broadcast call await hass.services.async_call( DOMAIN, @@ -1070,8 +1243,42 @@ async def test_ping( blocking=True, ) + # assert client.async_send_command.call_args_list is None assert len(client.async_send_command.call_args_list) == 2 - args = client.async_send_command.call_args[0][0] + args = client.async_send_command.call_args_list[0][0][0] + assert args["command"] == "node.ping" + assert ( + args["nodeId"] + == climate_radio_thermostat_ct100_plus_different_endpoints.node_id + ) + args = client.async_send_command.call_args_list[1][0][0] + assert args["command"] == "node.ping" + assert args["nodeId"] == climate_danfoss_lc_13.node_id + + client.async_send_command.reset_mock() + + # Test groups get expanded for multicast call + assert await async_setup_component(hass, "group", {}) + await Group.async_create_group( + hass, "test", [CLIMATE_DANFOSS_LC13_ENTITY, CLIMATE_RADIO_THERMOSTAT_ENTITY] + ) + await hass.services.async_call( + DOMAIN, + SERVICE_PING, + { + ATTR_ENTITY_ID: "group.test", + }, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 2 + args = client.async_send_command.call_args_list[0][0][0] + assert args["command"] == "node.ping" + assert ( + args["nodeId"] + == climate_radio_thermostat_ct100_plus_different_endpoints.node_id + ) + args = client.async_send_command.call_args_list[1][0][0] assert args["command"] == "node.ping" assert args["nodeId"] == climate_danfoss_lc_13.node_id