mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
ConfigFlow default discovery without unique ID (#36754)
This commit is contained in:
parent
dfac9c5e03
commit
3cc94f7d6a
@ -4,5 +4,8 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/abode",
|
||||
"requirements": ["abodepy==0.19.0"],
|
||||
"codeowners": ["@shred86"]
|
||||
"codeowners": ["@shred86"],
|
||||
"homekit": {
|
||||
"models": ["Abode", "Iota"]
|
||||
}
|
||||
}
|
||||
|
@ -105,7 +105,7 @@ class AdGuardHomeFlowHandler(ConfigFlow):
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_hassio(self, user_input=None):
|
||||
async def async_step_hassio(self, discovery_info):
|
||||
"""Prepare configuration for a Hass.io AdGuard Home add-on.
|
||||
|
||||
This flow is triggered by the discovery component.
|
||||
@ -113,14 +113,14 @@ class AdGuardHomeFlowHandler(ConfigFlow):
|
||||
entries = self._async_current_entries()
|
||||
|
||||
if not entries:
|
||||
self._hassio_discovery = user_input
|
||||
self._hassio_discovery = discovery_info
|
||||
return await self.async_step_hassio_confirm()
|
||||
|
||||
cur_entry = entries[0]
|
||||
|
||||
if (
|
||||
cur_entry.data[CONF_HOST] == user_input[CONF_HOST]
|
||||
and cur_entry.data[CONF_PORT] == user_input[CONF_PORT]
|
||||
cur_entry.data[CONF_HOST] == discovery_info[CONF_HOST]
|
||||
and cur_entry.data[CONF_PORT] == discovery_info[CONF_PORT]
|
||||
):
|
||||
return self.async_abort(reason="single_instance_allowed")
|
||||
|
||||
@ -133,8 +133,8 @@ class AdGuardHomeFlowHandler(ConfigFlow):
|
||||
cur_entry,
|
||||
data={
|
||||
**cur_entry.data,
|
||||
CONF_HOST: user_input[CONF_HOST],
|
||||
CONF_PORT: user_input[CONF_PORT],
|
||||
CONF_HOST: discovery_info[CONF_HOST],
|
||||
CONF_PORT: discovery_info[CONF_PORT],
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -23,13 +23,13 @@ class AgentFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Initialize the Agent config flow."""
|
||||
self.device_config = {}
|
||||
|
||||
async def async_step_user(self, info=None):
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle an Agent config flow."""
|
||||
errors = {}
|
||||
|
||||
if info is not None:
|
||||
host = info[CONF_HOST]
|
||||
port = info[CONF_PORT]
|
||||
if user_input is not None:
|
||||
host = user_input[CONF_HOST]
|
||||
port = user_input[CONF_PORT]
|
||||
|
||||
server_origin = generate_url(host, port)
|
||||
agent_client = Agent(server_origin, async_get_clientsession(self.hass))
|
||||
@ -48,8 +48,8 @@ class AgentFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={
|
||||
CONF_HOST: info[CONF_HOST],
|
||||
CONF_PORT: info[CONF_PORT],
|
||||
CONF_HOST: user_input[CONF_HOST],
|
||||
CONF_PORT: user_input[CONF_PORT],
|
||||
SERVER_URL: server_origin,
|
||||
}
|
||||
)
|
||||
|
@ -94,12 +94,12 @@ class AlmondFlowHandler(config_entry_oauth2_flow.AbstractOAuth2FlowHandler):
|
||||
data={"type": TYPE_LOCAL, "host": user_input["host"]},
|
||||
)
|
||||
|
||||
async def async_step_hassio(self, user_input=None):
|
||||
async def async_step_hassio(self, discovery_info):
|
||||
"""Receive a Hass.io discovery."""
|
||||
if self._async_current_entries():
|
||||
return self.async_abort(reason="already_setup")
|
||||
|
||||
self.hassio_discovery = user_input
|
||||
self.hassio_discovery = discovery_info
|
||||
|
||||
return await self.async_step_hassio_confirm()
|
||||
|
||||
|
@ -45,21 +45,21 @@ class ArcamFmjFlowHandler(config_entries.ConfigFlow):
|
||||
title=f"{DEFAULT_NAME} ({host})", data={CONF_HOST: host, CONF_PORT: port},
|
||||
)
|
||||
|
||||
async def async_step_user(self, user_info=None):
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle a discovered device."""
|
||||
errors = {}
|
||||
|
||||
if user_info is not None:
|
||||
if user_input is not None:
|
||||
uuid = await get_uniqueid_from_host(
|
||||
async_get_clientsession(self.hass), user_info[CONF_HOST]
|
||||
async_get_clientsession(self.hass), user_input[CONF_HOST]
|
||||
)
|
||||
if uuid:
|
||||
await self._async_set_unique_id_and_update(
|
||||
user_info[CONF_HOST], user_info[CONF_PORT], uuid
|
||||
user_input[CONF_HOST], user_input[CONF_PORT], uuid
|
||||
)
|
||||
|
||||
return await self._async_check_and_create(
|
||||
user_info[CONF_HOST], user_info[CONF_PORT]
|
||||
user_input[CONF_HOST], user_input[CONF_PORT]
|
||||
)
|
||||
|
||||
fields = {
|
||||
|
@ -69,16 +69,18 @@ class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
step_id="user", data_schema=DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_zeroconf(self, user_input=None):
|
||||
async def async_step_zeroconf(self, discovery_info):
|
||||
"""Handle zeroconf discovery."""
|
||||
if user_input is None:
|
||||
if discovery_info is None:
|
||||
return self.async_abort(reason="connection_error")
|
||||
|
||||
if not user_input.get("name") or not user_input["name"].startswith("Brother"):
|
||||
if not discovery_info.get("name") or not discovery_info["name"].startswith(
|
||||
"Brother"
|
||||
):
|
||||
return self.async_abort(reason="not_brother_printer")
|
||||
|
||||
# Hostname is format: brother.local.
|
||||
self.host = user_input["hostname"].rstrip(".")
|
||||
self.host = discovery_info["hostname"].rstrip(".")
|
||||
|
||||
self.brother = Brother(self.host)
|
||||
try:
|
||||
|
@ -128,7 +128,7 @@ class FlowHandler(config_entries.ConfigFlow):
|
||||
|
||||
async def async_step_zeroconf(self, discovery_info):
|
||||
"""Prepare configuration for a discovered Daikin device."""
|
||||
_LOGGER.debug("Zeroconf discovery_info: %s", discovery_info)
|
||||
_LOGGER.debug("Zeroconf user_input: %s", discovery_info)
|
||||
devices = Discovery.poll(discovery_info[CONF_HOST])
|
||||
await self.async_set_unique_id(next(iter(devices.values()))[KEY_MAC])
|
||||
self._abort_if_unique_id_configured()
|
||||
|
@ -205,25 +205,25 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
return await self.async_step_link()
|
||||
|
||||
async def async_step_hassio(self, user_input=None):
|
||||
async def async_step_hassio(self, discovery_info):
|
||||
"""Prepare configuration for a Hass.io deCONZ bridge.
|
||||
|
||||
This flow is triggered by the discovery component.
|
||||
"""
|
||||
LOGGER.debug("deCONZ HASSIO discovery %s", pformat(user_input))
|
||||
LOGGER.debug("deCONZ HASSIO discovery %s", pformat(discovery_info))
|
||||
|
||||
self.bridge_id = normalize_bridge_id(user_input[CONF_SERIAL])
|
||||
self.bridge_id = normalize_bridge_id(discovery_info[CONF_SERIAL])
|
||||
await self.async_set_unique_id(self.bridge_id)
|
||||
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={
|
||||
CONF_HOST: user_input[CONF_HOST],
|
||||
CONF_PORT: user_input[CONF_PORT],
|
||||
CONF_API_KEY: user_input[CONF_API_KEY],
|
||||
CONF_HOST: discovery_info[CONF_HOST],
|
||||
CONF_PORT: discovery_info[CONF_PORT],
|
||||
CONF_API_KEY: discovery_info[CONF_API_KEY],
|
||||
}
|
||||
)
|
||||
|
||||
self._hassio_discovery = user_input
|
||||
self._hassio_discovery = discovery_info
|
||||
|
||||
return await self.async_step_hassio_confirm()
|
||||
|
||||
|
@ -29,7 +29,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: Optional[ConfigType] = None, error: Optional[str] = None
|
||||
):
|
||||
): # pylint: disable=arguments-differ
|
||||
"""Handle a flow initialized by the user."""
|
||||
if user_input is not None:
|
||||
return await self._async_authenticate_or_add(user_input)
|
||||
|
@ -55,7 +55,7 @@ class FlickConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
else:
|
||||
return token is not None
|
||||
|
||||
async def async_step_user(self, user_input):
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle gathering login info."""
|
||||
errors = {}
|
||||
if user_input is not None:
|
||||
|
@ -105,6 +105,6 @@ class FreeboxFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Import a config entry."""
|
||||
return await self.async_step_user(user_input)
|
||||
|
||||
async def async_step_discovery(self, user_input=None):
|
||||
async def async_step_discovery(self, discovery_info):
|
||||
"""Initialize step from discovery."""
|
||||
return await self.async_step_user(user_input)
|
||||
return await self.async_step_user(discovery_info)
|
||||
|
@ -110,12 +110,12 @@ class FritzboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
step_id="user", data_schema=DATA_SCHEMA_USER, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_ssdp(self, user_input):
|
||||
async def async_step_ssdp(self, discovery_info):
|
||||
"""Handle a flow initialized by discovery."""
|
||||
host = urlparse(user_input[ATTR_SSDP_LOCATION]).hostname
|
||||
host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname
|
||||
self.context[CONF_HOST] = host
|
||||
|
||||
uuid = user_input.get(ATTR_UPNP_UDN)
|
||||
uuid = discovery_info.get(ATTR_UPNP_UDN)
|
||||
if uuid:
|
||||
if uuid.startswith("uuid:"):
|
||||
uuid = uuid[5:]
|
||||
@ -134,7 +134,7 @@ class FritzboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
return self.async_abort(reason="already_configured")
|
||||
|
||||
self._host = host
|
||||
self._name = user_input.get(ATTR_UPNP_FRIENDLY_NAME) or host
|
||||
self._name = discovery_info.get(ATTR_UPNP_FRIENDLY_NAME) or host
|
||||
|
||||
self.context["title_placeholders"] = {"name": self._name}
|
||||
return await self.async_step_confirm()
|
||||
|
@ -80,7 +80,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
title=info[CONF_UID], data={CONF_UID: info["uid"], **user_input}
|
||||
)
|
||||
|
||||
async def async_step_zeroconf(self, discovery_info=None):
|
||||
async def async_step_zeroconf(self, discovery_info):
|
||||
"""Handle the configuration via zeroconf."""
|
||||
if discovery_info is None:
|
||||
return self.async_abort(reason="connection_error")
|
||||
|
@ -152,16 +152,17 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
_LOGGER.debug(
|
||||
"Unable to determine unique id from discovery info and IPP response"
|
||||
)
|
||||
return self.async_abort(reason="unique_id_required")
|
||||
|
||||
await self.async_set_unique_id(unique_id)
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={
|
||||
CONF_HOST: self.discovery_info[CONF_HOST],
|
||||
CONF_NAME: self.discovery_info[CONF_NAME],
|
||||
},
|
||||
)
|
||||
if unique_id:
|
||||
await self.async_set_unique_id(unique_id)
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={
|
||||
CONF_HOST: self.discovery_info[CONF_HOST],
|
||||
CONF_NAME: self.discovery_info[CONF_NAME],
|
||||
},
|
||||
)
|
||||
|
||||
await self._async_handle_discovery_without_unique_id()
|
||||
return await self.async_step_zeroconf_confirm()
|
||||
|
||||
async def async_step_zeroconf_confirm(
|
||||
|
@ -74,12 +74,12 @@ class FlowHandler(config_entries.ConfigFlow):
|
||||
|
||||
return self.async_create_entry(title="configuration.yaml", data={})
|
||||
|
||||
async def async_step_hassio(self, user_input=None):
|
||||
async def async_step_hassio(self, discovery_info):
|
||||
"""Receive a Hass.io discovery."""
|
||||
if self._async_current_entries():
|
||||
return self.async_abort(reason="single_instance_allowed")
|
||||
|
||||
self._hassio_discovery = user_input
|
||||
self._hassio_discovery = discovery_info
|
||||
|
||||
return await self.async_step_hassio_confirm()
|
||||
|
||||
|
@ -68,9 +68,9 @@ class OpenThermGwConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
return self._show_form()
|
||||
|
||||
async def async_step_user(self, info=None):
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle manual initiation of the config flow."""
|
||||
return await self.async_step_init(info)
|
||||
return await self.async_step_init(user_input)
|
||||
|
||||
async def async_step_import(self, import_config):
|
||||
"""
|
||||
|
@ -96,7 +96,9 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
self.client_id = None
|
||||
self._manual = False
|
||||
|
||||
async def async_step_user(self, user_input=None, errors=None):
|
||||
async def async_step_user(
|
||||
self, user_input=None, errors=None
|
||||
): # pylint: disable=arguments-differ
|
||||
"""Handle a flow initialized by the user."""
|
||||
if user_input is not None:
|
||||
return await self.async_step_plex_website_auth()
|
||||
|
@ -116,17 +116,17 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA)
|
||||
|
||||
async def async_step_ssdp(self, user_input=None):
|
||||
async def async_step_ssdp(self, discovery_info):
|
||||
"""Handle a flow initialized by discovery."""
|
||||
host = urlparse(user_input[ATTR_SSDP_LOCATION]).hostname
|
||||
host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname
|
||||
ip_address = await self.hass.async_add_executor_job(_get_ip, host)
|
||||
|
||||
self._host = host
|
||||
self._ip = self.context[CONF_IP_ADDRESS] = ip_address
|
||||
self._manufacturer = user_input.get(ATTR_UPNP_MANUFACTURER)
|
||||
self._model = user_input.get(ATTR_UPNP_MODEL_NAME)
|
||||
self._manufacturer = discovery_info.get(ATTR_UPNP_MANUFACTURER)
|
||||
self._model = discovery_info.get(ATTR_UPNP_MODEL_NAME)
|
||||
self._name = f"Samsung {self._model}"
|
||||
self._id = user_input.get(ATTR_UPNP_UDN)
|
||||
self._id = discovery_info.get(ATTR_UPNP_UDN)
|
||||
self._title = self._model
|
||||
|
||||
# probably access denied
|
||||
|
@ -114,13 +114,14 @@ class FlowHandler(config_entries.ConfigFlow):
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_discovery(self, user_input):
|
||||
async def async_step_discovery(self, discovery_info):
|
||||
"""Run when a Tellstick is discovered."""
|
||||
await self._async_handle_discovery_without_unique_id()
|
||||
|
||||
_LOGGER.info("Discovered tellstick device: %s", user_input)
|
||||
if supports_local_api(user_input[1]):
|
||||
_LOGGER.info("%s support local API", user_input[1])
|
||||
self._hosts.append(user_input[0])
|
||||
_LOGGER.info("Discovered tellstick device: %s", discovery_info)
|
||||
if supports_local_api(discovery_info[1]):
|
||||
_LOGGER.info("%s support local API", discovery_info[1])
|
||||
self._hosts.append(discovery_info[0])
|
||||
|
||||
return await self.async_step_user()
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_setup": "TelldusLive is already configured",
|
||||
"already_configured": "TelldusLive is already configured",
|
||||
"authorize_url_fail": "Unknown error generating an authorize url.",
|
||||
"authorize_url_timeout": "Timeout generating authorize url.",
|
||||
"unknown": "Unknown error occurred"
|
||||
@ -22,4 +22,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -82,12 +82,12 @@ class FlowHandler(config_entries.ConfigFlow):
|
||||
step_id="auth", data_schema=vol.Schema(fields), errors=errors
|
||||
)
|
||||
|
||||
async def async_step_homekit(self, user_input):
|
||||
async def async_step_homekit(self, discovery_info):
|
||||
"""Handle homekit discovery."""
|
||||
await self.async_set_unique_id(user_input["properties"]["id"])
|
||||
self._abort_if_unique_id_configured({CONF_HOST: user_input["host"]})
|
||||
await self.async_set_unique_id(discovery_info["properties"]["id"])
|
||||
self._abort_if_unique_id_configured({CONF_HOST: discovery_info["host"]})
|
||||
|
||||
host = user_input["host"]
|
||||
host = discovery_info["host"]
|
||||
|
||||
for entry in self._async_current_entries():
|
||||
if entry.data[CONF_HOST] != host:
|
||||
@ -96,7 +96,7 @@ class FlowHandler(config_entries.ConfigFlow):
|
||||
# Backwards compat, we update old entries
|
||||
if not entry.unique_id:
|
||||
self.hass.config_entries.async_update_entry(
|
||||
entry, unique_id=user_input["properties"]["id"]
|
||||
entry, unique_id=discovery_info["properties"]["id"]
|
||||
)
|
||||
|
||||
return self.async_abort(reason="already_configured")
|
||||
|
@ -21,6 +21,8 @@ _LOGGER = logging.getLogger(__name__)
|
||||
_UNDEF: dict = {}
|
||||
|
||||
SOURCE_DISCOVERY = "discovery"
|
||||
SOURCE_HASSIO = "hassio"
|
||||
SOURCE_HOMEKIT = "homekit"
|
||||
SOURCE_IMPORT = "import"
|
||||
SOURCE_INTEGRATION_DISCOVERY = "integration_discovery"
|
||||
SOURCE_SSDP = "ssdp"
|
||||
@ -62,6 +64,7 @@ ENTRY_STATE_FAILED_UNLOAD = "failed_unload"
|
||||
|
||||
UNRECOVERABLE_STATES = (ENTRY_STATE_MIGRATION_ERROR, ENTRY_STATE_FAILED_UNLOAD)
|
||||
|
||||
DEFAULT_DISCOVERY_UNIQUE_ID = "default_discovery_unique_id"
|
||||
DISCOVERY_NOTIFICATION_ID = "config_entry_discovery"
|
||||
DISCOVERY_SOURCES = (
|
||||
SOURCE_SSDP,
|
||||
@ -466,6 +469,10 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager):
|
||||
):
|
||||
self.async_abort(progress_flow["flow_id"])
|
||||
|
||||
# Reset unique ID when the default discovery ID has been used
|
||||
if flow.unique_id == DEFAULT_DISCOVERY_UNIQUE_ID:
|
||||
await flow.async_set_unique_id(None)
|
||||
|
||||
# Find existing entry.
|
||||
for check_entry in self.config_entries.async_entries(result["handler"]):
|
||||
if check_entry.unique_id == flow.unique_id:
|
||||
@ -857,12 +864,16 @@ class ConfigFlow(data_entry_flow.FlowHandler):
|
||||
raise data_entry_flow.AbortFlow("already_configured")
|
||||
|
||||
async def async_set_unique_id(
|
||||
self, unique_id: str, *, raise_on_progress: bool = True
|
||||
self, unique_id: Optional[str] = None, *, raise_on_progress: bool = True
|
||||
) -> Optional[ConfigEntry]:
|
||||
"""Set a unique ID for the config flow.
|
||||
|
||||
Returns optionally existing config entry with same ID.
|
||||
"""
|
||||
if unique_id is None:
|
||||
self.context["unique_id"] = None # pylint: disable=no-member
|
||||
return None
|
||||
|
||||
if raise_on_progress:
|
||||
for progress in self._async_in_progress():
|
||||
if progress["context"].get("unique_id") == unique_id:
|
||||
@ -870,6 +881,13 @@ class ConfigFlow(data_entry_flow.FlowHandler):
|
||||
|
||||
self.context["unique_id"] = unique_id # pylint: disable=no-member
|
||||
|
||||
# Abort discoveries done using the default discovery unique id
|
||||
assert self.hass is not None
|
||||
if unique_id != DEFAULT_DISCOVERY_UNIQUE_ID:
|
||||
for progress in self._async_in_progress():
|
||||
if progress["context"].get("unique_id") == DEFAULT_DISCOVERY_UNIQUE_ID:
|
||||
self.hass.config_entries.flow.async_abort(progress["flow_id"])
|
||||
|
||||
for entry in self._async_current_entries():
|
||||
if entry.unique_id == unique_id:
|
||||
return entry
|
||||
@ -911,6 +929,49 @@ class ConfigFlow(data_entry_flow.FlowHandler):
|
||||
"""Rediscover a config entry by it's unique_id."""
|
||||
return self.async_abort(reason="not_implemented")
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Handle a flow initiated by the user."""
|
||||
return self.async_abort(reason="not_implemented")
|
||||
|
||||
async def _async_handle_discovery_without_unique_id(self) -> None:
|
||||
"""Mark this flow discovered, without a unique identifier.
|
||||
|
||||
If a flow initiated by discovery, doesn't have a unique ID, this can
|
||||
be used alternatively. It will ensure only 1 flow is started and only
|
||||
when the handler has no existing config entries.
|
||||
|
||||
It ensures that the discovery can be ignored by the user.
|
||||
"""
|
||||
if self.unique_id is not None:
|
||||
return
|
||||
|
||||
# Abort if the handler has config entries already
|
||||
if self._async_current_entries():
|
||||
raise data_entry_flow.AbortFlow("already_configured")
|
||||
|
||||
# Use an special unique id to differentiate
|
||||
await self.async_set_unique_id(DEFAULT_DISCOVERY_UNIQUE_ID)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
# Abort if any other flow for this handler is already in progress
|
||||
assert self.hass is not None
|
||||
if self._async_in_progress():
|
||||
raise data_entry_flow.AbortFlow("already_in_progress")
|
||||
|
||||
async def async_step_discovery(
|
||||
self, discovery_info: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""Handle a flow initialized by discovery."""
|
||||
await self._async_handle_discovery_without_unique_id()
|
||||
return await self.async_step_user()
|
||||
|
||||
async_step_hassio = async_step_discovery
|
||||
async_step_homekit = async_step_discovery
|
||||
async_step_ssdp = async_step_discovery
|
||||
async_step_zeroconf = async_step_discovery
|
||||
|
||||
|
||||
class OptionsFlowManager(data_entry_flow.FlowManager):
|
||||
"""Flow to set options for a configuration entry."""
|
||||
|
@ -57,8 +57,10 @@ ZEROCONF = {
|
||||
HOMEKIT = {
|
||||
"819LMB": "myq",
|
||||
"AC02": "tado",
|
||||
"Abode": "abode",
|
||||
"BSB002": "hue",
|
||||
"Healty Home Coach": "netatmo",
|
||||
"Iota": "abode",
|
||||
"LIFX": "lifx",
|
||||
"Netatmo Relay": "netatmo",
|
||||
"PowerView": "hunterdouglas_powerview",
|
||||
|
@ -1,5 +1,5 @@
|
||||
"""Helpers for data entry flows for config entries."""
|
||||
from typing import Awaitable, Callable, Union
|
||||
from typing import Any, Awaitable, Callable, Dict, Optional, Union
|
||||
|
||||
from homeassistant import config_entries
|
||||
|
||||
@ -28,7 +28,9 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow):
|
||||
self._discovery_function = discovery_function
|
||||
self.CONNECTION_CLASS = connection_class # pylint: disable=invalid-name
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
async def async_step_user(
|
||||
self, user_input: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Handle a flow initialized by the user."""
|
||||
if self._async_current_entries():
|
||||
return self.async_abort(reason="single_instance_allowed")
|
||||
@ -37,7 +39,9 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow):
|
||||
|
||||
return await self.async_step_confirm()
|
||||
|
||||
async def async_step_confirm(self, user_input=None):
|
||||
async def async_step_confirm(
|
||||
self, user_input: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Confirm setup."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(step_id="confirm")
|
||||
@ -48,7 +52,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow):
|
||||
|
||||
has_devices = in_progress
|
||||
if not has_devices:
|
||||
has_devices = await self.hass.async_add_job(
|
||||
has_devices = await self.hass.async_add_job( # type: ignore
|
||||
self._discovery_function, self.hass
|
||||
)
|
||||
|
||||
@ -56,6 +60,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow):
|
||||
return self.async_abort(reason="no_devices_found")
|
||||
|
||||
# Cancel the discovered one.
|
||||
assert self.hass is not None
|
||||
for flow in in_progress:
|
||||
self.hass.config_entries.flow.async_abort(flow["flow_id"])
|
||||
|
||||
@ -64,7 +69,9 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow):
|
||||
|
||||
return self.async_create_entry(title=self._title, data={})
|
||||
|
||||
async def async_step_discovery(self, discovery_info):
|
||||
async def async_step_discovery(
|
||||
self, discovery_info: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""Handle a flow initialized by discovery."""
|
||||
if self._async_in_progress() or self._async_current_entries():
|
||||
return self.async_abort(reason="single_instance_allowed")
|
||||
@ -77,12 +84,13 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow):
|
||||
async_step_ssdp = async_step_discovery
|
||||
async_step_homekit = async_step_discovery
|
||||
|
||||
async def async_step_import(self, _):
|
||||
async def async_step_import(self, _: Optional[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
"""Handle a flow initialized by import."""
|
||||
if self._async_current_entries():
|
||||
return self.async_abort(reason="single_instance_allowed")
|
||||
|
||||
# Cancel other flows.
|
||||
assert self.hass is not None
|
||||
in_progress = self._async_in_progress()
|
||||
for flow in in_progress:
|
||||
self.hass.config_entries.flow.async_abort(flow["flow_id"])
|
||||
@ -125,7 +133,9 @@ class WebhookFlowHandler(config_entries.ConfigFlow):
|
||||
self._description_placeholder = description_placeholder
|
||||
self._allow_multiple = allow_multiple
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
async def async_step_user(
|
||||
self, user_input: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Handle a user initiated set up flow to create a webhook."""
|
||||
if not self._allow_multiple and self._async_current_entries():
|
||||
return self.async_abort(reason="one_instance_allowed")
|
||||
@ -133,6 +143,7 @@ class WebhookFlowHandler(config_entries.ConfigFlow):
|
||||
if user_input is None:
|
||||
return self.async_show_form(step_id="user")
|
||||
|
||||
assert self.hass is not None
|
||||
webhook_id = self.hass.components.webhook.async_generate_id()
|
||||
|
||||
if (
|
||||
|
@ -226,7 +226,9 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta):
|
||||
),
|
||||
)
|
||||
|
||||
async def async_step_auth(self, user_input: Optional[dict] = None) -> dict:
|
||||
async def async_step_auth(
|
||||
self, user_input: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Create an entry for auth."""
|
||||
# Flow has been triggered by external data
|
||||
if user_input:
|
||||
@ -243,7 +245,9 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta):
|
||||
|
||||
return self.async_external_step(step_id="auth", url=url)
|
||||
|
||||
async def async_step_creation(self, user_input: Optional[dict] = None) -> dict:
|
||||
async def async_step_creation(
|
||||
self, user_input: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Create config entry from external data."""
|
||||
token = await self.flow_impl.async_resolve_external_data(self.external_data)
|
||||
token["expires_at"] = time.time() + token["expires_in"]
|
||||
@ -261,7 +265,9 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta):
|
||||
"""
|
||||
return self.async_create_entry(title=self.flow_impl.name, data=data)
|
||||
|
||||
async def async_step_discovery(self, user_input: Optional[dict] = None) -> dict:
|
||||
async def async_step_discovery(
|
||||
self, discovery_info: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""Handle a flow initialized by discovery."""
|
||||
await self.async_set_unique_id(self.DOMAIN)
|
||||
|
||||
|
@ -2,8 +2,6 @@
|
||||
import json
|
||||
from typing import Dict
|
||||
|
||||
from homeassistant.requirements import DISCOVERY_INTEGRATIONS
|
||||
|
||||
from .model import Config, Integration
|
||||
|
||||
BASE = """
|
||||
@ -25,20 +23,36 @@ def validate_integration(config: Config, integration: Integration):
|
||||
config_flow_file = integration.path / "config_flow.py"
|
||||
|
||||
if not config_flow_file.is_file():
|
||||
integration.add_error(
|
||||
"config_flow", "Config flows need to be defined in the file config_flow.py"
|
||||
)
|
||||
if integration.get("config_flow"):
|
||||
integration.add_error(
|
||||
"config_flow",
|
||||
"Config flows need to be defined in the file config_flow.py",
|
||||
)
|
||||
if integration.get("homekit"):
|
||||
integration.add_error(
|
||||
"config_flow",
|
||||
"HomeKit information in a manifest requires a config flow to exist",
|
||||
)
|
||||
if integration.get("ssdp"):
|
||||
integration.add_error(
|
||||
"config_flow",
|
||||
"SSDP information in a manifest requires a config flow to exist",
|
||||
)
|
||||
if integration.get("zeroconf"):
|
||||
integration.add_error(
|
||||
"config_flow",
|
||||
"Zeroconf information in a manifest requires a config flow to exist",
|
||||
)
|
||||
return
|
||||
|
||||
config_flow = config_flow_file.read_text()
|
||||
|
||||
needs_unique_id = integration.domain not in UNIQUE_ID_IGNORE and (
|
||||
"async_step_hassio" in config_flow
|
||||
or any(
|
||||
bool(integration.manifest.get(key))
|
||||
for keys in DISCOVERY_INTEGRATIONS.values()
|
||||
for key in keys
|
||||
)
|
||||
"async_step_discovery" in config_flow
|
||||
or "async_step_hassio" in config_flow
|
||||
or "async_step_homekit" in config_flow
|
||||
or "async_step_ssdp" in config_flow
|
||||
or "async_step_zeroconf" in config_flow
|
||||
)
|
||||
|
||||
if not needs_unique_id:
|
||||
@ -46,8 +60,9 @@ def validate_integration(config: Config, integration: Integration):
|
||||
|
||||
has_unique_id = (
|
||||
"self.async_set_unique_id" in config_flow
|
||||
or "config_entry_flow.register_discovery_flow" in config_flow
|
||||
or "config_entry_oauth2_flow.AbstractOAuth2FlowHandler" in config_flow
|
||||
or "self._async_handle_discovery_without_unique_id" in config_flow
|
||||
or "register_discovery_flow" in config_flow
|
||||
or "AbstractOAuth2FlowHandler" in config_flow
|
||||
)
|
||||
|
||||
if has_unique_id:
|
||||
@ -73,9 +88,12 @@ def generate_and_validate(integrations: Dict[str, Integration], config: Config):
|
||||
if not integration.manifest:
|
||||
continue
|
||||
|
||||
config_flow = integration.manifest.get("config_flow")
|
||||
|
||||
if not config_flow:
|
||||
if not (
|
||||
integration.manifest.get("config_flow")
|
||||
or integration.manifest.get("homekit")
|
||||
or integration.manifest.get("ssdp")
|
||||
or integration.manifest.get("zeroconf")
|
||||
):
|
||||
continue
|
||||
|
||||
validate_integration(config, integration)
|
||||
|
@ -38,22 +38,6 @@ def generate_and_validate(integrations: Dict[str, Integration]):
|
||||
if not ssdp:
|
||||
continue
|
||||
|
||||
try:
|
||||
with open(str(integration.path / "config_flow.py")) as fp:
|
||||
content = fp.read()
|
||||
if (
|
||||
" async_step_ssdp" not in content
|
||||
and "AbstractOAuth2FlowHandler" not in content
|
||||
and "register_discovery_flow" not in content
|
||||
):
|
||||
integration.add_error("ssdp", "Config flow has no async_step_ssdp")
|
||||
continue
|
||||
except FileNotFoundError:
|
||||
integration.add_error(
|
||||
"ssdp", "SSDP info in a manifest requires a config flow to exist"
|
||||
)
|
||||
continue
|
||||
|
||||
for matcher in ssdp:
|
||||
data[domain].append(sort_dict(matcher))
|
||||
|
||||
|
@ -34,42 +34,7 @@ def generate_and_validate(integrations: Dict[str, Integration]):
|
||||
homekit = integration.manifest.get("homekit", {})
|
||||
homekit_models = homekit.get("models", [])
|
||||
|
||||
if not service_types and not homekit_models:
|
||||
continue
|
||||
|
||||
try:
|
||||
with open(str(integration.path / "config_flow.py")) as fp:
|
||||
content = fp.read()
|
||||
uses_discovery_flow = "register_discovery_flow" in content
|
||||
uses_oauth2_flow = "AbstractOAuth2FlowHandler" in content
|
||||
|
||||
if (
|
||||
service_types
|
||||
and not uses_discovery_flow
|
||||
and not uses_oauth2_flow
|
||||
and " async_step_zeroconf" not in content
|
||||
):
|
||||
integration.add_error(
|
||||
"zeroconf", "Config flow has no async_step_zeroconf"
|
||||
)
|
||||
continue
|
||||
|
||||
if (
|
||||
homekit_models
|
||||
and not uses_discovery_flow
|
||||
and not uses_oauth2_flow
|
||||
and " async_step_homekit" not in content
|
||||
):
|
||||
integration.add_error(
|
||||
"zeroconf", "Config flow has no async_step_homekit"
|
||||
)
|
||||
continue
|
||||
|
||||
except FileNotFoundError:
|
||||
integration.add_error(
|
||||
"zeroconf",
|
||||
"Zeroconf info in a manifest requires a config flow to exist",
|
||||
)
|
||||
if not (service_types or homekit_models):
|
||||
continue
|
||||
|
||||
for service_type in service_types:
|
||||
|
@ -82,7 +82,7 @@ async def test_abort_if_existing_entry(hass):
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "already_setup"
|
||||
|
||||
result = await flow.async_step_hassio()
|
||||
result = await flow.async_step_hassio({})
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "already_setup"
|
||||
|
||||
|
@ -264,10 +264,10 @@ async def test_zeroconf_with_uuid_device_exists_abort(
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_zeroconf_empty_unique_id_required_abort(
|
||||
async def test_zeroconf_empty_unique_id(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test we abort zeroconf flow if printer lacks (empty) unique identification."""
|
||||
"""Test zeroconf flow if printer lacks (empty) unique identification."""
|
||||
mock_connection(aioclient_mock, no_unique_id=True)
|
||||
|
||||
discovery_info = {
|
||||
@ -278,14 +278,13 @@ async def test_zeroconf_empty_unique_id_required_abort(
|
||||
DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info,
|
||||
)
|
||||
|
||||
assert result["type"] == RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "unique_id_required"
|
||||
assert result["type"] == RESULT_TYPE_FORM
|
||||
|
||||
|
||||
async def test_zeroconf_unique_id_required_abort(
|
||||
async def test_zeroconf_no_unique_id(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test we abort zeroconf flow if printer lacks unique identification."""
|
||||
"""Test zeroconf flow if printer lacks unique identification."""
|
||||
mock_connection(aioclient_mock, no_unique_id=True)
|
||||
|
||||
discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy()
|
||||
@ -293,8 +292,7 @@ async def test_zeroconf_unique_id_required_abort(
|
||||
DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info,
|
||||
)
|
||||
|
||||
assert result["type"] == RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "unique_id_required"
|
||||
assert result["type"] == RESULT_TYPE_FORM
|
||||
|
||||
|
||||
async def test_full_user_flow_implementation(
|
||||
|
@ -13,6 +13,7 @@ from homeassistant.components.tellduslive import (
|
||||
SCAN_INTERVAL,
|
||||
config_flow,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_DISCOVERY
|
||||
from homeassistant.const import CONF_HOST
|
||||
|
||||
from tests.common import MockConfigEntry, mock_coro
|
||||
@ -73,6 +74,7 @@ async def test_abort_if_already_setup(hass):
|
||||
async def test_full_flow_implementation(hass, mock_tellduslive):
|
||||
"""Test registering an implementation and finishing flow works."""
|
||||
flow = init_config_flow(hass)
|
||||
flow.context = {"source": SOURCE_DISCOVERY}
|
||||
result = await flow.async_step_discovery(["localhost", "tellstick"])
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "user"
|
||||
@ -166,6 +168,7 @@ async def test_step_import_load_json(hass, mock_tellduslive):
|
||||
async def test_step_disco_no_local_api(hass, mock_tellduslive):
|
||||
"""Test that we trigger when configuring from discovery, not supporting local api."""
|
||||
flow = init_config_flow(hass)
|
||||
flow.context = {"source": SOURCE_DISCOVERY}
|
||||
|
||||
result = await flow.async_step_discovery(["localhost", "tellstick"])
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
@ -242,7 +245,7 @@ async def test_discovery_already_configured(hass, mock_tellduslive):
|
||||
"""Test abort if already configured fires from discovery."""
|
||||
MockConfigEntry(domain="tellduslive", data={"host": "some-host"}).add_to_hass(hass)
|
||||
flow = init_config_flow(hass)
|
||||
flow.context = {"source": SOURCE_DISCOVERY}
|
||||
|
||||
result = await flow.async_step_discovery(["some-host", ""])
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "already_setup"
|
||||
with pytest.raises(data_entry_flow.AbortFlow):
|
||||
result = await flow.async_step_discovery(["some-host", ""])
|
||||
|
@ -554,13 +554,15 @@ async def test_discovery_notification(hass):
|
||||
|
||||
VERSION = 5
|
||||
|
||||
async def async_step_discovery(self, user_input=None):
|
||||
async def async_step_discovery(self, discovery_info):
|
||||
"""Test discovery step."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(
|
||||
title="Test Title", data={"token": "abcd"}
|
||||
)
|
||||
return self.async_show_form(step_id="discovery")
|
||||
return self.async_show_form(step_id="discovery_confirm")
|
||||
|
||||
async def async_step_discovery_confirm(self, discovery_info):
|
||||
"""Test discovery confirm step."""
|
||||
return self.async_create_entry(
|
||||
title="Test Title", data={"token": "abcd"}
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
"test", context={"source": config_entries.SOURCE_DISCOVERY}
|
||||
@ -589,7 +591,7 @@ async def test_discovery_notification_not_created(hass):
|
||||
|
||||
VERSION = 5
|
||||
|
||||
async def async_step_discovery(self, user_input=None):
|
||||
async def async_step_discovery(self, discovery_info):
|
||||
"""Test discovery step."""
|
||||
return self.async_abort(reason="test")
|
||||
|
||||
@ -1447,7 +1449,7 @@ async def test_partial_flows_hidden(hass, manager):
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_discovery(self, user_input):
|
||||
async def async_step_discovery(self, discovery_info):
|
||||
"""Test discovery step."""
|
||||
discovery_started.set()
|
||||
await pause_discovery.wait()
|
||||
@ -1577,3 +1579,182 @@ async def test_async_setup_update_entry(hass):
|
||||
assert len(entries) == 1
|
||||
assert entries[0].state == config_entries.ENTRY_STATE_LOADED
|
||||
assert entries[0].data == {"value": "updated"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"discovery_source",
|
||||
(
|
||||
config_entries.SOURCE_DISCOVERY,
|
||||
config_entries.SOURCE_SSDP,
|
||||
config_entries.SOURCE_HOMEKIT,
|
||||
config_entries.SOURCE_ZEROCONF,
|
||||
config_entries.SOURCE_HASSIO,
|
||||
),
|
||||
)
|
||||
async def test_flow_with_default_discovery(hass, manager, discovery_source):
|
||||
"""Test that finishing a default discovery flow removes the unique ID in the entry."""
|
||||
mock_integration(
|
||||
hass, MockModule("comp", async_setup_entry=AsyncMock(return_value=True)),
|
||||
)
|
||||
mock_entity_platform(hass, "config_flow.comp", None)
|
||||
|
||||
class TestFlow(config_entries.ConfigFlow):
|
||||
"""Test flow."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Test user step."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(step_id="user")
|
||||
|
||||
return self.async_create_entry(title="yo", data={})
|
||||
|
||||
with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}):
|
||||
# Create one to be in progress
|
||||
result = await manager.flow.async_init(
|
||||
"comp", context={"source": discovery_source}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
|
||||
flows = hass.config_entries.flow.async_progress()
|
||||
assert len(flows) == 1
|
||||
assert (
|
||||
flows[0]["context"]["unique_id"]
|
||||
== config_entries.DEFAULT_DISCOVERY_UNIQUE_ID
|
||||
)
|
||||
|
||||
# Finish flow
|
||||
result2 = await manager.flow.async_configure(
|
||||
result["flow_id"], user_input={"fake": "data"}
|
||||
)
|
||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
|
||||
assert len(hass.config_entries.flow.async_progress()) == 0
|
||||
|
||||
entry = hass.config_entries.async_entries("comp")[0]
|
||||
assert entry.title == "yo"
|
||||
assert entry.source == discovery_source
|
||||
assert entry.unique_id is None
|
||||
|
||||
|
||||
async def test_flow_with_default_discovery_with_unique_id(hass, manager):
|
||||
"""Test discovery flow using the default discovery is ignored when unique ID is set."""
|
||||
mock_integration(hass, MockModule("comp"))
|
||||
mock_entity_platform(hass, "config_flow.comp", None)
|
||||
|
||||
class TestFlow(config_entries.ConfigFlow):
|
||||
"""Test flow."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_discovery(self, discovery_info):
|
||||
"""Test discovery step."""
|
||||
await self.async_set_unique_id("mock-unique-id")
|
||||
# This call should make no difference, as a unique ID is set
|
||||
await self._async_handle_discovery_without_unique_id()
|
||||
return self.async_show_form(step_id="mock")
|
||||
|
||||
with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}):
|
||||
result = await manager.flow.async_init(
|
||||
"comp", context={"source": config_entries.SOURCE_DISCOVERY}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
|
||||
flows = hass.config_entries.flow.async_progress()
|
||||
assert len(flows) == 1
|
||||
assert flows[0]["context"]["unique_id"] == "mock-unique-id"
|
||||
|
||||
|
||||
async def test_default_discovery_abort_existing_entries(hass, manager):
|
||||
"""Test that a flow without discovery implementation aborts when a config entry exists."""
|
||||
hass.config.components.add("comp")
|
||||
entry = MockConfigEntry(domain="comp", data={}, unique_id="mock-unique-id")
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
mock_integration(hass, MockModule("comp"))
|
||||
mock_entity_platform(hass, "config_flow.comp", None)
|
||||
|
||||
class TestFlow(config_entries.ConfigFlow):
|
||||
"""Test flow."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}):
|
||||
result = await manager.flow.async_init(
|
||||
"comp", context={"source": config_entries.SOURCE_DISCOVERY}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_default_discovery_in_progress(hass, manager):
|
||||
"""Test that a flow using default discovery can only be triggered once."""
|
||||
mock_integration(hass, MockModule("comp"))
|
||||
mock_entity_platform(hass, "config_flow.comp", None)
|
||||
|
||||
class TestFlow(config_entries.ConfigFlow):
|
||||
"""Test flow."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_discovery(self, discovery_info):
|
||||
"""Test discovery step."""
|
||||
await self.async_set_unique_id(discovery_info.get("unique_id"))
|
||||
await self._async_handle_discovery_without_unique_id()
|
||||
return self.async_show_form(step_id="mock")
|
||||
|
||||
with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}):
|
||||
result = await manager.flow.async_init(
|
||||
"comp",
|
||||
context={"source": config_entries.SOURCE_DISCOVERY},
|
||||
data={"unique_id": "mock-unique-id"},
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
|
||||
# Second discovery without a unique ID
|
||||
result2 = await manager.flow.async_init(
|
||||
"comp", context={"source": config_entries.SOURCE_DISCOVERY}, data={}
|
||||
)
|
||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
|
||||
flows = hass.config_entries.flow.async_progress()
|
||||
assert len(flows) == 1
|
||||
assert flows[0]["context"]["unique_id"] == "mock-unique-id"
|
||||
|
||||
|
||||
async def test_default_discovery_abort_on_new_unique_flow(hass, manager):
|
||||
"""Test that a flow using default discovery is aborted when a second flow with unique ID is created."""
|
||||
mock_integration(hass, MockModule("comp"))
|
||||
mock_entity_platform(hass, "config_flow.comp", None)
|
||||
|
||||
class TestFlow(config_entries.ConfigFlow):
|
||||
"""Test flow."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_discovery(self, discovery_info):
|
||||
"""Test discovery step."""
|
||||
await self.async_set_unique_id(discovery_info.get("unique_id"))
|
||||
await self._async_handle_discovery_without_unique_id()
|
||||
return self.async_show_form(step_id="mock")
|
||||
|
||||
with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}):
|
||||
# First discovery with default, no unique ID
|
||||
result2 = await manager.flow.async_init(
|
||||
"comp", context={"source": config_entries.SOURCE_DISCOVERY}, data={}
|
||||
)
|
||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
|
||||
# Second discovery brings in a unique ID
|
||||
result = await manager.flow.async_init(
|
||||
"comp",
|
||||
context={"source": config_entries.SOURCE_DISCOVERY},
|
||||
data={"unique_id": "mock-unique-id"},
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
|
||||
# Ensure the first one is cancelled and we end up with just the last one
|
||||
flows = hass.config_entries.flow.async_progress()
|
||||
assert len(flows) == 1
|
||||
assert flows[0]["context"]["unique_id"] == "mock-unique-id"
|
||||
|
Loading…
x
Reference in New Issue
Block a user