Add integration type (#68349)

This commit is contained in:
Paulus Schoutsen 2022-03-20 20:38:13 -07:00 committed by GitHub
parent 4f9df1fd0f
commit 3213091b8d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 608 additions and 499 deletions

View File

@ -1,13 +1,14 @@
"""Http views to control the config manager."""
from __future__ import annotations
import asyncio
from http import HTTPStatus
from aiohttp import web
import aiohttp.web_exceptions
import voluptuous as vol
from homeassistant import config_entries, data_entry_flow
from homeassistant import config_entries, data_entry_flow, loader
from homeassistant.auth.permissions.const import CAT_CONFIG_ENTRIES, POLICY_EDIT
from homeassistant.components import websocket_api
from homeassistant.components.http import HomeAssistantView
@ -48,11 +49,36 @@ class ConfigManagerEntryIndexView(HomeAssistantView):
async def get(self, request):
"""List available config entries."""
hass = request.app["hass"]
hass: HomeAssistant = request.app["hass"]
return self.json(
[entry_json(entry) for entry in hass.config_entries.async_entries()]
kwargs = {}
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"]
# Fetch all the integrations so we can check their type
for integration in await asyncio.gather(
*(
loader.async_get_integration(hass, domain)
for domain in {entry.domain for entry in entries}
)
):
integrations[integration.domain] = integration
entries = [
entry
for entry in entries
if integrations[entry.domain].integration_type == type_filter
]
return self.json([entry_json(entry) for entry in entries])
class ConfigManagerEntryResourceView(HomeAssistantView):
@ -179,7 +205,10 @@ class ConfigManagerAvailableFlowView(HomeAssistantView):
async def get(self, request):
"""List available flow handlers."""
hass = request.app["hass"]
return self.json(await async_get_config_flows(hass))
kwargs = {}
if "type" in request.query:
kwargs["type_filter"] = request.query["type"]
return self.json(await async_get_config_flows(hass, **kwargs))
class OptionManagerFlowIndexView(FlowManagerIndexView):

View File

@ -1,5 +1,6 @@
{
"domain": "derivative",
"integration_type": "helper",
"name": "Derivative",
"documentation": "https://www.home-assistant.io/integrations/derivative",
"codeowners": [

View File

@ -5,7 +5,8 @@ To update, run python3 -m script.hassfest
# fmt: off
FLOWS = [
FLOWS = {
"integration": [
"abode",
"accuweather",
"acmeda",
@ -66,7 +67,6 @@ FLOWS = [
"daikin",
"deconz",
"denonavr",
"derivative",
"devolo_home_control",
"devolo_home_network",
"dexcom",
@ -395,4 +395,8 @@ FLOWS = [
"zha",
"zwave_js",
"zwave_me"
]
],
"helper": [
"derivative"
]
}

View File

@ -16,7 +16,7 @@ import logging
import pathlib
import sys
from types import ModuleType
from typing import TYPE_CHECKING, Any, TypedDict, TypeVar, cast
from typing import TYPE_CHECKING, Any, Literal, TypedDict, TypeVar, cast
from awesomeversion import (
AwesomeVersion,
@ -87,6 +87,7 @@ class Manifest(TypedDict, total=False):
name: str
disabled: str
domain: str
integration_type: Literal["integration", "helper"]
dependencies: list[str]
after_dependencies: list[str]
requirements: list[str]
@ -180,20 +181,29 @@ async def async_get_custom_components(
return cast(dict[str, "Integration"], reg_or_evt)
async def async_get_config_flows(hass: HomeAssistant) -> set[str]:
async def async_get_config_flows(
hass: HomeAssistant,
type_filter: Literal["helper", "integration"] | None = None,
) -> set[str]:
"""Return cached list of config flows."""
# pylint: disable=import-outside-toplevel
from .generated.config_flows import FLOWS
flows: set[str] = set()
flows.update(FLOWS)
integrations = await async_get_custom_components(hass)
flows: set[str] = set()
if type_filter is not None:
flows.update(FLOWS[type_filter])
else:
for type_flows in FLOWS.values():
flows.update(type_flows)
flows.update(
[
integration.domain
for integration in integrations.values()
if integration.config_flow
and (type_filter is None or integration.integration_type == type_filter)
]
)
@ -474,6 +484,11 @@ class Integration:
"""Return the integration IoT Class."""
return self.manifest.get("iot_class")
@property
def integration_type(self) -> Literal["integration", "helper"]:
"""Return the integration type."""
return self.manifest.get("integration_type", "integration")
@property
def mqtt(self) -> list[str] | None:
"""Return Integration MQTT entries."""

View File

@ -69,7 +69,10 @@ def validate_integration(config: Config, integration: Integration):
def generate_and_validate(integrations: dict[str, Integration], config: Config):
"""Validate and generate config flow data."""
domains = []
domains = {
"integration": [],
"helper": [],
}
for domain in sorted(integrations):
integration = integrations[domain]
@ -79,7 +82,7 @@ def generate_and_validate(integrations: dict[str, Integration], config: Config):
validate_integration(config, integration)
domains.append(domain)
domains[integration.integration_type].append(domain)
return BASE.format(json.dumps(domains, indent=4))

View File

@ -152,6 +152,7 @@ MANIFEST_SCHEMA = vol.Schema(
{
vol.Required("domain"): str,
vol.Required("name"): str,
vol.Optional("integration_type"): "helper",
vol.Optional("config_flow"): bool,
vol.Optional("mqtt"): [str],
vol.Optional("zeroconf"): [

View File

@ -112,6 +112,11 @@ class Integration:
"""List of dependencies."""
return self.manifest.get("dependencies", [])
@property
def integration_type(self) -> str:
"""Get integration_type."""
return self.manifest.get("integration_type", "integration")
def add_error(self, *args: Any, **kwargs: Any) -> None:
"""Add an error."""
self.errors.append(Error(*args, **kwargs))

View File

@ -23,6 +23,13 @@ from tests.common import (
)
@pytest.fixture
def clear_handlers():
"""Clear config entry handlers."""
with patch.dict(HANDLERS, clear=True):
yield
@pytest.fixture(autouse=True)
def mock_test_component(hass):
"""Ensure a component called 'test' exists."""
@ -30,16 +37,20 @@ def mock_test_component(hass):
@pytest.fixture
def client(hass, hass_client):
async def client(hass, hass_client):
"""Fixture that can interact with the config manager API."""
hass.loop.run_until_complete(async_setup_component(hass, "http", {}))
hass.loop.run_until_complete(config_entries.async_setup(hass))
yield hass.loop.run_until_complete(hass_client())
await async_setup_component(hass, "http", {})
await config_entries.async_setup(hass)
return await hass_client()
async def test_get_entries(hass, client):
async def test_get_entries(hass, client, clear_handlers):
"""Test get entries."""
with patch.dict(HANDLERS, clear=True):
mock_integration(hass, MockModule("comp1"))
mock_integration(
hass, MockModule("comp2", partial_manifest={"integration_type": "helper"})
)
mock_integration(hass, MockModule("comp3"))
@HANDLERS.register("comp1")
class Comp1ConfigFlow:
@ -129,6 +140,31 @@ async def test_get_entries(hass, client):
},
]
resp = await client.get("/api/config/config_entries/entry?domain=comp3")
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert len(data) == 1
assert data[0]["domain"] == "comp3"
resp = await client.get("/api/config/config_entries/entry?domain=comp3&type=helper")
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert len(data) == 0
resp = await client.get(
"/api/config/config_entries/entry?domain=comp3&type=integration"
)
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert len(data) == 1
resp = await client.get("/api/config/config_entries/entry?type=integration")
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert len(data) == 2
assert data[0]["domain"] == "comp1"
assert data[1]["domain"] == "comp3"
async def test_remove_entry(hass, client):
"""Test removing an entry via the API."""
@ -224,13 +260,28 @@ async def test_reload_entry_in_setup_retry(hass, client, hass_admin_user):
assert len(hass.config_entries.async_entries()) == 1
async def test_available_flows(hass, client):
@pytest.mark.parametrize(
"type_filter,result",
(
(None, {"hello", "another", "world"}),
("integration", {"hello", "another"}),
("helper", {"world"}),
),
)
async def test_available_flows(hass, client, type_filter, result):
"""Test querying the available flows."""
with patch.object(config_flows, "FLOWS", ["hello", "world"]):
resp = await client.get("/api/config/config_entries/flow_handlers")
with patch.object(
config_flows,
"FLOWS",
{"integration": ["hello", "another"], "helper": ["world"]},
):
resp = await client.get(
"/api/config/config_entries/flow_handlers",
params={"type": type_filter} if type_filter else {},
)
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert set(data) == {"hello", "world"}
assert set(data) == result
############################

View File

@ -15,7 +15,7 @@ from homeassistant.setup import async_setup_component
@pytest.fixture
def mock_config_flows():
"""Mock the config flows."""
flows = []
flows = {"integration": [], "helper": {}}
with patch.object(config_flows, "FLOWS", flows):
yield flows
@ -124,7 +124,7 @@ async def test_get_translations(hass, mock_config_flows, enable_custom_integrati
async def test_get_translations_loads_config_flows(hass, mock_config_flows):
"""Test the get translations helper loads config flow translations."""
mock_config_flows.append("component1")
mock_config_flows["integration"].append("component1")
integration = Mock(file_path=pathlib.Path(__file__))
integration.name = "Component 1"
@ -153,7 +153,7 @@ async def test_get_translations_loads_config_flows(hass, mock_config_flows):
assert "component1" not in hass.config.components
mock_config_flows.append("component2")
mock_config_flows["integration"].append("component2")
integration = Mock(file_path=pathlib.Path(__file__))
integration.name = "Component 2"