Support zwave config parameters not on endpoint 0 (#93383)

* Support zwave config parameters not on endpoint 0

* Update device automation logic

* Make endpoint required

* Update homeassistant/components/zwave_js/services.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update homeassistant/components/zwave_js/services.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Raman Gupta 2023-05-30 23:52:12 -04:00 committed by GitHub
parent 8244887bb3
commit 4119d3198a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 102 additions and 21 deletions

View File

@ -98,6 +98,7 @@ COMMAND_CLASS_ID = "command_class_id"
TYPE = "type" TYPE = "type"
PROPERTY = "property" PROPERTY = "property"
PROPERTY_KEY = "property_key" PROPERTY_KEY = "property_key"
ENDPOINT = "endpoint"
VALUE = "value" VALUE = "value"
# constants for log config commands # constants for log config commands
@ -1608,6 +1609,7 @@ async def websocket_refresh_node_cc_values(
vol.Required(TYPE): "zwave_js/set_config_parameter", vol.Required(TYPE): "zwave_js/set_config_parameter",
vol.Required(DEVICE_ID): str, vol.Required(DEVICE_ID): str,
vol.Required(PROPERTY): int, vol.Required(PROPERTY): int,
vol.Optional(ENDPOINT, default=0): int,
vol.Optional(PROPERTY_KEY): int, vol.Optional(PROPERTY_KEY): int,
vol.Required(VALUE): vol.Any(int, BITMASK_SCHEMA), vol.Required(VALUE): vol.Any(int, BITMASK_SCHEMA),
} }
@ -1623,12 +1625,13 @@ async def websocket_set_config_parameter(
) -> None: ) -> None:
"""Set a config parameter value for a Z-Wave node.""" """Set a config parameter value for a Z-Wave node."""
property_ = msg[PROPERTY] property_ = msg[PROPERTY]
endpoint = msg[ENDPOINT]
property_key = msg.get(PROPERTY_KEY) property_key = msg.get(PROPERTY_KEY)
value = msg[VALUE] value = msg[VALUE]
try: try:
zwave_value, cmd_status = await async_set_config_parameter( zwave_value, cmd_status = await async_set_config_parameter(
node, value, property_, property_key=property_key node, value, property_, property_key=property_key, endpoint=endpoint
) )
except (InvalidNewValue, NotFoundError, NotImplementedError, SetValueFailed) as err: except (InvalidNewValue, NotFoundError, NotImplementedError, SetValueFailed) as err:
code = ERR_UNKNOWN_ERROR code = ERR_UNKNOWN_ERROR
@ -1673,6 +1676,7 @@ async def websocket_get_config_parameters(
result[value_id] = { result[value_id] = {
"property": zwave_value.property_, "property": zwave_value.property_,
"property_key": zwave_value.property_key, "property_key": zwave_value.property_key,
"endpoint": zwave_value.endpoint,
"configuration_value_type": zwave_value.configuration_value_type.value, "configuration_value_type": zwave_value.configuration_value_type.value,
"metadata": { "metadata": {
"description": metadata.description, "description": metadata.description,

View File

@ -101,6 +101,7 @@ RESET_METER_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
SET_CONFIG_PARAMETER_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( SET_CONFIG_PARAMETER_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
{ {
vol.Required(CONF_TYPE): SERVICE_SET_CONFIG_PARAMETER, vol.Required(CONF_TYPE): SERVICE_SET_CONFIG_PARAMETER,
vol.Required(ATTR_ENDPOINT): vol.Coerce(int),
vol.Required(ATTR_CONFIG_PARAMETER): vol.Any(int, str), vol.Required(ATTR_CONFIG_PARAMETER): vol.Any(int, str),
vol.Required(ATTR_CONFIG_PARAMETER_BITMASK): vol.Any(None, int, str), vol.Required(ATTR_CONFIG_PARAMETER_BITMASK): vol.Any(None, int, str),
vol.Required(ATTR_VALUE): vol.Coerce(int), vol.Required(ATTR_VALUE): vol.Coerce(int),
@ -168,6 +169,7 @@ async def async_get_actions(
{ {
**base_action, **base_action,
CONF_TYPE: SERVICE_SET_CONFIG_PARAMETER, CONF_TYPE: SERVICE_SET_CONFIG_PARAMETER,
ATTR_ENDPOINT: config_value.endpoint,
ATTR_CONFIG_PARAMETER: config_value.property_, ATTR_CONFIG_PARAMETER: config_value.property_,
ATTR_CONFIG_PARAMETER_BITMASK: config_value.property_key, ATTR_CONFIG_PARAMETER_BITMASK: config_value.property_key,
CONF_SUBTYPE: generate_config_parameter_subtype(config_value), CONF_SUBTYPE: generate_config_parameter_subtype(config_value),
@ -347,6 +349,7 @@ async def async_get_action_capabilities(
CommandClass.CONFIGURATION, CommandClass.CONFIGURATION,
config[ATTR_CONFIG_PARAMETER], config[ATTR_CONFIG_PARAMETER],
property_key=config[ATTR_CONFIG_PARAMETER_BITMASK], property_key=config[ATTR_CONFIG_PARAMETER_BITMASK],
endpoint=config[ATTR_ENDPOINT],
) )
value_schema = get_config_parameter_value_schema(node, value_id) value_schema = get_config_parameter_value_schema(node, value_id)
if value_schema is None: if value_schema is None:

View File

@ -47,9 +47,15 @@ def generate_config_parameter_subtype(config_value: ConfigurationValue) -> str:
if config_value.property_key: if config_value.property_key:
# Property keys for config values are always an int # Property keys for config values are always an int
assert isinstance(config_value.property_key, int) assert isinstance(config_value.property_key, int)
parameter = f"{parameter}[{hex(config_value.property_key)}]" parameter = (
f"{parameter}[{hex(config_value.property_key)}] on endpoint "
f"{config_value.endpoint}"
)
return f"{parameter} ({config_value.property_name})" return (
f"{parameter} ({config_value.property_name}) on endpoint "
f"{config_value.endpoint}"
)
@callback @callback

View File

@ -213,6 +213,7 @@ class ZWaveServices:
cv.ensure_list, [cv.string] cv.ensure_list, [cv.string]
), ),
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Optional(const.ATTR_ENDPOINT, default=0): vol.Coerce(int),
vol.Required(const.ATTR_CONFIG_PARAMETER): vol.Any( vol.Required(const.ATTR_CONFIG_PARAMETER): vol.Any(
vol.Coerce(int), cv.string vol.Coerce(int), cv.string
), ),
@ -247,6 +248,7 @@ class ZWaveServices:
cv.ensure_list, [cv.string] cv.ensure_list, [cv.string]
), ),
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Optional(const.ATTR_ENDPOINT, default=0): vol.Coerce(int),
vol.Required(const.ATTR_CONFIG_PARAMETER): vol.Coerce(int), vol.Required(const.ATTR_CONFIG_PARAMETER): vol.Coerce(int),
vol.Required(const.ATTR_CONFIG_VALUE): vol.Any( vol.Required(const.ATTR_CONFIG_VALUE): vol.Any(
vol.Coerce(int), vol.Coerce(int),
@ -413,6 +415,7 @@ class ZWaveServices:
async def async_set_config_parameter(self, service: ServiceCall) -> None: async def async_set_config_parameter(self, service: ServiceCall) -> None:
"""Set a config value on a node.""" """Set a config value on a node."""
nodes: set[ZwaveNode] = service.data[const.ATTR_NODES] nodes: set[ZwaveNode] = service.data[const.ATTR_NODES]
endpoint = service.data[const.ATTR_ENDPOINT]
property_or_property_name = service.data[const.ATTR_CONFIG_PARAMETER] property_or_property_name = service.data[const.ATTR_CONFIG_PARAMETER]
property_key = service.data.get(const.ATTR_CONFIG_PARAMETER_BITMASK) property_key = service.data.get(const.ATTR_CONFIG_PARAMETER_BITMASK)
new_value = service.data[const.ATTR_CONFIG_VALUE] new_value = service.data[const.ATTR_CONFIG_VALUE]
@ -424,6 +427,7 @@ class ZWaveServices:
new_value, new_value,
property_or_property_name, property_or_property_name,
property_key=property_key, property_key=property_key,
endpoint=endpoint,
) )
for node in nodes for node in nodes
), ),
@ -448,6 +452,7 @@ class ZWaveServices:
) -> None: ) -> None:
"""Bulk set multiple partial config values on a node.""" """Bulk set multiple partial config values on a node."""
nodes: set[ZwaveNode] = service.data[const.ATTR_NODES] nodes: set[ZwaveNode] = service.data[const.ATTR_NODES]
endpoint = service.data[const.ATTR_ENDPOINT]
property_ = service.data[const.ATTR_CONFIG_PARAMETER] property_ = service.data[const.ATTR_CONFIG_PARAMETER]
new_value = service.data[const.ATTR_CONFIG_VALUE] new_value = service.data[const.ATTR_CONFIG_VALUE]
@ -457,6 +462,7 @@ class ZWaveServices:
node, node,
property_, property_,
new_value, new_value,
endpoint=endpoint,
) )
for node in nodes for node in nodes
), ),

