From 4b189bd8c55a9cd614f6191b9030ca26c06446f4 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 26 Jul 2021 11:59:16 -0400 Subject: [PATCH] Add zwave_js WS API commands to get statistics (#53393) * Add zwave_js WS API commands to get statistics * update function name * move nested functions to top level functions --- homeassistant/components/zwave_js/api.py | 156 ++++++++++++++++-- tests/components/zwave_js/test_api.py | 192 +++++++++++++++++++++++ 2 files changed, 336 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 379376bb98d..a55ae47b935 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -19,13 +19,14 @@ from zwave_js_server.exceptions import ( SetValueFailed, ) from zwave_js_server.firmware import begin_firmware_update +from zwave_js_server.model.controller import ControllerStatistics from zwave_js_server.model.firmware import ( FirmwareUpdateFinished, FirmwareUpdateProgress, ) from zwave_js_server.model.log_config import LogConfig from zwave_js_server.model.log_message import LogMessage -from zwave_js_server.model.node import Node +from zwave_js_server.model.node import Node, NodeStatistics from zwave_js_server.util.node import async_set_config_parameter from homeassistant.components import websocket_api @@ -200,6 +201,10 @@ def async_register_api(hass: HomeAssistant) -> None: ) websocket_api.async_register_command(hass, websocket_check_for_config_updates) websocket_api.async_register_command(hass, websocket_install_config_update) + websocket_api.async_register_command( + hass, websocket_subscribe_controller_statistics + ) + websocket_api.async_register_command(hass, websocket_subscribe_node_statistics) hass.http.register_view(DumpView()) hass.http.register_view(FirmwareUploadView()) @@ -1332,6 +1337,16 @@ async def websocket_abort_firmware_update( connection.send_result(msg[ID]) +def _get_firmware_update_progress_dict( + progress: FirmwareUpdateProgress, +) -> dict[str, int]: + """Get a dictionary of firmware update progress.""" + return { + "sent_fragments": progress.sent_fragments, + "total_fragments": progress.total_fragments, + } + + @websocket_api.require_admin @websocket_api.websocket_command( { @@ -1348,7 +1363,7 @@ async def websocket_subscribe_firmware_update_status( msg: dict, node: Node, ) -> None: - """Subsribe to the status of a firmware update.""" + """Subscribe to the status of a firmware update.""" @callback def async_cleanup() -> None: @@ -1364,8 +1379,7 @@ async def websocket_subscribe_firmware_update_status( msg[ID], { "event": event["event"], - "sent_fragments": progress.sent_fragments, - "total_fragments": progress.total_fragments, + **_get_firmware_update_progress_dict(progress), }, ) ) @@ -1390,15 +1404,10 @@ async def websocket_subscribe_firmware_update_status( ] connection.subscriptions[msg["id"]] = async_cleanup - result = ( - { - "sent_fragments": node.firmware_update_progress.sent_fragments, - "total_fragments": node.firmware_update_progress.total_fragments, - } - if node.firmware_update_progress - else None + progress = node.firmware_update_progress + connection.send_result( + msg[ID], _get_firmware_update_progress_dict(progress) if progress else None ) - connection.send_result(msg[ID], result) class FirmwareUploadView(HomeAssistantView): @@ -1495,3 +1504,126 @@ async def websocket_install_config_update( """Check for config updates.""" success = await client.driver.async_install_config_update() connection.send_result(msg[ID], success) + + +def _get_controller_statistics_dict( + statistics: ControllerStatistics, +) -> dict[str, int]: + """Get dictionary of controller statistics.""" + return { + "messages_tx": statistics.messages_tx, + "messages_rx": statistics.messages_rx, + "messages_dropped_tx": statistics.messages_dropped_tx, + "messages_dropped_rx": statistics.messages_dropped_rx, + "nak": statistics.nak, + "can": statistics.can, + "timeout_ack": statistics.timeout_ack, + "timout_response": statistics.timeout_response, + "timeout_callback": statistics.timeout_callback, + } + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/subscribe_controller_statistics", + vol.Required(ENTRY_ID): str, + } +) +@websocket_api.async_response +@async_get_entry +async def websocket_subscribe_controller_statistics( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + entry: ConfigEntry, + client: Client, +) -> None: + """Subsribe to the statistics updates for a controller.""" + + @callback + def async_cleanup() -> None: + """Remove signal listeners.""" + for unsub in unsubs: + unsub() + + @callback + def forward_stats(event: dict) -> None: + statistics: ControllerStatistics = event["statistics_updated"] + connection.send_message( + websocket_api.event_message( + msg[ID], + { + "event": event["event"], + "source": "controller", + **_get_controller_statistics_dict(statistics), + }, + ) + ) + + controller = client.driver.controller + + msg[DATA_UNSUBSCRIBE] = unsubs = [ + controller.on("statistics updated", forward_stats) + ] + connection.subscriptions[msg["id"]] = async_cleanup + + connection.send_result( + msg[ID], _get_controller_statistics_dict(controller.statistics) + ) + + +def _get_node_statistics_dict(statistics: NodeStatistics) -> dict[str, int]: + """Get dictionary of node statistics.""" + return { + "commands_tx": statistics.commands_tx, + "commands_rx": statistics.commands_rx, + "commands_dropped_tx": statistics.commands_dropped_tx, + "commands_dropped_rx": statistics.commands_dropped_rx, + "timeout_response": statistics.timeout_response, + } + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/subscribe_node_statistics", + vol.Required(ENTRY_ID): str, + vol.Required(NODE_ID): int, + } +) +@websocket_api.async_response +@async_get_node +async def websocket_subscribe_node_statistics( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + node: Node, +) -> None: + """Subsribe to the statistics updates for a node.""" + + @callback + def async_cleanup() -> None: + """Remove signal listeners.""" + for unsub in unsubs: + unsub() + + @callback + def forward_stats(event: dict) -> None: + statistics: NodeStatistics = event["statistics_updated"] + connection.send_message( + websocket_api.event_message( + msg[ID], + { + "event": event["event"], + "source": "node", + "node_id": node.node_id, + **_get_node_statistics_dict(statistics), + }, + ) + ) + + msg[DATA_UNSUBSCRIBE] = unsubs = [node.on("statistics updated", forward_stats)] + connection.subscriptions[msg["id"]] = async_cleanup + + connection.send_result(msg[ID], _get_node_statistics_dict(node.statistics)) diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index a96e76be865..75fca7f11ff 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -2703,3 +2703,195 @@ async def test_install_config_update(hass, client, integration, hass_ws_client): assert not msg["success"] assert msg["error"]["code"] == ERR_NOT_FOUND + + +async def test_subscribe_controller_statistics( + hass, integration, client, hass_ws_client +): + """Test the subscribe_controller_statistics command.""" + entry = integration + ws_client = await hass_ws_client(hass) + + await ws_client.send_json( + { + ID: 1, + TYPE: "zwave_js/subscribe_controller_statistics", + ENTRY_ID: entry.entry_id, + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + assert msg["result"] == { + "messages_tx": 0, + "messages_rx": 0, + "messages_dropped_tx": 0, + "messages_dropped_rx": 0, + "nak": 0, + "can": 0, + "timeout_ack": 0, + "timout_response": 0, + "timeout_callback": 0, + } + + # Fire statistics updated + event = Event( + "statistics updated", + { + "source": "controller", + "event": "statistics updated", + "statistics": { + "messagesTX": 1, + "messagesRX": 1, + "messagesDroppedTX": 1, + "messagesDroppedRX": 1, + "NAK": 1, + "CAN": 1, + "timeoutACK": 1, + "timeoutResponse": 1, + "timeoutCallback": 1, + }, + }, + ) + client.driver.controller.receive_event(event) + msg = await ws_client.receive_json() + assert msg["event"] == { + "event": "statistics updated", + "source": "controller", + "messages_tx": 1, + "messages_rx": 1, + "messages_dropped_tx": 1, + "messages_dropped_rx": 1, + "nak": 1, + "can": 1, + "timeout_ack": 1, + "timout_response": 1, + "timeout_callback": 1, + } + + # Test sending command with improper entry ID fails + await ws_client.send_json( + { + ID: 2, + TYPE: "zwave_js/subscribe_controller_statistics", + ENTRY_ID: "fake_entry_id", + } + ) + 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( + { + ID: 3, + TYPE: "zwave_js/subscribe_controller_statistics", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + + +async def test_subscribe_node_statistics( + hass, multisensor_6, integration, client, hass_ws_client +): + """Test the subscribe_node_statistics command.""" + entry = integration + ws_client = await hass_ws_client(hass) + + await ws_client.send_json( + { + ID: 1, + TYPE: "zwave_js/subscribe_node_statistics", + ENTRY_ID: entry.entry_id, + NODE_ID: multisensor_6.node_id, + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + assert msg["result"] == { + "commands_tx": 0, + "commands_rx": 0, + "commands_dropped_tx": 0, + "commands_dropped_rx": 0, + "timeout_response": 0, + } + + # Fire statistics updated + event = Event( + "statistics updated", + { + "source": "node", + "event": "statistics updated", + "nodeId": multisensor_6.node_id, + "statistics": { + "commandsTX": 1, + "commandsRX": 1, + "commandsDroppedTX": 1, + "commandsDroppedRX": 1, + "timeoutResponse": 1, + }, + }, + ) + client.driver.controller.receive_event(event) + msg = await ws_client.receive_json() + assert msg["event"] == { + "event": "statistics updated", + "source": "node", + "node_id": multisensor_6.node_id, + "commands_tx": 1, + "commands_rx": 1, + "commands_dropped_tx": 1, + "commands_dropped_rx": 1, + "timeout_response": 1, + } + + # Test sending command with improper entry ID fails + await ws_client.send_json( + { + ID: 2, + TYPE: "zwave_js/subscribe_node_statistics", + ENTRY_ID: "fake_entry_id", + NODE_ID: multisensor_6.node_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_FOUND + + # Test sending command with improper node ID fails + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/subscribe_node_statistics", + ENTRY_ID: entry.entry_id, + NODE_ID: multisensor_6.node_id + 100, + } + ) + msg = await ws_client.receive_json() + + # 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( + { + ID: 4, + TYPE: "zwave_js/subscribe_node_statistics", + ENTRY_ID: entry.entry_id, + NODE_ID: multisensor_6.node_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED