Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add zwave_js.set_value service #48487

Merged
merged 5 commits into from Mar 30, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions homeassistant/components/zwave_js/const.py
Expand Up @@ -49,6 +49,7 @@
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"
Expand All @@ -62,4 +63,6 @@

ATTR_REFRESH_ALL_VALUES = "refresh_all_values"

SERVICE_SET_VALUE = "set_value"

ADDON_SLUG = "core_zwave_js"
65 changes: 65 additions & 0 deletions homeassistant/components/zwave_js/services.py
Expand Up @@ -5,7 +5,9 @@

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,
Expand Down Expand Up @@ -120,6 +122,29 @@ def async_register(self) -> None:
),
)

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()
Expand Down Expand Up @@ -203,3 +228,43 @@ async def async_poll_value(self, service: ServiceCall) -> None:
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 config value on a node."""
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved
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"
)
50 changes: 50 additions & 0 deletions homeassistant/components/zwave_js/services.yaml
Expand Up @@ -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:
93 changes: 92 additions & 1 deletion tests/components/zwave_js/test_services.py
@@ -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 (
Expand All @@ -19,7 +25,11 @@
)
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

Expand Down Expand Up @@ -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