From 82eb39bb3fd5e5c00ef4a6ce83026fb6f06cfe1f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 23 Jan 2021 07:39:32 +0100 Subject: [PATCH 1/9] Add an HTTP view to dump the Z-Wave JS state --- .../components/http/data_validator.py | 2 +- homeassistant/components/zwave_js/__init__.py | 2 +- .../zwave_js/{websocket_api.py => api.py} | 36 ++++++++++++++++++- .../components/zwave_js/manifest.json | 3 +- .../{test_websocket_api.py => test_api.py} | 20 +++++++++-- 5 files changed, 57 insertions(+), 6 deletions(-) rename homeassistant/components/zwave_js/{websocket_api.py => api.py} (86%) rename tests/components/zwave_js/{test_websocket_api.py => test_api.py} (86%) diff --git a/homeassistant/components/http/data_validator.py b/homeassistant/components/http/data_validator.py index 8f3e9a3e1e2f6c..d63912360a2a67 100644 --- a/homeassistant/components/http/data_validator.py +++ b/homeassistant/components/http/data_validator.py @@ -16,7 +16,7 @@ class RequestDataValidator: """Decorator that will validate the incoming data. - Takes in a voluptuous schema and adds 'post_data' as + Takes in a voluptuous schema and adds 'data' as keyword argument to the function call. Will return a 400 if no JSON provided or doesn't match schema. diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 4c05b7a4fab6dd..1b920b936da120 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -14,6 +14,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import async_dispatcher_send +from .api import async_register_api from .const import ( DATA_CLIENT, DATA_UNSUBSCRIBE, @@ -22,7 +23,6 @@ PLATFORMS, ) from .discovery import async_discover_values -from .websocket_api import async_register_api LOGGER = logging.getLogger(__name__) CONNECT_TIMEOUT = 10 diff --git a/homeassistant/components/zwave_js/websocket_api.py b/homeassistant/components/zwave_js/api.py similarity index 86% rename from homeassistant/components/zwave_js/websocket_api.py rename to homeassistant/components/zwave_js/api.py index cb412875e1ed88..165a20b712a0d6 100644 --- a/homeassistant/components/zwave_js/websocket_api.py +++ b/homeassistant/components/zwave_js/api.py @@ -1,15 +1,21 @@ """Websocket API for Z-Wave JS.""" - +import json import logging +from aiohttp import hdrs, http_exceptions, web import voluptuous as vol +from zwave_js_server import dump from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.model.node import Node as ZwaveNode from homeassistant.components import websocket_api +from homeassistant.components.http.data_validator import RequestDataValidator +from homeassistant.components.http.view import HomeAssistantView from homeassistant.components.websocket_api.connection import ActiveConnection +from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -32,6 +38,7 @@ 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) + hass.http.register_view(DumpView) # type: ignore @websocket_api.require_admin @@ -278,3 +285,30 @@ async def remove_from_device_registry( return registry.async_remove_device(device.id) + + +class DumpView(HomeAssistantView): + """View to dump the state of the Z-Wave JS server.""" + + url = "/api/zwave_js/dump" + name = "api:zwave_js:dump" + + @RequestDataValidator({"config_entry_id": str}) + async def post(self, request: web.Request, data: dict) -> web.Response: + """Dump the state of Z-Wave.""" + hass = request.app["hass"] + + if data["config_entry_id"] not in hass.data[DOMAIN]: + raise http_exceptions.HttpBadRequest + + entry = hass.config_entries.async_get_entry(data["config_entry_id"]) + + msgs = await dump.dump_msgs(entry.data[CONF_URL], async_get_clientsession(hass)) + + return web.Response( + body="\n".join(json.dumps(msg) for msg in msgs) + "\n", + headers={ + hdrs.CONTENT_TYPE: "application/jsonl", + hdrs.CONTENT_DISPOSITION: 'attachment; filename="zwavejs_dump.jsonl"', + }, + ) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index fc98e8f3a6e6b8..40224e55119c71 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", "requirements": ["zwave-js-server-python==0.13.0"], - "codeowners": ["@home-assistant/z-wave"] + "codeowners": ["@home-assistant/z-wave"], + "dependencies": ["http", "websocket_api"] } diff --git a/tests/components/zwave_js/test_websocket_api.py b/tests/components/zwave_js/test_api.py similarity index 86% rename from tests/components/zwave_js/test_websocket_api.py rename to tests/components/zwave_js/test_api.py index e3167ae4ad9a2d..0b8304e4afd1db 100644 --- a/tests/components/zwave_js/test_websocket_api.py +++ b/tests/components/zwave_js/test_api.py @@ -1,12 +1,14 @@ """Test the Z-Wave JS Websocket API.""" +from unittest.mock import patch + from zwave_js_server.event import Event +from homeassistant.components.zwave_js.api import ENTRY_ID, ID, NODE_ID, TYPE from homeassistant.components.zwave_js.const import DOMAIN -from homeassistant.components.zwave_js.websocket_api import ENTRY_ID, ID, NODE_ID, TYPE from homeassistant.helpers.device_registry import async_get_registry -async def test_websocket_api(hass, integration, multisensor_6, hass_ws_client): +async def test_api(hass, integration, multisensor_6, hass_ws_client): """Test the network and node status websocket commands.""" entry = integration ws_client = await hass_ws_client(hass) @@ -151,3 +153,17 @@ async def test_remove_node( identifiers={(DOMAIN, "3245146787-67")}, ) assert device is None + + +async def test_dump_view(integration, hass_client): + """Test the HTTP dump view.""" + client = await hass_client() + with patch( + "zwave_js_server.dump.dump_msgs", + return_value=[{"hello": "world"}, {"second": "msg"}], + ): + resp = await client.post( + "/api/zwave_js/dump", json={"config_entry_id": integration.entry_id} + ) + assert resp.status == 200 + assert await resp.text() == '{"hello": "world"}\n{"second": "msg"}\n' From 678033abf81a4de6577bbf671ad1caeede3b51c2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 23 Jan 2021 14:46:53 +0100 Subject: [PATCH 2/9] Update homeassistant/components/zwave_js/api.py Co-authored-by: Martin Hjelmare --- homeassistant/components/zwave_js/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 165a20b712a0d6..d4765fbe57407a 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -309,6 +309,6 @@ async def post(self, request: web.Request, data: dict) -> web.Response: body="\n".join(json.dumps(msg) for msg in msgs) + "\n", headers={ hdrs.CONTENT_TYPE: "application/jsonl", - hdrs.CONTENT_DISPOSITION: 'attachment; filename="zwavejs_dump.jsonl"', + hdrs.CONTENT_DISPOSITION: 'attachment; filename="zwave_js_dump.jsonl"', }, ) From 24da7c2a1e26751f220a8e692600067bdacc76ad Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 25 Jan 2021 11:53:28 +0100 Subject: [PATCH 3/9] Keep test name --- tests/components/zwave_js/test_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 0b8304e4afd1db..e31e67e02c9280 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -8,7 +8,7 @@ from homeassistant.helpers.device_registry import async_get_registry -async def test_api(hass, integration, multisensor_6, hass_ws_client): +async def test_websocket_api(hass, integration, multisensor_6, hass_ws_client): """Test the network and node status websocket commands.""" entry = integration ws_client = await hass_ws_client(hass) From 09dc589ec31e29657ae6f972bf10d730c70be893 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 25 Jan 2021 12:54:24 +0100 Subject: [PATCH 4/9] Add test for invalid config entry id --- homeassistant/components/zwave_js/api.py | 4 +++- tests/components/zwave_js/test_api.py | 13 +++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index d4765fbe57407a..0d2fc43a14352f 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -299,7 +299,9 @@ async def post(self, request: web.Request, data: dict) -> web.Response: hass = request.app["hass"] if data["config_entry_id"] not in hass.data[DOMAIN]: - raise http_exceptions.HttpBadRequest + raise http_exceptions.HttpBadRequest( + f"Invalid config entry ID: {data['config_entry_id']}." + ) entry = hass.config_entries.async_get_entry(data["config_entry_id"]) diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index e31e67e02c9280..5a4f755c4e945a 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -167,3 +167,16 @@ async def test_dump_view(integration, hass_client): ) assert resp.status == 200 assert await resp.text() == '{"hello": "world"}\n{"second": "msg"}\n' + + +async def test_dump_view_invalid_entry_id(integration, hass_client): + """Test an invalid config entry id parameter.""" + client = await hass_client() + with patch( + "zwave_js_server.dump.dump_msgs", + return_value=[{"hello": "world"}, {"second": "msg"}], + ): + resp = await client.post( + "/api/zwave_js/dump", json={"config_entry_id": "INVALID"} + ) + assert resp.status == 400 From f40be7120e8675e64003aed23871905f42ab739e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 25 Jan 2021 12:58:47 +0100 Subject: [PATCH 5/9] Fix test --- homeassistant/components/zwave_js/api.py | 6 ++---- tests/components/zwave_js/test_api.py | 8 +------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 0d2fc43a14352f..93cd585f4e9802 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -2,7 +2,7 @@ import json import logging -from aiohttp import hdrs, http_exceptions, web +from aiohttp import hdrs, web, web_exceptions import voluptuous as vol from zwave_js_server import dump from zwave_js_server.client import Client as ZwaveClient @@ -299,9 +299,7 @@ async def post(self, request: web.Request, data: dict) -> web.Response: hass = request.app["hass"] if data["config_entry_id"] not in hass.data[DOMAIN]: - raise http_exceptions.HttpBadRequest( - f"Invalid config entry ID: {data['config_entry_id']}." - ) + raise web_exceptions.HTTPBadRequest entry = hass.config_entries.async_get_entry(data["config_entry_id"]) diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 5a4f755c4e945a..efcc8c5b3b8582 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -172,11 +172,5 @@ async def test_dump_view(integration, hass_client): async def test_dump_view_invalid_entry_id(integration, hass_client): """Test an invalid config entry id parameter.""" client = await hass_client() - with patch( - "zwave_js_server.dump.dump_msgs", - return_value=[{"hello": "world"}, {"second": "msg"}], - ): - resp = await client.post( - "/api/zwave_js/dump", json={"config_entry_id": "INVALID"} - ) + resp = await client.post("/api/zwave_js/dump", json={"config_entry_id": "INVALID"}) assert resp.status == 400 From 0cad1a514d5215c6056e600abf8162b246a76f6e Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 25 Jan 2021 14:40:10 +0100 Subject: [PATCH 6/9] Change to get --- homeassistant/components/zwave_js/api.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 93cd585f4e9802..45236f023b254f 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -9,7 +9,6 @@ from zwave_js_server.model.node import Node as ZwaveNode from homeassistant.components import websocket_api -from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components.http.view import HomeAssistantView from homeassistant.components.websocket_api.connection import ActiveConnection from homeassistant.const import CONF_URL @@ -290,20 +289,21 @@ async def remove_from_device_registry( class DumpView(HomeAssistantView): """View to dump the state of the Z-Wave JS server.""" - url = "/api/zwave_js/dump" + url = "/api/zwave_js/dump/{config_entry_id}" name = "api:zwave_js:dump" - @RequestDataValidator({"config_entry_id": str}) - async def post(self, request: web.Request, data: dict) -> web.Response: + async def get(self, request: web.Request, config_entry_id: str) -> web.Response: """Dump the state of Z-Wave.""" hass = request.app["hass"] - if data["config_entry_id"] not in hass.data[DOMAIN]: + if config_entry_id not in hass.data[DOMAIN]: raise web_exceptions.HTTPBadRequest - entry = hass.config_entries.async_get_entry(data["config_entry_id"]) + entry = hass.config_entries.async_get_entry(config_entry_id) - msgs = await dump.dump_msgs(entry.data[CONF_URL], async_get_clientsession(hass)) + msgs = await dump.dump_msgs( + entry.data[CONF_URL], async_get_clientsession(hass), wait_nodes_ready=False + ) return web.Response( body="\n".join(json.dumps(msg) for msg in msgs) + "\n", From e181f826f5854e07e99c344ebd384434bcd34644 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 25 Jan 2021 14:44:18 +0100 Subject: [PATCH 7/9] Update test_api.py --- tests/components/zwave_js/test_api.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index efcc8c5b3b8582..1ede274c15506f 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -162,9 +162,7 @@ async def test_dump_view(integration, hass_client): "zwave_js_server.dump.dump_msgs", return_value=[{"hello": "world"}, {"second": "msg"}], ): - resp = await client.post( - "/api/zwave_js/dump", json={"config_entry_id": integration.entry_id} - ) + resp = await client.get("/api/zwave_js/dump/integration.entry_id") assert resp.status == 200 assert await resp.text() == '{"hello": "world"}\n{"second": "msg"}\n' @@ -172,5 +170,5 @@ async def test_dump_view(integration, hass_client): async def test_dump_view_invalid_entry_id(integration, hass_client): """Test an invalid config entry id parameter.""" client = await hass_client() - resp = await client.post("/api/zwave_js/dump", json={"config_entry_id": "INVALID"}) + resp = await client.get("/api/zwave_js/dump/INVALID") assert resp.status == 400 From d6e9f7b4b010e1ccdb09e51ffc01273e29ca6140 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 25 Jan 2021 15:32:33 +0100 Subject: [PATCH 8/9] Replace node_count with array of node ids --- homeassistant/components/zwave_js/api.py | 2 +- tests/components/zwave_js/test_api.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 45236f023b254f..d91bc782e099eb 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -60,7 +60,7 @@ def websocket_network_status( }, "controller": { "home_id": client.driver.controller.data["homeId"], - "node_count": len(client.driver.controller.nodes), + "nodes": list(client.driver.controller.nodes.keys()), }, } connection.send_result( diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 1ede274c15506f..88e8acc577156f 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -162,7 +162,7 @@ async def test_dump_view(integration, hass_client): "zwave_js_server.dump.dump_msgs", return_value=[{"hello": "world"}, {"second": "msg"}], ): - resp = await client.get("/api/zwave_js/dump/integration.entry_id") + resp = await client.get(f"/api/zwave_js/dump/{integration.entry_id}") assert resp.status == 200 assert await resp.text() == '{"hello": "world"}\n{"second": "msg"}\n' From f36b4fa0fa4c2f848f4cb451777c9cd7ab3e23b3 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 25 Jan 2021 15:35:03 +0100 Subject: [PATCH 9/9] Update homeassistant/components/zwave_js/api.py Co-authored-by: Martin Hjelmare --- homeassistant/components/zwave_js/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index d91bc782e099eb..b85d63a8912f88 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -60,7 +60,7 @@ def websocket_network_status( }, "controller": { "home_id": client.driver.controller.data["homeId"], - "nodes": list(client.driver.controller.nodes.keys()), + "nodes": list(client.driver.controller.nodes), }, } connection.send_result(