Expand `zwave_js.set_config_parameter` with additional parameters (#102092)
parent
1a51d863cf
commit
f511a8a26a
|
@ -106,6 +106,8 @@ ATTR_NODES = "nodes"
|
|||
ATTR_CONFIG_PARAMETER = "parameter"
|
||||
ATTR_CONFIG_PARAMETER_BITMASK = "bitmask"
|
||||
ATTR_CONFIG_VALUE = "value"
|
||||
ATTR_VALUE_SIZE = "value_size"
|
||||
ATTR_VALUE_FORMAT = "value_format"
|
||||
# refresh value
|
||||
ATTR_REFRESH_ALL_VALUES = "refresh_all_values"
|
||||
# multicast
|
||||
|
|
|
@ -4,6 +4,7 @@ from __future__ import annotations
|
|||
import asyncio
|
||||
from collections.abc import Generator, Sequence
|
||||
import logging
|
||||
import math
|
||||
from typing import Any, TypeVar
|
||||
|
||||
import voluptuous as vol
|
||||
|
@ -13,7 +14,11 @@ from zwave_js_server.const.command_class.notification import NotificationType
|
|||
from zwave_js_server.exceptions import FailedZWaveCommand, SetValueFailed
|
||||
from zwave_js_server.model.endpoint import Endpoint
|
||||
from zwave_js_server.model.node import Node as ZwaveNode
|
||||
from zwave_js_server.model.value import ValueDataType, get_value_id_str
|
||||
from zwave_js_server.model.value import (
|
||||
ConfigurationValueFormat,
|
||||
ValueDataType,
|
||||
get_value_id_str,
|
||||
)
|
||||
from zwave_js_server.util.multicast import async_multicast_set_value
|
||||
from zwave_js_server.util.node import (
|
||||
async_bulk_set_partial_config_parameters,
|
||||
|
@ -58,6 +63,13 @@ def parameter_name_does_not_need_bitmask(
|
|||
return val
|
||||
|
||||
|
||||
def check_base_2(val: int) -> int:
|
||||
"""Check if value is a power of 2."""
|
||||
if not math.log2(val).is_integer():
|
||||
raise vol.Invalid("Value must be a power of 2.")
|
||||
return val
|
||||
|
||||
|
||||
def broadcast_command(val: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Validate that the service call is for a broadcast command."""
|
||||
if val.get(const.ATTR_BROADCAST):
|
||||
|
@ -78,10 +90,10 @@ def get_valid_responses_from_results(
|
|||
|
||||
|
||||
def raise_exceptions_from_results(
|
||||
zwave_objects: Sequence[ZwaveNode | Endpoint],
|
||||
results: Sequence[Any],
|
||||
zwave_objects: Sequence[T], results: Sequence[Any]
|
||||
) -> None:
|
||||
"""Raise list of exceptions from a list of results."""
|
||||
errors: Sequence[tuple[T, Any]]
|
||||
if errors := [
|
||||
tup for tup in zip(zwave_objects, results) if isinstance(tup[1], Exception)
|
||||
]:
|
||||
|
@ -263,10 +275,19 @@ class ZWaveServices:
|
|||
vol.Required(const.ATTR_CONFIG_VALUE): vol.Any(
|
||||
vol.Coerce(int), BITMASK_SCHEMA, cv.string
|
||||
),
|
||||
vol.Inclusive(const.ATTR_VALUE_SIZE, "raw"): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=1, max=4), check_base_2
|
||||
),
|
||||
vol.Inclusive(const.ATTR_VALUE_FORMAT, "raw"): vol.Coerce(
|
||||
ConfigurationValueFormat
|
||||
),
|
||||
},
|
||||
cv.has_at_least_one_key(
|
||||
ATTR_DEVICE_ID, ATTR_ENTITY_ID, ATTR_AREA_ID
|
||||
),
|
||||
cv.has_at_most_one_key(
|
||||
const.ATTR_CONFIG_PARAMETER_BITMASK, const.ATTR_VALUE_SIZE
|
||||
),
|
||||
parameter_name_does_not_need_bitmask,
|
||||
get_nodes_from_service_data,
|
||||
has_at_least_one_node,
|
||||
|
@ -487,7 +508,33 @@ class ZWaveServices:
|
|||
property_or_property_name = service.data[const.ATTR_CONFIG_PARAMETER]
|
||||
property_key = service.data.get(const.ATTR_CONFIG_PARAMETER_BITMASK)
|
||||
new_value = service.data[const.ATTR_CONFIG_VALUE]
|
||||
value_size = service.data.get(const.ATTR_VALUE_SIZE)
|
||||
value_format = service.data.get(const.ATTR_VALUE_FORMAT)
|
||||
|
||||
nodes_without_endpoints: set[ZwaveNode] = set()
|
||||
# Remove nodes that don't have the specified endpoint
|
||||
for node in nodes:
|
||||
if endpoint not in node.endpoints:
|
||||
nodes_without_endpoints.add(node)
|
||||
nodes = nodes.difference(nodes_without_endpoints)
|
||||
if not nodes:
|
||||
raise HomeAssistantError(
|
||||
"None of the specified nodes have the specified endpoint"
|
||||
)
|
||||
if nodes_without_endpoints and _LOGGER.isEnabledFor(logging.WARNING):
|
||||
_LOGGER.warning(
|
||||
(
|
||||
"The following nodes do not have endpoint %x and will be "
|
||||
"skipped: %s"
|
||||
),
|
||||
endpoint,
|
||||
nodes_without_endpoints,
|
||||
)
|
||||
|
||||
# If value_size isn't provided, we will use the utility function which includes
|
||||
# additional checks and protections. If it is provided, we will use the
|
||||
# node.async_set_raw_config_parameter_value method which calls the
|
||||
# Configuration CC set API.
|
||||
results = await asyncio.gather(
|
||||
*(
|
||||
async_set_config_parameter(
|
||||
|
@ -497,23 +544,42 @@ class ZWaveServices:
|
|||
property_key=property_key,
|
||||
endpoint=endpoint,
|
||||
)
|
||||
if value_size is None
|
||||
else node.endpoints[endpoint].async_set_raw_config_parameter_value(
|
||||
new_value,
|
||||
property_or_property_name,
|
||||
property_key=property_key,
|
||||
value_size=value_size,
|
||||
value_format=value_format,
|
||||
)
|
||||
for node in nodes
|
||||
),
|
||||
return_exceptions=True,
|
||||
)
|
||||
nodes_list = list(nodes)
|
||||
for node, result in get_valid_responses_from_results(nodes_list, results):
|
||||
zwave_value = result[0]
|
||||
cmd_status = result[1]
|
||||
if cmd_status == CommandStatus.ACCEPTED:
|
||||
msg = "Set configuration parameter %s on Node %s with value %s"
|
||||
else:
|
||||
msg = (
|
||||
"Added command to queue to set configuration parameter %s on Node "
|
||||
"%s with value %s. Parameter will be set when the device wakes up"
|
||||
)
|
||||
_LOGGER.info(msg, zwave_value, node, new_value)
|
||||
raise_exceptions_from_results(nodes_list, results)
|
||||
|
||||
def process_results(
|
||||
nodes_or_endpoints_list: list[T], _results: list[Any]
|
||||
) -> None:
|
||||
"""Process results for given nodes or endpoints."""
|
||||
for node_or_endpoint, result in get_valid_responses_from_results(
|
||||
nodes_or_endpoints_list, _results
|
||||
):
|
||||
zwave_value = result[0]
|
||||
cmd_status = result[1]
|
||||
if cmd_status.status == CommandStatus.ACCEPTED:
|
||||
msg = "Set configuration parameter %s on Node %s with value %s"
|
||||
else:
|
||||
msg = (
|
||||
"Added command to queue to set configuration parameter %s on %s "
|
||||
"with value %s. Parameter will be set when the device wakes up"
|
||||
)
|
||||
_LOGGER.info(msg, zwave_value, node_or_endpoint, new_value)
|
||||
raise_exceptions_from_results(nodes_or_endpoints_list, _results)
|
||||
|
||||
if value_size is None:
|
||||
process_results(list(nodes), results)
|
||||
else:
|
||||
process_results([node.endpoints[endpoint] for node in nodes], results)
|
||||
|
||||
async def async_bulk_set_partial_config_parameters(
|
||||
self, service: ServiceCall
|
||||
|
@ -605,7 +671,7 @@ class ZWaveServices:
|
|||
results = await asyncio.gather(*coros, return_exceptions=True)
|
||||
nodes_list = list(nodes)
|
||||
# multiple set_values my fail so we will track the entire list
|
||||
set_value_failed_nodes_list: list[ZwaveNode | Endpoint] = []
|
||||
set_value_failed_nodes_list: list[ZwaveNode] = []
|
||||
set_value_failed_error_list: list[SetValueFailed] = []
|
||||
for node_, result in get_valid_responses_from_results(nodes_list, results):
|
||||
if result and result.status not in SET_VALUE_SUCCESS:
|
||||
|
|
|
@ -54,6 +54,18 @@ set_config_parameter:
|
|||
required: true
|
||||
selector:
|
||||
text:
|
||||
value_size:
|
||||
example: 1
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 4
|
||||
value_format:
|
||||
example: 1
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 3
|
||||
|
||||
bulk_set_partial_config_parameters:
|
||||
target:
|
||||
|
|
|
@ -216,11 +216,19 @@
|
|||
},
|
||||
"bitmask": {
|
||||
"name": "Bitmask",
|
||||
"description": "Target a specific bitmask (see the documentation for more information)."
|
||||
"description": "Target a specific bitmask (see the documentation for more information). Cannot be combined with value_size or value_format."
|
||||
},
|
||||
"value": {
|
||||
"name": "Value",
|
||||
"description": "The new value to set for this configuration parameter."
|
||||
},
|
||||
"value_size": {
|
||||
"name": "Value size",
|
||||
"description": "Size of the value, either 1, 2, or 4. Used in combination with value_format when a config parameter is not defined in your device's configuration file. Cannot be combined with bitmask."
|
||||
},
|
||||
"value_format": {
|
||||
"name": "Value format",
|
||||
"description": "Format of the value, 0 for signed integer, 1 for unsigned integer, 2 for enumerated, 3 for bitfield. Used in combination with value_size when a config parameter is not defined in your device's configuration file. Cannot be combined with bitmask."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -4,6 +4,7 @@ from unittest.mock import MagicMock, patch
|
|||
import pytest
|
||||
import voluptuous as vol
|
||||
from zwave_js_server.exceptions import FailedZWaveCommand
|
||||
from zwave_js_server.model.value import SetConfigParameterResult
|
||||
|
||||
from homeassistant.components.group import Group
|
||||
from homeassistant.components.zwave_js.const import (
|
||||
|
@ -22,6 +23,8 @@ from homeassistant.components.zwave_js.const import (
|
|||
ATTR_PROPERTY_KEY,
|
||||
ATTR_REFRESH_ALL_VALUES,
|
||||
ATTR_VALUE,
|
||||
ATTR_VALUE_FORMAT,
|
||||
ATTR_VALUE_SIZE,
|
||||
ATTR_WAIT_FOR_RESULT,
|
||||
DOMAIN,
|
||||
SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS,
|
||||
|
@ -56,7 +59,12 @@ from tests.common import MockConfigEntry
|
|||
|
||||
|
||||
async def test_set_config_parameter(
|
||||
hass: HomeAssistant, client, multisensor_6, integration
|
||||
hass: HomeAssistant,
|
||||
client,
|
||||
multisensor_6,
|
||||
aeotec_zw164_siren,
|
||||
integration,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test the set_config_parameter service."""
|
||||
dev_reg = async_get_dev_reg(hass)
|
||||
|
@ -225,6 +233,63 @@ async def test_set_config_parameter(
|
|||
|
||||
client.async_send_command_no_wait.reset_mock()
|
||||
|
||||
# Test setting parameter by value_size
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_CONFIG_PARAMETER,
|
||||
{
|
||||
ATTR_ENTITY_ID: AIR_TEMPERATURE_SENSOR,
|
||||
ATTR_CONFIG_PARAMETER: 2,
|
||||
ATTR_VALUE_SIZE: 2,
|
||||
ATTR_VALUE_FORMAT: 1,
|
||||
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"] == "endpoint.set_raw_config_parameter_value"
|
||||
assert args["nodeId"] == 52
|
||||
assert args["endpoint"] == 0
|
||||
options = args["options"]
|
||||
assert options["parameter"] == 2
|
||||
assert options["value"] == 1
|
||||
assert options["valueSize"] == 2
|
||||
assert options["valueFormat"] == 1
|
||||
|
||||
client.async_send_command_no_wait.reset_mock()
|
||||
|
||||
# Test setting parameter when one node has endpoint and other doesn't
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_CONFIG_PARAMETER,
|
||||
{
|
||||
ATTR_ENTITY_ID: [AIR_TEMPERATURE_SENSOR, "siren.indoor_siren_6_tone_id"],
|
||||
ATTR_ENDPOINT: 1,
|
||||
ATTR_CONFIG_PARAMETER: 32,
|
||||
ATTR_VALUE_SIZE: 2,
|
||||
ATTR_VALUE_FORMAT: 1,
|
||||
ATTR_CONFIG_VALUE: 1,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert len(client.async_send_command_no_wait.call_args_list) == 0
|
||||
assert len(client.async_send_command.call_args_list) == 1
|
||||
args = client.async_send_command.call_args[0][0]
|
||||
assert args["command"] == "endpoint.set_raw_config_parameter_value"
|
||||
assert args["nodeId"] == 2
|
||||
assert args["endpoint"] == 1
|
||||
options = args["options"]
|
||||
assert options["parameter"] == 32
|
||||
assert options["value"] == 1
|
||||
assert options["valueSize"] == 2
|
||||
assert options["valueFormat"] == 1
|
||||
|
||||
client.async_send_command_no_wait.reset_mock()
|
||||
client.async_send_command.reset_mock()
|
||||
|
||||
# Test groups get expanded
|
||||
assert await async_setup_component(hass, "group", {})
|
||||
await Group.async_create_group(
|
||||
|
@ -296,6 +361,54 @@ async def test_set_config_parameter(
|
|||
config_entry=non_zwave_js_config_entry,
|
||||
)
|
||||
|
||||
# Test unknown endpoint throws error when None are remaining
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_CONFIG_PARAMETER,
|
||||
{
|
||||
ATTR_ENTITY_ID: AIR_TEMPERATURE_SENSOR,
|
||||
ATTR_ENDPOINT: 5,
|
||||
ATTR_CONFIG_PARAMETER: 2,
|
||||
ATTR_VALUE_SIZE: 2,
|
||||
ATTR_VALUE_FORMAT: 1,
|
||||
ATTR_CONFIG_VALUE: 1,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# Test that we can't include bitmask and value size and value format
|
||||
with pytest.raises(vol.Invalid):
|
||||
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: "Fahrenheit",
|
||||
ATTR_VALUE_FORMAT: 1,
|
||||
ATTR_VALUE_SIZE: 2,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# Test that value size must be 1, 2, or 4 (not 3)
|
||||
with pytest.raises(vol.Invalid):
|
||||
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: "Fahrenheit",
|
||||
ATTR_VALUE_FORMAT: 1,
|
||||
ATTR_VALUE_SIZE: 3,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# Test that a Z-Wave JS device with an invalid node ID, non Z-Wave JS entity,
|
||||
# non Z-Wave JS device, invalid device_id, and invalid node_id gets filtered out.
|
||||
await hass.services.async_call(
|
||||
|
@ -376,6 +489,75 @@ async def test_set_config_parameter(
|
|||
blocking=True,
|
||||
)
|
||||
|
||||
client.async_send_command_no_wait.reset_mock()
|
||||
client.async_send_command.reset_mock()
|
||||
|
||||
caplog.clear()
|
||||
|
||||
config_value = aeotec_zw164_siren.values["2-112-0-32"]
|
||||
cmd_result = SetConfigParameterResult("accepted", {"status": 255})
|
||||
|
||||
# Test accepted return
|
||||
with patch(
|
||||
"homeassistant.components.zwave_js.services.Endpoint.async_set_raw_config_parameter_value",
|
||||
return_value=(config_value, cmd_result),
|
||||
) as mock_set_raw_config_parameter_value:
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_CONFIG_PARAMETER,
|
||||
{
|
||||
ATTR_ENTITY_ID: ["siren.indoor_siren_6_tone_id"],
|
||||
ATTR_ENDPOINT: 0,
|
||||
ATTR_CONFIG_PARAMETER: 32,
|
||||
ATTR_VALUE_SIZE: 2,
|
||||
ATTR_VALUE_FORMAT: 1,
|
||||
ATTR_CONFIG_VALUE: 1,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
assert len(mock_set_raw_config_parameter_value.call_args_list) == 1
|
||||
assert mock_set_raw_config_parameter_value.call_args[0][0] == 1
|
||||
assert mock_set_raw_config_parameter_value.call_args[0][1] == 32
|
||||
assert mock_set_raw_config_parameter_value.call_args[1] == {
|
||||
"property_key": None,
|
||||
"value_size": 2,
|
||||
"value_format": 1,
|
||||
}
|
||||
|
||||
assert "Set configuration parameter" in caplog.text
|
||||
caplog.clear()
|
||||
|
||||
# Test queued return
|
||||
cmd_result.status = "queued"
|
||||
with patch(
|
||||
"homeassistant.components.zwave_js.services.Endpoint.async_set_raw_config_parameter_value",
|
||||
return_value=(config_value, cmd_result),
|
||||
) as mock_set_raw_config_parameter_value:
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_CONFIG_PARAMETER,
|
||||
{
|
||||
ATTR_ENTITY_ID: ["siren.indoor_siren_6_tone_id"],
|
||||
ATTR_ENDPOINT: 0,
|
||||
ATTR_CONFIG_PARAMETER: 32,
|
||||
ATTR_VALUE_SIZE: 2,
|
||||
ATTR_VALUE_FORMAT: 1,
|
||||
ATTR_CONFIG_VALUE: 1,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
assert len(mock_set_raw_config_parameter_value.call_args_list) == 1
|
||||
assert mock_set_raw_config_parameter_value.call_args[0][0] == 1
|
||||
assert mock_set_raw_config_parameter_value.call_args[0][1] == 32
|
||||
assert mock_set_raw_config_parameter_value.call_args[1] == {
|
||||
"property_key": None,
|
||||
"value_size": 2,
|
||||
"value_format": 1,
|
||||
}
|
||||
|
||||
assert "Added command to queue" in caplog.text
|
||||
caplog.clear()
|
||||
|
||||
|
||||
async def test_set_config_parameter_gather(
|
||||
hass: HomeAssistant,
|
||||
|
|
Loading…
Reference in New Issue