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
from http import HTTPStatus
from typing import Any
from aiohttp import web
import aiohttp.web_exceptions
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.components import websocket_api
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.websocket_api.connection import ActiveConnection
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import DependencyError, Unauthorized
from homeassistant.helpers.data_entry_flow import (
FlowManagerIndexView,
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):
@ -33,6 +40,7 @@ async def async_setup(hass):
hass.http.register_view(OptionManagerFlowIndexView(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_update)
websocket_api.async_register_command(hass, config_entries_progress)
@ -50,49 +58,13 @@ class ConfigManagerEntryIndexView(HomeAssistantView):
async def get(self, request):
"""List available config entries."""
hass: HomeAssistant = request.app["hass"]
kwargs = {}
domain = None
if "domain" in request.query:
kwargs["domain"] = request.query["domain"]
entries = hass.config_entries.async_entries(**kwargs)
if "type" not in request.query:
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])
domain = request.query["domain"]
type_filter = None
if "type" in request.query:
type_filter = request.query["type"]
return self.json(await async_matching_config_entries(hass, type_filter, domain))
class ConfigManagerEntryResourceView(HomeAssistantView):
@ -415,6 +387,64 @@ async def ignore_config_flow(hass, connection, msg):
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
def entry_json(entry: config_entries.ConfigEntry) -> dict:
"""Return JSON value of a config entry."""

View file

@ -2,7 +2,7 @@
from collections import OrderedDict
from http import HTTPStatus
from unittest.mock import AsyncMock, patch
from unittest.mock import ANY, AsyncMock, patch
import pytest
import voluptuous as vol
@ -13,6 +13,7 @@ from homeassistant.config_entries import HANDLERS, ConfigFlow
from homeassistant.core import callback
from homeassistant.generated import config_flows
from homeassistant.helpers import config_entry_flow, config_validation as cv
from homeassistant.loader import IntegrationNotFound
from homeassistant.setup import async_setup_component
from tests.common import (
@ -1113,3 +1114,192 @@ async def test_ignore_flow_nonexisting(hass, hass_ws_client):
assert not response["success"]
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