Add websocket api to fetch config entries (#73570)

* Add websocket api to fetch config entries

* add coverage for failure case
This commit is contained in:
J. Nick Koston 2022-06-21 13:09:22 -05:00 committed by GitHub
parent 9940a85e28
commit adf0f62963
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 265 additions and 45 deletions

View file

@ -3,22 +3,29 @@ from __future__ import annotations
import asyncio import asyncio
from http import HTTPStatus from http import HTTPStatus
from typing import Any
from aiohttp import web from aiohttp import web
import aiohttp.web_exceptions import aiohttp.web_exceptions
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries, data_entry_flow, loader from homeassistant import config_entries, data_entry_flow
from homeassistant.auth.permissions.const import CAT_CONFIG_ENTRIES, POLICY_EDIT from homeassistant.auth.permissions.const import CAT_CONFIG_ENTRIES, POLICY_EDIT
from homeassistant.components import websocket_api from homeassistant.components import websocket_api
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
from homeassistant.components.websocket_api.connection import ActiveConnection
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import DependencyError, Unauthorized from homeassistant.exceptions import DependencyError, Unauthorized
from homeassistant.helpers.data_entry_flow import ( from homeassistant.helpers.data_entry_flow import (
FlowManagerIndexView, FlowManagerIndexView,
FlowManagerResourceView, FlowManagerResourceView,
) )
from homeassistant.loader import Integration, async_get_config_flows from homeassistant.loader import (
Integration,
IntegrationNotFound,
async_get_config_flows,
async_get_integration,
)
async def async_setup(hass): async def async_setup(hass):
@ -33,6 +40,7 @@ async def async_setup(hass):
hass.http.register_view(OptionManagerFlowIndexView(hass.config_entries.options)) hass.http.register_view(OptionManagerFlowIndexView(hass.config_entries.options))
hass.http.register_view(OptionManagerFlowResourceView(hass.config_entries.options)) hass.http.register_view(OptionManagerFlowResourceView(hass.config_entries.options))
websocket_api.async_register_command(hass, config_entries_get)
websocket_api.async_register_command(hass, config_entry_disable) websocket_api.async_register_command(hass, config_entry_disable)
websocket_api.async_register_command(hass, config_entry_update) websocket_api.async_register_command(hass, config_entry_update)
websocket_api.async_register_command(hass, config_entries_progress) websocket_api.async_register_command(hass, config_entries_progress)
@ -50,49 +58,13 @@ class ConfigManagerEntryIndexView(HomeAssistantView):
async def get(self, request): async def get(self, request):
"""List available config entries.""" """List available config entries."""
hass: HomeAssistant = request.app["hass"] hass: HomeAssistant = request.app["hass"]
domain = None
kwargs = {}
if "domain" in request.query: if "domain" in request.query:
kwargs["domain"] = request.query["domain"] domain = request.query["domain"]
type_filter = None
entries = hass.config_entries.async_entries(**kwargs) if "type" in request.query:
type_filter = request.query["type"]
if "type" not in request.query: return self.json(await async_matching_config_entries(hass, type_filter, domain))
return self.json([entry_json(entry) for entry in entries])
integrations = {}
type_filter = request.query["type"]
async def load_integration(
hass: HomeAssistant, domain: str
) -> Integration | None:
"""Load integration."""
try:
return await loader.async_get_integration(hass, domain)
except loader.IntegrationNotFound:
return None
# Fetch all the integrations so we can check their type
for integration in await asyncio.gather(
*(
load_integration(hass, domain)
for domain in {entry.domain for entry in entries}
)
):
if integration:
integrations[integration.domain] = integration
entries = [
entry
for entry in entries
if (type_filter != "helper" and entry.domain not in integrations)
or (
entry.domain in integrations
and integrations[entry.domain].integration_type == type_filter
)
]
return self.json([entry_json(entry) for entry in entries])
class ConfigManagerEntryResourceView(HomeAssistantView): class ConfigManagerEntryResourceView(HomeAssistantView):
@ -415,6 +387,64 @@ async def ignore_config_flow(hass, connection, msg):
connection.send_result(msg["id"]) connection.send_result(msg["id"])
@websocket_api.websocket_command(
{
vol.Required("type"): "config_entries/get",
vol.Optional("type_filter"): str,
vol.Optional("domain"): str,
}
)
@websocket_api.async_response
async def config_entries_get(
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
) -> None:
"""Return matching config entries by type and/or domain."""
connection.send_result(
msg["id"],
await async_matching_config_entries(
hass, msg.get("type_filter"), msg.get("domain")
),
)
async def async_matching_config_entries(
hass: HomeAssistant, type_filter: str | None, domain: str | None
) -> list[dict[str, Any]]:
"""Return matching config entries by type and/or domain."""
kwargs = {}
if domain:
kwargs["domain"] = domain
entries = hass.config_entries.async_entries(**kwargs)
if type_filter is None:
return [entry_json(entry) for entry in entries]
integrations = {}
# Fetch all the integrations so we can check their type
tasks = (
async_get_integration(hass, domain)
for domain in {entry.domain for entry in entries}
)
results = await asyncio.gather(*tasks, return_exceptions=True)
for integration_or_exc in results:
if isinstance(integration_or_exc, Integration):
integrations[integration_or_exc.domain] = integration_or_exc
elif not isinstance(integration_or_exc, IntegrationNotFound):
raise integration_or_exc
entries = [
entry
for entry in entries
if (type_filter != "helper" and entry.domain not in integrations)
or (
entry.domain in integrations
and integrations[entry.domain].integration_type == type_filter
)
]
return [entry_json(entry) for entry in entries]
@callback @callback
def entry_json(entry: config_entries.ConfigEntry) -> dict: def entry_json(entry: config_entries.ConfigEntry) -> dict:
"""Return JSON value of a config entry.""" """Return JSON value of a config entry."""

View file

@ -2,7 +2,7 @@
from collections import OrderedDict from collections import OrderedDict
from http import HTTPStatus from http import HTTPStatus
from unittest.mock import AsyncMock, patch from unittest.mock import ANY, AsyncMock, patch
import pytest import pytest
import voluptuous as vol import voluptuous as vol
@ -13,6 +13,7 @@ from homeassistant.config_entries import HANDLERS, ConfigFlow
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.generated import config_flows from homeassistant.generated import config_flows
from homeassistant.helpers import config_entry_flow, config_validation as cv from homeassistant.helpers import config_entry_flow, config_validation as cv
from homeassistant.loader import IntegrationNotFound
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from tests.common import ( from tests.common import (
@ -1113,3 +1114,192 @@ async def test_ignore_flow_nonexisting(hass, hass_ws_client):
assert not response["success"] assert not response["success"]
assert response["error"]["code"] == "not_found" assert response["error"]["code"] == "not_found"
async def test_get_entries_ws(hass, hass_ws_client, clear_handlers):
"""Test get entries with the websocket api."""
assert await async_setup_component(hass, "config", {})
mock_integration(hass, MockModule("comp1"))
mock_integration(
hass, MockModule("comp2", partial_manifest={"integration_type": "helper"})
)
mock_integration(hass, MockModule("comp3"))
entry = MockConfigEntry(
domain="comp1",
title="Test 1",
source="bla",
)
entry.add_to_hass(hass)
MockConfigEntry(
domain="comp2",
title="Test 2",
source="bla2",
state=core_ce.ConfigEntryState.SETUP_ERROR,
reason="Unsupported API",
).add_to_hass(hass)
MockConfigEntry(
domain="comp3",
title="Test 3",
source="bla3",
disabled_by=core_ce.ConfigEntryDisabler.USER,
).add_to_hass(hass)
ws_client = await hass_ws_client(hass)
await ws_client.send_json(
{
"id": 5,
"type": "config_entries/get",
}
)
response = await ws_client.receive_json()
assert response["id"] == 5
assert response["result"] == [
{
"disabled_by": None,
"domain": "comp1",
"entry_id": ANY,
"pref_disable_new_entities": False,
"pref_disable_polling": False,
"reason": None,
"source": "bla",
"state": "not_loaded",
"supports_options": False,
"supports_remove_device": False,
"supports_unload": False,
"title": "Test 1",
},
{
"disabled_by": None,
"domain": "comp2",
"entry_id": ANY,
"pref_disable_new_entities": False,
"pref_disable_polling": False,
"reason": "Unsupported API",
"source": "bla2",
"state": "setup_error",
"supports_options": False,
"supports_remove_device": False,
"supports_unload": False,
"title": "Test 2",
},
{
"disabled_by": "user",
"domain": "comp3",
"entry_id": ANY,
"pref_disable_new_entities": False,
"pref_disable_polling": False,
"reason": None,
"source": "bla3",
"state": "not_loaded",
"supports_options": False,
"supports_remove_device": False,
"supports_unload": False,
"title": "Test 3",
},
]
await ws_client.send_json(
{
"id": 6,
"type": "config_entries/get",
"domain": "comp1",
"type_filter": "integration",
}
)
response = await ws_client.receive_json()
assert response["id"] == 6
assert response["result"] == [
{
"disabled_by": None,
"domain": "comp1",
"entry_id": ANY,
"pref_disable_new_entities": False,
"pref_disable_polling": False,
"reason": None,
"source": "bla",
"state": "not_loaded",
"supports_options": False,
"supports_remove_device": False,
"supports_unload": False,
"title": "Test 1",
}
]
# Verify we skip broken integrations
with patch(
"homeassistant.components.config.config_entries.async_get_integration",
side_effect=IntegrationNotFound("any"),
):
await ws_client.send_json(
{
"id": 7,
"type": "config_entries/get",
"type_filter": "integration",
}
)
response = await ws_client.receive_json()
assert response["id"] == 7
assert response["result"] == [
{
"disabled_by": None,
"domain": "comp1",
"entry_id": ANY,
"pref_disable_new_entities": False,
"pref_disable_polling": False,
"reason": None,
"source": "bla",
"state": "not_loaded",
"supports_options": False,
"supports_remove_device": False,
"supports_unload": False,
"title": "Test 1",
},
{
"disabled_by": None,
"domain": "comp2",
"entry_id": ANY,
"pref_disable_new_entities": False,
"pref_disable_polling": False,
"reason": "Unsupported API",
"source": "bla2",
"state": "setup_error",
"supports_options": False,
"supports_remove_device": False,
"supports_unload": False,
"title": "Test 2",
},
{
"disabled_by": "user",
"domain": "comp3",
"entry_id": ANY,
"pref_disable_new_entities": False,
"pref_disable_polling": False,
"reason": None,
"source": "bla3",
"state": "not_loaded",
"supports_options": False,
"supports_remove_device": False,
"supports_unload": False,
"title": "Test 3",
},
]
# Verify we raise if something really goes wrong
with patch(
"homeassistant.components.config.config_entries.async_get_integration",
side_effect=Exception,
):
await ws_client.send_json(
{
"id": 8,
"type": "config_entries/get",
"type_filter": "integration",
}
)
response = await ws_client.receive_json()
assert response["id"] == 8
assert response["success"] is False