From 9f1bffe3be097017afc1140f2fd66f434df858ab Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 15 Apr 2020 13:36:16 -0700 Subject: [PATCH] Add command to get integration manifests (#34262) * Add command to get integration manifests * Add is_built_in value to manifest * Update APIs --- .../components/websocket_api/commands.py | 75 +++++++++++-------- homeassistant/loader.py | 1 + .../components/websocket_api/test_commands.py | 44 +++++++++++ 3 files changed, 88 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 3e43f824e69..3de41bc8918 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -1,4 +1,6 @@ """Commands part of Websocket API.""" +import asyncio + import voluptuous as vol from homeassistant.auth.permissions.const import POLICY_READ @@ -8,6 +10,7 @@ from homeassistant.exceptions import HomeAssistantError, ServiceNotFound, Unauth from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.service import async_get_all_descriptions +from homeassistant.loader import IntegrationNotFound, async_get_integration from . import const, decorators, messages @@ -25,6 +28,8 @@ def async_register_commands(hass, async_reg): async_reg(hass, handle_get_config) async_reg(hass, handle_ping) async_reg(hass, handle_render_template) + async_reg(hass, handle_manifest_list) + async_reg(hass, handle_manifest_get) def pong_message(iden): @@ -40,10 +45,7 @@ def pong_message(iden): } ) def handle_subscribe_events(hass, connection, msg): - """Handle subscribe events command. - - Async friendly. - """ + """Handle subscribe events command.""" # Circular dep # pylint: disable=import-outside-toplevel from .permissions import SUBSCRIBE_WHITELIST @@ -90,10 +92,7 @@ def handle_subscribe_events(hass, connection, msg): } ) def handle_unsubscribe_events(hass, connection, msg): - """Handle unsubscribe events command. - - Async friendly. - """ + """Handle unsubscribe events command.""" subscription = msg["subscription"] if subscription in connection.subscriptions: @@ -117,10 +116,7 @@ def handle_unsubscribe_events(hass, connection, msg): ) @decorators.async_response async def handle_call_service(hass, connection, msg): - """Handle call service command. - - Async friendly. - """ + """Handle call service command.""" blocking = True if msg["domain"] == HASS_DOMAIN and msg["service"] in ["restart", "stop"]: blocking = False @@ -164,10 +160,7 @@ async def handle_call_service(hass, connection, msg): @callback @decorators.websocket_command({vol.Required("type"): "get_states"}) def handle_get_states(hass, connection, msg): - """Handle get states command. - - Async friendly. - """ + """Handle get states command.""" if connection.user.permissions.access_all_entities("read"): states = hass.states.async_all() else: @@ -184,10 +177,7 @@ def handle_get_states(hass, connection, msg): @decorators.websocket_command({vol.Required("type"): "get_services"}) @decorators.async_response async def handle_get_services(hass, connection, msg): - """Handle get services command. - - Async friendly. - """ + """Handle get services command.""" descriptions = await async_get_all_descriptions(hass) connection.send_message(messages.result_message(msg["id"], descriptions)) @@ -195,20 +185,44 @@ async def handle_get_services(hass, connection, msg): @callback @decorators.websocket_command({vol.Required("type"): "get_config"}) def handle_get_config(hass, connection, msg): - """Handle get config command. - - Async friendly. - """ + """Handle get config command.""" connection.send_message(messages.result_message(msg["id"], hass.config.as_dict())) +@decorators.websocket_command({vol.Required("type"): "manifest/list"}) +@decorators.async_response +async def handle_manifest_list(hass, connection, msg): + """Handle integrations command.""" + integrations = await asyncio.gather( + *[ + async_get_integration(hass, domain) + for domain in hass.config.components + # Filter out platforms. + if "." not in domain + ] + ) + connection.send_result( + msg["id"], [integration.manifest for integration in integrations] + ) + + +@decorators.websocket_command( + {vol.Required("type"): "manifest/get", vol.Required("integration"): str} +) +@decorators.async_response +async def handle_manifest_get(hass, connection, msg): + """Handle integrations command.""" + try: + integration = await async_get_integration(hass, msg["integration"]) + connection.send_result(msg["id"], integration.manifest) + except IntegrationNotFound: + connection.send_error(msg["id"], const.ERR_NOT_FOUND, "Integration not found") + + @callback @decorators.websocket_command({vol.Required("type"): "ping"}) def handle_ping(hass, connection, msg): - """Handle ping command. - - Async friendly. - """ + """Handle ping command.""" connection.send_message(pong_message(msg["id"])) @@ -222,10 +236,7 @@ def handle_ping(hass, connection, msg): } ) def handle_render_template(hass, connection, msg): - """Handle render_template command. - - Async friendly. - """ + """Handle render_template command.""" template = msg["template"] template.hass = hass diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 3e82adeb2e2..3c7e4699127 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -205,6 +205,7 @@ class Integration: self.pkg_path = pkg_path self.file_path = file_path self.manifest = manifest + manifest["is_built_in"] = self.is_built_in _LOGGER.info("Loaded %s from %s", self.domain, pkg_path) @property diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 58d904c8f4b..3754767dd9e 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -10,6 +10,7 @@ from homeassistant.components.websocket_api.auth import ( from homeassistant.components.websocket_api.const import URL from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError +from homeassistant.loader import async_get_integration from homeassistant.setup import async_setup_component from tests.common import async_mock_service @@ -467,3 +468,46 @@ async def test_render_template_returns_with_match_all( assert msg["id"] == 5 assert msg["type"] == const.TYPE_RESULT assert msg["success"] + + +async def test_manifest_list(hass, websocket_client): + """Test loading manifests.""" + http = await async_get_integration(hass, "http") + websocket_api = await async_get_integration(hass, "websocket_api") + + await websocket_client.send_json({"id": 5, "type": "manifest/list"}) + + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + assert sorted(msg["result"], key=lambda manifest: manifest["domain"]) == [ + http.manifest, + websocket_api.manifest, + ] + + +async def test_manifest_get(hass, websocket_client): + """Test getting a manifest.""" + hue = await async_get_integration(hass, "hue") + + await websocket_client.send_json( + {"id": 6, "type": "manifest/get", "integration": "hue"} + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 6 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + assert msg["result"] == hue.manifest + + # Non existing + await websocket_client.send_json( + {"id": 7, "type": "manifest/get", "integration": "non_existing"} + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == const.TYPE_RESULT + assert not msg["success"] + assert msg["error"]["code"] == "not_found"