View File

@ -46,6 +46,14 @@ set_config_parameter:
entity: entity:
integration: zwave_js integration: zwave_js
fields: fields:
endpoint:
name: Endpoint
description: The configuration parameter's endpoint.
example: 1
default: 0
required: false
selector:
text:
parameter: parameter:
name: Parameter name: Parameter
description: The (name or id of the) configuration parameter you want to configure. description: The (name or id of the) configuration parameter you want to configure.
@ -53,6 +61,12 @@ set_config_parameter:
required: true required: true
selector: selector:
text: text:
bitmask:
name: Bitmask
description: Target a specific bitmask (see the documentation for more information).
advanced: true
selector:
text:
value: value:
name: Value name: Value
description: The new value to set for this configuration parameter. description: The new value to set for this configuration parameter.
@ -60,12 +74,6 @@ set_config_parameter:
required: true required: true
selector: selector:
text: text:
bitmask:
name: Bitmask
description: Target a specific bitmask (see the documentation for more information).
advanced: true
selector:
text:
bulk_set_partial_config_parameters: bulk_set_partial_config_parameters:
name: Bulk set partial configuration parameters for a Z-Wave device (Advanced). name: Bulk set partial configuration parameters for a Z-Wave device (Advanced).
@ -74,6 +82,14 @@ bulk_set_partial_config_parameters:
entity: entity:
integration: zwave_js integration: zwave_js
fields: fields:
endpoint:
name: Endpoint
description: The configuration parameter's endpoint.
example: 1
default: 0
required: false
selector:
text:
parameter: parameter:
name: Parameter name: Parameter
description: The id of the configuration parameter you want to configure. description: The id of the configuration parameter you want to configure.

View File

@ -32,6 +32,7 @@ from zwave_js_server.model.controller import (
from zwave_js_server.model.controller.firmware import ControllerFirmwareUpdateData from zwave_js_server.model.controller.firmware import ControllerFirmwareUpdateData
from zwave_js_server.model.node import Node from zwave_js_server.model.node import Node
from zwave_js_server.model.node.firmware import NodeFirmwareUpdateData from zwave_js_server.model.node.firmware import NodeFirmwareUpdateData
from zwave_js_server.model.value import ConfigurationValue, get_value_id_str
from homeassistant.components.websocket_api import ERR_INVALID_FORMAT, ERR_NOT_FOUND from homeassistant.components.websocket_api import ERR_INVALID_FORMAT, ERR_NOT_FOUND
from homeassistant.components.zwave_js.api import ( from homeassistant.components.zwave_js.api import (
@ -43,6 +44,7 @@ from homeassistant.components.zwave_js.api import (
DEVICE_ID, DEVICE_ID,
DSK, DSK,
ENABLED, ENABLED,
ENDPOINT,
ENTRY_ID, ENTRY_ID,
ERR_NOT_LOADED, ERR_NOT_LOADED,
FEATURE, FEATURE,
@ -2756,6 +2758,12 @@ async def test_set_config_parameter(
entry = integration entry = integration
ws_client = await hass_ws_client(hass) ws_client = await hass_ws_client(hass)
device = get_device(hass, multisensor_6) device = get_device(hass, multisensor_6)
new_value_data = multisensor_6.values[
get_value_id_str(multisensor_6, 112, 102, 0, 1)
].data.copy()
new_value_data["endpoint"] = 1
new_value = ConfigurationValue(multisensor_6, new_value_data)
multisensor_6.values[get_value_id_str(multisensor_6, 112, 102, 1, 1)] = new_value
client.async_send_command_no_wait.return_value = None client.async_send_command_no_wait.return_value = None
@ -2787,12 +2795,44 @@ async def test_set_config_parameter(
client.async_send_command_no_wait.reset_mock() client.async_send_command_no_wait.reset_mock()
client.async_send_command_no_wait.return_value = None
# Test using a different endpoint
await ws_client.send_json(
{
ID: 2,
TYPE: "zwave_js/set_config_parameter",
DEVICE_ID: device.id,
ENDPOINT: 1,
PROPERTY: 102,
PROPERTY_KEY: 1,
VALUE: 1,
}
)
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"] == {
"commandClass": 112,
"endpoint": 1,
"property": 102,
"propertyKey": 1,
}
assert args["value"] == 1
client.async_send_command_no_wait.reset_mock()
# Test that hex strings are accepted and converted as expected # Test that hex strings are accepted and converted as expected
client.async_send_command_no_wait.return_value = None client.async_send_command_no_wait.return_value = None
await ws_client.send_json( await ws_client.send_json(
{ {
ID: 2, ID: 3,
TYPE: "zwave_js/set_config_parameter", TYPE: "zwave_js/set_config_parameter",
DEVICE_ID: device.id, DEVICE_ID: device.id,
PROPERTY: 102, PROPERTY: 102,
@ -2824,7 +2864,7 @@ async def test_set_config_parameter(
set_param_mock.side_effect = InvalidNewValue("test") set_param_mock.side_effect = InvalidNewValue("test")
await ws_client.send_json( await ws_client.send_json(
{ {
ID: 3, ID: 4,
TYPE: "zwave_js/set_config_parameter", TYPE: "zwave_js/set_config_parameter",
DEVICE_ID: device.id, DEVICE_ID: device.id,
PROPERTY: 102, PROPERTY: 102,
@ -2843,7 +2883,7 @@ async def test_set_config_parameter(
set_param_mock.side_effect = NotFoundError("test") set_param_mock.side_effect = NotFoundError("test")
await ws_client.send_json( await ws_client.send_json(
{ {
ID: 4, ID: 5,
TYPE: "zwave_js/set_config_parameter", TYPE: "zwave_js/set_config_parameter",
DEVICE_ID: device.id, DEVICE_ID: device.id,
PROPERTY: 102, PROPERTY: 102,
@ -2862,7 +2902,7 @@ async def test_set_config_parameter(
set_param_mock.side_effect = SetValueFailed("test") set_param_mock.side_effect = SetValueFailed("test")
await ws_client.send_json( await ws_client.send_json(
{ {
ID: 5, ID: 6,
TYPE: "zwave_js/set_config_parameter", TYPE: "zwave_js/set_config_parameter",
DEVICE_ID: device.id, DEVICE_ID: device.id,
PROPERTY: 102, PROPERTY: 102,
@ -2881,7 +2921,7 @@ async def test_set_config_parameter(
# Test getting non-existent node fails # Test getting non-existent node fails
await ws_client.send_json( await ws_client.send_json(
{ {
ID: 6, ID: 7,
TYPE: "zwave_js/set_config_parameter", TYPE: "zwave_js/set_config_parameter",
DEVICE_ID: "fake_device", DEVICE_ID: "fake_device",
PROPERTY: 102, PROPERTY: 102,
@ -2900,7 +2940,7 @@ async def test_set_config_parameter(
): ):
await ws_client.send_json( await ws_client.send_json(
{ {
ID: 7, ID: 8,
TYPE: "zwave_js/set_config_parameter", TYPE: "zwave_js/set_config_parameter",
DEVICE_ID: device.id, DEVICE_ID: device.id,
PROPERTY: 102, PROPERTY: 102,
@ -2920,7 +2960,7 @@ async def test_set_config_parameter(
await ws_client.send_json( await ws_client.send_json(
{ {
ID: 8, ID: 9,
TYPE: "zwave_js/set_config_parameter", TYPE: "zwave_js/set_config_parameter",
DEVICE_ID: device.id, DEVICE_ID: device.id,
PROPERTY: 102, PROPERTY: 102,
@ -2959,6 +2999,7 @@ async def test_get_config_parameters(
key = "52-112-0-2" key = "52-112-0-2"
assert result[key]["property"] == 2 assert result[key]["property"] == 2
assert result[key]["property_key"] is None assert result[key]["property_key"] is None
assert result[key]["endpoint"] == 0
assert result[key]["metadata"]["type"] == "number" assert result[key]["metadata"]["type"] == "number"
assert result[key]["configuration_value_type"] == "enumerated" assert result[key]["configuration_value_type"] == "enumerated"
assert result[key]["metadata"]["states"] assert result[key]["metadata"]["states"]

View File

@ -79,9 +79,10 @@ async def test_get_actions(
"domain": DOMAIN, "domain": DOMAIN,
"type": "set_config_parameter", "type": "set_config_parameter",
"device_id": device.id, "device_id": device.id,
"endpoint": 0,
"parameter": 3, "parameter": 3,
"bitmask": None, "bitmask": None,
"subtype": "3 (Beeper)", "subtype": "3 (Beeper) on endpoint 0",
"metadata": {}, "metadata": {},
}, },
] ]
@ -188,6 +189,7 @@ async def test_actions(
"domain": DOMAIN, "domain": DOMAIN,
"type": "set_config_parameter", "type": "set_config_parameter",
"device_id": device.id, "device_id": device.id,
"endpoint": 0,
"parameter": 1, "parameter": 1,
"bitmask": None, "bitmask": None,
"subtype": "3 (Beeper)", "subtype": "3 (Beeper)",
@ -510,6 +512,7 @@ async def test_get_action_capabilities(
"domain": DOMAIN, "domain": DOMAIN,
"device_id": device.id, "device_id": device.id,
"type": "set_config_parameter", "type": "set_config_parameter",
"endpoint": 0,
"parameter": 1, "parameter": 1,
"bitmask": None, "bitmask": None,
"subtype": "1 (Temperature Reporting Threshold)", "subtype": "1 (Temperature Reporting Threshold)",
@ -542,6 +545,7 @@ async def test_get_action_capabilities(
"domain": DOMAIN, "domain": DOMAIN,
"device_id": device.id, "device_id": device.id,
"type": "set_config_parameter", "type": "set_config_parameter",
"endpoint": 0,
"parameter": 10, "parameter": 10,
"bitmask": None, "bitmask": None,
"subtype": "10 (Temperature Reporting Filter)", "subtype": "10 (Temperature Reporting Filter)",
@ -569,6 +573,7 @@ async def test_get_action_capabilities(
"domain": DOMAIN, "domain": DOMAIN,
"device_id": device.id, "device_id": device.id,
"type": "set_config_parameter", "type": "set_config_parameter",
"endpoint": 0,
"parameter": 2, "parameter": 2,
"bitmask": None, "bitmask": None,
"subtype": "2 (HVAC Settings)", "subtype": "2 (HVAC Settings)",

View File

@ -28,7 +28,7 @@ from tests.common import async_get_device_automations, async_mock_service
@pytest.fixture @pytest.fixture
def calls(hass): def calls(hass: HomeAssistant):
"""Track calls to a mock service.""" """Track calls to a mock service."""
return async_mock_service(hass, "test", "automation") return async_mock_service(hass, "test", "automation")
@ -63,7 +63,7 @@ async def test_get_conditions(
"type": "config_parameter", "type": "config_parameter",
"device_id": device.id, "device_id": device.id,
"value_id": value_id, "value_id": value_id,
"subtype": f"{config_value.property_} ({name})", "subtype": f"{config_value.property_} ({name}) on endpoint 0",
"metadata": {}, "metadata": {},
}, },
{ {

View File

@ -32,7 +32,7 @@ from tests.common import (
@pytest.fixture @pytest.fixture
def calls(hass): def calls(hass: HomeAssistant):
"""Track calls to a mock service.""" """Track calls to a mock service."""
return async_mock_service(hass, "test", "automation") return async_mock_service(hass, "test", "automation")
@ -1292,7 +1292,7 @@ async def test_get_value_updated_config_parameter_triggers(
"property_key": None, "property_key": None,
"endpoint": 0, "endpoint": 0,
"command_class": CommandClass.CONFIGURATION.value, "command_class": CommandClass.CONFIGURATION.value,
"subtype": "3 (Beeper)", "subtype": "3 (Beeper) on endpoint 0",
"metadata": {}, "metadata": {},
} }
triggers = await async_get_device_automations( triggers = await async_get_device_automations(