ConfigFlow default discovery without unique ID (#36754)

This commit is contained in:
Franck Nijhof 2020-06-15 13:38:38 +02:00 committed by GitHub
parent dfac9c5e03
commit 3cc94f7d6a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 408 additions and 170 deletions

View File

@ -4,5 +4,8 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/abode", "documentation": "https://www.home-assistant.io/integrations/abode",
"requirements": ["abodepy==0.19.0"], "requirements": ["abodepy==0.19.0"],
"codeowners": ["@shred86"] "codeowners": ["@shred86"],
"homekit": {
"models": ["Abode", "Iota"]
}
} }

View File

@ -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. """Prepare configuration for a Hass.io AdGuard Home add-on.
This flow is triggered by the discovery component. This flow is triggered by the discovery component.
@ -113,14 +113,14 @@ class AdGuardHomeFlowHandler(ConfigFlow):
entries = self._async_current_entries() entries = self._async_current_entries()
if not entries: if not entries:
self._hassio_discovery = user_input self._hassio_discovery = discovery_info
return await self.async_step_hassio_confirm() return await self.async_step_hassio_confirm()
cur_entry = entries[0] cur_entry = entries[0]
if ( if (
cur_entry.data[CONF_HOST] == user_input[CONF_HOST] cur_entry.data[CONF_HOST] == discovery_info[CONF_HOST]
and cur_entry.data[CONF_PORT] == user_input[CONF_PORT] and cur_entry.data[CONF_PORT] == discovery_info[CONF_PORT]
): ):
return self.async_abort(reason="single_instance_allowed") return self.async_abort(reason="single_instance_allowed")
@ -133,8 +133,8 @@ class AdGuardHomeFlowHandler(ConfigFlow):
cur_entry, cur_entry,
data={ data={
**cur_entry.data, **cur_entry.data,
CONF_HOST: user_input[CONF_HOST], CONF_HOST: discovery_info[CONF_HOST],
CONF_PORT: user_input[CONF_PORT], CONF_PORT: discovery_info[CONF_PORT],
}, },
) )

View File

@ -23,13 +23,13 @@ class AgentFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Initialize the Agent config flow.""" """Initialize the Agent config flow."""
self.device_config = {} 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.""" """Handle an Agent config flow."""
errors = {} errors = {}
if info is not None: if user_input is not None:
host = info[CONF_HOST] host = user_input[CONF_HOST]
port = info[CONF_PORT] port = user_input[CONF_PORT]
server_origin = generate_url(host, port) server_origin = generate_url(host, port)
agent_client = Agent(server_origin, async_get_clientsession(self.hass)) 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( self._abort_if_unique_id_configured(
updates={ updates={
CONF_HOST: info[CONF_HOST], CONF_HOST: user_input[CONF_HOST],
CONF_PORT: info[CONF_PORT], CONF_PORT: user_input[CONF_PORT],
SERVER_URL: server_origin, SERVER_URL: server_origin,
} }
) )

View File

@ -94,12 +94,12 @@ class AlmondFlowHandler(config_entry_oauth2_flow.AbstractOAuth2FlowHandler):
data={"type": TYPE_LOCAL, "host": user_input["host"]}, 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.""" """Receive a Hass.io discovery."""
if self._async_current_entries(): if self._async_current_entries():
return self.async_abort(reason="already_setup") return self.async_abort(reason="already_setup")
self.hassio_discovery = user_input self.hassio_discovery = discovery_info
return await self.async_step_hassio_confirm() return await self.async_step_hassio_confirm()

View File

@ -45,21 +45,21 @@ class ArcamFmjFlowHandler(config_entries.ConfigFlow):
title=f"{DEFAULT_NAME} ({host})", data={CONF_HOST: host, CONF_PORT: port}, 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.""" """Handle a discovered device."""
errors = {} errors = {}
if user_info is not None: if user_input is not None:
uuid = await get_uniqueid_from_host( 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: if uuid:
await self._async_set_unique_id_and_update( 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( return await self._async_check_and_create(
user_info[CONF_HOST], user_info[CONF_PORT] user_input[CONF_HOST], user_input[CONF_PORT]
) )
fields = { fields = {

View File

@ -69,16 +69,18 @@ class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
step_id="user", data_schema=DATA_SCHEMA, errors=errors 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.""" """Handle zeroconf discovery."""
if user_input is None: if discovery_info is None:
return self.async_abort(reason="connection_error") 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") return self.async_abort(reason="not_brother_printer")
# Hostname is format: brother.local. # Hostname is format: brother.local.
self.host = user_input["hostname"].rstrip(".") self.host = discovery_info["hostname"].rstrip(".")
self.brother = Brother(self.host) self.brother = Brother(self.host)
try: try:

