mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 12:17:07 +00:00
Add integration type (#68349)
This commit is contained in:
parent
4f9df1fd0f
commit
3213091b8d
@ -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):
|
||||
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"domain": "derivative",
|
||||
"integration_type": "helper",
|
||||
"name": "Derivative",
|
||||
"documentation": "https://www.home-assistant.io/integrations/derivative",
|
||||
"codeowners": [
|
||||
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
@ -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."""
|
||||
|
@ -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))
|
||||
|
||||
|
@ -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"): [
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
||||
|
||||
############################
|
||||
|
@ -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"
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user