Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions homeassistant/components/zwave_js/const.py
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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 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"
)
50 changes: 50 additions & 0 deletions homeassistant/components/zwave_js/services.yaml
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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