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.""" """Http views to control the config manager."""
from __future__ import annotations from __future__ import annotations
import asyncio
from http import HTTPStatus from http import HTTPStatus
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 from homeassistant import config_entries, data_entry_flow, loader
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
@ -48,11 +49,36 @@ class ConfigManagerEntryIndexView(HomeAssistantView):
async def get(self, request): async def get(self, request):
"""List available config entries.""" """List available config entries."""
hass = request.app["hass"] hass: HomeAssistant = request.app["hass"]
return self.json( kwargs = {}
[entry_json(entry) for entry in hass.config_entries.async_entries()] 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): class ConfigManagerEntryResourceView(HomeAssistantView):
@ -179,7 +205,10 @@ class ConfigManagerAvailableFlowView(HomeAssistantView):
async def get(self, request): async def get(self, request):
"""List available flow handlers.""" """List available flow handlers."""
hass = request.app["hass"] 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): class OptionManagerFlowIndexView(FlowManagerIndexView):

View file

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

View file

@ -5,394 +5,398 @@ To update, run python3 -m script.hassfest
# fmt: off # fmt: off
FLOWS = [ FLOWS = {
"abode", "integration": [
"accuweather", "abode",
"acmeda", "accuweather",
"adax", "acmeda",
"adguard", "adax",
"advantage_air", "adguard",
"aemet", "advantage_air",
"agent_dvr", "aemet",
"airly", "agent_dvr",
"airnow", "airly",
"airthings", "airnow",
"airtouch4", "airthings",
"airvisual", "airtouch4",
"airzone", "airvisual",
"alarmdecoder", "airzone",
"almond", "alarmdecoder",
"ambee", "almond",
"amberelectric", "ambee",
"ambiclimate", "amberelectric",
"ambient_station", "ambiclimate",
"androidtv", "ambient_station",
"apple_tv", "androidtv",
"arcam_fmj", "apple_tv",
"aseko_pool_live", "arcam_fmj",
"asuswrt", "aseko_pool_live",
"atag", "asuswrt",
"august", "atag",
"aurora", "august",
"aurora_abb_powerone", "aurora",
"aussie_broadband", "aurora_abb_powerone",
"awair", "aussie_broadband",
"axis", "awair",
"azure_devops", "axis",
"azure_event_hub", "azure_devops",
"balboa", "azure_event_hub",
"blebox", "balboa",
"blink", "blebox",
"bmw_connected_drive", "blink",
"bond", "bmw_connected_drive",
"bosch_shc", "bond",
"braviatv", "bosch_shc",
"broadlink", "braviatv",
"brother", "broadlink",
"brunt", "brother",
"bsblan", "brunt",
"buienradar", "bsblan",
"canary", "buienradar",
"cast", "canary",
"cert_expiry", "cast",
"cloudflare", "cert_expiry",
"co2signal", "cloudflare",
"coinbase", "co2signal",
"control4", "coinbase",
"coolmaster", "control4",
"coronavirus", "coolmaster",
"cpuspeed", "coronavirus",
"crownstone", "cpuspeed",
"daikin", "crownstone",
"deconz", "daikin",
"denonavr", "deconz",
"derivative", "denonavr",
"devolo_home_control", "devolo_home_control",
"devolo_home_network", "devolo_home_network",
"dexcom", "dexcom",
"dialogflow", "dialogflow",
"directv", "directv",
"dlna_dmr", "dlna_dmr",
"dlna_dms", "dlna_dms",
"dnsip", "dnsip",
"doorbird", "doorbird",
"dsmr", "dsmr",
"dunehd", "dunehd",
"dynalite", "dynalite",
"eafm", "eafm",
"ecobee", "ecobee",
"econet", "econet",
"efergy", "efergy",
"elgato", "elgato",
"elkm1", "elkm1",
"elmax", "elmax",
"emonitor", "emonitor",
"emulated_roku", "emulated_roku",
"enocean", "enocean",
"enphase_envoy", "enphase_envoy",
"environment_canada", "environment_canada",
"epson", "epson",
"esphome", "esphome",
"evil_genius_labs", "evil_genius_labs",
"ezviz", "ezviz",
"faa_delays", "faa_delays",
"fireservicerota", "fireservicerota",
"fivem", "fivem",
"fjaraskupan", "fjaraskupan",
"flick_electric", "flick_electric",
"flipr", "flipr",
"flo", "flo",
"flume", "flume",
"flunearyou", "flunearyou",
"flux_led", "flux_led",
"forecast_solar", "forecast_solar",
"forked_daapd", "forked_daapd",
"foscam", "foscam",
"freebox", "freebox",
"freedompro", "freedompro",
"fritz", "fritz",
"fritzbox", "fritzbox",
"fritzbox_callmonitor", "fritzbox_callmonitor",
"fronius", "fronius",
"garages_amsterdam", "garages_amsterdam",
"gdacs", "gdacs",
"geofency", "geofency",
"geonetnz_quakes", "geonetnz_quakes",
"geonetnz_volcano", "geonetnz_volcano",
"gios", "gios",
"github", "github",
"glances", "glances",
"goalzero", "goalzero",
"gogogate2", "gogogate2",
"goodwe", "goodwe",
"google", "google",
"google_travel_time", "google_travel_time",
"gpslogger", "gpslogger",
"gree", "gree",
"group", "group",
"growatt_server", "growatt_server",
"guardian", "guardian",
"habitica", "habitica",
"hangouts", "hangouts",
"harmony", "harmony",
"heos", "heos",
"hisense_aehw4a1", "hisense_aehw4a1",
"hive", "hive",
"hlk_sw16", "hlk_sw16",
"home_connect", "home_connect",
"home_plus_control", "home_plus_control",
"homekit", "homekit",
"homekit_controller", "homekit_controller",
"homematicip_cloud", "homematicip_cloud",
"homewizard", "homewizard",
"honeywell", "honeywell",
"huawei_lte", "huawei_lte",
"hue", "hue",
"huisbaasje", "huisbaasje",
"hunterdouglas_powerview", "hunterdouglas_powerview",
"hvv_departures", "hvv_departures",
"hyperion", "hyperion",
"ialarm", "ialarm",
"iaqualink", "iaqualink",
"icloud", "icloud",
"ifttt", "ifttt",
"insteon", "insteon",
"integration", "integration",
"intellifire", "intellifire",
"ios", "ios",
"iotawatt", "iotawatt",
"ipma", "ipma",
"ipp", "ipp",
"iqvia", "iqvia",
"islamic_prayer_times", "islamic_prayer_times",
"iss", "iss",
"isy994", "isy994",
"izone", "izone",
"jellyfin", "jellyfin",
"juicenet", "juicenet",
"kaleidescape", "kaleidescape",
"keenetic_ndms2", "keenetic_ndms2",
"kmtronic", "kmtronic",
"knx", "knx",
"kodi", "kodi",
"konnected", "konnected",
"kostal_plenticore", "kostal_plenticore",
"kraken", "kraken",
"kulersky", "kulersky",
"launch_library", "launch_library",
"life360", "life360",
"lifx", "lifx",
"litejet", "litejet",
"litterrobot", "litterrobot",
"local_ip", "local_ip",
"locative", "locative",
"logi_circle", "logi_circle",
"lookin", "lookin",
"luftdaten", "luftdaten",
"lutron_caseta", "lutron_caseta",
"lyric", "lyric",
"mailgun", "mailgun",
"mazda", "mazda",
"melcloud", "melcloud",
"met", "met",
"met_eireann", "met_eireann",
"meteo_france", "meteo_france",
"meteoclimatic", "meteoclimatic",
"metoffice", "metoffice",
"mikrotik", "mikrotik",
"mill", "mill",
"minecraft_server", "minecraft_server",
"mjpeg", "mjpeg",
"mobile_app", "mobile_app",
"modem_callerid", "modem_callerid",
"modern_forms", "modern_forms",
"moehlenhoff_alpha2", "moehlenhoff_alpha2",
"monoprice", "monoprice",
"moon", "moon",
"motion_blinds", "motion_blinds",
"motioneye", "motioneye",
"mqtt", "mqtt",
"mullvad", "mullvad",
"mutesync", "mutesync",
"myq", "myq",
"mysensors", "mysensors",
"nam", "nam",
"nanoleaf", "nanoleaf",
"neato", "neato",
"nest", "nest",
"netatmo", "netatmo",
"netgear", "netgear",
"nexia", "nexia",
"nfandroidtv", "nfandroidtv",
"nightscout", "nightscout",
"nina", "nina",
"nmap_tracker", "nmap_tracker",
"notion", "notion",
"nuheat", "nuheat",
"nuki", "nuki",
"nut", "nut",
"nws", "nws",
"nzbget", "nzbget",
"octoprint", "octoprint",
"omnilogic", "omnilogic",
"oncue", "oncue",
"ondilo_ico", "ondilo_ico",
"onewire", "onewire",
"onvif", "onvif",
"open_meteo", "open_meteo",
"opengarage", "opengarage",
"opentherm_gw", "opentherm_gw",
"openuv", "openuv",
"openweathermap", "openweathermap",
"overkiz", "overkiz",
"ovo_energy", "ovo_energy",
"owntracks", "owntracks",
"p1_monitor", "p1_monitor",
"panasonic_viera", "panasonic_viera",
"philips_js", "philips_js",
"pi_hole", "pi_hole",
"picnic", "picnic",
"plaato", "plaato",
"plex", "plex",
"plugwise", "plugwise",
"plum_lightpad", "plum_lightpad",
"point", "point",
"poolsense", "poolsense",
"powerwall", "powerwall",
"profiler", "profiler",
"progettihwsw", "progettihwsw",
"prosegur", "prosegur",
"ps4", "ps4",
"pure_energie", "pure_energie",
"pvoutput", "pvoutput",
"pvpc_hourly_pricing", "pvpc_hourly_pricing",
"rachio", "rachio",
"radio_browser", "radio_browser",
"rainforest_eagle", "rainforest_eagle",
"rainmachine", "rainmachine",
"rdw", "rdw",
"recollect_waste", "recollect_waste",
"renault", "renault",
"rfxtrx", "rfxtrx",
"ridwell", "ridwell",
"ring", "ring",
"risco", "risco",
"rituals_perfume_genie", "rituals_perfume_genie",
"roku", "roku",
"roomba", "roomba",
"roon", "roon",
"rpi_power", "rpi_power",
"rtsp_to_webrtc", "rtsp_to_webrtc",
"ruckus_unleashed", "ruckus_unleashed",
"samsungtv", "samsungtv",
"screenlogic", "screenlogic",
"season", "season",
"sense", "sense",
"senseme", "senseme",
"sensibo", "sensibo",
"sentry", "sentry",
"sharkiq", "sharkiq",
"shelly", "shelly",
"shopping_list", "shopping_list",
"sia", "sia",
"simplisafe", "simplisafe",
"sleepiq", "sleepiq",
"sma", "sma",
"smappee", "smappee",
"smart_meter_texas", "smart_meter_texas",
"smartthings", "smartthings",
"smarttub", "smarttub",
"smhi", "smhi",
"sms", "sms",
"solaredge", "solaredge",
"solarlog", "solarlog",
"solax", "solax",
"soma", "soma",
"somfy", "somfy",
"somfy_mylink", "somfy_mylink",
"sonarr", "sonarr",
"songpal", "songpal",
"sonos", "sonos",
"speedtestdotnet", "speedtestdotnet",
"spider", "spider",
"spotify", "spotify",
"squeezebox", "squeezebox",
"srp_energy", "srp_energy",
"starline", "starline",
"steamist", "steamist",
"stookalert", "stookalert",
"subaru", "subaru",
"sun", "sun",
"surepetcare", "surepetcare",
"switch_as_x", "switch_as_x",
"switchbot", "switchbot",
"switcher_kis", "switcher_kis",
"syncthing", "syncthing",
"syncthru", "syncthru",
"synology_dsm", "synology_dsm",
"system_bridge", "system_bridge",
"tado", "tado",
"tailscale", "tailscale",
"tasmota", "tasmota",
"tellduslive", "tellduslive",
"tesla_wall_connector", "tesla_wall_connector",
"tibber", "tibber",
"tile", "tile",
"tolo", "tolo",
"tomorrowio", "tomorrowio",
"toon", "toon",
"totalconnect", "totalconnect",
"tplink", "tplink",
"traccar", "traccar",
"tractive", "tractive",
"tradfri", "tradfri",
"trafikverket_weatherstation", "trafikverket_weatherstation",
"transmission", "transmission",
"tuya", "tuya",
"twentemilieu", "twentemilieu",
"twilio", "twilio",
"twinkly", "twinkly",
"unifi", "unifi",
"unifiprotect", "unifiprotect",
"upb", "upb",
"upcloud", "upcloud",
"upnp", "upnp",
"uptime", "uptime",
"uptimerobot", "uptimerobot",
"vallox", "vallox",
"velbus", "velbus",
"venstar", "venstar",
"vera", "vera",
"verisure", "verisure",
"version", "version",
"vesync", "vesync",
"vicare", "vicare",
"vilfo", "vilfo",
"vizio", "vizio",
"vlc_telnet", "vlc_telnet",
"volumio", "volumio",
"wallbox", "wallbox",
"watttime", "watttime",
"waze_travel_time", "waze_travel_time",
"webostv", "webostv",
"wemo", "wemo",
"whirlpool", "whirlpool",
"whois", "whois",
"wiffi", "wiffi",
"wilight", "wilight",
"withings", "withings",
"wiz", "wiz",
"wled", "wled",
"wolflink", "wolflink",
"xbox", "xbox",
"xiaomi_aqara", "xiaomi_aqara",
"xiaomi_miio", "xiaomi_miio",
"yale_smart_alarm", "yale_smart_alarm",
"yamaha_musiccast", "yamaha_musiccast",
"yeelight", "yeelight",
"youless", "youless",
"zerproc", "zerproc",
"zha", "zha",
"zwave_js", "zwave_js",
"zwave_me" "zwave_me"
] ],
"helper": [
"derivative"
]
}

View file

@ -16,7 +16,7 @@ import logging
import pathlib import pathlib
import sys import sys
from types import ModuleType 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 ( from awesomeversion import (
AwesomeVersion, AwesomeVersion,
@ -87,6 +87,7 @@ class Manifest(TypedDict, total=False):
name: str name: str
disabled: str disabled: str
domain: str domain: str
integration_type: Literal["integration", "helper"]
dependencies: list[str] dependencies: list[str]
after_dependencies: list[str] after_dependencies: list[str]
requirements: list[str] requirements: list[str]
@ -180,20 +181,29 @@ async def async_get_custom_components(
return cast(dict[str, "Integration"], reg_or_evt) 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.""" """Return cached list of config flows."""
# pylint: disable=import-outside-toplevel # pylint: disable=import-outside-toplevel
from .generated.config_flows import FLOWS from .generated.config_flows import FLOWS
flows: set[str] = set()
flows.update(FLOWS)
integrations = await async_get_custom_components(hass) 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( flows.update(
[ [
integration.domain integration.domain
for integration in integrations.values() for integration in integrations.values()
if integration.config_flow 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 the integration IoT Class."""
return self.manifest.get("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 @property
def mqtt(self) -> list[str] | None: def mqtt(self) -> list[str] | None:
"""Return Integration MQTT entries.""" """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): def generate_and_validate(integrations: dict[str, Integration], config: Config):
"""Validate and generate config flow data.""" """Validate and generate config flow data."""
domains = [] domains = {
"integration": [],
"helper": [],
}
for domain in sorted(integrations): for domain in sorted(integrations):
integration = integrations[domain] integration = integrations[domain]
@ -79,7 +82,7 @@ def generate_and_validate(integrations: dict[str, Integration], config: Config):
validate_integration(config, integration) validate_integration(config, integration)
domains.append(domain) domains[integration.integration_type].append(domain)
return BASE.format(json.dumps(domains, indent=4)) return BASE.format(json.dumps(domains, indent=4))

View file

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

View file

@ -112,6 +112,11 @@ class Integration:
"""List of dependencies.""" """List of dependencies."""
return self.manifest.get("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: def add_error(self, *args: Any, **kwargs: Any) -> None:
"""Add an error.""" """Add an error."""
self.errors.append(Error(*args, **kwargs)) 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) @pytest.fixture(autouse=True)
def mock_test_component(hass): def mock_test_component(hass):
"""Ensure a component called 'test' exists.""" """Ensure a component called 'test' exists."""
@ -30,104 +37,133 @@ def mock_test_component(hass):
@pytest.fixture @pytest.fixture
def client(hass, hass_client): async def client(hass, hass_client):
"""Fixture that can interact with the config manager API.""" """Fixture that can interact with the config manager API."""
hass.loop.run_until_complete(async_setup_component(hass, "http", {})) await async_setup_component(hass, "http", {})
hass.loop.run_until_complete(config_entries.async_setup(hass)) await config_entries.async_setup(hass)
yield hass.loop.run_until_complete(hass_client()) return await hass_client()
async def test_get_entries(hass, client): async def test_get_entries(hass, client, clear_handlers):
"""Test get entries.""" """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") @HANDLERS.register("comp1")
class Comp1ConfigFlow: class Comp1ConfigFlow:
"""Config flow with options flow.""" """Config flow with options flow."""
@staticmethod @staticmethod
@callback @callback
def async_get_options_flow(config_entry): def async_get_options_flow(config_entry):
"""Get options flow.""" """Get options flow."""
pass pass
@classmethod @classmethod
@callback @callback
def async_supports_options_flow(cls, config_entry): def async_supports_options_flow(cls, config_entry):
"""Return options flow support for this handler.""" """Return options flow support for this handler."""
return True return True
hass.helpers.config_entry_flow.register_discovery_flow( hass.helpers.config_entry_flow.register_discovery_flow(
"comp2", "Comp 2", lambda: None "comp2", "Comp 2", lambda: None
) )
entry = MockConfigEntry( entry = MockConfigEntry(
domain="comp1", domain="comp1",
title="Test 1", title="Test 1",
source="bla", source="bla",
) )
entry.supports_unload = True entry.supports_unload = True
entry.add_to_hass(hass) entry.add_to_hass(hass)
MockConfigEntry( MockConfigEntry(
domain="comp2", domain="comp2",
title="Test 2", title="Test 2",
source="bla2", source="bla2",
state=core_ce.ConfigEntryState.SETUP_ERROR, state=core_ce.ConfigEntryState.SETUP_ERROR,
reason="Unsupported API", reason="Unsupported API",
).add_to_hass(hass) ).add_to_hass(hass)
MockConfigEntry( MockConfigEntry(
domain="comp3", domain="comp3",
title="Test 3", title="Test 3",
source="bla3", source="bla3",
disabled_by=core_ce.ConfigEntryDisabler.USER, disabled_by=core_ce.ConfigEntryDisabler.USER,
).add_to_hass(hass) ).add_to_hass(hass)
resp = await client.get("/api/config/config_entries/entry") resp = await client.get("/api/config/config_entries/entry")
assert resp.status == HTTPStatus.OK assert resp.status == HTTPStatus.OK
data = await resp.json() data = await resp.json()
for entry in data: for entry in data:
entry.pop("entry_id") entry.pop("entry_id")
assert data == [ assert data == [
{ {
"domain": "comp1", "domain": "comp1",
"title": "Test 1", "title": "Test 1",
"source": "bla", "source": "bla",
"state": core_ce.ConfigEntryState.NOT_LOADED.value, "state": core_ce.ConfigEntryState.NOT_LOADED.value,
"supports_options": True, "supports_options": True,
"supports_remove_device": False, "supports_remove_device": False,
"supports_unload": True, "supports_unload": True,
"pref_disable_new_entities": False, "pref_disable_new_entities": False,
"pref_disable_polling": False, "pref_disable_polling": False,
"disabled_by": None, "disabled_by": None,
"reason": None, "reason": None,
}, },
{ {
"domain": "comp2", "domain": "comp2",
"title": "Test 2", "title": "Test 2",
"source": "bla2", "source": "bla2",
"state": core_ce.ConfigEntryState.SETUP_ERROR.value, "state": core_ce.ConfigEntryState.SETUP_ERROR.value,
"supports_options": False, "supports_options": False,
"supports_remove_device": False, "supports_remove_device": False,
"supports_unload": False, "supports_unload": False,
"pref_disable_new_entities": False, "pref_disable_new_entities": False,
"pref_disable_polling": False, "pref_disable_polling": False,
"disabled_by": None, "disabled_by": None,
"reason": "Unsupported API", "reason": "Unsupported API",
}, },
{ {
"domain": "comp3", "domain": "comp3",
"title": "Test 3", "title": "Test 3",
"source": "bla3", "source": "bla3",
"state": core_ce.ConfigEntryState.NOT_LOADED.value, "state": core_ce.ConfigEntryState.NOT_LOADED.value,
"supports_options": False, "supports_options": False,
"supports_remove_device": False, "supports_remove_device": False,
"supports_unload": False, "supports_unload": False,
"pref_disable_new_entities": False, "pref_disable_new_entities": False,
"pref_disable_polling": False, "pref_disable_polling": False,
"disabled_by": core_ce.ConfigEntryDisabler.USER, "disabled_by": core_ce.ConfigEntryDisabler.USER,
"reason": None, "reason": None,
}, },
] ]
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): async def test_remove_entry(hass, client):
@ -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 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.""" """Test querying the available flows."""
with patch.object(config_flows, "FLOWS", ["hello", "world"]): with patch.object(
resp = await client.get("/api/config/config_entries/flow_handlers") 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 assert resp.status == HTTPStatus.OK
data = await resp.json() 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 @pytest.fixture
def mock_config_flows(): def mock_config_flows():
"""Mock the config flows.""" """Mock the config flows."""
flows = [] flows = {"integration": [], "helper": {}}
with patch.object(config_flows, "FLOWS", flows): with patch.object(config_flows, "FLOWS", flows):
yield 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): async def test_get_translations_loads_config_flows(hass, mock_config_flows):
"""Test the get translations helper loads config flow translations.""" """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 = Mock(file_path=pathlib.Path(__file__))
integration.name = "Component 1" 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 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 = Mock(file_path=pathlib.Path(__file__))
integration.name = "Component 2" integration.name = "Component 2"