From 25fc54e50e87a316003f8badc571c912b48fafa1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 14 Sep 2026 06:30:52 -0400 Subject: [PATCH] Handle gone next flow in improv_ble provisioning (#182147) Co-authored-by: Claude --- .../components/improv_ble/config_flow.py | 18 ++-- .../components/improv_ble/test_config_flow.py | 85 +++++++++++++++++++ 2 files changed, 97 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/improv_ble/config_flow.py b/homeassistant/components/improv_ble/config_flow.py index 2746c091f6bf..1c668b05c10a 100644 --- a/homeassistant/components/improv_ble/config_flow.py +++ b/homeassistant/components/improv_ble/config_flow.py @@ -28,7 +28,7 @@ from homeassistant.config_entries import ( ) from homeassistant.const import CONF_ADDRESS from homeassistant.core import callback -from homeassistant.data_entry_flow import AbortFlow +from homeassistant.data_entry_flow import AbortFlow, UnknownFlow from homeassistant.helpers.device_registry import format_mac from . import async_get_provisioning_futures @@ -407,11 +407,17 @@ class ImprovBLEConfigFlow(ConfigFlow, domain=DOMAIN): if next_flow_id: _LOGGER.debug("Received next flow ID: %s", next_flow_id) - self._provision_result = self.async_abort( - reason="provision_successful", - next_flow=(FlowType.CONFIG_FLOW, next_flow_id), - ) - return + try: + self._provision_result = self.async_abort( + reason="provision_successful", + next_flow=(FlowType.CONFIG_FLOW, next_flow_id), + ) + except UnknownFlow: + # The other integration aborted its flow, for example + # because the device is already configured + _LOGGER.debug("Next flow %s is gone", next_flow_id) + else: + return if redirect_url: self._provision_result = self.async_abort( diff --git a/tests/components/improv_ble/test_config_flow.py b/tests/components/improv_ble/test_config_flow.py index 73acf905aaee..84e74974b369 100644 --- a/tests/components/improv_ble/test_config_flow.py +++ b/tests/components/improv_ble/test_config_flow.py @@ -1108,6 +1108,91 @@ async def test_flow_chaining_with_redirect_url(hass: HomeAssistant) -> None: assert result["next_flow"] == (FlowType.CONFIG_FLOW, esphome_flow_id) +@pytest.mark.parametrize( + ("redirect_url", "expected_reason"), + [ + pytest.param(None, "provision_successful", id="no_redirect_url"), + pytest.param( + "http://blabla.local", "provision_successful_url", id="redirect_url" + ), + ], +) +async def test_flow_chaining_next_flow_gone( + hass: HomeAssistant, redirect_url: str | None, expected_reason: str +) -> None: + """Test the next flow is aborted before provisioning completes.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=IMPROV_BLE_DISCOVERY_INFO, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + # Confirm bluetooth setup + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + # Start provisioning + with ( + patch( + f"{IMPROV_BLE}.config_flow.ImprovBLEClient.can_identify", + return_value=False, + new_callable=PropertyMock, + ), + patch(f"{IMPROV_BLE}.config_flow.ImprovBLEClient.ensure_connected"), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_ADDRESS: IMPROV_BLE_DISCOVERY_INFO.address}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "provision" + + with ( + patch( + f"{IMPROV_BLE}.config_flow.ImprovBLEClient.need_authorization", + return_value=False, + ), + patch( + f"{IMPROV_BLE}.config_flow.ImprovBLEClient.provision", + return_value=redirect_url, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"ssid": "TestNetwork", "password": "secret"} + ) + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["progress_action"] == "provisioning" + assert result["step_id"] == "do_provision" + + # Yield to allow the background task to create the future + await asyncio.sleep(0) # task is created with eager_start=False + + # Create a dummy target flow using a different device address + target_result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=IMPROV_BLE_DISCOVERY_INFO_TARGET1, + ) + next_config_flow_id = target_result["flow_id"] + + # Simulate another integration registering a flow which it then aborts, + # for example because the device is already configured + improv_ble.async_register_next_flow( + hass, IMPROV_BLE_DISCOVERY_INFO.address, next_config_flow_id + ) + hass.config_entries.flow.async_abort(next_config_flow_id) + + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == expected_reason + assert "next_flow" not in result + + async def test_flow_chaining_future_already_done( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: