Skip to content

Commit

Permalink
Add zwave_js.bulk_set_partial_config_parameters service (#48306)
Browse files Browse the repository at this point in the history
* Add zwave_js.bulk_set_partial_config_parameters service

* update to handle command status

* add test for awake node

* test using a device in service call
  • Loading branch information
raman325 authored Mar 30, 2021
1 parent 114a97b commit 9a75019
Show file tree
Hide file tree
Showing 4 changed files with 222 additions and 4 deletions.
1 change: 1 addition & 0 deletions homeassistant/components/zwave_js/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@

# service constants
SERVICE_SET_CONFIG_PARAMETER = "set_config_parameter"
SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS = "bulk_set_partial_config_parameters"

ATTR_CONFIG_PARAMETER = "parameter"
ATTR_CONFIG_PARAMETER_BITMASK = "bitmask"
Expand Down
72 changes: 70 additions & 2 deletions homeassistant/components/zwave_js/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
import voluptuous as vol
from zwave_js_server.const import CommandStatus
from zwave_js_server.model.node import Node as ZwaveNode
from zwave_js_server.util.node import async_set_config_parameter
from zwave_js_server.util.node import (
async_bulk_set_partial_config_parameters,
async_set_config_parameter,
)

from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant, ServiceCall, callback
Expand Down Expand Up @@ -37,7 +40,13 @@ def parameter_name_does_not_need_bitmask(
# Validates that a bitmask is provided in hex form and converts it to decimal
# int equivalent since that's what the library uses
BITMASK_SCHEMA = vol.All(
cv.string, vol.Lower, vol.Match(r"^(0x)?[0-9a-f]+$"), lambda value: int(value, 16)
cv.string,
vol.Lower,
vol.Match(
r"^(0x)?[0-9a-f]+$",
msg="Must provide an integer (e.g. 255) or a bitmask in hex form (e.g. 0xff)",
),
lambda value: int(value, 16),
)


Expand Down Expand Up @@ -75,6 +84,30 @@ def async_register(self) -> None:
),
)

self._hass.services.async_register(
const.DOMAIN,
const.SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS,
self.async_bulk_set_partial_config_parameters,
schema=vol.All(
{
vol.Optional(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(const.ATTR_CONFIG_PARAMETER): vol.Any(
vol.Coerce(int), cv.string
),
vol.Required(const.ATTR_CONFIG_VALUE): vol.Any(
vol.Coerce(int),
{
vol.Any(vol.Coerce(int), BITMASK_SCHEMA): vol.Any(
vol.Coerce(int), cv.string
)
},
),
},
cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID),
),
)

self._hass.services.async_register(
const.DOMAIN,
const.SERVICE_REFRESH_VALUE,
Expand Down Expand Up @@ -122,6 +155,41 @@ async def async_set_config_parameter(self, service: ServiceCall) -> None:

_LOGGER.info(msg, zwave_value, node, new_value)

async def async_bulk_set_partial_config_parameters(
self, service: ServiceCall
) -> None:
"""Bulk set multiple partial config values on a node."""
nodes: set[ZwaveNode] = set()
if ATTR_ENTITY_ID in service.data:
nodes |= {
async_get_node_from_entity_id(self._hass, entity_id)
for entity_id in service.data[ATTR_ENTITY_ID]
}
if ATTR_DEVICE_ID in service.data:
nodes |= {
async_get_node_from_device_id(self._hass, device_id)
for device_id in service.data[ATTR_DEVICE_ID]
}
property_ = service.data[const.ATTR_CONFIG_PARAMETER]
new_value = service.data[const.ATTR_CONFIG_VALUE]

for node in nodes:
cmd_status = await async_bulk_set_partial_config_parameters(
node,
property_,
new_value,
)

if cmd_status == CommandStatus.ACCEPTED:
msg = "Bulk set partials for configuration parameter %s on Node %s"
else:
msg = (
"Added command to queue to bulk set partials for configuration "
"parameter %s on Node %s"
)

_LOGGER.info(msg, property_, node)

async def async_poll_value(self, service: ServiceCall) -> None:
"""Poll value on a node."""
for entity_id in service.data[ATTR_ENTITY_ID]:
Expand Down
28 changes: 27 additions & 1 deletion homeassistant/components/zwave_js/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,37 @@ set_config_parameter:
example: 5
required: true
selector:
object:
text:
bitmask:
name: Bitmask
description: Target a specific bitmask (see the documentation for more information).
advanced: true
selector:
text:

bulk_set_partial_config_parameters:
name: Bulk set partial configuration parameters for a Z-Wave device (Advanced).
description: Allow for bulk setting partial parameters. Useful when multiple partial parameters have to be set at the same time.
target:
entity:
integration: zwave_js
fields:
parameter:
name: Parameter
description: The id of the configuration parameter you want to configure.
example: 9
required: true
selector:
text:
value:
name: Value
description: The new value(s) to set for this configuration parameter. Can either be a raw integer value to represent the bulk change or a mapping where the key is the bitmask (either in hex or integer form) and the value is the new value you want to set for that partial parameter.
example:
"0x1": 1
"0x10": 1
"0x20": 1
"0x40": 1
required: true
selector:
object:

Expand Down
125 changes: 124 additions & 1 deletion tests/components/zwave_js/test_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@
ATTR_CONFIG_VALUE,
ATTR_REFRESH_ALL_VALUES,
DOMAIN,
SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS,
SERVICE_REFRESH_VALUE,
SERVICE_SET_CONFIG_PARAMETER,
)
from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID
from homeassistant.helpers.device_registry import async_get as async_get_dev_reg
from homeassistant.helpers.device_registry import (
async_entries_for_config_entry,
async_get as async_get_dev_reg,
)
from homeassistant.helpers.entity_registry import async_get as async_get_ent_reg

