From ccf0011ac2d7bb47f0aaec69d7d38754a365707f Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 24 Sep 2025 11:31:04 -0400 Subject: [PATCH] Skip ignored discovery entries when showing migrate/setup config flow steps for ZHA and Hardware (#152895) --- .../firmware_config_flow.py | 8 ++- homeassistant/components/zha/config_flow.py | 16 +++-- .../test_config_flow.py | 64 ++++++++++++++++++- tests/components/zha/test_config_flow.py | 49 ++++++++++++++ 4 files changed, 129 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/homeassistant_hardware/firmware_config_flow.py b/homeassistant/components/homeassistant_hardware/firmware_config_flow.py index 98a2fb2f881..895c7e72618 100644 --- a/homeassistant/components/homeassistant_hardware/firmware_config_flow.py +++ b/homeassistant/components/homeassistant_hardware/firmware_config_flow.py @@ -123,8 +123,12 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC): ) -> ConfigFlowResult: """Pick Thread or Zigbee firmware.""" # Determine if ZHA or Thread are already configured to present migrate options - zha_entries = self.hass.config_entries.async_entries(ZHA_DOMAIN) - otbr_entries = self.hass.config_entries.async_entries(OTBR_DOMAIN) + zha_entries = self.hass.config_entries.async_entries( + ZHA_DOMAIN, include_ignore=False + ) + otbr_entries = self.hass.config_entries.async_entries( + OTBR_DOMAIN, include_ignore=False + ) return self.async_show_menu( step_id="pick_firmware", diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index 4aa5c95accc..5f90a3fc7d6 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -364,7 +364,7 @@ class BaseZhaFlow(ConfigEntryBaseFlow): if user_input is not None or self._radio_mgr.radio_type in RECOMMENDED_RADIOS: # ZHA disables the single instance check and will decide at runtime if we # are migrating or setting up from scratch - if self.hass.config_entries.async_entries(DOMAIN): + if self.hass.config_entries.async_entries(DOMAIN, include_ignore=False): return await self.async_step_choose_migration_strategy() return await self.async_step_choose_setup_strategy() @@ -386,7 +386,7 @@ class BaseZhaFlow(ConfigEntryBaseFlow): # Allow onboarding for new users to just create a new network automatically if ( not onboarding.async_is_onboarded(self.hass) - and not self.hass.config_entries.async_entries(DOMAIN) + and not self.hass.config_entries.async_entries(DOMAIN, include_ignore=False) and not self._radio_mgr.backups ): return await self.async_step_setup_strategy_recommended() @@ -438,7 +438,9 @@ class BaseZhaFlow(ConfigEntryBaseFlow): """Erase the old radio's network settings before migration.""" # Like in the options flow, pull the correct settings from the config entry - config_entries = self.hass.config_entries.async_entries(DOMAIN) + config_entries = self.hass.config_entries.async_entries( + DOMAIN, include_ignore=False + ) if config_entries: assert len(config_entries) == 1 @@ -697,7 +699,9 @@ class ZhaConfigFlowHandler(BaseZhaFlow, ConfigFlow, domain=DOMAIN): self._set_confirm_only() - zha_config_entries = self.hass.config_entries.async_entries(DOMAIN) + zha_config_entries = self.hass.config_entries.async_entries( + DOMAIN, include_ignore=False + ) # Without confirmation, discovery can automatically progress into parts of the # config flow logic that interacts with hardware. @@ -866,7 +870,9 @@ class ZhaConfigFlowHandler(BaseZhaFlow, ConfigFlow, domain=DOMAIN): # ZHA is still single instance only, even though we use discovery to allow for # migrating to a new radio - zha_config_entries = self.hass.config_entries.async_entries(DOMAIN) + zha_config_entries = self.hass.config_entries.async_entries( + DOMAIN, include_ignore=False + ) data = await self._get_config_entry_data() if len(zha_config_entries) == 1: diff --git a/tests/components/homeassistant_hardware/test_config_flow.py b/tests/components/homeassistant_hardware/test_config_flow.py index 296e067ae6b..da81f2bff88 100644 --- a/tests/components/homeassistant_hardware/test_config_flow.py +++ b/tests/components/homeassistant_hardware/test_config_flow.py @@ -26,7 +26,13 @@ from homeassistant.components.homeassistant_hardware.util import ( ApplicationType, FirmwareInfo, ) -from homeassistant.config_entries import ConfigEntry, ConfigFlowResult, OptionsFlow +from homeassistant.config_entries import ( + SOURCE_IGNORE, + SOURCE_USER, + ConfigEntry, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResultType from homeassistant.exceptions import HomeAssistantError @@ -1100,3 +1106,59 @@ async def test_config_flow_thread_migrate_handler(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["progress_action"] == "install_firmware" assert result["step_id"] == "install_thread_firmware" + + +@pytest.mark.parametrize( + ("zha_source", "otbr_source", "expected_menu"), + [ + ( + SOURCE_USER, + SOURCE_USER, + ["pick_firmware_zigbee_migrate", "pick_firmware_thread_migrate"], + ), + ( + SOURCE_IGNORE, + SOURCE_USER, + ["pick_firmware_zigbee", "pick_firmware_thread_migrate"], + ), + ( + SOURCE_USER, + SOURCE_IGNORE, + ["pick_firmware_zigbee_migrate", "pick_firmware_thread"], + ), + ( + SOURCE_IGNORE, + SOURCE_IGNORE, + ["pick_firmware_zigbee", "pick_firmware_thread"], + ), + ], +) +async def test_config_flow_pick_firmware_with_ignored_entries( + hass: HomeAssistant, zha_source: str, otbr_source: str, expected_menu: str +) -> None: + """Test that ignored entries are properly excluded from migration menu options.""" + zha_entry = MockConfigEntry( + domain="zha", + data={"device": {"path": "/dev/ttyUSB1"}}, + title="ZHA", + source=zha_source, + ) + zha_entry.add_to_hass(hass) + + otbr_entry = MockConfigEntry( + domain="otbr", + data={"url": "http://192.168.1.100:8081"}, + title="OTBR", + source=otbr_source, + ) + otbr_entry.add_to_hass(hass) + + # Set up the flow + init_result = await hass.config_entries.flow.async_init( + TEST_DOMAIN, context={"source": "hardware"} + ) + + assert init_result["type"] is FlowResultType.MENU + assert init_result["step_id"] == "pick_firmware" + + assert init_result["menu_options"] == expected_menu diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index c5093dcd400..ff4c7443fa1 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -2378,3 +2378,52 @@ async def test_formation_strategy_restore_manual_backup_overwrite_ieee_ezsp_writ assert mock_restore_backup.call_count == 1 assert mock_restore_backup.mock_calls[0].kwargs["overwrite_ieee"] is True + + +@patch(f"bellows.{PROBE_FUNCTION_PATH}", AsyncMock(return_value=True)) +async def test_migrate_setup_options_with_ignored_discovery( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> None: + """Test that ignored discovery info is migrated to options.""" + + # Ignored ZHA + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="AAAA:AAAA_1234_test_zigbee radio", + data={ + CONF_DEVICE: { + CONF_DEVICE_PATH: "/dev/ttyUSB1", + CONF_BAUDRATE: 115200, + CONF_FLOW_CONTROL: None, + } + }, + source=config_entries.SOURCE_IGNORE, + ) + entry.add_to_hass(hass) + + # Set up one discovery entry + discovery_info = UsbServiceInfo( + device="/dev/ttyZIGBEE", + pid="BBBB", + vid="BBBB", + serial_number="5678", + description="zigbee radio", + manufacturer="test manufacturer", + ) + discovery_result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USB}, data=discovery_info + ) + await hass.async_block_till_done() + + # Progress the discovery + confirm_result = await hass.config_entries.flow.async_configure( + discovery_result["flow_id"], user_input={} + ) + await hass.async_block_till_done() + + # We only show "setup" options, not "migrate" + assert confirm_result["step_id"] == "choose_setup_strategy" + assert confirm_result["menu_options"] == [ + "setup_strategy_recommended", + "setup_strategy_advanced", + ]