From 6d9b67ddb213e5ce3fd5d8ef530ec38447048820 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 26 May 2021 11:38:02 -0400 Subject: [PATCH] Add zwave_js heal node and network WS API commands (#51047) Co-authored-by: Martin Hjelmare --- homeassistant/components/zwave_js/api.py | 154 ++++++++++++++- tests/components/zwave_js/test_api.py | 175 ++++++++++++++++++ tests/fixtures/zwave_js/controller_state.json | 3 +- 3 files changed, 326 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 2d0fee54a18..3e13de5ada9 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -2,7 +2,7 @@ from __future__ import annotations import dataclasses -from functools import wraps +from functools import partial, wraps import json from typing import Callable @@ -142,18 +142,24 @@ def async_register_api(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, websocket_ping_node) websocket_api.async_register_command(hass, websocket_add_node) websocket_api.async_register_command(hass, websocket_stop_inclusion) + websocket_api.async_register_command(hass, websocket_stop_exclusion) websocket_api.async_register_command(hass, websocket_remove_node) websocket_api.async_register_command(hass, websocket_remove_failed_node) websocket_api.async_register_command(hass, websocket_replace_failed_node) - websocket_api.async_register_command(hass, websocket_stop_exclusion) + websocket_api.async_register_command(hass, websocket_begin_healing_network) + websocket_api.async_register_command( + hass, websocket_subscribe_heal_network_progress + ) + websocket_api.async_register_command(hass, websocket_stop_healing_network) websocket_api.async_register_command(hass, websocket_refresh_node_info) websocket_api.async_register_command(hass, websocket_refresh_node_values) websocket_api.async_register_command(hass, websocket_refresh_node_cc_values) + websocket_api.async_register_command(hass, websocket_heal_node) + websocket_api.async_register_command(hass, websocket_set_config_parameter) + websocket_api.async_register_command(hass, websocket_get_config_parameters) websocket_api.async_register_command(hass, websocket_subscribe_logs) websocket_api.async_register_command(hass, websocket_update_log_config) websocket_api.async_register_command(hass, websocket_get_log_config) - websocket_api.async_register_command(hass, websocket_get_config_parameters) - websocket_api.async_register_command(hass, websocket_set_config_parameter) websocket_api.async_register_command( hass, websocket_update_data_collection_preference ) @@ -180,6 +186,7 @@ async def websocket_network_status( client: Client, ) -> None: """Get the status of the Z-Wave JS network.""" + controller = client.driver.controller data = { "client": { "ws_server_url": client.ws_server_url, @@ -188,7 +195,24 @@ async def websocket_network_status( "server_version": client.version.server_version, }, "controller": { - "home_id": client.driver.controller.data["homeId"], + "home_id": controller.home_id, + "library_version": controller.library_version, + "type": controller.controller_type, + "own_node_id": controller.own_node_id, + "is_secondary": controller.is_secondary, + "is_using_home_id_from_other_network": controller.is_using_home_id_from_other_network, + "is_sis_present": controller.is_SIS_present, + "was_real_primary": controller.was_real_primary, + "is_static_update_controller": controller.is_static_update_controller, + "is_slave": controller.is_slave, + "serial_api_version": controller.serial_api_version, + "manufacturer_id": controller.manufacturer_id, + "product_id": controller.product_id, + "product_type": controller.product_type, + "supported_function_types": controller.supported_function_types, + "suc_node_id": controller.suc_node_id, + "supports_timers": controller.supports_timers, + "is_heal_network_active": controller.is_heal_network_active, "nodes": list(client.driver.controller.nodes), }, } @@ -643,6 +667,126 @@ async def websocket_remove_failed_node( ) +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/begin_healing_network", + vol.Required(ENTRY_ID): str, + } +) +@websocket_api.async_response +@async_get_entry +async def websocket_begin_healing_network( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + entry: ConfigEntry, + client: Client, +) -> None: + """Begin healing the Z-Wave network.""" + controller = client.driver.controller + + result = await controller.async_begin_healing_network() + connection.send_result( + msg[ID], + result, + ) + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/subscribe_heal_network_progress", + vol.Required(ENTRY_ID): str, + } +) +@websocket_api.async_response +@async_get_entry +async def websocket_subscribe_heal_network_progress( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + entry: ConfigEntry, + client: Client, +) -> None: + """Subscribe to heal Z-Wave network status updates.""" + controller = client.driver.controller + + @callback + def async_cleanup() -> None: + """Remove signal listeners.""" + for unsub in unsubs: + unsub() + + @callback + def forward_event(key: str, event: dict) -> None: + connection.send_message( + websocket_api.event_message( + msg[ID], {"event": event["event"], "heal_node_status": event[key]} + ) + ) + + connection.subscriptions[msg["id"]] = async_cleanup + unsubs = [ + controller.on("heal network progress", partial(forward_event, "progress")), + controller.on("heal network done", partial(forward_event, "result")), + ] + + connection.send_result(msg[ID]) + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/stop_healing_network", + vol.Required(ENTRY_ID): str, + } +) +@websocket_api.async_response +@async_get_entry +async def websocket_stop_healing_network( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + entry: ConfigEntry, + client: Client, +) -> None: + """Stop healing the Z-Wave network.""" + controller = client.driver.controller + result = await controller.async_stop_healing_network() + connection.send_result( + msg[ID], + result, + ) + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/heal_node", + vol.Required(ENTRY_ID): str, + vol.Required(NODE_ID): int, + } +) +@websocket_api.async_response +@async_get_entry +async def websocket_heal_node( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + entry: ConfigEntry, + client: Client, +) -> None: + """Heal a node on the Z-Wave network.""" + controller = client.driver.controller + node_id = msg[NODE_ID] + result = await controller.async_heal_node(node_id) + connection.send_result( + msg[ID], + result, + ) + + @websocket_api.require_admin @websocket_api.websocket_command( { diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index fd6161b6f00..a1a06f140db 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -496,6 +496,7 @@ async def test_replace_failed_node( msg = await ws_client.receive_json() assert msg["success"] + assert msg["result"] event = Event( type="inclusion started", @@ -674,6 +675,180 @@ async def test_remove_failed_node( assert msg["error"]["code"] == ERR_NOT_LOADED +async def test_begin_healing_network( + hass, + integration, + client, + hass_ws_client, +): + """Test the begin_healing_network websocket command.""" + entry = integration + ws_client = await hass_ws_client(hass) + + client.async_send_command.return_value = {"success": True} + + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/begin_healing_network", + ENTRY_ID: entry.entry_id, + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + assert msg["result"] + + # 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/begin_healing_network", + 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_heal_network_progress( + hass, integration, client, hass_ws_client +): + """Test the subscribe_heal_network_progress command.""" + entry = integration + ws_client = await hass_ws_client(hass) + + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/subscribe_heal_network_progress", + ENTRY_ID: entry.entry_id, + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + + # Fire heal network progress + event = Event( + "heal network progress", + { + "source": "controller", + "event": "heal network progress", + "progress": {67: "pending"}, + }, + ) + client.driver.controller.receive_event(event) + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "heal network progress" + assert msg["event"]["heal_node_status"] == {"67": "pending"} + + # 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_heal_network_progress", + 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_stop_healing_network( + hass, + integration, + client, + hass_ws_client, +): + """Test the stop_healing_network websocket command.""" + entry = integration + ws_client = await hass_ws_client(hass) + + client.async_send_command.return_value = {"success": True} + + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/stop_healing_network", + ENTRY_ID: entry.entry_id, + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + assert msg["result"] + + # 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/stop_healing_network", + 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_heal_node( + hass, + integration, + client, + hass_ws_client, +): + """Test the heal_node websocket command.""" + entry = integration + ws_client = await hass_ws_client(hass) + + client.async_send_command.return_value = {"success": True} + + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/heal_node", + ENTRY_ID: entry.entry_id, + NODE_ID: 67, + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + assert msg["result"] + + # 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/heal_node", + ENTRY_ID: entry.entry_id, + NODE_ID: 67, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + + async def test_refresh_node_info( hass, client, multisensor_6, integration, hass_ws_client ): diff --git a/tests/fixtures/zwave_js/controller_state.json b/tests/fixtures/zwave_js/controller_state.json index df026e8fd2c..d4bf58a53ce 100644 --- a/tests/fixtures/zwave_js/controller_state.json +++ b/tests/fixtures/zwave_js/controller_state.json @@ -91,7 +91,8 @@ 239 ], "sucNodeId": 1, - "supportsTimers": false + "supportsTimers": false, + "isHealNetworkActive": false }, "nodes": [ ]