Add zwave_js.set_value service (#48487)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
f8265f44be
commit
bc3881db29
4 changed files with 210 additions and 1 deletions
|
@ -49,6 +49,7 @@ ATTR_EVENT_LABEL = "event_label"
|
|||
ATTR_EVENT_TYPE = "event_type"
|
||||
ATTR_EVENT_DATA = "event_data"
|
||||
ATTR_DATA_TYPE = "data_type"
|
||||
ATTR_WAIT_FOR_RESULT = "wait_for_result"
|
||||
|
||||
# service constants
|
||||
SERVICE_SET_CONFIG_PARAMETER = "set_config_parameter"
|
||||
|
@ -62,4 +63,6 @@ SERVICE_REFRESH_VALUE = "refresh_value"
|
|||
|
||||
ATTR_REFRESH_ALL_VALUES = "refresh_all_values"
|
||||
|
||||
SERVICE_SET_VALUE = "set_value"
|
||||
|
||||
ADDON_SLUG = "core_zwave_js"
|
||||
|
|
|
@ -5,7 +5,9 @@ import logging
|
|||
|
||||
import voluptuous as vol
|
||||
from zwave_js_server.const import CommandStatus
|
||||
from zwave_js_server.exceptions import SetValueFailed
|
||||
from zwave_js_server.model.node import Node as ZwaveNode
|
||||
from zwave_js_server.model.value import get_value_id
|
||||
from zwave_js_server.util.node import (
|
||||
async_bulk_set_partial_config_parameters,
|
||||
async_set_config_parameter,
|
||||
|
@ -120,6 +122,29 @@ class ZWaveServices:
|
|||
),
|
||||
)
|
||||
|
||||
self._hass.services.async_register(
|
||||
const.DOMAIN,
|
||||
const.SERVICE_SET_VALUE,
|
||||
self.async_set_value,
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Optional(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Required(const.ATTR_COMMAND_CLASS): vol.Coerce(int),
|
||||
vol.Required(const.ATTR_PROPERTY): vol.Any(vol.Coerce(int), str),
|
||||
vol.Optional(const.ATTR_PROPERTY_KEY): vol.Any(
|
||||
vol.Coerce(int), str
|
||||
),
|
||||
vol.Optional(const.ATTR_ENDPOINT): vol.Coerce(int),
|
||||
vol.Required(const.ATTR_VALUE): vol.Any(
|
||||
bool, vol.Coerce(int), vol.Coerce(float), cv.string
|
||||
),
|
||||
vol.Optional(const.ATTR_WAIT_FOR_RESULT): vol.Coerce(bool),
|
||||
},
|
||||
cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID),
|
||||
),
|
||||
)
|
||||
|
||||
async def async_set_config_parameter(self, service: ServiceCall) -> None:
|
||||
"""Set a config value on a node."""
|
||||
nodes: set[ZwaveNode] = set()
|
||||
|
@ -203,3 +228,43 @@ class ZWaveServices:
|
|||
f"{const.DOMAIN}_{entry.unique_id}_poll_value",
|
||||
service.data[const.ATTR_REFRESH_ALL_VALUES],
|
||||
)
|
||||
|
||||
async def async_set_value(self, service: ServiceCall) -> None:
|
||||
"""Set a value 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]
|
||||
}
|
||||
command_class = service.data[const.ATTR_COMMAND_CLASS]
|
||||
property_ = service.data[const.ATTR_PROPERTY]
|
||||
property_key = service.data.get(const.ATTR_PROPERTY_KEY)
|
||||
endpoint = service.data.get(const.ATTR_ENDPOINT)
|
||||
new_value = service.data[const.ATTR_VALUE]
|
||||
wait_for_result = service.data.get(const.ATTR_WAIT_FOR_RESULT)
|
||||
|
||||
for node in nodes:
|
||||
success = await node.async_set_value(
|
||||
get_value_id(
|
||||
node,
|
||||
command_class,
|
||||
property_,
|
||||
endpoint=endpoint,
|
||||
property_key=property_key,
|
||||
),
|
||||
new_value,
|
||||
wait_for_result=wait_for_result,
|
||||
)
|
||||
|
||||
if success is False:
|
||||
raise SetValueFailed(
|
||||
"Unable to set value, refer to "
|
||||
"https://zwave-js.github.io/node-zwave-js/#/api/node?id=setvalue "
|
||||
"for possible reasons"
|
||||
)
|
||||
|
|
|
@ -113,3 +113,53 @@ refresh_value:
|
|||
default: false
|
||||
selector:
|
||||
boolean:
|
||||
|
||||
set_value:
|
||||
name: Set a value on a Z-Wave device (Advanced)
|
||||
description: Allow for changing any value that Z-Wave JS recognizes on a Z-Wave device. This service has minimal validation so only use this service if you know what you are doing.
|
||||
target:
|
||||
entity:
|
||||
integration: zwave_js
|
||||
fields:
|
||||
command_class:
|
||||
name: Command Class
|
||||
description: The ID of the command class for the value.
|
||||
example: 117
|
||||
required: true
|
||||
selector:
|
||||
text:
|
||||
endpoint:
|
||||
name: Endpoint
|
||||
description: The endpoint for the value.
|
||||
example: 1
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
property:
|
||||
name: Property
|
||||
description: The ID of the property for the value.
|
||||
example: currentValue
|
||||
required: true
|
||||
selector:
|
||||
text:
|
||||
property_key:
|
||||
name: Property Key
|
||||
description: The ID of the property key for the value
|
||||
example: 1
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
value:
|
||||
name: Value
|
||||
description: The new value to set.
|
||||
example: "ffbb99"
|
||||
required: true
|
||||
selector:
|
||||
object:
|
||||
wait_for_result:
|
||||
name: Wait for result?
|
||||
description: Whether or not to wait for a response from the node. If not included in the payload, the integration will decide whether to wait or not. If set to `true`, note that the service call can take a while if setting a value on an asleep battery device.
|
||||
example: false
|
||||
required: false
|
||||
selector:
|
||||
boolean:
|
||||
|
|
|
@ -1,16 +1,22 @@
|
|||
"""Test the Z-Wave JS services."""
|
||||
import pytest
|
||||
import voluptuous as vol
|
||||
from zwave_js_server.exceptions import SetValueFailed
|
||||
|
||||
from homeassistant.components.zwave_js.const import (
|
||||
ATTR_COMMAND_CLASS,
|
||||
ATTR_CONFIG_PARAMETER,
|
||||
ATTR_CONFIG_PARAMETER_BITMASK,
|
||||
ATTR_CONFIG_VALUE,
|
||||
ATTR_PROPERTY,
|
||||
ATTR_REFRESH_ALL_VALUES,
|
||||
ATTR_VALUE,
|
||||
ATTR_WAIT_FOR_RESULT,
|
||||
DOMAIN,
|
||||
SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS,
|
||||
SERVICE_REFRESH_VALUE,
|
||||
SERVICE_SET_CONFIG_PARAMETER,
|
||||
SERVICE_SET_VALUE,
|
||||
)
|
||||
from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID
|
||||
from homeassistant.helpers.device_registry import (
|
||||
|
@ -19,7 +25,11 @@ from homeassistant.helpers.device_registry import (
|
|||
)
|
||||
from homeassistant.helpers.entity_registry import async_get as async_get_ent_reg
|
||||
|
||||
from .common import AIR_TEMPERATURE_SENSOR, CLIMATE_RADIO_THERMOSTAT_ENTITY
|
||||
from .common import (
|
||||
AIR_TEMPERATURE_SENSOR,
|
||||
CLIMATE_DANFOSS_LC13_ENTITY,
|
||||
CLIMATE_RADIO_THERMOSTAT_ENTITY,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
@ -531,3 +541,84 @@ async def test_poll_value(
|
|||
{ATTR_ENTITY_ID: "sensor.fake_entity_id"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_set_value(hass, client, climate_danfoss_lc_13, integration):
|
||||
"""Test set_value service."""
|
||||
dev_reg = async_get_dev_reg(hass)
|
||||
device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0]
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: CLIMATE_DANFOSS_LC13_ENTITY,
|
||||
ATTR_COMMAND_CLASS: 117,
|
||||
ATTR_PROPERTY: "local",
|
||||
ATTR_VALUE: 2,
|
||||
},
|
||||
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"] == 5
|
||||
assert args["valueId"] == {
|
||||
"commandClassName": "Protection",
|
||||
"commandClass": 117,
|
||||
"endpoint": 0,
|
||||
"property": "local",
|
||||
"propertyName": "local",
|
||||
"ccVersion": 2,
|
||||
"metadata": {
|
||||
"type": "number",
|
||||
"readable": True,
|
||||
"writeable": True,
|
||||
"label": "Local protection state",
|
||||
"states": {"0": "Unprotected", "2": "NoOperationPossible"},
|
||||
},
|
||||
"value": 0,
|
||||
}
|
||||
assert args["value"] == 2
|
||||
|
||||
client.async_send_command_no_wait.reset_mock()
|
||||
|
||||
# Test that when a command fails we raise an exception
|
||||
client.async_send_command.return_value = {"success": False}
|
||||
|
||||
with pytest.raises(SetValueFailed):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_DEVICE_ID: device.id,
|
||||
ATTR_COMMAND_CLASS: 117,
|
||||
ATTR_PROPERTY: "local",
|
||||
ATTR_VALUE: 2,
|
||||
ATTR_WAIT_FOR_RESULT: True,
|
||||
},
|
||||
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"] == 5
|
||||
assert args["valueId"] == {
|
||||
"commandClassName": "Protection",
|
||||
"commandClass": 117,
|
||||
"endpoint": 0,
|
||||
"property": "local",
|
||||
"propertyName": "local",
|
||||
"ccVersion": 2,
|
||||
"metadata": {
|
||||
"type": "number",
|
||||
"readable": True,
|
||||
"writeable": True,
|
||||
"label": "Local protection state",
|
||||
"states": {"0": "Unprotected", "2": "NoOperationPossible"},
|
||||
},
|
||||
"value": 0,
|
||||
}
|
||||
assert args["value"] == 2
|
||||
|
|
Loading…
Add table
Reference in a new issue