mirror of
https://github.com/home-assistant/core.git
synced 2025-07-18 18:57:06 +00:00
Create zwave_js.invoke_cc_api service (#70466)
This commit is contained in:
parent
f84c33203b
commit
24b090a038
@ -70,15 +70,16 @@ ATTR_CONFIG_ENTRY_ID = "config_entry_id"
|
||||
ATTR_PARTIAL_DICT_MATCH = "partial_dict_match"
|
||||
|
||||
# service constants
|
||||
SERVICE_SET_LOCK_USERCODE = "set_lock_usercode"
|
||||
SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS = "bulk_set_partial_config_parameters"
|
||||
SERVICE_CLEAR_LOCK_USERCODE = "clear_lock_usercode"
|
||||
SERVICE_SET_VALUE = "set_value"
|
||||
SERVICE_RESET_METER = "reset_meter"
|
||||
SERVICE_INVOKE_CC_API = "invoke_cc_api"
|
||||
SERVICE_MULTICAST_SET_VALUE = "multicast_set_value"
|
||||
SERVICE_PING = "ping"
|
||||
SERVICE_REFRESH_VALUE = "refresh_value"
|
||||
SERVICE_RESET_METER = "reset_meter"
|
||||
SERVICE_SET_CONFIG_PARAMETER = "set_config_parameter"
|
||||
SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS = "bulk_set_partial_config_parameters"
|
||||
SERVICE_SET_LOCK_USERCODE = "set_lock_usercode"
|
||||
SERVICE_SET_VALUE = "set_value"
|
||||
|
||||
ATTR_NODES = "nodes"
|
||||
# config parameter
|
||||
@ -92,6 +93,9 @@ ATTR_BROADCAST = "broadcast"
|
||||
# meter reset
|
||||
ATTR_METER_TYPE = "meter_type"
|
||||
ATTR_METER_TYPE_NAME = "meter_type_name"
|
||||
# invoke CC API
|
||||
ATTR_METHOD_NAME = "method_name"
|
||||
ATTR_PARAMETERS = "parameters"
|
||||
|
||||
ADDON_SLUG = "core_zwave_js"
|
||||
|
||||
|
@ -15,13 +15,16 @@ from homeassistant.components.diagnostics.util import async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_URL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.device_registry import DeviceEntry
|
||||
from homeassistant.helpers.entity_registry import async_entries_for_device, async_get
|
||||
|
||||
from .const import DATA_CLIENT, DOMAIN
|
||||
from .helpers import ZwaveValueID, get_home_and_node_id_from_device_entry
|
||||
from .helpers import (
|
||||
ZwaveValueID,
|
||||
get_home_and_node_id_from_device_entry,
|
||||
get_state_key_from_unique_id,
|
||||
get_value_id_from_unique_id,
|
||||
)
|
||||
|
||||
KEYS_TO_REDACT = {"homeId", "location"}
|
||||
|
||||
@ -61,28 +64,19 @@ def redact_node_state(node_state: NodeDataType) -> NodeDataType:
|
||||
|
||||
|
||||
def get_device_entities(
|
||||
hass: HomeAssistant, node: Node, device: DeviceEntry
|
||||
hass: HomeAssistant, node: Node, device: dr.DeviceEntry
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Get entities for a device."""
|
||||
entity_entries = async_entries_for_device(
|
||||
async_get(hass), device.id, include_disabled_entities=True
|
||||
entity_entries = er.async_entries_for_device(
|
||||
er.async_get(hass), device.id, include_disabled_entities=True
|
||||
)
|
||||
entities = []
|
||||
for entry in entity_entries:
|
||||
state_key = None
|
||||
split_unique_id = entry.unique_id.split(".")
|
||||
# If the unique ID has three parts, it's either one of the generic per node
|
||||
# entities (node status sensor, ping button) or a binary sensor for a particular
|
||||
# state. If we can get the state key, we will add it to the dictionary.
|
||||
if len(split_unique_id) == 3:
|
||||
try:
|
||||
state_key = int(split_unique_id[-1])
|
||||
# If the third part of the unique ID isn't a state key, the entity must be a
|
||||
# generic entity. We won't add those since they won't help with
|
||||
# troubleshooting.
|
||||
except ValueError:
|
||||
continue
|
||||
value_id = split_unique_id[1]
|
||||
# If the value ID returns as None, we don't need to include this entity
|
||||
if (value_id := get_value_id_from_unique_id(entry.unique_id)) is None:
|
||||
continue
|
||||
state_key = get_state_key_from_unique_id(entry.unique_id)
|
||||
|
||||
zwave_value = node.values[value_id]
|
||||
primary_value_data = {
|
||||
"command_class": zwave_value.command_class,
|
||||
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import astuple, dataclass
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
import voluptuous as vol
|
||||
@ -57,6 +58,34 @@ class ZwaveValueID:
|
||||
raise ValueError("At least one of the fields must be set.")
|
||||
|
||||
|
||||
@callback
|
||||
def get_value_id_from_unique_id(unique_id: str) -> str | None:
|
||||
"""
|
||||
Get the value ID and optional state key from a unique ID.
|
||||
|
||||
Raises ValueError
|
||||
"""
|
||||
split_unique_id = unique_id.split(".")
|
||||
# If the unique ID contains a `-` in its second part, the unique ID contains
|
||||
# a value ID and we can return it.
|
||||
if "-" in (value_id := split_unique_id[1]):
|
||||
return value_id
|
||||
return None
|
||||
|
||||
|
||||
@callback
|
||||
def get_state_key_from_unique_id(unique_id: str) -> int | None:
|
||||
"""Get the state key from a unique ID."""
|
||||
# If the unique ID has more than two parts, it's a special unique ID. If the last
|
||||
# part of the unique ID is an int, then it's a state key and we return it.
|
||||
if len(split_unique_id := unique_id.split(".")) > 2:
|
||||
try:
|
||||
return int(split_unique_id[-1])
|
||||
except ValueError:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
@callback
|
||||
def get_value_of_zwave_value(value: ZwaveValue | None) -> Any | None:
|
||||
"""Return the value of a ZwaveValue."""
|
||||
@ -251,6 +280,7 @@ def async_get_nodes_from_targets(
|
||||
val: dict[str, Any],
|
||||
ent_reg: er.EntityRegistry | None = None,
|
||||
dev_reg: dr.DeviceRegistry | None = None,
|
||||
logger: logging.Logger = LOGGER,
|
||||
) -> set[ZwaveNode]:
|
||||
"""
|
||||
Get nodes for all targets.
|
||||
@ -263,7 +293,7 @@ def async_get_nodes_from_targets(
|
||||
try:
|
||||
nodes.add(async_get_node_from_entity_id(hass, entity_id, ent_reg, dev_reg))
|
||||
except ValueError as err:
|
||||
LOGGER.warning(err.args[0])
|
||||
logger.warning(err.args[0])
|
||||
|
||||
# Convert all area IDs to nodes
|
||||
for area_id in val.get(ATTR_AREA_ID, []):
|
||||
@ -274,7 +304,7 @@ def async_get_nodes_from_targets(
|
||||
try:
|
||||
nodes.add(async_get_node_from_device_id(hass, device_id, dev_reg))
|
||||
except ValueError as err:
|
||||
LOGGER.warning(err.args[0])
|
||||
logger.warning(err.args[0])
|
||||
|
||||
return nodes
|
||||
|
||||
|
@ -3,12 +3,13 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import Any, cast
|
||||
|
||||
import voluptuous as vol
|
||||
from zwave_js_server.client import Client as ZwaveClient
|
||||
from zwave_js_server.const import CommandStatus
|
||||
from zwave_js_server.exceptions import SetValueFailed
|
||||
from zwave_js_server.const import CommandClass, CommandStatus
|
||||
from zwave_js_server.exceptions import FailedCommand, SetValueFailed
|
||||
from zwave_js_server.model.endpoint import Endpoint
|
||||
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.multicast import async_multicast_set_value
|
||||
@ -20,13 +21,20 @@ from zwave_js_server.util.node import (
|
||||
from homeassistant.components.group import expand_entity_ids
|
||||
from homeassistant.const import ATTR_AREA_ID, ATTR_DEVICE_ID, ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
from . import const
|
||||
from .config_validation import BITMASK_SCHEMA, VALUE_SCHEMA
|
||||
from .helpers import async_get_nodes_from_targets
|
||||
from .helpers import (
|
||||
async_get_node_from_device_id,
|
||||
async_get_node_from_entity_id,
|
||||
async_get_nodes_from_area_id,
|
||||
async_get_nodes_from_targets,
|
||||
get_value_id_from_unique_id,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -78,7 +86,7 @@ class ZWaveServices:
|
||||
def get_nodes_from_service_data(val: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Get nodes set from service data."""
|
||||
val[const.ATTR_NODES] = async_get_nodes_from_targets(
|
||||
self._hass, val, self._ent_reg, self._dev_reg
|
||||
self._hass, val, self._ent_reg, self._dev_reg, _LOGGER
|
||||
)
|
||||
return val
|
||||
|
||||
@ -132,8 +140,8 @@ class ZWaveServices:
|
||||
for entity_id in val[ATTR_ENTITY_ID]:
|
||||
entry = self._ent_reg.async_get(entity_id)
|
||||
if entry is None or entry.platform != const.DOMAIN:
|
||||
const.LOGGER.info(
|
||||
"Entity %s is not a valid %s entity.", entity_id, const.DOMAIN
|
||||
_LOGGER.info(
|
||||
"Entity %s is not a valid %s entity", entity_id, const.DOMAIN
|
||||
)
|
||||
invalid_entities.append(entity_id)
|
||||
|
||||
@ -326,6 +334,36 @@ class ZWaveServices:
|
||||
),
|
||||
)
|
||||
|
||||
self._hass.services.async_register(
|
||||
const.DOMAIN,
|
||||
const.SERVICE_INVOKE_CC_API,
|
||||
self.async_invoke_cc_api,
|
||||
schema=vol.Schema(
|
||||
vol.All(
|
||||
{
|
||||
vol.Optional(ATTR_AREA_ID): vol.All(
|
||||
cv.ensure_list, [cv.string]
|
||||
),
|
||||
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.All(
|
||||
vol.Coerce(int), vol.Coerce(CommandClass)
|
||||
),
|
||||
vol.Optional(const.ATTR_ENDPOINT): vol.Coerce(int),
|
||||
vol.Required(const.ATTR_METHOD_NAME): cv.string,
|
||||
vol.Required(const.ATTR_PARAMETERS): list,
|
||||
},
|
||||
cv.has_at_least_one_key(
|
||||
ATTR_DEVICE_ID, ATTR_ENTITY_ID, ATTR_AREA_ID
|
||||
),
|
||||
get_nodes_from_service_data,
|
||||
has_at_least_one_node,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
async def async_set_config_parameter(self, service: ServiceCall) -> None:
|
||||
"""Set a config value on a node."""
|
||||
nodes = service.data[const.ATTR_NODES]
|
||||
@ -425,11 +463,11 @@ class ZWaveServices:
|
||||
)
|
||||
|
||||
if success is False:
|
||||
raise SetValueFailed(
|
||||
raise HomeAssistantError(
|
||||
"Unable to set value, refer to "
|
||||
"https://zwave-js.github.io/node-zwave-js/#/api/node?id=setvalue "
|
||||
"for possible reasons"
|
||||
)
|
||||
) from SetValueFailed
|
||||
|
||||
async def async_multicast_set_value(self, service: ServiceCall) -> None:
|
||||
"""Set a value via multicast to multiple nodes."""
|
||||
@ -438,7 +476,7 @@ class ZWaveServices:
|
||||
options = service.data.get(const.ATTR_OPTIONS)
|
||||
|
||||
if not broadcast and len(nodes) == 1:
|
||||
const.LOGGER.info(
|
||||
_LOGGER.info(
|
||||
"Passing the zwave_js.multicast_set_value service call to the "
|
||||
"zwave_js.set_value service since only one node was targeted"
|
||||
)
|
||||
@ -496,14 +534,96 @@ class ZWaveServices:
|
||||
)
|
||||
|
||||
if success is False:
|
||||
raise SetValueFailed("Unable to set value via multicast")
|
||||
raise HomeAssistantError(
|
||||
"Unable to set value via multicast"
|
||||
) from SetValueFailed
|
||||
|
||||
async def async_ping(self, service: ServiceCall) -> None:
|
||||
"""Ping node(s)."""
|
||||
const.LOGGER.warning(
|
||||
# pylint: disable=no-self-use
|
||||
_LOGGER.warning(
|
||||
"This service is deprecated in favor of the ping button entity. Service "
|
||||
"calls will still work for now but the service will be removed in a "
|
||||
"future release"
|
||||
)
|
||||
nodes: set[ZwaveNode] = service.data[const.ATTR_NODES]
|
||||
await asyncio.gather(*(node.async_ping() for node in nodes))
|
||||
|
||||
async def async_invoke_cc_api(self, service: ServiceCall) -> None:
|
||||
"""Invoke a command class API."""
|
||||
command_class: CommandClass = service.data[const.ATTR_COMMAND_CLASS]
|
||||
method_name: str = service.data[const.ATTR_METHOD_NAME]
|
||||
parameters: list[Any] = service.data[const.ATTR_PARAMETERS]
|
||||
|
||||
async def _async_invoke_cc_api(endpoints: set[Endpoint]) -> None:
|
||||
"""Invoke the CC API on a node endpoint."""
|
||||
errors: list[str] = []
|
||||
for endpoint in endpoints:
|
||||
_LOGGER.info(
|
||||
"Invoking %s CC API method %s on endpoint %s",
|
||||
command_class.name,
|
||||
method_name,
|
||||
endpoint,
|
||||
)
|
||||
try:
|
||||
await endpoint.async_invoke_cc_api(
|
||||
command_class, method_name, *parameters
|
||||
)
|
||||
except FailedCommand as err:
|
||||
errors.append(cast(str, err.args[0]))
|
||||
if errors:
|
||||
raise HomeAssistantError(
|
||||
"\n".join([f"{len(errors)} error(s):", *errors])
|
||||
)
|
||||
|
||||
# If an endpoint is provided, we assume the user wants to call the CC API on
|
||||
# that endpoint for all target nodes
|
||||
if (endpoint := service.data.get(const.ATTR_ENDPOINT)) is not None:
|
||||
await _async_invoke_cc_api(
|
||||
{node.endpoints[endpoint] for node in service.data[const.ATTR_NODES]}
|
||||
)
|
||||
return
|
||||
|
||||
# If no endpoint is provided, we target endpoint 0 for all device and area
|
||||
# nodes and we target the endpoint of the primary value for all entities
|
||||
# specified.
|
||||
endpoints: set[Endpoint] = set()
|
||||
for area_id in service.data.get(ATTR_AREA_ID, []):
|
||||
for node in async_get_nodes_from_area_id(
|
||||
self._hass, area_id, self._ent_reg, self._dev_reg
|
||||
):
|
||||
endpoints.add(node.endpoints[0])
|
||||
|
||||
for device_id in service.data.get(ATTR_DEVICE_ID, []):
|
||||
try:
|
||||
node = async_get_node_from_device_id(
|
||||
self._hass, device_id, self._dev_reg
|
||||
)
|
||||
except ValueError as err:
|
||||
_LOGGER.warning(err.args[0])
|
||||
continue
|
||||
endpoints.add(node.endpoints[0])
|
||||
|
||||
for entity_id in service.data.get(ATTR_ENTITY_ID, []):
|
||||
if (
|
||||
not (entity_entry := self._ent_reg.async_get(entity_id))
|
||||
or entity_entry.platform != const.DOMAIN
|
||||
):
|
||||
_LOGGER.warning(
|
||||
"Skipping entity %s as it is not a valid %s entity",
|
||||
entity_id,
|
||||
const.DOMAIN,
|
||||
)
|
||||
continue
|
||||
node = async_get_node_from_entity_id(
|
||||
self._hass, entity_id, self._ent_reg, self._dev_reg
|
||||
)
|
||||
if (
|
||||
value_id := get_value_id_from_unique_id(entity_entry.unique_id)
|
||||
) is None:
|
||||
_LOGGER.warning("Skipping entity %s as it has no value ID", entity_id)
|
||||
continue
|
||||
|
||||
endpoints.add(node.endpoints[node.values[value_id].endpoint])
|
||||
|
||||
await _async_invoke_cc_api(endpoints)
|
||||
|
@ -252,3 +252,39 @@ reset_meter:
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
|
||||
invoke_cc_api:
|
||||
name: Invoke a Command Class API on a node (Advanced)
|
||||
description: Allows for calling a Command Class API on a node. Some Command Classes can't be fully controlled via the `set_value` service and require direct calls to the Command Class API.
|
||||
target:
|
||||
entity:
|
||||
integration: zwave_js
|
||||
fields:
|
||||
command_class:
|
||||
name: Command Class
|
||||
description: The ID of the command class that you want to issue a command to.
|
||||
example: 132
|
||||
required: true
|
||||
selector:
|
||||
text:
|
||||
endpoint:
|
||||
name: Endpoint
|
||||
description: The endpoint to call the API on. If an endpoint is specified, that endpoint will be targeted for all nodes associated with the target areas, devices, and/or entities. If an endpoint is not specified, the root endpoint (0) will be targeted for nodes associated with target areas and devices, and the endpoint for the primary value of each entity will be targeted.
|
||||
example: 1
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
method_name:
|
||||
name: Method Name
|
||||
description: The name of the API method to call. Refer to the Z-Wave JS Command Class API documentation (https://zwave-js.github.io/node-zwave-js/#/api/CCs/index) for available methods.
|
||||
example: setInterval
|
||||
required: true
|
||||
selector:
|
||||
text:
|
||||
parameters:
|
||||
name: Parameters
|
||||
description: A list of parameters to pass to the API method. Refer to the Z-Wave JS Command Class API documentation (https://zwave-js.github.io/node-zwave-js/#/api/CCs/index) for parameters.
|
||||
example: [1, 1]
|
||||
required: true
|
||||
selector:
|
||||
object:
|
||||
|
@ -3,7 +3,7 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
import voluptuous as vol
|
||||
from zwave_js_server.exceptions import SetValueFailed
|
||||
from zwave_js_server.exceptions import FailedZWaveCommand
|
||||
|
||||
from homeassistant.components.group import Group
|
||||
from homeassistant.components.zwave_js.const import (
|
||||
@ -12,7 +12,10 @@ from homeassistant.components.zwave_js.const import (
|
||||
ATTR_CONFIG_PARAMETER,
|
||||
ATTR_CONFIG_PARAMETER_BITMASK,
|
||||
ATTR_CONFIG_VALUE,
|
||||
ATTR_ENDPOINT,
|
||||
ATTR_METHOD_NAME,
|
||||
ATTR_OPTIONS,
|
||||
ATTR_PARAMETERS,
|
||||
ATTR_PROPERTY,
|
||||
ATTR_PROPERTY_KEY,
|
||||
ATTR_REFRESH_ALL_VALUES,
|
||||
@ -20,6 +23,7 @@ from homeassistant.components.zwave_js.const import (
|
||||
ATTR_WAIT_FOR_RESULT,
|
||||
DOMAIN,
|
||||
SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS,
|
||||
SERVICE_INVOKE_CC_API,
|
||||
SERVICE_MULTICAST_SET_VALUE,
|
||||
SERVICE_PING,
|
||||
SERVICE_REFRESH_VALUE,
|
||||
@ -28,6 +32,7 @@ from homeassistant.components.zwave_js.const import (
|
||||
)
|
||||
from homeassistant.components.zwave_js.helpers import get_device_id
|
||||
from homeassistant.const import ATTR_AREA_ID, ATTR_DEVICE_ID, ATTR_ENTITY_ID
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.area_registry import async_get as async_get_area_reg
|
||||
from homeassistant.helpers.device_registry import (
|
||||
async_entries_for_config_entry,
|
||||
@ -982,7 +987,7 @@ async def test_set_value(hass, client, climate_danfoss_lc_13, integration):
|
||||
# Test that when a command fails we raise an exception
|
||||
client.async_send_command.return_value = {"success": False}
|
||||
|
||||
with pytest.raises(SetValueFailed):
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
@ -1335,7 +1340,7 @@ async def test_multicast_set_value(
|
||||
# Test that when a command fails we raise an exception
|
||||
client.async_send_command.return_value = {"success": False}
|
||||
|
||||
with pytest.raises(SetValueFailed):
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_MULTICAST_SET_VALUE,
|
||||
@ -1608,3 +1613,163 @@ async def test_ping(
|
||||
{},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_invoke_cc_api(
|
||||
hass,
|
||||
client,
|
||||
climate_danfoss_lc_13,
|
||||
climate_radio_thermostat_ct100_plus_different_endpoints,
|
||||
integration,
|
||||
):
|
||||
"""Test invoke_cc_api service."""
|
||||
dev_reg = async_get_dev_reg(hass)
|
||||
device_radio_thermostat = dev_reg.async_get_device(
|
||||
{get_device_id(client, climate_radio_thermostat_ct100_plus_different_endpoints)}
|
||||
)
|
||||
assert device_radio_thermostat
|
||||
device_danfoss = dev_reg.async_get_device(
|
||||
{get_device_id(client, climate_danfoss_lc_13)}
|
||||
)
|
||||
assert device_danfoss
|
||||
|
||||
# Test successful invoke_cc_api call with a static endpoint
|
||||
client.async_send_command.return_value = {"response": True}
|
||||
client.async_send_command_no_wait.return_value = {"response": True}
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_INVOKE_CC_API,
|
||||
{
|
||||
ATTR_DEVICE_ID: [
|
||||
device_radio_thermostat.id,
|
||||
device_danfoss.id,
|
||||
],
|
||||
ATTR_COMMAND_CLASS: 132,
|
||||
ATTR_ENDPOINT: 0,
|
||||
ATTR_METHOD_NAME: "someMethod",
|
||||
ATTR_PARAMETERS: [1, 2],
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
assert len(client.async_send_command.call_args_list) == 1
|
||||
args = client.async_send_command.call_args[0][0]
|
||||
assert args["command"] == "endpoint.invoke_cc_api"
|
||||
assert args["commandClass"] == 132
|
||||
assert args["endpoint"] == 0
|
||||
assert args["methodName"] == "someMethod"
|
||||
assert args["args"] == [1, 2]
|
||||
assert (
|
||||
args["nodeId"]
|
||||
== climate_radio_thermostat_ct100_plus_different_endpoints.node_id
|
||||
)
|
||||
|
||||
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"] == "endpoint.invoke_cc_api"
|
||||
assert args["commandClass"] == 132
|
||||
assert args["endpoint"] == 0
|
||||
assert args["methodName"] == "someMethod"
|
||||
assert args["args"] == [1, 2]
|
||||
assert args["nodeId"] == climate_danfoss_lc_13.node_id
|
||||
|
||||
client.async_send_command.reset_mock()
|
||||
client.async_send_command_no_wait.reset_mock()
|
||||
|
||||
# Test successful invoke_cc_api call without an endpoint (include area)
|
||||
area_reg = async_get_area_reg(hass)
|
||||
area = area_reg.async_get_or_create("test")
|
||||
dev_reg.async_update_device(device_danfoss.id, area_id=area.id)
|
||||
|
||||
client.async_send_command.return_value = {"response": True}
|
||||
client.async_send_command_no_wait.return_value = {"response": True}
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_INVOKE_CC_API,
|
||||
{
|
||||
ATTR_AREA_ID: area.id,
|
||||
ATTR_DEVICE_ID: [
|
||||
device_radio_thermostat.id,
|
||||
"fake_device_id",
|
||||
],
|
||||
ATTR_ENTITY_ID: [
|
||||
"sensor.not_real",
|
||||
"select.living_connect_z_thermostat_local_protection_state",
|
||||
"sensor.living_connect_z_thermostat_node_status",
|
||||
],
|
||||
ATTR_COMMAND_CLASS: 132,
|
||||
ATTR_METHOD_NAME: "someMethod",
|
||||
ATTR_PARAMETERS: [1, 2],
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
assert len(client.async_send_command.call_args_list) == 1
|
||||
args = client.async_send_command.call_args[0][0]
|
||||
assert args["command"] == "endpoint.invoke_cc_api"
|
||||
assert args["commandClass"] == 132
|
||||
assert args["endpoint"] == 0
|
||||
assert args["methodName"] == "someMethod"
|
||||
assert args["args"] == [1, 2]
|
||||
assert (
|
||||
args["nodeId"]
|
||||
== climate_radio_thermostat_ct100_plus_different_endpoints.node_id
|
||||
)
|
||||
|
||||
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"] == "endpoint.invoke_cc_api"
|
||||
assert args["commandClass"] == 132
|
||||
assert args["endpoint"] == 0
|
||||
assert args["methodName"] == "someMethod"
|
||||
assert args["args"] == [1, 2]
|
||||
assert args["nodeId"] == climate_danfoss_lc_13.node_id
|
||||
|
||||
client.async_send_command.reset_mock()
|
||||
client.async_send_command_no_wait.reset_mock()
|
||||
|
||||
# Test failed invoke_cc_api call on one node
|
||||
client.async_send_command.return_value = {"response": True}
|
||||
client.async_send_command_no_wait.side_effect = FailedZWaveCommand(
|
||||
"test", 12, "test"
|
||||
)
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_INVOKE_CC_API,
|
||||
{
|
||||
ATTR_DEVICE_ID: [
|
||||
device_radio_thermostat.id,
|
||||
device_danfoss.id,
|
||||
],
|
||||
ATTR_COMMAND_CLASS: 132,
|
||||
ATTR_ENDPOINT: 0,
|
||||
ATTR_METHOD_NAME: "someMethod",
|
||||
ATTR_PARAMETERS: [1, 2],
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
assert len(client.async_send_command.call_args_list) == 1
|
||||
args = client.async_send_command.call_args[0][0]
|
||||
assert args["command"] == "endpoint.invoke_cc_api"
|
||||
assert args["commandClass"] == 132
|
||||
assert args["endpoint"] == 0
|
||||
assert args["methodName"] == "someMethod"
|
||||
assert args["args"] == [1, 2]
|
||||
assert (
|
||||
args["nodeId"]
|
||||
== climate_radio_thermostat_ct100_plus_different_endpoints.node_id
|
||||
)
|
||||
|
||||
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"] == "endpoint.invoke_cc_api"
|
||||
assert args["commandClass"] == 132
|
||||
assert args["endpoint"] == 0
|
||||
assert args["methodName"] == "someMethod"
|
||||
assert args["args"] == [1, 2]
|
||||
assert args["nodeId"] == climate_danfoss_lc_13.node_id
|
||||
|
||||
client.async_send_command.reset_mock()
|
||||
client.async_send_command_no_wait.reset_mock()
|
||||
|
Loading…
x
Reference in New Issue
Block a user