From 3213091b8df5fa649264cd653ad38aa29ae9cce0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 20 Mar 2022 20:38:13 -0700 Subject: [PATCH] Add integration type (#68349) --- .../components/config/config_entries.py | 41 +- .../components/derivative/manifest.json | 1 + homeassistant/generated/config_flows.py | 786 +++++++++--------- homeassistant/loader.py | 25 +- script/hassfest/config_flow.py | 7 +- script/hassfest/manifest.py | 1 + script/hassfest/model.py | 5 + .../components/config/test_config_entries.py | 235 ++++-- tests/helpers/test_translation.py | 6 +- 9 files changed, 608 insertions(+), 499 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 07bdc794128..f3a239b5822 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -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): diff --git a/homeassistant/components/derivative/manifest.json b/homeassistant/components/derivative/manifest.json index bed23d33e15..665c4cb0192 100644 --- a/homeassistant/components/derivative/manifest.json +++ b/homeassistant/components/derivative/manifest.json @@ -1,5 +1,6 @@ { "domain": "derivative", + "integration_type": "helper", "name": "Derivative", "documentation": "https://www.home-assistant.io/integrations/derivative", "codeowners": [ diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 9fd116aa715..6a668b0d666 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -5,394 +5,398 @@ To update, run python3 -m script.hassfest # fmt: off -FLOWS = [ - "abode", - "accuweather", - "acmeda", - "adax", - "adguard", - "advantage_air", - "aemet", - "agent_dvr", - "airly", - "airnow", - "airthings", - "airtouch4", - "airvisual", - "airzone", - "alarmdecoder", - "almond", - "ambee", - "amberelectric", - "ambiclimate", - "ambient_station", - "androidtv", - "apple_tv", - "arcam_fmj", - "aseko_pool_live", - "asuswrt", - "atag", - "august", - "aurora", - "aurora_abb_powerone", - "aussie_broadband", - "awair", - "axis", - "azure_devops", - "azure_event_hub", - "balboa", - "blebox", - "blink", - "bmw_connected_drive", - "bond", - "bosch_shc", - "braviatv", - "broadlink", - "brother", - "brunt", - "bsblan", - "buienradar", - "canary", - "cast", - "cert_expiry", - "cloudflare", - "co2signal", - "coinbase", - "control4", - "coolmaster", - "coronavirus", - "cpuspeed", - "crownstone", - "daikin", - "deconz", - "denonavr", - "derivative", - "devolo_home_control", - "devolo_home_network", - "dexcom", - "dialogflow", - "directv", - "dlna_dmr", - "dlna_dms", - "dnsip", - "doorbird", - "dsmr", - "dunehd", - "dynalite", - "eafm", - "ecobee", - "econet", - "efergy", - "elgato", - "elkm1", - "elmax", - "emonitor", - "emulated_roku", - "enocean", - "enphase_envoy", - "environment_canada", - "epson", - "esphome", - "evil_genius_labs", - "ezviz", - "faa_delays", - "fireservicerota", - "fivem", - "fjaraskupan", - "flick_electric", - "flipr", - "flo", - "flume", - "flunearyou", - "flux_led", - "forecast_solar", - "forked_daapd", - "foscam", - "freebox", - "freedompro", - "fritz", - "fritzbox", - "fritzbox_callmonitor", - "fronius", - "garages_amsterdam", - "gdacs", - "geofency", - "geonetnz_quakes", - "geonetnz_volcano", - "gios", - "github", - "glances", - "goalzero", - "gogogate2", - "goodwe", - "google", - "google_travel_time", - "gpslogger", - "gree", - "group", - "growatt_server", - "guardian", - "habitica", - "hangouts", - "harmony", - "heos", - "hisense_aehw4a1", - "hive", - "hlk_sw16", - "home_connect", - "home_plus_control", - "homekit", - "homekit_controller", - "homematicip_cloud", - "homewizard", - "honeywell", - "huawei_lte", - "hue", - "huisbaasje", - "hunterdouglas_powerview", - "hvv_departures", - "hyperion", - "ialarm", - "iaqualink", - "icloud", - "ifttt", - "insteon", - "integration", - "intellifire", - "ios", - "iotawatt", - "ipma", - "ipp", - "iqvia", - "islamic_prayer_times", - "iss", - "isy994", - "izone", - "jellyfin", - "juicenet", - "kaleidescape", - "keenetic_ndms2", - "kmtronic", - "knx", - "kodi", - "konnected", - "kostal_plenticore", - "kraken", - "kulersky", - "launch_library", - "life360", - "lifx", - "litejet", - "litterrobot", - "local_ip", - "locative", - "logi_circle", - "lookin", - "luftdaten", - "lutron_caseta", - "lyric", - "mailgun", - "mazda", - "melcloud", - "met", - "met_eireann", - "meteo_france", - "meteoclimatic", - "metoffice", - "mikrotik", - "mill", - "minecraft_server", - "mjpeg", - "mobile_app", - "modem_callerid", - "modern_forms", - "moehlenhoff_alpha2", - "monoprice", - "moon", - "motion_blinds", - "motioneye", - "mqtt", - "mullvad", - "mutesync", - "myq", - "mysensors", - "nam", - "nanoleaf", - "neato", - "nest", - "netatmo", - "netgear", - "nexia", - "nfandroidtv", - "nightscout", - "nina", - "nmap_tracker", - "notion", - "nuheat", - "nuki", - "nut", - "nws", - "nzbget", - "octoprint", - "omnilogic", - "oncue", - "ondilo_ico", - "onewire", - "onvif", - "open_meteo", - "opengarage", - "opentherm_gw", - "openuv", - "openweathermap", - "overkiz", - "ovo_energy", - "owntracks", - "p1_monitor", - "panasonic_viera", - "philips_js", - "pi_hole", - "picnic", - "plaato", - "plex", - "plugwise", - "plum_lightpad", - "point", - "poolsense", - "powerwall", - "profiler", - "progettihwsw", - "prosegur", - "ps4", - "pure_energie", - "pvoutput", - "pvpc_hourly_pricing", - "rachio", - "radio_browser", - "rainforest_eagle", - "rainmachine", - "rdw", - "recollect_waste", - "renault", - "rfxtrx", - "ridwell", - "ring", - "risco", - "rituals_perfume_genie", - "roku", - "roomba", - "roon", - "rpi_power", - "rtsp_to_webrtc", - "ruckus_unleashed", - "samsungtv", - "screenlogic", - "season", - "sense", - "senseme", - "sensibo", - "sentry", - "sharkiq", - "shelly", - "shopping_list", - "sia", - "simplisafe", - "sleepiq", - "sma", - "smappee", - "smart_meter_texas", - "smartthings", - "smarttub", - "smhi", - "sms", - "solaredge", - "solarlog", - "solax", - "soma", - "somfy", - "somfy_mylink", - "sonarr", - "songpal", - "sonos", - "speedtestdotnet", - "spider", - "spotify", - "squeezebox", - "srp_energy", - "starline", - "steamist", - "stookalert", - "subaru", - "sun", - "surepetcare", - "switch_as_x", - "switchbot", - "switcher_kis", - "syncthing", - "syncthru", - "synology_dsm", - "system_bridge", - "tado", - "tailscale", - "tasmota", - "tellduslive", - "tesla_wall_connector", - "tibber", - "tile", - "tolo", - "tomorrowio", - "toon", - "totalconnect", - "tplink", - "traccar", - "tractive", - "tradfri", - "trafikverket_weatherstation", - "transmission", - "tuya", - "twentemilieu", - "twilio", - "twinkly", - "unifi", - "unifiprotect", - "upb", - "upcloud", - "upnp", - "uptime", - "uptimerobot", - "vallox", - "velbus", - "venstar", - "vera", - "verisure", - "version", - "vesync", - "vicare", - "vilfo", - "vizio", - "vlc_telnet", - "volumio", - "wallbox", - "watttime", - "waze_travel_time", - "webostv", - "wemo", - "whirlpool", - "whois", - "wiffi", - "wilight", - "withings", - "wiz", - "wled", - "wolflink", - "xbox", - "xiaomi_aqara", - "xiaomi_miio", - "yale_smart_alarm", - "yamaha_musiccast", - "yeelight", - "youless", - "zerproc", - "zha", - "zwave_js", - "zwave_me" -] +FLOWS = { + "integration": [ + "abode", + "accuweather", + "acmeda", + "adax", + "adguard", + "advantage_air", + "aemet", + "agent_dvr", + "airly", + "airnow", + "airthings", + "airtouch4", + "airvisual", + "airzone", + "alarmdecoder", + "almond", + "ambee", + "amberelectric", + "ambiclimate", + "ambient_station", + "androidtv", + "apple_tv", + "arcam_fmj", + "aseko_pool_live", + "asuswrt", + "atag", + "august", + "aurora", + "aurora_abb_powerone", + "aussie_broadband", + "awair", + "axis", + "azure_devops", + "azure_event_hub", + "balboa", + "blebox", + "blink", + "bmw_connected_drive", + "bond", + "bosch_shc", + "braviatv", + "broadlink", + "brother", + "brunt", + "bsblan", + "buienradar", + "canary", + "cast", + "cert_expiry", + "cloudflare", + "co2signal", + "coinbase", + "control4", + "coolmaster", + "coronavirus", + "cpuspeed", + "crownstone", + "daikin", + "deconz", + "denonavr", + "devolo_home_control", + "devolo_home_network", + "dexcom", + "dialogflow", + "directv", + "dlna_dmr", + "dlna_dms", + "dnsip", + "doorbird", + "dsmr", + "dunehd", + "dynalite", + "eafm", + "ecobee", + "econet", + "efergy", + "elgato", + "elkm1", + "elmax", + "emonitor", + "emulated_roku", + "enocean", + "enphase_envoy", + "environment_canada", + "epson", + "esphome", + "evil_genius_labs", + "ezviz", + "faa_delays", + "fireservicerota", + "fivem", + "fjaraskupan", + "flick_electric", + "flipr", + "flo", + "flume", + "flunearyou", + "flux_led", + "forecast_solar", + "forked_daapd", + "foscam", + "freebox", + "freedompro", + "fritz", + "fritzbox", + "fritzbox_callmonitor", + "fronius", + "garages_amsterdam", + "gdacs", + "geofency", + "geonetnz_quakes", + "geonetnz_volcano", + "gios", + "github", + "glances", + "goalzero", + "gogogate2", + "goodwe", + "google", + "google_travel_time", + "gpslogger", + "gree", + "group", + "growatt_server", + "guardian", + "habitica", + "hangouts", + "harmony", + "heos", + "hisense_aehw4a1", + "hive", + "hlk_sw16", + "home_connect", + "home_plus_control", + "homekit", + "homekit_controller", + "homematicip_cloud", + "homewizard", + "honeywell", + "huawei_lte", + "hue", + "huisbaasje", + "hunterdouglas_powerview", + "hvv_departures", + "hyperion", + "ialarm", + "iaqualink", + "icloud", + "ifttt", + "insteon", + "integration", + "intellifire", + "ios", + "iotawatt", + "ipma", + "ipp", + "iqvia", + "islamic_prayer_times", + "iss", + "isy994", + "izone", + "jellyfin", + "juicenet", + "kaleidescape", + "keenetic_ndms2", + "kmtronic", + "knx", + "kodi", + "konnected", + "kostal_plenticore", + "kraken", + "kulersky", + "launch_library", + "life360", + "lifx", + "litejet", + "litterrobot", + "local_ip", + "locative", + "logi_circle", + "lookin", + "luftdaten", + "lutron_caseta", + "lyric", + "mailgun", + "mazda", + "melcloud", + "met", + "met_eireann", + "meteo_france", + "meteoclimatic", + "metoffice", + "mikrotik", + "mill", + "minecraft_server", + "mjpeg", + "mobile_app", + "modem_callerid", + "modern_forms", + "moehlenhoff_alpha2", + "monoprice", + "moon", + "motion_blinds", + "motioneye", + "mqtt", + "mullvad", + "mutesync", + "myq", + "mysensors", + "nam", + "nanoleaf", + "neato", + "nest", + "netatmo", + "netgear", + "nexia", + "nfandroidtv", + "nightscout", + "nina", + "nmap_tracker", + "notion", + "nuheat", + "nuki", + "nut", + "nws", + "nzbget", + "octoprint", + "omnilogic", + "oncue", + "ondilo_ico", + "onewire", + "onvif", + "open_meteo", + "opengarage", + "opentherm_gw", + "openuv", + "openweathermap", + "overkiz", + "ovo_energy", + "owntracks", + "p1_monitor", + "panasonic_viera", + "philips_js", + "pi_hole", + "picnic", + "plaato", + "plex", + "plugwise", + "plum_lightpad", + "point", + "poolsense", + "powerwall", + "profiler", + "progettihwsw", + "prosegur", + "ps4", + "pure_energie", + "pvoutput", + "pvpc_hourly_pricing", + "rachio", + "radio_browser", + "rainforest_eagle", + "rainmachine", + "rdw", + "recollect_waste", + "renault", + "rfxtrx", + "ridwell", + "ring", + "risco", + "rituals_perfume_genie", + "roku", + "roomba", + "roon", + "rpi_power", + "rtsp_to_webrtc", + "ruckus_unleashed", + "samsungtv", + "screenlogic", + "season", + "sense", + "senseme", + "sensibo", + "sentry", + "sharkiq", + "shelly", + "shopping_list", + "sia", + "simplisafe", + "sleepiq", + "sma", + "smappee", + "smart_meter_texas", + "smartthings", + "smarttub", + "smhi", + "sms", + "solaredge", + "solarlog", + "solax", + "soma", + "somfy", + "somfy_mylink", + "sonarr", + "songpal", + "sonos", + "speedtestdotnet", + "spider", + "spotify", + "squeezebox", + "srp_energy", + "starline", + "steamist", + "stookalert", + "subaru", + "sun", + "surepetcare", + "switch_as_x", + "switchbot", + "switcher_kis", + "syncthing", + "syncthru", + "synology_dsm", + "system_bridge", + "tado", + "tailscale", + "tasmota", + "tellduslive", + "tesla_wall_connector", + "tibber", + "tile", + "tolo", + "tomorrowio", + "toon", + "totalconnect", + "tplink", + "traccar", + "tractive", + "tradfri", + "trafikverket_weatherstation", + "transmission", + "tuya", + "twentemilieu", + "twilio", + "twinkly", + "unifi", + "unifiprotect", + "upb", + "upcloud", + "upnp", + "uptime", + "uptimerobot", + "vallox", + "velbus", + "venstar", + "vera", + "verisure", + "version", + "vesync", + "vicare", + "vilfo", + "vizio", + "vlc_telnet", + "volumio", + "wallbox", + "watttime", + "waze_travel_time", + "webostv", + "wemo", + "whirlpool", + "whois", + "wiffi", + "wilight", + "withings", + "wiz", + "wled", + "wolflink", + "xbox", + "xiaomi_aqara", + "xiaomi_miio", + "yale_smart_alarm", + "yamaha_musiccast", + "yeelight", + "youless", + "zerproc", + "zha", + "zwave_js", + "zwave_me" + ], + "helper": [ + "derivative" + ] +} diff --git a/homeassistant/loader.py b/homeassistant/loader.py index be70dc50f0c..364f212a1be 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -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.""" diff --git a/script/hassfest/config_flow.py b/script/hassfest/config_flow.py index 87e9bea6291..169ccedf4a1 100644 --- a/script/hassfest/config_flow.py +++ b/script/hassfest/config_flow.py @@ -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)) diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 5b344ed505d..ca9acedd515 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -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"): [ diff --git a/script/hassfest/model.py b/script/hassfest/model.py index 7006c1e6032..2a6ea9ca85f 100644 --- a/script/hassfest/model.py +++ b/script/hassfest/model.py @@ -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)) diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index a24e0961f9c..6366eca4c6d 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -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,104 +37,133 @@ 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: - """Config flow with options flow.""" + @HANDLERS.register("comp1") + class Comp1ConfigFlow: + """Config flow with options flow.""" - @staticmethod - @callback - def async_get_options_flow(config_entry): - """Get options flow.""" - pass + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get options flow.""" + pass - @classmethod - @callback - def async_supports_options_flow(cls, config_entry): - """Return options flow support for this handler.""" - return True + @classmethod + @callback + def async_supports_options_flow(cls, config_entry): + """Return options flow support for this handler.""" + return True - hass.helpers.config_entry_flow.register_discovery_flow( - "comp2", "Comp 2", lambda: None - ) + hass.helpers.config_entry_flow.register_discovery_flow( + "comp2", "Comp 2", lambda: None + ) - entry = MockConfigEntry( - domain="comp1", - title="Test 1", - source="bla", - ) - entry.supports_unload = True - 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) + entry = MockConfigEntry( + domain="comp1", + title="Test 1", + source="bla", + ) + entry.supports_unload = True + 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) - resp = await client.get("/api/config/config_entries/entry") - assert resp.status == HTTPStatus.OK - data = await resp.json() - for entry in data: - entry.pop("entry_id") - assert data == [ - { - "domain": "comp1", - "title": "Test 1", - "source": "bla", - "state": core_ce.ConfigEntryState.NOT_LOADED.value, - "supports_options": True, - "supports_remove_device": False, - "supports_unload": True, - "pref_disable_new_entities": False, - "pref_disable_polling": False, - "disabled_by": None, - "reason": None, - }, - { - "domain": "comp2", - "title": "Test 2", - "source": "bla2", - "state": core_ce.ConfigEntryState.SETUP_ERROR.value, - "supports_options": False, - "supports_remove_device": False, - "supports_unload": False, - "pref_disable_new_entities": False, - "pref_disable_polling": False, - "disabled_by": None, - "reason": "Unsupported API", - }, - { - "domain": "comp3", - "title": "Test 3", - "source": "bla3", - "state": core_ce.ConfigEntryState.NOT_LOADED.value, - "supports_options": False, - "supports_remove_device": False, - "supports_unload": False, - "pref_disable_new_entities": False, - "pref_disable_polling": False, - "disabled_by": core_ce.ConfigEntryDisabler.USER, - "reason": None, - }, - ] + resp = await client.get("/api/config/config_entries/entry") + assert resp.status == HTTPStatus.OK + data = await resp.json() + for entry in data: + entry.pop("entry_id") + assert data == [ + { + "domain": "comp1", + "title": "Test 1", + "source": "bla", + "state": core_ce.ConfigEntryState.NOT_LOADED.value, + "supports_options": True, + "supports_remove_device": False, + "supports_unload": True, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "disabled_by": None, + "reason": None, + }, + { + "domain": "comp2", + "title": "Test 2", + "source": "bla2", + "state": core_ce.ConfigEntryState.SETUP_ERROR.value, + "supports_options": False, + "supports_remove_device": False, + "supports_unload": False, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "disabled_by": None, + "reason": "Unsupported API", + }, + { + "domain": "comp3", + "title": "Test 3", + "source": "bla3", + "state": core_ce.ConfigEntryState.NOT_LOADED.value, + "supports_options": False, + "supports_remove_device": False, + "supports_unload": False, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "disabled_by": core_ce.ConfigEntryDisabler.USER, + "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): @@ -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 ############################ diff --git a/tests/helpers/test_translation.py b/tests/helpers/test_translation.py index be57b2d52e7..5520269ca9d 100644 --- a/tests/helpers/test_translation.py +++ b/tests/helpers/test_translation.py @@ -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"