View File

@ -128,7 +128,7 @@ class FlowHandler(config_entries.ConfigFlow):
async def async_step_zeroconf(self, discovery_info): async def async_step_zeroconf(self, discovery_info):
"""Prepare configuration for a discovered Daikin device.""" """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]) devices = Discovery.poll(discovery_info[CONF_HOST])
await self.async_set_unique_id(next(iter(devices.values()))[KEY_MAC]) await self.async_set_unique_id(next(iter(devices.values()))[KEY_MAC])
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()

View File

@ -205,25 +205,25 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return await self.async_step_link() 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. """Prepare configuration for a Hass.io deCONZ bridge.
This flow is triggered by the discovery component. 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) await self.async_set_unique_id(self.bridge_id)
self._abort_if_unique_id_configured( self._abort_if_unique_id_configured(
updates={ updates={
CONF_HOST: user_input[CONF_HOST], CONF_HOST: discovery_info[CONF_HOST],
CONF_PORT: user_input[CONF_PORT], CONF_PORT: discovery_info[CONF_PORT],
CONF_API_KEY: user_input[CONF_API_KEY], 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() return await self.async_step_hassio_confirm()

View File

@ -29,7 +29,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
async def async_step_user( async def async_step_user(
self, user_input: Optional[ConfigType] = None, error: Optional[str] = None self, user_input: Optional[ConfigType] = None, error: Optional[str] = None
): ): # pylint: disable=arguments-differ
"""Handle a flow initialized by the user.""" """Handle a flow initialized by the user."""
if user_input is not None: if user_input is not None:
return await self._async_authenticate_or_add(user_input) return await self._async_authenticate_or_add(user_input)

View File

@ -55,7 +55,7 @@ class FlickConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
else: else:
return token is not None 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.""" """Handle gathering login info."""
errors = {} errors = {}
if user_input is not None: if user_input is not None:

View File

@ -105,6 +105,6 @@ class FreeboxFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Import a config entry.""" """Import a config entry."""
return await self.async_step_user(user_input) 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.""" """Initialize step from discovery."""
return await self.async_step_user(user_input) return await self.async_step_user(discovery_info)

View File

@ -110,12 +110,12 @@ class FritzboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
step_id="user", data_schema=DATA_SCHEMA_USER, errors=errors 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.""" """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 self.context[CONF_HOST] = host
uuid = user_input.get(ATTR_UPNP_UDN) uuid = discovery_info.get(ATTR_UPNP_UDN)
if uuid: if uuid:
if uuid.startswith("uuid:"): if uuid.startswith("uuid:"):
uuid = uuid[5:] uuid = uuid[5:]
@ -134,7 +134,7 @@ class FritzboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_abort(reason="already_configured") return self.async_abort(reason="already_configured")
self._host = host 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} self.context["title_placeholders"] = {"name": self._name}
return await self.async_step_confirm() return await self.async_step_confirm()

View File

@ -80,7 +80,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
title=info[CONF_UID], data={CONF_UID: info["uid"], **user_input} 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.""" """Handle the configuration via zeroconf."""
if discovery_info is None: if discovery_info is None:
return self.async_abort(reason="connection_error") return self.async_abort(reason="connection_error")

View File

@ -152,8 +152,8 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN):
_LOGGER.debug( _LOGGER.debug(
"Unable to determine unique id from discovery info and IPP response" "Unable to determine unique id from discovery info and IPP response"
) )
return self.async_abort(reason="unique_id_required")
if unique_id:
await self.async_set_unique_id(unique_id) await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured( self._abort_if_unique_id_configured(
updates={ updates={
@ -162,6 +162,7 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN):
}, },
) )
await self._async_handle_discovery_without_unique_id()
return await self.async_step_zeroconf_confirm() return await self.async_step_zeroconf_confirm()
async def async_step_zeroconf_confirm( async def async_step_zeroconf_confirm(

View File

@ -74,12 +74,12 @@ class FlowHandler(config_entries.ConfigFlow):
return self.async_create_entry(title="configuration.yaml", data={}) 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.""" """Receive a Hass.io discovery."""
if self._async_current_entries(): if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed") 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() return await self.async_step_hassio_confirm()

View File

@ -68,9 +68,9 @@ class OpenThermGwConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return self._show_form() 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.""" """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): async def async_step_import(self, import_config):
""" """

View File

@ -96,7 +96,9 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
self.client_id = None self.client_id = None
self._manual = False 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.""" """Handle a flow initialized by the user."""
if user_input is not None: if user_input is not None:
return await self.async_step_plex_website_auth() return await self.async_step_plex_website_auth()

View File

@ -116,17 +116,17 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA) 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.""" """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) ip_address = await self.hass.async_add_executor_job(_get_ip, host)
self._host = host self._host = host
self._ip = self.context[CONF_IP_ADDRESS] = ip_address self._ip = self.context[CONF_IP_ADDRESS] = ip_address
self._manufacturer = user_input.get(ATTR_UPNP_MANUFACTURER) self._manufacturer = discovery_info.get(ATTR_UPNP_MANUFACTURER)
self._model = user_input.get(ATTR_UPNP_MODEL_NAME) self._model = discovery_info.get(ATTR_UPNP_MODEL_NAME)
self._name = f"Samsung {self._model}" 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 self._title = self._model
# probably access denied # probably access denied

View File

@ -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.""" """Run when a Tellstick is discovered."""
await self._async_handle_discovery_without_unique_id()
_LOGGER.info("Discovered tellstick device: %s", user_input) _LOGGER.info("Discovered tellstick device: %s", discovery_info)
if supports_local_api(user_input[1]): if supports_local_api(discovery_info[1]):
_LOGGER.info("%s support local API", user_input[1]) _LOGGER.info("%s support local API", discovery_info[1])
self._hosts.append(user_input[0]) self._hosts.append(discovery_info[0])
return await self.async_step_user() return await self.async_step_user()

View File

@ -1,7 +1,7 @@
{ {
"config": { "config": {
"abort": { "abort": {
"already_setup": "TelldusLive is already configured", "already_configured": "TelldusLive is already configured",
"authorize_url_fail": "Unknown error generating an authorize url.", "authorize_url_fail": "Unknown error generating an authorize url.",
"authorize_url_timeout": "Timeout generating authorize url.", "authorize_url_timeout": "Timeout generating authorize url.",
"unknown": "Unknown error occurred" "unknown": "Unknown error occurred"

View File

@ -82,12 +82,12 @@ class FlowHandler(config_entries.ConfigFlow):
step_id="auth", data_schema=vol.Schema(fields), errors=errors 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.""" """Handle homekit discovery."""
await self.async_set_unique_id(user_input["properties"]["id"]) await self.async_set_unique_id(discovery_info["properties"]["id"])
self._abort_if_unique_id_configured({CONF_HOST: user_input["host"]}) 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(): for entry in self._async_current_entries():
if entry.data[CONF_HOST] != host: if entry.data[CONF_HOST] != host:
@ -96,7 +96,7 @@ class FlowHandler(config_entries.ConfigFlow):
# Backwards compat, we update old entries # Backwards compat, we update old entries
if not entry.unique_id: if not entry.unique_id:
self.hass.config_entries.async_update_entry( 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") return self.async_abort(reason="already_configured")

View File

@ -21,6 +21,8 @@ _LOGGER = logging.getLogger(__name__)
_UNDEF: dict = {} _UNDEF: dict = {}
SOURCE_DISCOVERY = "discovery" SOURCE_DISCOVERY = "discovery"
SOURCE_HASSIO = "hassio"
SOURCE_HOMEKIT = "homekit"
SOURCE_IMPORT = "import" SOURCE_IMPORT = "import"
SOURCE_INTEGRATION_DISCOVERY = "integration_discovery" SOURCE_INTEGRATION_DISCOVERY = "integration_discovery"
SOURCE_SSDP = "ssdp" SOURCE_SSDP = "ssdp"
@ -62,6 +64,7 @@ ENTRY_STATE_FAILED_UNLOAD = "failed_unload"
UNRECOVERABLE_STATES = (ENTRY_STATE_MIGRATION_ERROR, ENTRY_STATE_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_NOTIFICATION_ID = "config_entry_discovery"
DISCOVERY_SOURCES = ( DISCOVERY_SOURCES = (
SOURCE_SSDP, SOURCE_SSDP,
@ -466,6 +469,10 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager):
): ):
self.async_abort(progress_flow["flow_id"]) 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. # Find existing entry.
for check_entry in self.config_entries.async_entries(result["handler"]): for check_entry in self.config_entries.async_entries(result["handler"]):
if check_entry.unique_id == flow.unique_id: 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") raise data_entry_flow.AbortFlow("already_configured")
async def async_set_unique_id( 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]: ) -> Optional[ConfigEntry]:
"""Set a unique ID for the config flow. """Set a unique ID for the config flow.
Returns optionally existing config entry with same ID. 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: if raise_on_progress:
for progress in self._async_in_progress(): for progress in self._async_in_progress():
if progress["context"].get("unique_id") == unique_id: 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 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(): for entry in self._async_current_entries():
if entry.unique_id == unique_id: if entry.unique_id == unique_id:
return entry return entry
@ -911,6 +929,49 @@ class ConfigFlow(data_entry_flow.FlowHandler):
"""Rediscover a config entry by it's unique_id.""" """Rediscover a config entry by it's unique_id."""
return self.async_abort(reason="not_implemented") 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): class OptionsFlowManager(data_entry_flow.FlowManager):
"""Flow to set options for a configuration entry.""" """Flow to set options for a configuration entry."""

View File

@ -57,8 +57,10 @@ ZEROCONF = {
HOMEKIT = { HOMEKIT = {
"819LMB": "myq", "819LMB": "myq",
"AC02": "tado", "AC02": "tado",
"Abode": "abode",
"BSB002": "hue", "BSB002": "hue",
"Healty Home Coach": "netatmo", "Healty Home Coach": "netatmo",
"Iota": "abode",
"LIFX": "lifx", "LIFX": "lifx",
"Netatmo Relay": "netatmo", "Netatmo Relay": "netatmo",
"PowerView": "hunterdouglas_powerview", "PowerView": "hunterdouglas_powerview",

View File

@ -1,5 +1,5 @@
"""Helpers for data entry flows for config entries.""" """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 from homeassistant import config_entries
@ -28,7 +28,9 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow):
self._discovery_function = discovery_function self._discovery_function = discovery_function
self.CONNECTION_CLASS = connection_class # pylint: disable=invalid-name 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.""" """Handle a flow initialized by the user."""
if self._async_current_entries(): if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed") return self.async_abort(reason="single_instance_allowed")
@ -37,7 +39,9 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow):
return await self.async_step_confirm() 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.""" """Confirm setup."""
if user_input is None: if user_input is None:
return self.async_show_form(step_id="confirm") return self.async_show_form(step_id="confirm")
@ -48,7 +52,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow):
has_devices = in_progress has_devices = in_progress
if not has_devices: 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 self._discovery_function, self.hass
) )
@ -56,6 +60,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow):
return self.async_abort(reason="no_devices_found") return self.async_abort(reason="no_devices_found")
# Cancel the discovered one. # Cancel the discovered one.
assert self.hass is not None
for flow in in_progress: for flow in in_progress:
self.hass.config_entries.flow.async_abort(flow["flow_id"]) 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={}) 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.""" """Handle a flow initialized by discovery."""
if self._async_in_progress() or self._async_current_entries(): if self._async_in_progress() or self._async_current_entries():
return self.async_abort(reason="single_instance_allowed") 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_ssdp = async_step_discovery
async_step_homekit = 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.""" """Handle a flow initialized by import."""
if self._async_current_entries(): if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed") return self.async_abort(reason="single_instance_allowed")
# Cancel other flows. # Cancel other flows.
assert self.hass is not None
in_progress = self._async_in_progress() in_progress = self._async_in_progress()
for flow in in_progress: for flow in in_progress:
self.hass.config_entries.flow.async_abort(flow["flow_id"]) 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._description_placeholder = description_placeholder
self._allow_multiple = allow_multiple 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.""" """Handle a user initiated set up flow to create a webhook."""
if not self._allow_multiple and self._async_current_entries(): if not self._allow_multiple and self._async_current_entries():
return self.async_abort(reason="one_instance_allowed") return self.async_abort(reason="one_instance_allowed")
@ -133,6 +143,7 @@ class WebhookFlowHandler(config_entries.ConfigFlow):
if user_input is None: if user_input is None:
return self.async_show_form(step_id="user") return self.async_show_form(step_id="user")
assert self.hass is not None
webhook_id = self.hass.components.webhook.async_generate_id() webhook_id = self.hass.components.webhook.async_generate_id()
if ( if (

View File

@ -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.""" """Create an entry for auth."""
# Flow has been triggered by external data # Flow has been triggered by external data
if user_input: if user_input:
@ -243,7 +245,9 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta):
return self.async_external_step(step_id="auth", url=url) 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.""" """Create config entry from external data."""
token = await self.flow_impl.async_resolve_external_data(self.external_data) token = await self.flow_impl.async_resolve_external_data(self.external_data)
token["expires_at"] = time.time() + token["expires_in"] 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) 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.""" """Handle a flow initialized by discovery."""
await self.async_set_unique_id(self.DOMAIN) await self.async_set_unique_id(self.DOMAIN)

View File

@ -2,8 +2,6 @@
import json import json
from typing import Dict from typing import Dict
from homeassistant.requirements import DISCOVERY_INTEGRATIONS
from .model import Config, Integration from .model import Config, Integration
BASE = """ BASE = """
@ -25,20 +23,36 @@ def validate_integration(config: Config, integration: Integration):
config_flow_file = integration.path / "config_flow.py" config_flow_file = integration.path / "config_flow.py"
if not config_flow_file.is_file(): if not config_flow_file.is_file():
if integration.get("config_flow"):
integration.add_error( integration.add_error(
"config_flow", "Config flows need to be defined in the file config_flow.py" "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 return
config_flow = config_flow_file.read_text() config_flow = config_flow_file.read_text()
needs_unique_id = integration.domain not in UNIQUE_ID_IGNORE and ( needs_unique_id = integration.domain not in UNIQUE_ID_IGNORE and (
"async_step_hassio" in config_flow "async_step_discovery" in config_flow
or any( or "async_step_hassio" in config_flow
bool(integration.manifest.get(key)) or "async_step_homekit" in config_flow
for keys in DISCOVERY_INTEGRATIONS.values() or "async_step_ssdp" in config_flow
for key in keys or "async_step_zeroconf" in config_flow
)
) )
if not needs_unique_id: if not needs_unique_id:
@ -46,8 +60,9 @@ def validate_integration(config: Config, integration: Integration):
has_unique_id = ( has_unique_id = (
"self.async_set_unique_id" in config_flow "self.async_set_unique_id" in config_flow
or "config_entry_flow.register_discovery_flow" in config_flow or "self._async_handle_discovery_without_unique_id" in config_flow
or "config_entry_oauth2_flow.AbstractOAuth2FlowHandler" in config_flow or "register_discovery_flow" in config_flow
or "AbstractOAuth2FlowHandler" in config_flow
) )
if has_unique_id: if has_unique_id:
@ -73,9 +88,12 @@ def generate_and_validate(integrations: Dict[str, Integration], config: Config):
if not integration.manifest: if not integration.manifest:
continue continue
config_flow = integration.manifest.get("config_flow") if not (
integration.manifest.get("config_flow")
if not config_flow: or integration.manifest.get("homekit")
or integration.manifest.get("ssdp")
or integration.manifest.get("zeroconf")
):
continue continue
validate_integration(config, integration) validate_integration(config, integration)

View File

@ -38,22 +38,6 @@ def generate_and_validate(integrations: Dict[str, Integration]):
if not ssdp: if not ssdp:
continue 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: for matcher in ssdp:
data[domain].append(sort_dict(matcher)) data[domain].append(sort_dict(matcher))

View File

@ -34,42 +34,7 @@ def generate_and_validate(integrations: Dict[str, Integration]):
homekit = integration.manifest.get("homekit", {}) homekit = integration.manifest.get("homekit", {})
homekit_models = homekit.get("models", []) homekit_models = homekit.get("models", [])
if not service_types and not homekit_models: if not (service_types or 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",
)
continue continue
for service_type in service_types: for service_type in service_types:

View File

@ -82,7 +82,7 @@ async def test_abort_if_existing_entry(hass):
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "already_setup" 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["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "already_setup" assert result["reason"] == "already_setup"

View File

@ -264,10 +264,10 @@ async def test_zeroconf_with_uuid_device_exists_abort(
assert result["reason"] == "already_configured" 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 hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None: ) -> 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) mock_connection(aioclient_mock, no_unique_id=True)
discovery_info = { discovery_info = {
@ -278,14 +278,13 @@ async def test_zeroconf_empty_unique_id_required_abort(
DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info,
) )
assert result["type"] == RESULT_TYPE_ABORT assert result["type"] == RESULT_TYPE_FORM
assert result["reason"] == "unique_id_required"
async def test_zeroconf_unique_id_required_abort( async def test_zeroconf_no_unique_id(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None: ) -> 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) mock_connection(aioclient_mock, no_unique_id=True)
discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() 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, DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info,
) )
assert result["type"] == RESULT_TYPE_ABORT assert result["type"] == RESULT_TYPE_FORM
assert result["reason"] == "unique_id_required"
async def test_full_user_flow_implementation( async def test_full_user_flow_implementation(

View File

@ -13,6 +13,7 @@ from homeassistant.components.tellduslive import (
SCAN_INTERVAL, SCAN_INTERVAL,
config_flow, config_flow,
) )
from homeassistant.config_entries import SOURCE_DISCOVERY
from homeassistant.const import CONF_HOST from homeassistant.const import CONF_HOST
from tests.common import MockConfigEntry, mock_coro 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): async def test_full_flow_implementation(hass, mock_tellduslive):
"""Test registering an implementation and finishing flow works.""" """Test registering an implementation and finishing flow works."""
flow = init_config_flow(hass) flow = init_config_flow(hass)
flow.context = {"source": SOURCE_DISCOVERY}
result = await flow.async_step_discovery(["localhost", "tellstick"]) result = await flow.async_step_discovery(["localhost", "tellstick"])
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user" 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): async def test_step_disco_no_local_api(hass, mock_tellduslive):
"""Test that we trigger when configuring from discovery, not supporting local api.""" """Test that we trigger when configuring from discovery, not supporting local api."""
flow = init_config_flow(hass) flow = init_config_flow(hass)
flow.context = {"source": SOURCE_DISCOVERY}
result = await flow.async_step_discovery(["localhost", "tellstick"]) result = await flow.async_step_discovery(["localhost", "tellstick"])
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM 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.""" """Test abort if already configured fires from discovery."""
MockConfigEntry(domain="tellduslive", data={"host": "some-host"}).add_to_hass(hass) MockConfigEntry(domain="tellduslive", data={"host": "some-host"}).add_to_hass(hass)
flow = init_config_flow(hass) flow = init_config_flow(hass)
flow.context = {"source": SOURCE_DISCOVERY}
with pytest.raises(data_entry_flow.AbortFlow):
result = await flow.async_step_discovery(["some-host", ""]) result = await flow.async_step_discovery(["some-host", ""])
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "already_setup"

View File

@ -554,13 +554,15 @@ async def test_discovery_notification(hass):
VERSION = 5 VERSION = 5
async def async_step_discovery(self, user_input=None): async def async_step_discovery(self, discovery_info):
"""Test discovery step.""" """Test discovery step."""
if user_input is not None: 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( return self.async_create_entry(
title="Test Title", data={"token": "abcd"} title="Test Title", data={"token": "abcd"}
) )
return self.async_show_form(step_id="discovery")
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
"test", context={"source": config_entries.SOURCE_DISCOVERY} "test", context={"source": config_entries.SOURCE_DISCOVERY}
@ -589,7 +591,7 @@ async def test_discovery_notification_not_created(hass):
VERSION = 5 VERSION = 5
async def async_step_discovery(self, user_input=None): async def async_step_discovery(self, discovery_info):
"""Test discovery step.""" """Test discovery step."""
return self.async_abort(reason="test") return self.async_abort(reason="test")
@ -1447,7 +1449,7 @@ async def test_partial_flows_hidden(hass, manager):
VERSION = 1 VERSION = 1
async def async_step_discovery(self, user_input): async def async_step_discovery(self, discovery_info):
"""Test discovery step.""" """Test discovery step."""
discovery_started.set() discovery_started.set()
await pause_discovery.wait() await pause_discovery.wait()
@ -1577,3 +1579,182 @@ async def test_async_setup_update_entry(hass):
assert len(entries) == 1 assert len(entries) == 1
assert entries[0].state == config_entries.ENTRY_STATE_LOADED assert entries[0].state == config_entries.ENTRY_STATE_LOADED
assert entries[0].data == {"value": "updated"} 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"