from .common import AIR_TEMPERATURE_SENSOR, CLIMATE_RADIO_THERMOSTAT_ENTITY
Expand Down Expand Up @@ -343,6 +347,125 @@ async def test_set_config_parameter(hass, client, multisensor_6, integration):
client.async_send_command.reset_mock()


async def test_bulk_set_config_parameters(hass, client, multisensor_6, integration):
"""Test the bulk_set_partial_config_parameters service."""
dev_reg = async_get_dev_reg(hass)
device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0]
# Test setting config parameter by property and property_key
await hass.services.async_call(
DOMAIN,
SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS,
{
ATTR_DEVICE_ID: device.id,
ATTR_CONFIG_PARAMETER: 102,
ATTR_CONFIG_VALUE: 241,
},
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"] == {
"commandClass": 112,
"property": 102,
}
assert args["value"] == 241

client.async_send_command_no_wait.reset_mock()

await hass.services.async_call(
DOMAIN,
SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS,
{
ATTR_ENTITY_ID: AIR_TEMPERATURE_SENSOR,
ATTR_CONFIG_PARAMETER: 102,
ATTR_CONFIG_VALUE: {
1: 1,
16: 1,
32: 1,
64: 1,
128: 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"] == {
"commandClass": 112,
"property": 102,
}
assert args["value"] == 241

client.async_send_command_no_wait.reset_mock()

await hass.services.async_call(
DOMAIN,
SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS,
{
ATTR_ENTITY_ID: AIR_TEMPERATURE_SENSOR,
ATTR_CONFIG_PARAMETER: 102,
ATTR_CONFIG_VALUE: {
"0x1": 1,
"0x10": 1,
"0x20": 1,
"0x40": 1,
"0x80": 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"] == {
"commandClass": 112,
"property": 102,
}
assert args["value"] == 241

client.async_send_command_no_wait.reset_mock()

# Test that when a device is awake, we call async_send_command instead of
# async_send_command_no_wait
multisensor_6.handle_wake_up(None)
await hass.services.async_call(
DOMAIN,
SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS,
{
ATTR_ENTITY_ID: AIR_TEMPERATURE_SENSOR,
ATTR_CONFIG_PARAMETER: 102,
ATTR_CONFIG_VALUE: {
1: 1,
16: 1,
32: 1,
64: 1,
128: 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"] == 52
assert args["valueId"] == {
"commandClass": 112,
"property": 102,
}
assert args["value"] == 241

client.async_send_command.reset_mock()


async def test_poll_value(
hass, client, climate_radio_thermostat_ct100_plus_different_endpoints, integration
):
Expand Down

0 comments on commit 9a75019

Please sign in to comment.