Add zwave_js.set_config_parameter service (#46673)
* create zwave_js.set_config_value service * update docstring * PR comments * make proposed changes * handle providing a label for the new value * fix docstring * use new library function * config param endpoint is always 0 * corresponding changes from upstream PR * bug fixes and add tests * create zwave_js.set_config_value service * update docstring * PR comments * make proposed changes * handle providing a label for the new value * fix docstring * use new library function * config param endpoint is always 0 * corresponding changes from upstream PR * bug fixes and add tests * use lambda to avoid extra function * add services description file * bring back the missing selector * move helper functions to helper file for reuse * allow target selector for automation editor * formatting * fix service schema * update docstrings * raise error in service if call to set value is unsuccessful * Update homeassistant/components/zwave_js/services.yaml Co-authored-by: Franck Nijhof <frenck@frenck.nl> * Update homeassistant/components/zwave_js/services.yaml Co-authored-by: Franck Nijhof <frenck@frenck.nl> * Update homeassistant/components/zwave_js/services.yaml Co-authored-by: Franck Nijhof <frenck@frenck.nl> * Update homeassistant/components/zwave_js/services.yaml Co-authored-by: Franck Nijhof <frenck@frenck.nl> * Update homeassistant/components/zwave_js/services.yaml Co-authored-by: Franck Nijhof <frenck@frenck.nl> * Update homeassistant/components/zwave_js/services.yaml Co-authored-by: Franck Nijhof <frenck@frenck.nl> * remove extra param to vol.Optional * switch to set over list for nodes * switch to set over list for nodes Co-authored-by: Marcel van der Veldt <m.vanderveldt@outlook.com> Co-authored-by: Franck Nijhof <frenck@frenck.nl>
This commit is contained in:
parent
c94968d811
commit
5a3bd30e01
8 changed files with 548 additions and 11 deletions
|
@ -43,7 +43,8 @@ from .const import (
|
||||||
ZWAVE_JS_EVENT,
|
ZWAVE_JS_EVENT,
|
||||||
)
|
)
|
||||||
from .discovery import async_discover_values
|
from .discovery import async_discover_values
|
||||||
from .entity import get_device_id
|
from .helpers import get_device_id
|
||||||
|
from .services import ZWaveServices
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__package__)
|
LOGGER = logging.getLogger(__package__)
|
||||||
CONNECT_TIMEOUT = 10
|
CONNECT_TIMEOUT = 10
|
||||||
|
@ -192,6 +193,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
DATA_UNSUBSCRIBE: unsubscribe_callbacks,
|
DATA_UNSUBSCRIBE: unsubscribe_callbacks,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
services = ZWaveServices(hass)
|
||||||
|
services.async_register()
|
||||||
|
|
||||||
# Set up websocket API
|
# Set up websocket API
|
||||||
async_register_api(hass)
|
async_register_api(hass)
|
||||||
|
|
||||||
|
|
|
@ -33,4 +33,11 @@ ATTR_PROPERTY_NAME = "property_name"
|
||||||
ATTR_PROPERTY_KEY_NAME = "property_key_name"
|
ATTR_PROPERTY_KEY_NAME = "property_key_name"
|
||||||
ATTR_PARAMETERS = "parameters"
|
ATTR_PARAMETERS = "parameters"
|
||||||
|
|
||||||
|
# service constants
|
||||||
|
SERVICE_SET_CONFIG_PARAMETER = "set_config_parameter"
|
||||||
|
|
||||||
|
ATTR_CONFIG_PARAMETER = "parameter"
|
||||||
|
ATTR_CONFIG_PARAMETER_BITMASK = "bitmask"
|
||||||
|
ATTR_CONFIG_VALUE = "value"
|
||||||
|
|
||||||
ADDON_SLUG = "core_zwave_js"
|
ADDON_SLUG = "core_zwave_js"
|
||||||
|
|
|
@ -1,30 +1,23 @@
|
||||||
"""Generic Z-Wave Entity Class."""
|
"""Generic Z-Wave Entity Class."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import List, Optional, Tuple, Union
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
from zwave_js_server.client import Client as ZwaveClient
|
from zwave_js_server.client import Client as ZwaveClient
|
||||||
from zwave_js_server.model.node import Node as ZwaveNode
|
|
||||||
from zwave_js_server.model.value import Value as ZwaveValue, get_value_id
|
from zwave_js_server.model.value import Value as ZwaveValue, get_value_id
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
from .const import DOMAIN
|
|
||||||
from .discovery import ZwaveDiscoveryInfo
|
from .discovery import ZwaveDiscoveryInfo
|
||||||
|
from .helpers import get_device_id
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
EVENT_VALUE_UPDATED = "value updated"
|
EVENT_VALUE_UPDATED = "value updated"
|
||||||
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def get_device_id(client: ZwaveClient, node: ZwaveNode) -> Tuple[str, str]:
|
|
||||||
"""Get device registry identifier for Z-Wave node."""
|
|
||||||
return (DOMAIN, f"{client.driver.controller.home_id}-{node.node_id}")
|
|
||||||
|
|
||||||
|
|
||||||
class ZWaveBaseEntity(Entity):
|
class ZWaveBaseEntity(Entity):
|
||||||
"""Generic Entity Class for a Z-Wave Device."""
|
"""Generic Entity Class for a Z-Wave Device."""
|
||||||
|
|
||||||
|
|
100
homeassistant/components/zwave_js/helpers.py
Normal file
100
homeassistant/components/zwave_js/helpers.py
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
"""Helper functions for Z-Wave JS integration."""
|
||||||
|
from typing import List, Tuple, cast
|
||||||
|
|
||||||
|
from zwave_js_server.client import Client as ZwaveClient
|
||||||
|
from zwave_js_server.model.node import Node as ZwaveNode
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers.device_registry import async_get as async_get_dev_reg
|
||||||
|
from homeassistant.helpers.entity_registry import async_get as async_get_ent_reg
|
||||||
|
|
||||||
|
from .const import DATA_CLIENT, DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def get_device_id(client: ZwaveClient, node: ZwaveNode) -> Tuple[str, str]:
|
||||||
|
"""Get device registry identifier for Z-Wave node."""
|
||||||
|
return (DOMAIN, f"{client.driver.controller.home_id}-{node.node_id}")
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def get_home_and_node_id_from_device_id(device_id: Tuple[str, str]) -> List[str]:
|
||||||
|
"""
|
||||||
|
Get home ID and node ID for Z-Wave device registry entry.
|
||||||
|
|
||||||
|
Returns [home_id, node_id]
|
||||||
|
"""
|
||||||
|
return device_id[1].split("-")
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_get_node_from_device_id(hass: HomeAssistant, device_id: str) -> ZwaveNode:
|
||||||
|
"""
|
||||||
|
Get node from a device ID.
|
||||||
|
|
||||||
|
Raises ValueError if device is invalid or node can't be found.
|
||||||
|
"""
|
||||||
|
device_entry = async_get_dev_reg(hass).async_get(device_id)
|
||||||
|
|
||||||
|
if not device_entry:
|
||||||
|
raise ValueError("Device ID is not valid")
|
||||||
|
|
||||||
|
# Use device config entry ID's to validate that this is a valid zwave_js device
|
||||||
|
# and to get the client
|
||||||
|
config_entry_ids = device_entry.config_entries
|
||||||
|
config_entry_id = next(
|
||||||
|
(
|
||||||
|
config_entry_id
|
||||||
|
for config_entry_id in config_entry_ids
|
||||||
|
if cast(
|
||||||
|
ConfigEntry,
|
||||||
|
hass.config_entries.async_get_entry(config_entry_id),
|
||||||
|
).domain
|
||||||
|
== DOMAIN
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if config_entry_id is None or config_entry_id not in hass.data[DOMAIN]:
|
||||||
|
raise ValueError("Device is not from an existing zwave_js config entry")
|
||||||
|
|
||||||
|
client = hass.data[DOMAIN][config_entry_id][DATA_CLIENT]
|
||||||
|
|
||||||
|
# Get node ID from device identifier, perform some validation, and then get the
|
||||||
|
# node
|
||||||
|
identifier = next(
|
||||||
|
(
|
||||||
|
get_home_and_node_id_from_device_id(identifier)
|
||||||
|
for identifier in device_entry.identifiers
|
||||||
|
if identifier[0] == DOMAIN
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
node_id = int(identifier[1]) if identifier is not None else None
|
||||||
|
|
||||||
|
if node_id is None or node_id not in client.driver.controller.nodes:
|
||||||
|
raise ValueError("Device node can't be found")
|
||||||
|
|
||||||
|
return client.driver.controller.nodes[node_id]
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_get_node_from_entity_id(hass: HomeAssistant, entity_id: str) -> ZwaveNode:
|
||||||
|
"""
|
||||||
|
Get node from an entity ID.
|
||||||
|
|
||||||
|
Raises ValueError if entity is invalid.
|
||||||
|
"""
|
||||||
|
entity_entry = async_get_ent_reg(hass).async_get(entity_id)
|
||||||
|
|
||||||
|
if not entity_entry:
|
||||||
|
raise ValueError("Entity ID is not valid")
|
||||||
|
|
||||||
|
if entity_entry.platform != DOMAIN:
|
||||||
|
raise ValueError("Entity is not from zwave_js integration")
|
||||||
|
|
||||||
|
# Assert for mypy, safe because we know that zwave_js entities are always
|
||||||
|
# tied to a device
|
||||||
|
assert entity_entry.device_id
|
||||||
|
return async_get_node_from_device_id(hass, entity_entry.device_id)
|
110
homeassistant/components/zwave_js/services.py
Normal file
110
homeassistant/components/zwave_js/services.py
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
"""Methods and classes related to executing Z-Wave commands and publishing these to hass."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Dict, Set, Union
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
from zwave_js_server.model.node import Node as ZwaveNode
|
||||||
|
from zwave_js_server.util.node import async_set_config_parameter
|
||||||
|
|
||||||
|
from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID
|
||||||
|
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
|
from . import const
|
||||||
|
from .helpers import async_get_node_from_device_id, async_get_node_from_entity_id
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def parameter_name_does_not_need_bitmask(
|
||||||
|
val: Dict[str, Union[int, str]]
|
||||||
|
) -> Dict[str, Union[int, str]]:
|
||||||
|
"""Validate that if a parameter name is provided, bitmask is not as well."""
|
||||||
|
if isinstance(val[const.ATTR_CONFIG_PARAMETER], str) and (
|
||||||
|
val.get(const.ATTR_CONFIG_PARAMETER_BITMASK)
|
||||||
|
):
|
||||||
|
raise vol.Invalid(
|
||||||
|
"Don't include a bitmask when a parameter name is specified",
|
||||||
|
path=[const.ATTR_CONFIG_PARAMETER, const.ATTR_CONFIG_PARAMETER_BITMASK],
|
||||||
|
)
|
||||||
|
return val
|
||||||
|
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ZWaveServices:
|
||||||
|
"""Class that holds our services (Zwave Commands) that should be published to hass."""
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant):
|
||||||
|
"""Initialize with hass object."""
|
||||||
|
self._hass = hass
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_register(self) -> None:
|
||||||
|
"""Register all our services."""
|
||||||
|
self._hass.services.async_register(
|
||||||
|
const.DOMAIN,
|
||||||
|
const.SERVICE_SET_CONFIG_PARAMETER,
|
||||||
|
self.async_set_config_parameter,
|
||||||
|
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.Optional(const.ATTR_CONFIG_PARAMETER_BITMASK): vol.Any(
|
||||||
|
vol.Coerce(int), BITMASK_SCHEMA
|
||||||
|
),
|
||||||
|
vol.Required(const.ATTR_CONFIG_VALUE): vol.Any(
|
||||||
|
vol.Coerce(int), cv.string
|
||||||
|
),
|
||||||
|
},
|
||||||
|
cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID),
|
||||||
|
parameter_name_does_not_need_bitmask,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_set_config_parameter(self, service: ServiceCall) -> None:
|
||||||
|
"""Set a config 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]
|
||||||
|
}
|
||||||
|
property_or_property_name = service.data[const.ATTR_CONFIG_PARAMETER]
|
||||||
|
property_key = service.data.get(const.ATTR_CONFIG_PARAMETER_BITMASK)
|
||||||
|
new_value = service.data[const.ATTR_CONFIG_VALUE]
|
||||||
|
|
||||||
|
for node in nodes:
|
||||||
|
zwave_value = await async_set_config_parameter(
|
||||||
|
node,
|
||||||
|
new_value,
|
||||||
|
property_or_property_name,
|
||||||
|
property_key=property_key,
|
||||||
|
)
|
||||||
|
|
||||||
|
if zwave_value:
|
||||||
|
_LOGGER.info(
|
||||||
|
"Set configuration parameter %s on Node %s with value %s",
|
||||||
|
zwave_value,
|
||||||
|
node,
|
||||||
|
new_value,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"Unable to set configuration parameter on Node {node} with "
|
||||||
|
f"value {new_value}"
|
||||||
|
)
|
|
@ -38,3 +38,31 @@ set_lock_usercode:
|
||||||
example: 1234
|
example: 1234
|
||||||
selector:
|
selector:
|
||||||
text:
|
text:
|
||||||
|
|
||||||
|
set_config_parameter:
|
||||||
|
name: Set a Z-Wave device configuration parameter
|
||||||
|
description: Allow for changing configuration parameters of your Z-Wave devices.
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
integration: zwave_js
|
||||||
|
fields:
|
||||||
|
parameter:
|
||||||
|
name: Parameter
|
||||||
|
description: The (name or id of the) configuration parameter you want to configure.
|
||||||
|
example: Minimum brightness level
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
text:
|
||||||
|
value:
|
||||||
|
name: Value
|
||||||
|
description: The new value to set for this configuration parameter.
|
||||||
|
example: 5
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
object:
|
||||||
|
bitmask:
|
||||||
|
name: Bitmask
|
||||||
|
description: Target a specific bitmask (see the documentation for more information).
|
||||||
|
advanced: true
|
||||||
|
selector:
|
||||||
|
object:
|
||||||
|
|
|
@ -8,7 +8,7 @@ from zwave_js_server.model.node import Node
|
||||||
|
|
||||||
from homeassistant.components.hassio.handler import HassioAPIError
|
from homeassistant.components.hassio.handler import HassioAPIError
|
||||||
from homeassistant.components.zwave_js.const import DOMAIN
|
from homeassistant.components.zwave_js.const import DOMAIN
|
||||||
from homeassistant.components.zwave_js.entity import get_device_id
|
from homeassistant.components.zwave_js.helpers import get_device_id
|
||||||
from homeassistant.config_entries import (
|
from homeassistant.config_entries import (
|
||||||
CONN_CLASS_LOCAL_PUSH,
|
CONN_CLASS_LOCAL_PUSH,
|
||||||
ENTRY_STATE_LOADED,
|
ENTRY_STATE_LOADED,
|
||||||
|
|
295
tests/components/zwave_js/test_services.py
Normal file
295
tests/components/zwave_js/test_services.py
Normal file
|
@ -0,0 +1,295 @@
|
||||||
|
"""Test the Z-Wave JS services."""
|
||||||
|
import pytest
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.components.zwave_js.const import (
|
||||||
|
ATTR_CONFIG_PARAMETER,
|
||||||
|
ATTR_CONFIG_PARAMETER_BITMASK,
|
||||||
|
ATTR_CONFIG_VALUE,
|
||||||
|
DOMAIN,
|
||||||
|
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.entity_registry import async_get as async_get_ent_reg
|
||||||
|
|
||||||
|
from .common import AIR_TEMPERATURE_SENSOR
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def test_set_config_parameter(hass, client, multisensor_6, integration):
|
||||||
|
"""Test the set_config_parameter service."""
|
||||||
|
dev_reg = async_get_dev_reg(hass)
|
||||||
|
ent_reg = async_get_ent_reg(hass)
|
||||||
|
entity_entry = ent_reg.async_get(AIR_TEMPERATURE_SENSOR)
|
||||||
|
|
||||||
|
# Test setting config parameter by property and property_key
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_SET_CONFIG_PARAMETER,
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: AIR_TEMPERATURE_SENSOR,
|
||||||
|
ATTR_CONFIG_PARAMETER: 102,
|
||||||
|
ATTR_CONFIG_PARAMETER_BITMASK: 1,
|
||||||
|
ATTR_CONFIG_VALUE: 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"] == {
|
||||||
|
"commandClassName": "Configuration",
|
||||||
|
"commandClass": 112,
|
||||||
|
"endpoint": 0,
|
||||||
|
"property": 102,
|
||||||
|
"propertyName": "Group 2: Send battery reports",
|
||||||
|
"propertyKey": 1,
|
||||||
|
"metadata": {
|
||||||
|
"type": "number",
|
||||||
|
"readable": True,
|
||||||
|
"writeable": True,
|
||||||
|
"valueSize": 4,
|
||||||
|
"min": 0,
|
||||||
|
"max": 1,
|
||||||
|
"default": 1,
|
||||||
|
"format": 0,
|
||||||
|
"allowManualEntry": True,
|
||||||
|
"label": "Group 2: Send battery reports",
|
||||||
|
"description": "Include battery information in periodic reports to Group 2",
|
||||||
|
"isFromConfig": True,
|
||||||
|
},
|
||||||
|
"value": 0,
|
||||||
|
}
|
||||||
|
assert args["value"] == 1
|
||||||
|
|
||||||
|
client.async_send_command.reset_mock()
|
||||||
|
|
||||||
|
# Test setting parameter by property name
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_SET_CONFIG_PARAMETER,
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: AIR_TEMPERATURE_SENSOR,
|
||||||
|
ATTR_CONFIG_PARAMETER: "Group 2: Send battery reports",
|
||||||
|
ATTR_CONFIG_VALUE: 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"] == {
|
||||||
|
"commandClassName": "Configuration",
|
||||||
|
"commandClass": 112,
|
||||||
|
"endpoint": 0,
|
||||||
|
"property": 102,
|
||||||
|
"propertyName": "Group 2: Send battery reports",
|
||||||
|
"propertyKey": 1,
|
||||||
|
"metadata": {
|
||||||
|
"type": "number",
|
||||||
|
"readable": True,
|
||||||
|
"writeable": True,
|
||||||
|
"valueSize": 4,
|
||||||
|
"min": 0,
|
||||||
|
"max": 1,
|
||||||
|
"default": 1,
|
||||||
|
"format": 0,
|
||||||
|
"allowManualEntry": True,
|
||||||
|
"label": "Group 2: Send battery reports",
|
||||||
|
"description": "Include battery information in periodic reports to Group 2",
|
||||||
|
"isFromConfig": True,
|
||||||
|
},
|
||||||
|
"value": 0,
|
||||||
|
}
|
||||||
|
assert args["value"] == 1
|
||||||
|
|
||||||
|
client.async_send_command.reset_mock()
|
||||||
|
|
||||||
|
# Test setting parameter by property name and state label
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_SET_CONFIG_PARAMETER,
|
||||||
|
{
|
||||||
|
ATTR_DEVICE_ID: entity_entry.device_id,
|
||||||
|
ATTR_CONFIG_PARAMETER: "Temperature Threshold (Unit)",
|
||||||
|
ATTR_CONFIG_VALUE: "Fahrenheit",
|
||||||
|
},
|
||||||
|
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"] == {
|
||||||
|
"commandClassName": "Configuration",
|
||||||
|
"commandClass": 112,
|
||||||
|
"endpoint": 0,
|
||||||
|
"property": 41,
|
||||||
|
"propertyName": "Temperature Threshold (Unit)",
|
||||||
|
"propertyKey": 15,
|
||||||
|
"metadata": {
|
||||||
|
"type": "number",
|
||||||
|
"readable": True,
|
||||||
|
"writeable": True,
|
||||||
|
"valueSize": 3,
|
||||||
|
"min": 1,
|
||||||
|
"max": 2,
|
||||||
|
"default": 1,
|
||||||
|
"format": 0,
|
||||||
|
"allowManualEntry": False,
|
||||||
|
"states": {"1": "Celsius", "2": "Fahrenheit"},
|
||||||
|
"label": "Temperature Threshold (Unit)",
|
||||||
|
"isFromConfig": True,
|
||||||
|
},
|
||||||
|
"value": 0,
|
||||||
|
}
|
||||||
|
assert args["value"] == 2
|
||||||
|
|
||||||
|
client.async_send_command.reset_mock()
|
||||||
|
|
||||||
|
# Test setting parameter by property and bitmask
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_SET_CONFIG_PARAMETER,
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: AIR_TEMPERATURE_SENSOR,
|
||||||
|
ATTR_CONFIG_PARAMETER: 102,
|
||||||
|
ATTR_CONFIG_PARAMETER_BITMASK: "0x01",
|
||||||
|
ATTR_CONFIG_VALUE: 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"] == {
|
||||||
|
"commandClassName": "Configuration",
|
||||||
|
"commandClass": 112,
|
||||||
|
"endpoint": 0,
|
||||||
|
"property": 102,
|
||||||
|
"propertyName": "Group 2: Send battery reports",
|
||||||
|
"propertyKey": 1,
|
||||||
|
"metadata": {
|
||||||
|
"type": "number",
|
||||||
|
"readable": True,
|
||||||
|
"writeable": True,
|
||||||
|
"valueSize": 4,
|
||||||
|
"min": 0,
|
||||||
|
"max": 1,
|
||||||
|
"default": 1,
|
||||||
|
"format": 0,
|
||||||
|
"allowManualEntry": True,
|
||||||
|
"label": "Group 2: Send battery reports",
|
||||||
|
"description": "Include battery information in periodic reports to Group 2",
|
||||||
|
"isFromConfig": True,
|
||||||
|
},
|
||||||
|
"value": 0,
|
||||||
|
}
|
||||||
|
assert args["value"] == 1
|
||||||
|
|
||||||
|
# Test that an invalid entity ID raises a ValueError
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_SET_CONFIG_PARAMETER,
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: "sensor.fake_entity",
|
||||||
|
ATTR_CONFIG_PARAMETER: "Temperature Threshold (Unit)",
|
||||||
|
ATTR_CONFIG_VALUE: "Fahrenheit",
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test that an invalid device ID raises a ValueError
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_SET_CONFIG_PARAMETER,
|
||||||
|
{
|
||||||
|
ATTR_DEVICE_ID: "fake_device_id",
|
||||||
|
ATTR_CONFIG_PARAMETER: "Temperature Threshold (Unit)",
|
||||||
|
ATTR_CONFIG_VALUE: "Fahrenheit",
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test that we can't include a bitmask value if parameter is a string
|
||||||
|
with pytest.raises(vol.Invalid):
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_SET_CONFIG_PARAMETER,
|
||||||
|
{
|
||||||
|
ATTR_DEVICE_ID: entity_entry.device_id,
|
||||||
|
ATTR_CONFIG_PARAMETER: "Temperature Threshold (Unit)",
|
||||||
|
ATTR_CONFIG_PARAMETER_BITMASK: 1,
|
||||||
|
ATTR_CONFIG_VALUE: "Fahrenheit",
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
non_zwave_js_config_entry = MockConfigEntry(entry_id="fake_entry_id")
|
||||||
|
non_zwave_js_config_entry.add_to_hass(hass)
|
||||||
|
non_zwave_js_device = dev_reg.async_get_or_create(
|
||||||
|
config_entry_id=non_zwave_js_config_entry.entry_id,
|
||||||
|
identifiers={("test", "test")},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test that a non Z-Wave JS device raises a ValueError
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_SET_CONFIG_PARAMETER,
|
||||||
|
{
|
||||||
|
ATTR_DEVICE_ID: non_zwave_js_device.id,
|
||||||
|
ATTR_CONFIG_PARAMETER: "Temperature Threshold (Unit)",
|
||||||
|
ATTR_CONFIG_VALUE: "Fahrenheit",
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
zwave_js_device_with_invalid_node_id = dev_reg.async_get_or_create(
|
||||||
|
config_entry_id=integration.entry_id, identifiers={(DOMAIN, "500-500")}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test that a Z-Wave JS device with an invalid node ID raises a ValueError
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_SET_CONFIG_PARAMETER,
|
||||||
|
{
|
||||||
|
ATTR_DEVICE_ID: zwave_js_device_with_invalid_node_id.id,
|
||||||
|
ATTR_CONFIG_PARAMETER: "Temperature Threshold (Unit)",
|
||||||
|
ATTR_CONFIG_VALUE: "Fahrenheit",
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
non_zwave_js_entity = ent_reg.async_get_or_create(
|
||||||
|
"test",
|
||||||
|
"sensor",
|
||||||
|
"test_sensor",
|
||||||
|
suggested_object_id="test_sensor",
|
||||||
|
config_entry=non_zwave_js_config_entry,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test that a non Z-Wave JS entity raises a ValueError
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_SET_CONFIG_PARAMETER,
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: non_zwave_js_entity.entity_id,
|
||||||
|
ATTR_CONFIG_PARAMETER: "Temperature Threshold (Unit)",
|
||||||
|
ATTR_CONFIG_VALUE: "Fahrenheit",
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
Loading…
Add table
Add a link
Reference in a new issue