Create zwave_js.invoke_cc_api service (#70466)

This commit is contained in:
Raman Gupta 2022-04-26 11:30:49 -04:00 committed by GitHub
parent f84c33203b
commit 24b090a038
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 391 additions and 42 deletions

View File

@ -70,15 +70,16 @@ ATTR_CONFIG_ENTRY_ID = "config_entry_id"
ATTR_PARTIAL_DICT_MATCH = "partial_dict_match" ATTR_PARTIAL_DICT_MATCH = "partial_dict_match"
# service constants # 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_CLEAR_LOCK_USERCODE = "clear_lock_usercode"
SERVICE_SET_VALUE = "set_value" SERVICE_INVOKE_CC_API = "invoke_cc_api"
SERVICE_RESET_METER = "reset_meter"
SERVICE_MULTICAST_SET_VALUE = "multicast_set_value" SERVICE_MULTICAST_SET_VALUE = "multicast_set_value"
SERVICE_PING = "ping" SERVICE_PING = "ping"
SERVICE_REFRESH_VALUE = "refresh_value" SERVICE_REFRESH_VALUE = "refresh_value"
SERVICE_RESET_METER = "reset_meter"
SERVICE_SET_CONFIG_PARAMETER = "set_config_parameter" 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" ATTR_NODES = "nodes"
# config parameter # config parameter
@ -92,6 +93,9 @@ ATTR_BROADCAST = "broadcast"
# meter reset # meter reset
ATTR_METER_TYPE = "meter_type" ATTR_METER_TYPE = "meter_type"
ATTR_METER_TYPE_NAME = "meter_type_name" ATTR_METER_TYPE_NAME = "meter_type_name"
# invoke CC API
ATTR_METHOD_NAME = "method_name"
ATTR_PARAMETERS = "parameters"
ADDON_SLUG = "core_zwave_js" ADDON_SLUG = "core_zwave_js"

View File

@ -15,13 +15,16 @@ from homeassistant.components.diagnostics.util import async_redact_data
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_URL from homeassistant.const import CONF_URL
from homeassistant.core import HomeAssistant 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.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 .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"} KEYS_TO_REDACT = {"homeId", "location"}
@ -61,28 +64,19 @@ def redact_node_state(node_state: NodeDataType) -> NodeDataType:
def get_device_entities( def get_device_entities(
hass: HomeAssistant, node: Node, device: DeviceEntry hass: HomeAssistant, node: Node, device: dr.DeviceEntry
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
"""Get entities for a device.""" """Get entities for a device."""
entity_entries = async_entries_for_device( entity_entries = er.async_entries_for_device(
async_get(hass), device.id, include_disabled_entities=True er.async_get(hass), device.id, include_disabled_entities=True
) )
entities = [] entities = []
for entry in entity_entries: for entry in entity_entries:
state_key = None # If the value ID returns as None, we don't need to include this entity
split_unique_id = entry.unique_id.split(".") if (value_id := get_value_id_from_unique_id(entry.unique_id)) is None:
# If the unique ID has three parts, it's either one of the generic per node continue
# entities (node status sensor, ping button) or a binary sensor for a particular state_key = get_state_key_from_unique_id(entry.unique_id)
# 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]
zwave_value = node.values[value_id] zwave_value = node.values[value_id]
primary_value_data = { primary_value_data = {
"command_class": zwave_value.command_class, "command_class": zwave_value.command_class,

View File

@ -3,6 +3,7 @@ from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable
from dataclasses import astuple, dataclass from dataclasses import astuple, dataclass
import logging
from typing import Any, cast from typing import Any, cast
import voluptuous as vol import voluptuous as vol
@ -57,6 +58,34 @@ class ZwaveValueID:
raise ValueError("At least one of the fields must be set.") 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 @callback
def get_value_of_zwave_value(value: ZwaveValue | None) -> Any | None: def get_value_of_zwave_value(value: ZwaveValue | None) -> Any | None:
"""Return the value of a ZwaveValue.""" """Return the value of a ZwaveValue."""
@ -251,6 +280,7 @@ def async_get_nodes_from_targets(
val: dict[str, Any], val: dict[str, Any],
ent_reg: er.EntityRegistry | None = None, ent_reg: er.EntityRegistry | None = None,
dev_reg: dr.DeviceRegistry | None = None, dev_reg: dr.DeviceRegistry | None = None,
logger: logging.Logger = LOGGER,
) -> set[ZwaveNode]: ) -> set[ZwaveNode]:
""" """
Get nodes for all targets. Get nodes for all targets.
@ -263,7 +293,7 @@ def async_get_nodes_from_targets(
try: try:
nodes.add(async_get_node_from_entity_id(hass, entity_id, ent_reg, dev_reg)) nodes.add(async_get_node_from_entity_id(hass, entity_id, ent_reg, dev_reg))
except ValueError as err: except ValueError as err:
LOGGER.warning(err.args[0]) logger.warning(err.args[0])
# Convert all area IDs to nodes # Convert all area IDs to nodes
for area_id in val.get(ATTR_AREA_ID, []): for area_id in val.get(ATTR_AREA_ID, []):
@ -274,7 +304,7 @@ def async_get_nodes_from_targets(
try: try:
nodes.add(async_get_node_from_device_id(hass, device_id, dev_reg)) nodes.add(async_get_node_from_device_id(hass, device_id, dev_reg))
except ValueError as err: except ValueError as err:
LOGGER.warning(err.args[0]) logger.warning(err.args[0])
return nodes return nodes

View File

@ -3,12 +3,13 @@ from __future__ import annotations
import asyncio import asyncio
import logging import logging
from typing import Any from typing import Any, cast
import voluptuous as vol import voluptuous as vol
from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.client import Client as ZwaveClient
from zwave_js_server.const import CommandStatus from zwave_js_server.const import CommandClass, CommandStatus
from zwave_js_server.exceptions import SetValueFailed 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.node import Node as ZwaveNode
from zwave_js_server.model.value import get_value_id from zwave_js_server.model.value import get_value_id
from zwave_js_server.util.multicast import async_multicast_set_value 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.components.group import expand_entity_ids
from homeassistant.const import ATTR_AREA_ID, ATTR_DEVICE_ID, ATTR_ENTITY_ID from homeassistant.const import ATTR_AREA_ID, ATTR_DEVICE_ID, ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers import device_registry as dr, entity_registry as er
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.dispatcher import async_dispatcher_send
from . import const from . import const
from .config_validation import BITMASK_SCHEMA, VALUE_SCHEMA 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__) _LOGGER = logging.getLogger(__name__)
@ -78,7 +86,7 @@ class ZWaveServices:
def get_nodes_from_service_data(val: dict[str, Any]) -> dict[str, Any]: def get_nodes_from_service_data(val: dict[str, Any]) -> dict[str, Any]:
"""Get nodes set from service data.""" """Get nodes set from service data."""
val[const.ATTR_NODES] = async_get_nodes_from_targets( 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 return val
@ -132,8 +140,8 @@ class ZWaveServices:
for entity_id in val[ATTR_ENTITY_ID]: for entity_id in val[ATTR_ENTITY_ID]:
entry = self._ent_reg.async_get(entity_id) entry = self._ent_reg.async_get(entity_id)
if entry is None or entry.platform != const.DOMAIN: if entry is None or entry.platform != const.DOMAIN:
const.LOGGER.info( _LOGGER.info(
"Entity %s is not a valid %s entity.", entity_id, const.DOMAIN "Entity %s is not a valid %s entity", entity_id, const.DOMAIN
) )
invalid_entities.append(entity_id) 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: async def async_set_config_parameter(self, service: ServiceCall) -> None:
"""Set a config value on a node.""" """Set a config value on a node."""
nodes = service.data[const.ATTR_NODES] nodes = service.data[const.ATTR_NODES]
@ -425,11 +463,11 @@ class ZWaveServices:
) )
if success is False: if success is False:
raise SetValueFailed( raise HomeAssistantError(
"Unable to set value, refer to " "Unable to set value, refer to "
"https://zwave-js.github.io/node-zwave-js/#/api/node?id=setvalue " "https://zwave-js.github.io/node-zwave-js/#/api/node?id=setvalue "
"for possible reasons" "for possible reasons"
) ) from SetValueFailed
async def async_multicast_set_value(self, service: ServiceCall) -> None: async def async_multicast_set_value(self, service: ServiceCall) -> None:
"""Set a value via multicast to multiple nodes.""" """Set a value via multicast to multiple nodes."""
@ -438,7 +476,7 @@ class ZWaveServices:
options = service.data.get(const.ATTR_OPTIONS) options = service.data.get(const.ATTR_OPTIONS)
if not broadcast and len(nodes) == 1: if not broadcast and len(nodes) == 1:
const.LOGGER.info( _LOGGER.info(
"Passing the zwave_js.multicast_set_value service call to the " "Passing the zwave_js.multicast_set_value service call to the "
"zwave_js.set_value service since only one node was targeted" "zwave_js.set_value service since only one node was targeted"
) )
@ -496,14 +534,96 @@ class ZWaveServices:
) )
if success is False: 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: async def async_ping(self, service: ServiceCall) -> None:
"""Ping node(s).""" """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 " "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 " "calls will still work for now but the service will be removed in a "
"future release" "future release"
) )
nodes: set[ZwaveNode] = service.data[const.ATTR_NODES] nodes: set[ZwaveNode] = service.data[const.ATTR_NODES]
await asyncio.gather(*(node.async_ping() for node in 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)

View File

@ -252,3 +252,39 @@ reset_meter:
required: false required: false
selector: selector:
text: 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:

View File

@ -3,7 +3,7 @@ from unittest.mock import MagicMock, patch
import pytest import pytest
import voluptuous as vol 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.group import Group
from homeassistant.components.zwave_js.const import ( from homeassistant.components.zwave_js.const import (
@ -12,7 +12,10 @@ from homeassistant.components.zwave_js.const import (
ATTR_CONFIG_PARAMETER, ATTR_CONFIG_PARAMETER,
ATTR_CONFIG_PARAMETER_BITMASK, ATTR_CONFIG_PARAMETER_BITMASK,
ATTR_CONFIG_VALUE, ATTR_CONFIG_VALUE,
ATTR_ENDPOINT,
ATTR_METHOD_NAME,
ATTR_OPTIONS, ATTR_OPTIONS,
ATTR_PARAMETERS,
ATTR_PROPERTY, ATTR_PROPERTY,
ATTR_PROPERTY_KEY, ATTR_PROPERTY_KEY,
ATTR_REFRESH_ALL_VALUES, ATTR_REFRESH_ALL_VALUES,
@ -20,6 +23,7 @@ from homeassistant.components.zwave_js.const import (
ATTR_WAIT_FOR_RESULT, ATTR_WAIT_FOR_RESULT,
DOMAIN, DOMAIN,
SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS, SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS,
SERVICE_INVOKE_CC_API,
SERVICE_MULTICAST_SET_VALUE, SERVICE_MULTICAST_SET_VALUE,
SERVICE_PING, SERVICE_PING,
SERVICE_REFRESH_VALUE, 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.components.zwave_js.helpers import get_device_id
from homeassistant.const import ATTR_AREA_ID, ATTR_DEVICE_ID, ATTR_ENTITY_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.area_registry import async_get as async_get_area_reg
from homeassistant.helpers.device_registry import ( from homeassistant.helpers.device_registry import (
async_entries_for_config_entry, 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 # Test that when a command fails we raise an exception
client.async_send_command.return_value = {"success": False} client.async_send_command.return_value = {"success": False}
with pytest.raises(SetValueFailed): with pytest.raises(HomeAssistantError):
await hass.services.async_call( await hass.services.async_call(
DOMAIN, DOMAIN,
SERVICE_SET_VALUE, SERVICE_SET_VALUE,
@ -1335,7 +1340,7 @@ async def test_multicast_set_value(
# Test that when a command fails we raise an exception # Test that when a command fails we raise an exception
client.async_send_command.return_value = {"success": False} client.async_send_command.return_value = {"success": False}
with pytest.raises(SetValueFailed): with pytest.raises(HomeAssistantError):
await hass.services.async_call( await hass.services.async_call(
DOMAIN, DOMAIN,
SERVICE_MULTICAST_SET_VALUE, SERVICE_MULTICAST_SET_VALUE,
@ -1608,3 +1613,163 @@ async def test_ping(
{}, {},
blocking=True, 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()