Add integration type (#68349)
This commit is contained in:
parent
4f9df1fd0f
commit
3213091b8d
9 changed files with 608 additions and 499 deletions
|
@ -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):
|
||||||
|
|
|
@ -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": [
|
||||||
|
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
|
@ -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."""
|
||||||
|
|
|
@ -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))
|
||||||
|
|
||||||
|
|
|
@ -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"): [
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue