diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index a6cd8c50a76..71994f8b00b 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -7,11 +7,18 @@ from aiohttp import hdrs, web, web_exceptions import voluptuous as vol from zwave_js_server import dump from zwave_js_server.const import LogLevel +from zwave_js_server.exceptions import InvalidNewValue, NotFoundError, SetValueFailed from zwave_js_server.model.log_config import LogConfig +from zwave_js_server.util.node import async_set_config_parameter from homeassistant.components import websocket_api from homeassistant.components.http.view import HomeAssistantView from homeassistant.components.websocket_api.connection import ActiveConnection +from homeassistant.components.websocket_api.const import ( + ERR_NOT_FOUND, + ERR_NOT_SUPPORTED, + ERR_UNKNOWN_ERROR, +) from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv @@ -26,6 +33,9 @@ ID = "id" ENTRY_ID = "entry_id" NODE_ID = "node_id" TYPE = "type" +PROPERTY = "property" +PROPERTY_KEY = "property_key" +VALUE = "value" # constants for log config commands CONFIG = "config" @@ -45,9 +55,10 @@ def async_register_api(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, websocket_stop_inclusion) websocket_api.async_register_command(hass, websocket_remove_node) websocket_api.async_register_command(hass, websocket_stop_exclusion) - websocket_api.async_register_command(hass, websocket_get_config_parameters) 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) hass.http.register_view(DumpView) # type: ignore @@ -280,6 +291,53 @@ async def websocket_remove_node( ) +@websocket_api.require_admin # type:ignore +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/set_config_parameter", + vol.Required(ENTRY_ID): str, + vol.Required(NODE_ID): int, + vol.Required(PROPERTY): int, + vol.Optional(PROPERTY_KEY): int, + vol.Required(VALUE): int, + } +) +async def websocket_set_config_parameter( + hass: HomeAssistant, connection: ActiveConnection, msg: dict +) -> None: + """Set a config parameter value for a Z-Wave node.""" + entry_id = msg[ENTRY_ID] + node_id = msg[NODE_ID] + property_ = msg[PROPERTY] + property_key = msg.get(PROPERTY_KEY) + value = msg[VALUE] + client = hass.data[DOMAIN][entry_id][DATA_CLIENT] + node = client.driver.controller.nodes[node_id] + try: + result = await async_set_config_parameter( + node, value, property_, property_key=property_key + ) + except (InvalidNewValue, NotFoundError, NotImplementedError, SetValueFailed) as err: + code = ERR_UNKNOWN_ERROR + if isinstance(err, NotFoundError): + code = ERR_NOT_FOUND + elif isinstance(err, (InvalidNewValue, NotImplementedError)): + code = ERR_NOT_SUPPORTED + + connection.send_error( + msg[ID], + code, + str(err), + ) + return + + connection.send_result( + msg[ID], + str(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 29a86d63c83..403a73a6767 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -4,6 +4,7 @@ from unittest.mock import patch from zwave_js_server.const import LogLevel from zwave_js_server.event import Event +from zwave_js_server.exceptions import InvalidNewValue, NotFoundError, SetValueFailed from homeassistant.components.zwave_js.api import ( CONFIG, @@ -15,7 +16,10 @@ from homeassistant.components.zwave_js.api import ( LEVEL, LOG_TO_FILE, NODE_ID, + PROPERTY, + PROPERTY_KEY, TYPE, + VALUE, ) from homeassistant.components.zwave_js.const import DOMAIN from homeassistant.helpers.device_registry import async_get_registry @@ -186,6 +190,125 @@ async def test_remove_node( assert device is None +async def test_set_config_parameter( + hass, client, hass_ws_client, multisensor_6, integration +): + """Test the set_config_parameter service.""" + entry = integration + ws_client = await hass_ws_client(hass) + + client.async_send_command.return_value = {"success": True} + + await ws_client.send_json( + { + ID: 1, + 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 msg["result"] + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 52 + assert args["valueId"] == { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 102, + "propertyName": "Group 2: Send battery reports", + "propertyKey": 1, + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": True, + "label": "Group 2: Send battery reports", + "description": "Include battery information in periodic reports to Group 2", + "isFromConfig": True, + }, + "value": 0, + } + assert args["value"] == 1 + + client.async_send_command.reset_mock() + + with patch( + "homeassistant.components.zwave_js.api.async_set_config_parameter", + ) as set_param_mock: + set_param_mock.side_effect = InvalidNewValue("test") + await ws_client.send_json( + { + ID: 2, + 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 len(client.async_send_command.call_args_list) == 0 + assert not msg["success"] + assert msg["error"]["code"] == "not_supported" + assert msg["error"]["message"] == "test" + + set_param_mock.side_effect = NotFoundError("test") + await ws_client.send_json( + { + ID: 3, + 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 len(client.async_send_command.call_args_list) == 0 + assert not msg["success"] + assert msg["error"]["code"] == "not_found" + assert msg["error"]["message"] == "test" + + set_param_mock.side_effect = SetValueFailed("test") + await ws_client.send_json( + { + ID: 4, + 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 len(client.async_send_command.call_args_list) == 0 + assert not msg["success"] + assert msg["error"]["code"] == "unknown_error" + assert msg["error"]["message"] == "test" + + async def test_dump_view(integration, hass_client): """Test the HTTP dump view.""" client = await hass_client()