Fix zwave_js device actions (#63769)

This commit is contained in:
Raman Gupta 2022-01-10 00:28:36 -05:00 committed by GitHub
parent b5bb692fe4
commit 7b3e5fdf9d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 360 additions and 86 deletions

View File

@ -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.lock import DOMAIN as LOCK_DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_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.core import Context, HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry from homeassistant.helpers import entity_registry
@ -227,7 +234,22 @@ async def async_call_action_from_config(
if action_type not in ACTION_TYPES: if action_type not in ACTION_TYPES:
raise HomeAssistantError(f"Unhandled action type {action_type}") 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( await hass.services.async_call(
DOMAIN, service, service_data, blocking=True, context=context DOMAIN, service, service_data, blocking=True, context=context
) )
@ -283,7 +305,10 @@ async def async_get_action_capabilities(
"extra_fields": vol.Schema( "extra_fields": vol.Schema(
{ {
vol.Required(ATTR_COMMAND_CLASS): vol.In( 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.Required(ATTR_PROPERTY): cv.string,
vol.Optional(ATTR_PROPERTY_KEY): cv.string, vol.Optional(ATTR_PROPERTY_KEY): cv.string,

View File

@ -57,7 +57,128 @@
}, },
{ "nodeId": 13, "index": 2 } { "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": [ "values": [
{ {
"commandClassName": "Manufacturer Specific", "commandClassName": "Manufacturer Specific",

View File

@ -1,4 +1,6 @@
"""The tests for Z-Wave JS device actions.""" """The tests for Z-Wave JS device actions."""
from unittest.mock import patch
import pytest import pytest
import voluptuous_serialize import voluptuous_serialize
from zwave_js_server.client import Client 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.helpers import config_validation as cv, device_registry
from homeassistant.setup import async_setup_component 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( async def test_get_actions(
@ -92,8 +94,130 @@ async def test_get_actions_meter(
assert len(filtered_actions) > 0 assert len(filtered_actions) > 0
async def test_action(hass: HomeAssistant) -> None: async def test_actions(
"""Test for turn_on and turn_off 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( assert await async_setup_component(
hass, hass,
automation.DOMAIN, automation.DOMAIN,
@ -107,7 +231,7 @@ async def test_action(hass: HomeAssistant) -> None:
"action": { "action": {
"domain": DOMAIN, "domain": DOMAIN,
"type": "clear_lock_usercode", "type": "clear_lock_usercode",
"device_id": "fake", "device_id": device.id,
"entity_id": "lock.touchscreen_deadbolt", "entity_id": "lock.touchscreen_deadbolt",
"code_slot": 1, "code_slot": 1,
}, },
@ -120,97 +244,80 @@ async def test_action(hass: HomeAssistant) -> None:
"action": { "action": {
"domain": DOMAIN, "domain": DOMAIN,
"type": "set_lock_usercode", "type": "set_lock_usercode",
"device_id": "fake", "device_id": device.id,
"entity_id": "lock.touchscreen_deadbolt", "entity_id": "lock.touchscreen_deadbolt",
"code_slot": 1, "code_slot": 1,
"usercode": "1234", "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") with patch("homeassistant.components.zwave_js.lock.clear_usercode") as mock_call:
hass.bus.async_fire("test_event_clear_lock_usercode") hass.bus.async_fire("test_event_clear_lock_usercode")
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(clear_lock_usercode) == 1 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") with patch("homeassistant.components.zwave_js.lock.set_usercode") as mock_call:
hass.bus.async_fire("test_event_set_lock_usercode") hass.bus.async_fire("test_event_set_lock_usercode")
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(set_lock_usercode) == 1 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") async def test_reset_meter_action(
hass.bus.async_fire("test_event_ping") hass: HomeAssistant,
await hass.async_block_till_done() client: Client,
assert len(ping) == 1 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") assert await async_setup_component(
hass.bus.async_fire("test_event_set_value") hass,
await hass.async_block_till_done() automation.DOMAIN,
assert len(set_value) == 1 {
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") with patch(
hass.bus.async_fire("test_event_set_config_parameter") "zwave_js_server.model.endpoint.Endpoint.async_invoke_cc_api"
await hass.async_block_till_done() ) as mock_call:
assert len(set_config_parameter) == 1 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( async def test_get_action_capabilities(
@ -266,7 +373,28 @@ async def test_get_action_capabilities(
) )
assert capabilities and "extra_fields" in 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( assert voluptuous_serialize.convert(
capabilities["extra_fields"], custom_serializer=cv.custom_serializer capabilities["extra_fields"], custom_serializer=cv.custom_serializer