mirror of
https://github.com/home-assistant/core.git
synced 2025-07-17 18:27:09 +00:00
Add zwave_js node_capabilities and invoke_cc_api websocket commands (#125327)
* Add zwave_js node_capabilities and invoke_cc_api websocket commands * Map isSecure to is_secure * Add tests * Add error handling * fix * Use to_dict function * Make response compatible with current expectations --------- Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
c2ceab741f
commit
5900413c08
@ -43,6 +43,7 @@ from zwave_js_server.model.controller.firmware import (
|
|||||||
ControllerFirmwareUpdateResult,
|
ControllerFirmwareUpdateResult,
|
||||||
)
|
)
|
||||||
from zwave_js_server.model.driver import Driver
|
from zwave_js_server.model.driver import Driver
|
||||||
|
from zwave_js_server.model.endpoint import Endpoint
|
||||||
from zwave_js_server.model.log_config import LogConfig
|
from zwave_js_server.model.log_config import LogConfig
|
||||||
from zwave_js_server.model.log_message import LogMessage
|
from zwave_js_server.model.log_message import LogMessage
|
||||||
from zwave_js_server.model.node import Node, NodeStatistics
|
from zwave_js_server.model.node import Node, NodeStatistics
|
||||||
@ -75,6 +76,11 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
|||||||
|
|
||||||
from .config_validation import BITMASK_SCHEMA
|
from .config_validation import BITMASK_SCHEMA
|
||||||
from .const import (
|
from .const import (
|
||||||
|
ATTR_COMMAND_CLASS,
|
||||||
|
ATTR_ENDPOINT,
|
||||||
|
ATTR_METHOD_NAME,
|
||||||
|
ATTR_PARAMETERS,
|
||||||
|
ATTR_WAIT_FOR_RESULT,
|
||||||
CONF_DATA_COLLECTION_OPTED_IN,
|
CONF_DATA_COLLECTION_OPTED_IN,
|
||||||
DATA_CLIENT,
|
DATA_CLIENT,
|
||||||
EVENT_DEVICE_ADDED_TO_REGISTRY,
|
EVENT_DEVICE_ADDED_TO_REGISTRY,
|
||||||
@ -437,6 +443,8 @@ def async_register_api(hass: HomeAssistant) -> None:
|
|||||||
)
|
)
|
||||||
websocket_api.async_register_command(hass, websocket_subscribe_node_statistics)
|
websocket_api.async_register_command(hass, websocket_subscribe_node_statistics)
|
||||||
websocket_api.async_register_command(hass, websocket_hard_reset_controller)
|
websocket_api.async_register_command(hass, websocket_hard_reset_controller)
|
||||||
|
websocket_api.async_register_command(hass, websocket_node_capabilities)
|
||||||
|
websocket_api.async_register_command(hass, websocket_invoke_cc_api)
|
||||||
hass.http.register_view(FirmwareUploadView(dr.async_get(hass)))
|
hass.http.register_view(FirmwareUploadView(dr.async_get(hass)))
|
||||||
|
|
||||||
|
|
||||||
@ -2525,3 +2533,81 @@ async def websocket_hard_reset_controller(
|
|||||||
)
|
)
|
||||||
]
|
]
|
||||||
await driver.async_hard_reset()
|
await driver.async_hard_reset()
|
||||||
|
|
||||||
|
|
||||||
|
@websocket_api.websocket_command(
|
||||||
|
{
|
||||||
|
vol.Required(TYPE): "zwave_js/node_capabilities",
|
||||||
|
vol.Required(DEVICE_ID): str,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@websocket_api.async_response
|
||||||
|
@async_handle_failed_command
|
||||||
|
@async_get_node
|
||||||
|
async def websocket_node_capabilities(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
connection: ActiveConnection,
|
||||||
|
msg: dict[str, Any],
|
||||||
|
node: Node,
|
||||||
|
) -> None:
|
||||||
|
"""Get node endpoints with their support command classes."""
|
||||||
|
# consumers expect snake_case at the moment
|
||||||
|
# remove that addition when consumers are updated
|
||||||
|
connection.send_result(
|
||||||
|
msg[ID],
|
||||||
|
{
|
||||||
|
idx: [
|
||||||
|
command_class.to_dict() | {"is_secure": command_class.is_secure}
|
||||||
|
for command_class in endpoint.command_classes
|
||||||
|
]
|
||||||
|
for idx, endpoint in node.endpoints.items()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@websocket_api.require_admin
|
||||||
|
@websocket_api.websocket_command(
|
||||||
|
{
|
||||||
|
vol.Required(TYPE): "zwave_js/invoke_cc_api",
|
||||||
|
vol.Required(DEVICE_ID): str,
|
||||||
|
vol.Required(ATTR_COMMAND_CLASS): vol.All(
|
||||||
|
vol.Coerce(int), vol.Coerce(CommandClass)
|
||||||
|
),
|
||||||
|
vol.Optional(ATTR_ENDPOINT): vol.Coerce(int),
|
||||||
|
vol.Required(ATTR_METHOD_NAME): cv.string,
|
||||||
|
vol.Required(ATTR_PARAMETERS): list,
|
||||||
|
vol.Optional(ATTR_WAIT_FOR_RESULT): cv.boolean,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@websocket_api.async_response
|
||||||
|
@async_handle_failed_command
|
||||||
|
@async_get_node
|
||||||
|
async def websocket_invoke_cc_api(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
connection: ActiveConnection,
|
||||||
|
msg: dict[str, Any],
|
||||||
|
node: Node,
|
||||||
|
) -> None:
|
||||||
|
"""Call invokeCCAPI on the node or provided endpoint."""
|
||||||
|
command_class: CommandClass = msg[ATTR_COMMAND_CLASS]
|
||||||
|
method_name: str = msg[ATTR_METHOD_NAME]
|
||||||
|
parameters: list[Any] = msg[ATTR_PARAMETERS]
|
||||||
|
|
||||||
|
node_or_endpoint: Node | Endpoint = node
|
||||||
|
if (endpoint := msg.get(ATTR_ENDPOINT)) is not None:
|
||||||
|
node_or_endpoint = node.endpoints[endpoint]
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await node_or_endpoint.async_invoke_cc_api(
|
||||||
|
command_class,
|
||||||
|
method_name,
|
||||||
|
*parameters,
|
||||||
|
wait_for_result=msg.get(ATTR_WAIT_FOR_RESULT, False),
|
||||||
|
)
|
||||||
|
except BaseZwaveJSServerError as err:
|
||||||
|
connection.send_error(msg[ID], err.__class__.__name__, str(err))
|
||||||
|
else:
|
||||||
|
connection.send_result(
|
||||||
|
msg[ID],
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
@ -81,6 +81,11 @@ from homeassistant.components.zwave_js.api import (
|
|||||||
VERSION,
|
VERSION,
|
||||||
)
|
)
|
||||||
from homeassistant.components.zwave_js.const import (
|
from homeassistant.components.zwave_js.const import (
|
||||||
|
ATTR_COMMAND_CLASS,
|
||||||
|
ATTR_ENDPOINT,
|
||||||
|
ATTR_METHOD_NAME,
|
||||||
|
ATTR_PARAMETERS,
|
||||||
|
ATTR_WAIT_FOR_RESULT,
|
||||||
CONF_DATA_COLLECTION_OPTED_IN,
|
CONF_DATA_COLLECTION_OPTED_IN,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
)
|
)
|
||||||
@ -88,7 +93,7 @@ from homeassistant.components.zwave_js.helpers import get_device_id
|
|||||||
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
|
||||||
|
|
||||||
from tests.common import MockUser
|
from tests.common import MockConfigEntry, MockUser
|
||||||
from tests.typing import ClientSessionGenerator, WebSocketGenerator
|
from tests.typing import ClientSessionGenerator, WebSocketGenerator
|
||||||
|
|
||||||
CONTROLLER_PATCH_PREFIX = "zwave_js_server.model.controller.Controller"
|
CONTROLLER_PATCH_PREFIX = "zwave_js_server.model.controller.Controller"
|
||||||
@ -4828,3 +4833,157 @@ async def test_hard_reset_controller(
|
|||||||
|
|
||||||
assert not msg["success"]
|
assert not msg["success"]
|
||||||
assert msg["error"]["code"] == ERR_NOT_FOUND
|
assert msg["error"]["code"] == ERR_NOT_FOUND
|
||||||
|
|
||||||
|
|
||||||
|
async def test_node_capabilities(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
multisensor_6: Node,
|
||||||
|
integration: MockConfigEntry,
|
||||||
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
) -> None:
|
||||||
|
"""Test the node_capabilities websocket command."""
|
||||||
|
entry = integration
|
||||||
|
ws_client = await hass_ws_client(hass)
|
||||||
|
|
||||||
|
node = multisensor_6
|
||||||
|
device = get_device(hass, node)
|
||||||
|
await ws_client.send_json_auto_id(
|
||||||
|
{
|
||||||
|
TYPE: "zwave_js/node_capabilities",
|
||||||
|
DEVICE_ID: device.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
msg = await ws_client.receive_json()
|
||||||
|
assert msg["result"] == {
|
||||||
|
"0": [
|
||||||
|
{
|
||||||
|
"id": 113,
|
||||||
|
"name": "Notification",
|
||||||
|
"version": 8,
|
||||||
|
"isSecure": False,
|
||||||
|
"is_secure": False,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test getting non-existent node fails
|
||||||
|
await ws_client.send_json_auto_id(
|
||||||
|
{
|
||||||
|
TYPE: "zwave_js/node_status",
|
||||||
|
DEVICE_ID: "fake_device",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
msg = await ws_client.receive_json()
|
||||||
|
assert not msg["success"]
|
||||||
|
assert msg["error"]["code"] == ERR_NOT_FOUND
|
||||||
|
|
||||||
|
# Test sending command with not loaded entry fails
|
||||||
|
await hass.config_entries.async_unload(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
await ws_client.send_json_auto_id(
|
||||||
|
{
|
||||||
|
TYPE: "zwave_js/node_status",
|
||||||
|
DEVICE_ID: device.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
msg = await ws_client.receive_json()
|
||||||
|
|
||||||
|
assert not msg["success"]
|
||||||
|
assert msg["error"]["code"] == ERR_NOT_LOADED
|
||||||
|
|
||||||
|
|
||||||
|
async def test_invoke_cc_api(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
client,
|
||||||
|
climate_radio_thermostat_ct100_plus_different_endpoints: Node,
|
||||||
|
integration: MockConfigEntry,
|
||||||
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
) -> None:
|
||||||
|
"""Test the invoke_cc_api websocket command."""
|
||||||
|
ws_client = await hass_ws_client(hass)
|
||||||
|
|
||||||
|
device_radio_thermostat = get_device(
|
||||||
|
hass, climate_radio_thermostat_ct100_plus_different_endpoints
|
||||||
|
)
|
||||||
|
assert device_radio_thermostat
|
||||||
|
|
||||||
|
# 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}
|
||||||
|
|
||||||
|
# Test with wait_for_result=False (default)
|
||||||
|
await ws_client.send_json_auto_id(
|
||||||
|
{
|
||||||
|
TYPE: "zwave_js/invoke_cc_api",
|
||||||
|
DEVICE_ID: device_radio_thermostat.id,
|
||||||
|
ATTR_COMMAND_CLASS: 67,
|
||||||
|
ATTR_METHOD_NAME: "someMethod",
|
||||||
|
ATTR_PARAMETERS: [1, 2],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
msg = await ws_client.receive_json()
|
||||||
|
assert msg["success"]
|
||||||
|
assert msg["result"] is None # We did not specify wait_for_result=True
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
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",
|
||||||
|
"nodeId": 26,
|
||||||
|
"endpoint": 0,
|
||||||
|
"commandClass": 67,
|
||||||
|
"methodName": "someMethod",
|
||||||
|
"args": [1, 2],
|
||||||
|
}
|
||||||
|
|
||||||
|
client.async_send_command_no_wait.reset_mock()
|
||||||
|
|
||||||
|
# Test with wait_for_result=True
|
||||||
|
await ws_client.send_json_auto_id(
|
||||||
|
{
|
||||||
|
TYPE: "zwave_js/invoke_cc_api",
|
||||||
|
DEVICE_ID: device_radio_thermostat.id,
|
||||||
|
ATTR_COMMAND_CLASS: 67,
|
||||||
|
ATTR_ENDPOINT: 0,
|
||||||
|
ATTR_METHOD_NAME: "someMethod",
|
||||||
|
ATTR_PARAMETERS: [1, 2],
|
||||||
|
ATTR_WAIT_FOR_RESULT: True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
msg = await ws_client.receive_json()
|
||||||
|
assert msg["success"]
|
||||||
|
assert msg["result"] is True
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
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",
|
||||||
|
"nodeId": 26,
|
||||||
|
"endpoint": 0,
|
||||||
|
"commandClass": 67,
|
||||||
|
"methodName": "someMethod",
|
||||||
|
"args": [1, 2],
|
||||||
|
}
|
||||||
|
|
||||||
|
client.async_send_command.side_effect = NotFoundError
|
||||||
|
|
||||||
|
# Ensure an error is returned
|
||||||
|
await ws_client.send_json_auto_id(
|
||||||
|
{
|
||||||
|
TYPE: "zwave_js/invoke_cc_api",
|
||||||
|
DEVICE_ID: device_radio_thermostat.id,
|
||||||
|
ATTR_COMMAND_CLASS: 67,
|
||||||
|
ATTR_ENDPOINT: 0,
|
||||||
|
ATTR_METHOD_NAME: "someMethod",
|
||||||
|
ATTR_PARAMETERS: [1, 2],
|
||||||
|
ATTR_WAIT_FOR_RESULT: True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
msg = await ws_client.receive_json()
|
||||||
|
assert not msg["success"]
|
||||||
|
assert msg["error"] == {"code": "NotFoundError", "message": ""}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user