diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 81065665e34..dbf0ee8f283 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -33,6 +33,7 @@ async def async_setup(hass): hass.components.websocket_api.async_register_command(config_entries_progress) hass.components.websocket_api.async_register_command(system_options_list) hass.components.websocket_api.async_register_command(system_options_update) + hass.components.websocket_api.async_register_command(ignore_config_flow) return True @@ -284,3 +285,37 @@ async def system_options_update(hass, connection, msg): hass.config_entries.async_update_entry(entry, system_options=changes) connection.send_result(msg["id"], entry.system_options.as_dict()) + + +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command({"type": "config_entries/ignore_flow", "flow_id": str}) +async def ignore_config_flow(hass, connection, msg): + """Ignore a config flow.""" + flow = next( + ( + flw + for flw in hass.config_entries.flow.async_progress() + if flw["flow_id"] == msg["flow_id"] + ), + None, + ) + + if flow is None: + connection.send_error( + msg["id"], websocket_api.const.ERR_NOT_FOUND, "Config entry not found" + ) + return + + if "unique_id" not in flow["context"]: + connection.send_error( + msg["id"], "no_unique_id", "Specified flow has no unique ID." + ) + return + + await hass.config_entries.flow.async_init( + flow["handler"], + context={"source": config_entries.SOURCE_IGNORE}, + data={"unique_id": flow["context"]["unique_id"]}, + ) + connection.send_result(msg["id"]) diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index f2d7c6d1e8a..97cc1a8d66c 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -75,7 +75,7 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="no_bridges") # Find already configured hosts - already_configured = self._async_current_ids() + already_configured = self._async_current_ids(False) bridges = [bridge for bridge in bridges if bridge.id not in already_configured] if not bridges: diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index d39fc4803ea..1a010b38e70 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -24,6 +24,7 @@ SOURCE_IMPORT = "import" SOURCE_SSDP = "ssdp" SOURCE_USER = "user" SOURCE_ZEROCONF = "zeroconf" +SOURCE_IGNORE = "ignore" HANDLERS = Registry() @@ -157,6 +158,9 @@ class ConfigEntry: tries: int = 0, ) -> None: """Set up an entry.""" + if self.source == SOURCE_IGNORE: + return + if integration is None: integration = await loader.async_get_integration(hass, self.domain) @@ -792,12 +796,13 @@ class ConfigFlow(data_entry_flow.FlowHandler): return self.hass.config_entries.async_entries(self.handler) @callback - def _async_current_ids(self) -> Set[Optional[str]]: + def _async_current_ids(self, include_ignore: bool = True) -> Set[Optional[str]]: """Return current unique IDs.""" assert self.hass is not None return set( entry.unique_id for entry in self.hass.config_entries.async_entries(self.handler) + if include_ignore or entry.source != SOURCE_IGNORE ) @callback @@ -810,6 +815,11 @@ class ConfigFlow(data_entry_flow.FlowHandler): if flw["handler"] == self.handler and flw["flow_id"] != self.flow_id ] + async def async_step_ignore(self, user_input: Dict[str, Any]) -> Dict[str, Any]: + """Ignore this config flow.""" + await self.async_set_unique_id(user_input["unique_id"], raise_on_progress=False) + return self.async_create_entry(title="Ignored", data={}) + class OptionsFlowManager: """Flow to set options for a configuration entry.""" diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 6176bd73c52..6631bbf8fbf 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -634,3 +634,42 @@ async def test_update_system_options(hass, hass_ws_client): assert response["success"] assert response["result"]["disable_new_entities"] assert entry.system_options.disable_new_entities + + +async def test_ignore_flow(hass, hass_ws_client): + """Test we can ignore a flow.""" + assert await async_setup_component(hass, "config", {}) + mock_integration(hass, MockModule("test", async_setup_entry=mock_coro_func(True))) + mock_entity_platform(hass, "config_flow.test", None) + + class TestFlow(core_ce.ConfigFlow): + VERSION = 1 + + async def async_step_user(self, user_input=None): + await self.async_set_unique_id("mock-unique-id") + return self.async_show_form(step_id="account", data_schema=vol.Schema({})) + + ws_client = await hass_ws_client(hass) + + with patch.dict(HANDLERS, {"test": TestFlow}): + result = await hass.config_entries.flow.async_init( + "test", context={"source": "user"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + await ws_client.send_json( + { + "id": 5, + "type": "config_entries/ignore_flow", + "flow_id": result["flow_id"], + } + ) + response = await ws_client.receive_json() + + assert response["success"] + + assert len(hass.config_entries.flow.async_progress()) == 0 + + entry = hass.config_entries.async_entries("test")[0] + assert entry.source == "ignore" + assert entry.unique_id == "mock-unique-id" diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index fe9a1f0e32c..64a9c81136a 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -6,7 +6,7 @@ import aiohue import pytest import voluptuous as vol -from homeassistant import data_entry_flow +from homeassistant import config_entries, data_entry_flow from homeassistant.components.hue import config_flow, const from tests.common import MockConfigEntry, mock_coro @@ -95,6 +95,11 @@ async def test_flow_one_bridge_discovered(hass, aioclient_mock): async def test_flow_two_bridges_discovered(hass, aioclient_mock): """Test config flow discovers two bridges.""" + # Add ignored config entry. Should still show up as option. + MockConfigEntry( + domain="hue", source=config_entries.SOURCE_IGNORE, unique_id="bla" + ).add_to_hass(hass) + aioclient_mock.get( const.API_NUPNP, json=[ diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index d83de450227..19f84e94570 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -1146,3 +1146,43 @@ async def test_finish_flow_aborts_progress(hass, manager): assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert len(hass.config_entries.flow.async_progress()) == 0 + + +async def test_unique_id_ignore(hass, manager): + """Test that we can ignore flows that are in progress and have a unique ID.""" + async_setup_entry = MagicMock(return_value=mock_coro(False)) + mock_integration(hass, MockModule("comp", async_setup_entry=async_setup_entry)) + mock_entity_platform(hass, "config_flow.comp", None) + + class TestFlow(config_entries.ConfigFlow): + + VERSION = 1 + + async def async_step_user(self, user_input=None): + await self.async_set_unique_id("mock-unique-id") + return self.async_show_form(step_id="discovery") + + with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}): + # Create one to be in progress + result = await manager.flow.async_init( + "comp", context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result2 = await manager.flow.async_init( + "comp", + context={"source": config_entries.SOURCE_IGNORE}, + data={"unique_id": "mock-unique-id"}, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + # assert len(hass.config_entries.flow.async_progress()) == 0 + + # We should never set up an ignored entry. + assert len(async_setup_entry.mock_calls) == 0 + + entry = hass.config_entries.async_entries("comp")[0] + + assert entry.source == "ignore" + assert entry.unique_id == "mock-unique-id"