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,
"documentation": "https://www.home-assistant.io/integrations/abode",
"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.
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],
},
)

View File

@ -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,
}
)

View File

@ -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()

View File

@ -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 = {

View File

@ -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:

View File

@ -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()

View File

@ -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()

View File

@ -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)

View File

@ -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:

View File

@ -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)

View File

@ -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()

View File

@ -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")

View File

@ -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(

View File

@ -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()

View File

@ -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):
"""

View File

@ -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()

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)
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

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."""
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()

View File

@ -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 @@
}
}
}
}
}

View File

@ -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")

View File

@ -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."""

View File

@ -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",

View File

@ -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 (

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."""
# 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)

View File

@ -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)

View File

@ -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))

View File

@ -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:

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["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"

View File

@ -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(

View File

@ -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", ""])

View File

@ -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"