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:
parent
9940a85e28
commit
adf0f62963
2 changed files with 265 additions and 45 deletions
|
@ -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."""
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue