From 7b3e5fdf9d6081a2bd3af96862de155f1313a40a Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 10 Jan 2022 00:28:36 -0500 Subject: [PATCH] Fix zwave_js device actions (#63769) --- .../components/zwave_js/device_action.py | 31 +- ...ate_radio_thermostat_ct100_plus_state.json | 123 +++++++- .../components/zwave_js/test_device_action.py | 292 +++++++++++++----- 3 files changed, 360 insertions(+), 86 deletions(-) diff --git a/homeassistant/components/zwave_js/device_action.py b/homeassistant/components/zwave_js/device_action.py index 14d64f87eb7..f819a33f1d4 100644 --- a/homeassistant/components/zwave_js/device_action.py +++ b/homeassistant/components/zwave_js/device_action.py @@ -14,7 +14,14 @@ from zwave_js_server.util.command_class.meter import get_meter_type from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_ENTITY_ID, CONF_TYPE +from homeassistant.const import ( + ATTR_DEVICE_ID, + ATTR_DOMAIN, + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_ENTITY_ID, + CONF_TYPE, +) from homeassistant.core import Context, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry @@ -227,7 +234,22 @@ async def async_call_action_from_config( if action_type not in ACTION_TYPES: raise HomeAssistantError(f"Unhandled action type {action_type}") - service_data = {k: v for k, v in config.items() if v not in (None, "")} + # Don't include domain, subtype or any null/empty values in the service call + service_data = { + k: v + for k, v in config.items() + if k not in (ATTR_DOMAIN, CONF_SUBTYPE) and v not in (None, "") + } + + # Entity services (including refresh value which is a fake entity service) expects + # just an entity ID + if action_type in ( + SERVICE_REFRESH_VALUE, + SERVICE_SET_LOCK_USERCODE, + SERVICE_CLEAR_LOCK_USERCODE, + SERVICE_RESET_METER, + ): + service_data.pop(ATTR_DEVICE_ID) await hass.services.async_call( DOMAIN, service, service_data, blocking=True, context=context ) @@ -283,7 +305,10 @@ async def async_get_action_capabilities( "extra_fields": vol.Schema( { vol.Required(ATTR_COMMAND_CLASS): vol.In( - {cc.value: cc.name for cc in CommandClass} + { + CommandClass(cc.id).value: cc.name + for cc in sorted(node.command_classes, key=lambda cc: cc.name) # type: ignore[no-any-return] + } ), vol.Required(ATTR_PROPERTY): cv.string, vol.Optional(ATTR_PROPERTY_KEY): cv.string, diff --git a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_state.json b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_state.json index 34df415301e..cd5a6bd4abe 100644 --- a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_state.json +++ b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_state.json @@ -57,7 +57,128 @@ }, { "nodeId": 13, "index": 2 } ], - "commandClasses": [], + "commandClasses": [ + { + "id": 49, + "name": "Multilevel Sensor", + "version": 5, + "isSecure": false + }, + { + "id": 64, + "name": "Thermostat Mode", + "version": 2, + "isSecure": false + }, + { + "id": 66, + "name": "Thermostat Operating State", + "version": 2, + "isSecure": false + }, + { + "id": 67, + "name": "Thermostat Setpoint", + "version": 2, + "isSecure": false + }, + { + "id": 68, + "name": "Thermostat Fan Mode", + "version": 1, + "isSecure": false + }, + { + "id": 69, + "name": "Thermostat Fan State", + "version": 1, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 4, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 115, + "name": "Powerlevel", + "version": 1, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 3, + "isSecure": false + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": false + }, + { + "id": 129, + "name": "Clock", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": false + }, + { + "id": 135, + "name": "Indicator", + "version": 1, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + } + ], "values": [ { "commandClassName": "Manufacturer Specific", diff --git a/tests/components/zwave_js/test_device_action.py b/tests/components/zwave_js/test_device_action.py index c222287b5c0..58f8091e2a1 100644 --- a/tests/components/zwave_js/test_device_action.py +++ b/tests/components/zwave_js/test_device_action.py @@ -1,4 +1,6 @@ """The tests for Z-Wave JS device actions.""" +from unittest.mock import patch + import pytest import voluptuous_serialize from zwave_js_server.client import Client @@ -15,7 +17,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, device_registry from homeassistant.setup import async_setup_component -from tests.common import async_get_device_automations, async_mock_service +from tests.common import async_get_device_automations async def test_get_actions( @@ -92,8 +94,130 @@ async def test_get_actions_meter( assert len(filtered_actions) > 0 -async def test_action(hass: HomeAssistant) -> None: - """Test for turn_on and turn_off actions.""" +async def test_actions( + hass: HomeAssistant, + client: Client, + climate_radio_thermostat_ct100_plus: Node, + integration: ConfigEntry, +) -> None: + """Test actions.""" + node = climate_radio_thermostat_ct100_plus + device_id = get_device_id(client, node) + dev_reg = device_registry.async_get(hass) + device = dev_reg.async_get_device({device_id}) + assert device + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "event", + "event_type": "test_event_refresh_value", + }, + "action": { + "domain": DOMAIN, + "type": "refresh_value", + "device_id": device.id, + "entity_id": "climate.z_wave_thermostat", + }, + }, + { + "trigger": { + "platform": "event", + "event_type": "test_event_ping", + }, + "action": { + "domain": DOMAIN, + "type": "ping", + "device_id": device.id, + }, + }, + { + "trigger": { + "platform": "event", + "event_type": "test_event_set_value", + }, + "action": { + "domain": DOMAIN, + "type": "set_value", + "device_id": device.id, + "command_class": 112, + "property": 1, + "value": 1, + }, + }, + { + "trigger": { + "platform": "event", + "event_type": "test_event_set_config_parameter", + }, + "action": { + "domain": DOMAIN, + "type": "set_config_parameter", + "device_id": device.id, + "parameter": 1, + "bitmask": None, + "subtype": "2-112-0-3 (Beeper)", + "value": 1, + }, + }, + ] + }, + ) + + with patch("zwave_js_server.model.node.Node.async_poll_value") as mock_call: + hass.bus.async_fire("test_event_refresh_value") + await hass.async_block_till_done() + mock_call.assert_called_once() + args = mock_call.call_args_list[0][0] + assert len(args) == 1 + assert args[0].value_id == "13-64-1-mode" + + with patch("zwave_js_server.model.node.Node.async_ping") as mock_call: + hass.bus.async_fire("test_event_ping") + await hass.async_block_till_done() + mock_call.assert_called_once() + args = mock_call.call_args_list[0][0] + assert len(args) == 0 + + with patch("zwave_js_server.model.node.Node.async_set_value") as mock_call: + hass.bus.async_fire("test_event_set_value") + await hass.async_block_till_done() + mock_call.assert_called_once() + args = mock_call.call_args_list[0][0] + assert len(args) == 2 + assert args[0] == "13-112-0-1" + assert args[1] == 1 + + with patch( + "homeassistant.components.zwave_js.services.async_set_config_parameter" + ) as mock_call: + hass.bus.async_fire("test_event_set_config_parameter") + await hass.async_block_till_done() + mock_call.assert_called_once() + args = mock_call.call_args_list[0][0] + assert len(args) == 3 + assert args[0].node_id == 13 + assert args[1] == 1 + assert args[2] == 1 + + +async def test_lock_actions( + hass: HomeAssistant, + client: Client, + lock_schlage_be469: Node, + integration: ConfigEntry, +) -> None: + """Test actions for locks.""" + node = lock_schlage_be469 + device_id = get_device_id(client, node) + dev_reg = device_registry.async_get(hass) + device = dev_reg.async_get_device({device_id}) + assert device + assert await async_setup_component( hass, automation.DOMAIN, @@ -107,7 +231,7 @@ async def test_action(hass: HomeAssistant) -> None: "action": { "domain": DOMAIN, "type": "clear_lock_usercode", - "device_id": "fake", + "device_id": device.id, "entity_id": "lock.touchscreen_deadbolt", "code_slot": 1, }, @@ -120,97 +244,80 @@ async def test_action(hass: HomeAssistant) -> None: "action": { "domain": DOMAIN, "type": "set_lock_usercode", - "device_id": "fake", + "device_id": device.id, "entity_id": "lock.touchscreen_deadbolt", "code_slot": 1, "usercode": "1234", }, }, - { - "trigger": { - "platform": "event", - "event_type": "test_event_refresh_value", - }, - "action": { - "domain": DOMAIN, - "type": "refresh_value", - "device_id": "fake", - "entity_id": "lock.touchscreen_deadbolt", - }, - }, - { - "trigger": { - "platform": "event", - "event_type": "test_event_ping", - }, - "action": { - "domain": DOMAIN, - "type": "ping", - "device_id": "fake", - }, - }, - { - "trigger": { - "platform": "event", - "event_type": "test_event_set_value", - }, - "action": { - "domain": DOMAIN, - "type": "set_value", - "device_id": "fake", - "command_class": 112, - "property": "test", - "value": 1, - }, - }, - { - "trigger": { - "platform": "event", - "event_type": "test_event_set_config_parameter", - }, - "action": { - "domain": DOMAIN, - "type": "set_config_parameter", - "device_id": "fake", - "parameter": 3, - "bitmask": None, - "subtype": "2-112-0-3 (Beeper)", - "value": 255, - }, - }, ] }, ) - clear_lock_usercode = async_mock_service(hass, "zwave_js", "clear_lock_usercode") - hass.bus.async_fire("test_event_clear_lock_usercode") - await hass.async_block_till_done() - assert len(clear_lock_usercode) == 1 + with patch("homeassistant.components.zwave_js.lock.clear_usercode") as mock_call: + hass.bus.async_fire("test_event_clear_lock_usercode") + await hass.async_block_till_done() + mock_call.assert_called_once() + args = mock_call.call_args_list[0][0] + assert len(args) == 2 + assert args[0].node_id == node.node_id + assert args[1] == 1 - set_lock_usercode = async_mock_service(hass, "zwave_js", "set_lock_usercode") - hass.bus.async_fire("test_event_set_lock_usercode") - await hass.async_block_till_done() - assert len(set_lock_usercode) == 1 + with patch("homeassistant.components.zwave_js.lock.set_usercode") as mock_call: + hass.bus.async_fire("test_event_set_lock_usercode") + await hass.async_block_till_done() + mock_call.assert_called_once() + args = mock_call.call_args_list[0][0] + assert len(args) == 3 + assert args[0].node_id == node.node_id + assert args[1] == 1 + assert args[2] == "1234" - refresh_value = async_mock_service(hass, "zwave_js", "refresh_value") - hass.bus.async_fire("test_event_refresh_value") - await hass.async_block_till_done() - assert len(refresh_value) == 1 - ping = async_mock_service(hass, "zwave_js", "ping") - hass.bus.async_fire("test_event_ping") - await hass.async_block_till_done() - assert len(ping) == 1 +async def test_reset_meter_action( + hass: HomeAssistant, + client: Client, + aeon_smart_switch_6: Node, + integration: ConfigEntry, +) -> None: + """Test reset_meter action.""" + node = aeon_smart_switch_6 + device_id = get_device_id(client, node) + dev_reg = device_registry.async_get(hass) + device = dev_reg.async_get_device({device_id}) + assert device - set_value = async_mock_service(hass, "zwave_js", "set_value") - hass.bus.async_fire("test_event_set_value") - await hass.async_block_till_done() - assert len(set_value) == 1 + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "event", + "event_type": "test_event_reset_meter", + }, + "action": { + "domain": DOMAIN, + "type": "reset_meter", + "device_id": device.id, + "entity_id": "sensor.smart_switch_6_electric_consumed_kwh", + }, + }, + ] + }, + ) - set_config_parameter = async_mock_service(hass, "zwave_js", "set_config_parameter") - hass.bus.async_fire("test_event_set_config_parameter") - await hass.async_block_till_done() - assert len(set_config_parameter) == 1 + with patch( + "zwave_js_server.model.endpoint.Endpoint.async_invoke_cc_api" + ) as mock_call: + hass.bus.async_fire("test_event_reset_meter") + await hass.async_block_till_done() + mock_call.assert_called_once() + args = mock_call.call_args_list[0][0] + assert len(args) == 2 + assert args[0] == CommandClass.METER + assert args[1] == "reset" async def test_get_action_capabilities( @@ -266,7 +373,28 @@ async def test_get_action_capabilities( ) assert capabilities and "extra_fields" in capabilities - cc_options = [(cc.value, cc.name) for cc in CommandClass] + cc_options = [ + (133, "Association"), + (89, "Association Group Information"), + (128, "Battery"), + (129, "Clock"), + (112, "Configuration"), + (90, "Device Reset Locally"), + (122, "Firmware Update Meta Data"), + (135, "Indicator"), + (114, "Manufacturer Specific"), + (96, "Multi Channel"), + (142, "Multi Channel Association"), + (49, "Multilevel Sensor"), + (115, "Powerlevel"), + (68, "Thermostat Fan Mode"), + (69, "Thermostat Fan State"), + (64, "Thermostat Mode"), + (66, "Thermostat Operating State"), + (67, "Thermostat Setpoint"), + (134, "Version"), + (94, "Z-Wave Plus Info"), + ] assert voluptuous_serialize.convert( capabilities["extra_fields"], custom_serializer=cv.custom_serializer