diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 5cf98d44802..1c303eea053 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -4,7 +4,7 @@ from __future__ import annotations import dataclasses from functools import partial, wraps import json -from typing import Callable +from typing import Any, Callable from aiohttp import hdrs, web, web_exceptions, web_request import voluptuous as vol @@ -13,6 +13,7 @@ from zwave_js_server.client import Client from zwave_js_server.const import CommandClass, LogLevel from zwave_js_server.exceptions import ( BaseZwaveJSServerError, + FailedCommand, InvalidNewValue, NotFoundError, SetValueFailed, @@ -47,6 +48,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( CONF_DATA_COLLECTION_OPTED_IN, DATA_CLIENT, + DATA_UNSUBSCRIBE, DOMAIN, EVENT_DEVICE_ADDED_TO_REGISTRY, ) @@ -134,6 +136,30 @@ def async_get_node(orig_func: Callable) -> Callable: return async_get_node_func +def async_handle_failed_command(orig_func: Callable) -> Callable: + """Decorate async function to handle FailedCommand and send relevant error.""" + + @wraps(orig_func) + async def async_handle_failed_command_func( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + *args: Any, + **kwargs: Any, + ) -> None: + """Handle FailedCommand within function and send relevant error.""" + try: + await orig_func(hass, connection, msg, *args, **kwargs) + except FailedCommand as err: + # Unsubscribe to callbacks + if unsubs := msg.get(DATA_UNSUBSCRIBE): + for unsub in unsubs: + unsub() + connection.send_error(msg[ID], err.error_code, err.args[0]) + + return async_handle_failed_command_func + + @callback def async_register_api(hass: HomeAssistant) -> None: """Register all of our api endpoints.""" @@ -318,6 +344,7 @@ async def websocket_node_metadata( } ) @websocket_api.async_response +@async_handle_failed_command @async_get_node async def websocket_ping_node( hass: HomeAssistant, @@ -342,6 +369,7 @@ async def websocket_ping_node( } ) @websocket_api.async_response +@async_handle_failed_command @async_get_entry async def websocket_add_node( hass: HomeAssistant, @@ -410,7 +438,7 @@ async def websocket_add_node( ) connection.subscriptions[msg["id"]] = async_cleanup - unsubs = [ + msg[DATA_UNSUBSCRIBE] = unsubs = [ controller.on("inclusion started", forward_event), controller.on("inclusion failed", forward_event), controller.on("inclusion stopped", forward_event), @@ -435,6 +463,7 @@ async def websocket_add_node( } ) @websocket_api.async_response +@async_handle_failed_command @async_get_entry async def websocket_stop_inclusion( hass: HomeAssistant, @@ -460,6 +489,7 @@ async def websocket_stop_inclusion( } ) @websocket_api.async_response +@async_handle_failed_command @async_get_entry async def websocket_stop_exclusion( hass: HomeAssistant, @@ -485,6 +515,7 @@ async def websocket_stop_exclusion( } ) @websocket_api.async_response +@async_handle_failed_command @async_get_entry async def websocket_remove_node( hass: HomeAssistant, @@ -522,7 +553,7 @@ async def websocket_remove_node( ) connection.subscriptions[msg["id"]] = async_cleanup - unsubs = [ + msg[DATA_UNSUBSCRIBE] = unsubs = [ controller.on("exclusion started", forward_event), controller.on("exclusion failed", forward_event), controller.on("exclusion stopped", forward_event), @@ -546,6 +577,7 @@ async def websocket_remove_node( } ) @websocket_api.async_response +@async_handle_failed_command @async_get_entry async def websocket_replace_failed_node( hass: HomeAssistant, @@ -628,7 +660,7 @@ async def websocket_replace_failed_node( ) connection.subscriptions[msg["id"]] = async_cleanup - unsubs = [ + msg[DATA_UNSUBSCRIBE] = unsubs = [ controller.on("inclusion started", forward_event), controller.on("inclusion failed", forward_event), controller.on("inclusion stopped", forward_event), @@ -655,6 +687,7 @@ async def websocket_replace_failed_node( } ) @websocket_api.async_response +@async_handle_failed_command @async_get_entry async def websocket_remove_failed_node( hass: HomeAssistant, @@ -670,7 +703,8 @@ async def websocket_remove_failed_node( @callback def async_cleanup() -> None: """Remove signal listeners.""" - unsub() + for unsub in unsubs: + unsub() @callback def node_removed(event: dict) -> None: @@ -686,7 +720,7 @@ async def websocket_remove_failed_node( ) connection.subscriptions[msg["id"]] = async_cleanup - unsub = controller.on("node removed", node_removed) + msg[DATA_UNSUBSCRIBE] = unsubs = [controller.on("node removed", node_removed)] result = await controller.async_remove_failed_node(node_id) connection.send_result( @@ -703,6 +737,7 @@ async def websocket_remove_failed_node( } ) @websocket_api.async_response +@async_handle_failed_command @async_get_entry async def websocket_begin_healing_network( hass: HomeAssistant, @@ -755,7 +790,7 @@ async def websocket_subscribe_heal_network_progress( ) connection.subscriptions[msg["id"]] = async_cleanup - unsubs = [ + msg[DATA_UNSUBSCRIBE] = unsubs = [ controller.on("heal network progress", partial(forward_event, "progress")), controller.on("heal network done", partial(forward_event, "result")), ] @@ -771,6 +806,7 @@ async def websocket_subscribe_heal_network_progress( } ) @websocket_api.async_response +@async_handle_failed_command @async_get_entry async def websocket_stop_healing_network( hass: HomeAssistant, @@ -797,6 +833,7 @@ async def websocket_stop_healing_network( } ) @websocket_api.async_response +@async_handle_failed_command @async_get_entry async def websocket_heal_node( hass: HomeAssistant, @@ -824,6 +861,7 @@ async def websocket_heal_node( }, ) @websocket_api.async_response +@async_handle_failed_command @async_get_node async def websocket_refresh_node_info( hass: HomeAssistant, @@ -854,7 +892,7 @@ async def websocket_refresh_node_info( ) connection.subscriptions[msg["id"]] = async_cleanup - unsubs = [ + msg[DATA_UNSUBSCRIBE] = unsubs = [ node.on("interview started", forward_event), node.on("interview completed", forward_event), node.on("interview stage completed", forward_stage), @@ -874,6 +912,7 @@ async def websocket_refresh_node_info( }, ) @websocket_api.async_response +@async_handle_failed_command @async_get_node async def websocket_refresh_node_values( hass: HomeAssistant, @@ -896,6 +935,7 @@ async def websocket_refresh_node_values( }, ) @websocket_api.async_response +@async_handle_failed_command @async_get_node async def websocket_refresh_node_cc_values( hass: HomeAssistant, @@ -930,6 +970,7 @@ async def websocket_refresh_node_cc_values( } ) @websocket_api.async_response +@async_handle_failed_command @async_get_node async def websocket_set_config_parameter( hass: HomeAssistant, @@ -1027,6 +1068,7 @@ def filename_is_present_if_logging_to_file(obj: dict) -> dict: } ) @websocket_api.async_response +@async_handle_failed_command @async_get_entry async def websocket_subscribe_log_updates( hass: HomeAssistant, @@ -1076,7 +1118,7 @@ async def websocket_subscribe_log_updates( ) ) - unsubs = [ + msg[DATA_UNSUBSCRIBE] = unsubs = [ driver.on("logging", log_messages), driver.on("log config updated", log_config_updates), ] @@ -1114,6 +1156,7 @@ async def websocket_subscribe_log_updates( }, ) @websocket_api.async_response +@async_handle_failed_command @async_get_entry async def websocket_update_log_config( hass: HomeAssistant, @@ -1161,6 +1204,7 @@ async def websocket_get_log_config( }, ) @websocket_api.async_response +@async_handle_failed_command @async_get_entry async def websocket_update_data_collection_preference( hass: HomeAssistant, @@ -1191,6 +1235,7 @@ async def websocket_update_data_collection_preference( }, ) @websocket_api.async_response +@async_handle_failed_command @async_get_entry async def websocket_data_collection_status( hass: HomeAssistant, @@ -1273,6 +1318,7 @@ async def websocket_version_info( } ) @websocket_api.async_response +@async_handle_failed_command @async_get_node async def websocket_abort_firmware_update( hass: HomeAssistant, @@ -1337,7 +1383,7 @@ async def websocket_subscribe_firmware_update_status( ) ) - unsubs = [ + msg[DATA_UNSUBSCRIBE] = unsubs = [ node.on("firmware update progress", forward_progress), node.on("firmware update finished", forward_finished), ] @@ -1400,6 +1446,7 @@ class FirmwareUploadView(HomeAssistantView): } ) @websocket_api.async_response +@async_handle_failed_command @async_get_entry async def websocket_check_for_config_updates( hass: HomeAssistant, @@ -1427,6 +1474,7 @@ async def websocket_check_for_config_updates( } ) @websocket_api.async_response +@async_handle_failed_command @async_get_entry async def websocket_install_config_update( hass: HomeAssistant, diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index b6d846898d3..80fe6da90f5 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -7,6 +7,7 @@ from zwave_js_server.const import LogLevel from zwave_js_server.event import Event from zwave_js_server.exceptions import ( FailedCommand, + FailedZWaveCommand, InvalidNewValue, NotFoundError, SetValueFailed, @@ -280,13 +281,32 @@ async def test_ping_node( assert not msg["success"] assert msg["error"]["code"] == ERR_NOT_FOUND + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.node.Node.async_ping", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 5, + TYPE: "zwave_js/ping_node", + ENTRY_ID: entry.entry_id, + NODE_ID: node.node_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + # 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: 5, + ID: 6, TYPE: "zwave_js/ping_node", ENTRY_ID: entry.entry_id, NODE_ID: node.node_id, @@ -385,12 +405,30 @@ async def test_add_node( msg = await ws_client.receive_json() assert msg["event"]["event"] == "interview failed" + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.controller.Controller.async_begin_inclusion", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/add_node", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + # 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/add_node", ENTRY_ID: entry.entry_id} + {ID: 5, TYPE: "zwave_js/add_node", ENTRY_ID: entry.entry_id} ) msg = await ws_client.receive_json() @@ -419,12 +457,48 @@ async def test_cancel_inclusion_exclusion(hass, integration, client, hass_ws_cli msg = await ws_client.receive_json() assert msg["success"] + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.controller.Controller.async_stop_inclusion", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 6, + TYPE: "zwave_js/stop_inclusion", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.controller.Controller.async_stop_exclusion", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 7, + TYPE: "zwave_js/stop_exclusion", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + # 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: 6, TYPE: "zwave_js/stop_inclusion", ENTRY_ID: entry.entry_id} + {ID: 8, TYPE: "zwave_js/stop_inclusion", ENTRY_ID: entry.entry_id} ) msg = await ws_client.receive_json() @@ -432,7 +506,7 @@ async def test_cancel_inclusion_exclusion(hass, integration, client, hass_ws_cli assert msg["error"]["code"] == ERR_NOT_LOADED await ws_client.send_json( - {ID: 7, TYPE: "zwave_js/stop_exclusion", ENTRY_ID: entry.entry_id} + {ID: 9, TYPE: "zwave_js/stop_exclusion", ENTRY_ID: entry.entry_id} ) msg = await ws_client.receive_json() @@ -494,12 +568,30 @@ async def test_remove_node( ) assert device is None + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.controller.Controller.async_begin_exclusion", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/remove_node", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + # 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/remove_node", ENTRY_ID: entry.entry_id} + {ID: 5, TYPE: "zwave_js/remove_node", ENTRY_ID: entry.entry_id} ) msg = await ws_client.receive_json() @@ -641,13 +733,32 @@ async def test_replace_failed_node( msg = await ws_client.receive_json() assert msg["event"]["event"] == "interview failed" + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.controller.Controller.async_replace_failed_node", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/replace_failed_node", + ENTRY_ID: entry.entry_id, + NODE_ID: 67, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + # 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, + ID: 5, TYPE: "zwave_js/replace_failed_node", ENTRY_ID: entry.entry_id, NODE_ID: 67, @@ -705,13 +816,32 @@ async def test_remove_failed_node( ) assert device is None + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.controller.Controller.async_remove_failed_node", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/remove_failed_node", + ENTRY_ID: entry.entry_id, + NODE_ID: 67, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + # 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, + ID: 5, TYPE: "zwave_js/remove_failed_node", ENTRY_ID: entry.entry_id, NODE_ID: 67, @@ -747,13 +877,31 @@ async def test_begin_healing_network( assert msg["success"] assert msg["result"] + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.controller.Controller.async_begin_healing_network", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + 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"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + # 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, + ID: 5, TYPE: "zwave_js/begin_healing_network", ENTRY_ID: entry.entry_id, } @@ -837,13 +985,31 @@ async def test_stop_healing_network( assert msg["success"] assert msg["result"] + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.controller.Controller.async_stop_healing_network", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + 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"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + # 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, + ID: 5, TYPE: "zwave_js/stop_healing_network", ENTRY_ID: entry.entry_id, } @@ -879,13 +1045,32 @@ async def test_heal_node( assert msg["success"] assert msg["result"] + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.controller.Controller.async_heal_node", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + 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"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + # 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, + ID: 5, TYPE: "zwave_js/heal_node", ENTRY_ID: entry.entry_id, NODE_ID: 67, @@ -978,13 +1163,32 @@ async def test_refresh_node_info( assert not msg["success"] assert msg["error"]["code"] == ERR_NOT_FOUND + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.node.Node.async_refresh_info", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/refresh_node_info", + ENTRY_ID: entry.entry_id, + NODE_ID: 52, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + # 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, + ID: 4, TYPE: "zwave_js/refresh_node_info", ENTRY_ID: entry.entry_id, NODE_ID: 52, @@ -1048,6 +1252,42 @@ async def test_refresh_node_values( assert not msg["success"] assert msg["error"]["code"] == ERR_NOT_FOUND + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.node.Node.async_refresh_values", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/refresh_node_values", + ENTRY_ID: entry.entry_id, + NODE_ID: 52, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + + # 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: 5, + TYPE: "zwave_js/refresh_node_values", + ENTRY_ID: entry.entry_id, + NODE_ID: 52, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + async def test_refresh_node_cc_values( hass, client, multisensor_6, integration, hass_ws_client @@ -1105,13 +1345,33 @@ async def test_refresh_node_cc_values( assert not msg["success"] assert msg["error"]["code"] == ERR_NOT_FOUND + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.node.Node.async_refresh_cc_values", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/refresh_node_cc_values", + ENTRY_ID: entry.entry_id, + NODE_ID: 52, + COMMAND_CLASS_ID: 112, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + # 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, + ID: 5, TYPE: "zwave_js/refresh_node_cc_values", ENTRY_ID: entry.entry_id, NODE_ID: 52, @@ -1307,13 +1567,35 @@ async def test_set_config_parameter( assert not msg["success"] assert msg["error"]["code"] == ERR_NOT_FOUND + # Test FailedZWaveCommand is caught + with patch( + "homeassistant.components.zwave_js.api.async_set_config_parameter", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 7, + TYPE: "zwave_js/set_config_parameter", + ENTRY_ID: entry.entry_id, + NODE_ID: 52, + PROPERTY: 102, + PROPERTY_KEY: 1, + VALUE: 1, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + # 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: 7, + ID: 8, TYPE: "zwave_js/set_config_parameter", ENTRY_ID: entry.entry_id, NODE_ID: 52, @@ -1625,12 +1907,30 @@ async def test_subscribe_log_updates(hass, integration, client, hass_ws_client): }, } + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.driver.Driver.async_start_listening_logs", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 2, + TYPE: "zwave_js/subscribe_log_updates", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + # 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: 2, TYPE: "zwave_js/subscribe_log_updates", ENTRY_ID: entry.entry_id} + {ID: 3, TYPE: "zwave_js/subscribe_log_updates", ENTRY_ID: entry.entry_id} ) msg = await ws_client.receive_json() @@ -1757,13 +2057,32 @@ async def test_update_log_config(hass, client, integration, hass_ws_client): and "must be provided if logging to file" in msg["error"]["message"] ) + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.driver.Driver.async_update_log_config", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 7, + TYPE: "zwave_js/update_log_config", + ENTRY_ID: entry.entry_id, + CONFIG: {LEVEL: "Error"}, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + # 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: 7, + ID: 8, TYPE: "zwave_js/update_log_config", ENTRY_ID: entry.entry_id, CONFIG: {LEVEL: "Error"}, @@ -1884,13 +2203,50 @@ async def test_data_collection(hass, client, integration, hass_ws_client): client.async_send_command.reset_mock() + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.driver.Driver.async_is_statistics_enabled", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/data_collection_status", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.driver.Driver.async_enable_statistics", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 5, + TYPE: "zwave_js/update_data_collection_preference", + ENTRY_ID: entry.entry_id, + OPTED_IN: True, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + # 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, + ID: 6, TYPE: "zwave_js/data_collection_status", ENTRY_ID: entry.entry_id, } @@ -1902,7 +2258,7 @@ async def test_data_collection(hass, client, integration, hass_ws_client): await ws_client.send_json( { - ID: 5, + ID: 7, TYPE: "zwave_js/update_data_collection_preference", ENTRY_ID: entry.entry_id, OPTED_IN: True, @@ -1938,6 +2294,42 @@ async def test_abort_firmware_update( assert args["command"] == "node.abort_firmware_update" assert args["nodeId"] == multisensor_6.node_id + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.node.Node.async_abort_firmware_update", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 2, + TYPE: "zwave_js/abort_firmware_update", + 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"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + + # 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/abort_firmware_update", + 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 + async def test_abort_firmware_update_failures( hass, integration, multisensor_6, client, hass_ws_client @@ -2128,13 +2520,31 @@ async def test_check_for_config_updates(hass, client, integration, hass_ws_clien assert config_update["update_available"] assert config_update["new_version"] == "test" + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.driver.Driver.async_check_for_config_updates", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 2, + TYPE: "zwave_js/check_for_config_updates", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + # 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: 2, + ID: 3, TYPE: "zwave_js/check_for_config_updates", ENTRY_ID: entry.entry_id, } @@ -2146,7 +2556,7 @@ async def test_check_for_config_updates(hass, client, integration, hass_ws_clien await ws_client.send_json( { - ID: 3, + ID: 4, TYPE: "zwave_js/check_for_config_updates", ENTRY_ID: "INVALID", } @@ -2175,13 +2585,31 @@ async def test_install_config_update(hass, client, integration, hass_ws_client): assert msg["result"] assert msg["success"] + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.driver.Driver.async_install_config_update", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 2, + TYPE: "zwave_js/install_config_update", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + # 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: 2, + ID: 3, TYPE: "zwave_js/install_config_update", ENTRY_ID: entry.entry_id, } @@ -2193,7 +2621,7 @@ async def test_install_config_update(hass, client, integration, hass_ws_client): await ws_client.send_json( { - ID: 3, + ID: 4, TYPE: "zwave_js/install_config_update", ENTRY_ID: "INVALID", }