diff --git a/homeassistant/components/homeassistant_hardware/firmware_config_flow.py b/homeassistant/components/homeassistant_hardware/firmware_config_flow.py index 8d7a302e786..83031587712 100644 --- a/homeassistant/components/homeassistant_hardware/firmware_config_flow.py +++ b/homeassistant/components/homeassistant_hardware/firmware_config_flow.py @@ -28,12 +28,13 @@ from . import silabs_multiprotocol_addon from .const import OTBR_DOMAIN, ZHA_DOMAIN from .util import ( ApplicationType, + FirmwareInfo, OwningAddon, OwningIntegration, get_otbr_addon_manager, get_zigbee_flasher_addon_manager, guess_hardware_owners, - probe_silabs_firmware_type, + probe_silabs_firmware_info, ) _LOGGER = logging.getLogger(__name__) @@ -52,7 +53,7 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC): """Instantiate base flow.""" super().__init__(*args, **kwargs) - self._probed_firmware_type: ApplicationType | None = None + self._probed_firmware_info: FirmwareInfo | None = None self._device: str | None = None # To be set in a subclass self._hardware_name: str = "unknown" # To be set in a subclass @@ -64,8 +65,8 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC): """Shared translation placeholders.""" placeholders = { "firmware_type": ( - self._probed_firmware_type.value - if self._probed_firmware_type is not None + self._probed_firmware_info.firmware_type.value + if self._probed_firmware_info is not None else "unknown" ), "model": self._hardware_name, @@ -120,39 +121,49 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC): description_placeholders=self._get_translation_placeholders(), ) - async def _probe_firmware_type(self) -> bool: - """Probe the firmware currently on the device.""" - assert self._device is not None - - self._probed_firmware_type = await probe_silabs_firmware_type( - self._device, - probe_methods=( - # We probe in order of frequency: Zigbee, Thread, then multi-PAN - ApplicationType.GECKO_BOOTLOADER, - ApplicationType.EZSP, - ApplicationType.SPINEL, - ApplicationType.CPC, - ), - ) - - return self._probed_firmware_type in ( + async def _probe_firmware_info( + self, + probe_methods: tuple[ApplicationType, ...] = ( + # We probe in order of frequency: Zigbee, Thread, then multi-PAN + ApplicationType.GECKO_BOOTLOADER, ApplicationType.EZSP, ApplicationType.SPINEL, ApplicationType.CPC, + ), + ) -> bool: + """Probe the firmware currently on the device.""" + assert self._device is not None + + self._probed_firmware_info = await probe_silabs_firmware_info( + self._device, + probe_methods=probe_methods, + ) + + return ( + self._probed_firmware_info is not None + and self._probed_firmware_info.firmware_type + in ( + ApplicationType.EZSP, + ApplicationType.SPINEL, + ApplicationType.CPC, + ) ) async def async_step_pick_firmware_zigbee( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Pick Zigbee firmware.""" - if not await self._probe_firmware_type(): + if not await self._probe_firmware_info(): return self.async_abort( reason="unsupported_firmware", description_placeholders=self._get_translation_placeholders(), ) # Allow the stick to be used with ZHA without flashing - if self._probed_firmware_type == ApplicationType.EZSP: + if ( + self._probed_firmware_info is not None + and self._probed_firmware_info.firmware_type == ApplicationType.EZSP + ): return await self.async_step_confirm_zigbee() if not is_hassio(self.hass): @@ -338,7 +349,12 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC): """Confirm Zigbee setup.""" assert self._device is not None assert self._hardware_name is not None - self._probed_firmware_type = ApplicationType.EZSP + + if not await self._probe_firmware_info(probe_methods=(ApplicationType.EZSP,)): + return self.async_abort( + reason="unsupported_firmware", + description_placeholders=self._get_translation_placeholders(), + ) if user_input is not None: await self.hass.config_entries.flow.async_init( @@ -366,7 +382,7 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC): self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Pick Thread firmware.""" - if not await self._probe_firmware_type(): + if not await self._probe_firmware_info(): return self.async_abort( reason="unsupported_firmware", description_placeholders=self._get_translation_placeholders(), @@ -458,7 +474,11 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC): """Confirm OTBR setup.""" assert self._device is not None - self._probed_firmware_type = ApplicationType.SPINEL + if not await self._probe_firmware_info(probe_methods=(ApplicationType.SPINEL,)): + return self.async_abort( + reason="unsupported_firmware", + description_placeholders=self._get_translation_placeholders(), + ) if user_input is not None: # OTBR discovery is done automatically via hassio @@ -497,14 +517,14 @@ class BaseFirmwareConfigFlow(BaseFirmwareInstallFlow, ConfigFlow): class BaseFirmwareOptionsFlow(BaseFirmwareInstallFlow, OptionsFlow): """Zigbee and Thread options flow handlers.""" + _probed_firmware_info: FirmwareInfo + def __init__(self, config_entry: ConfigEntry, *args: Any, **kwargs: Any) -> None: """Instantiate options flow.""" super().__init__(*args, **kwargs) self._config_entry = config_entry - self._probed_firmware_type = ApplicationType(self.config_entry.data["firmware"]) - # Make `context` a regular dictionary self.context = {} diff --git a/homeassistant/components/homeassistant_hardware/util.py b/homeassistant/components/homeassistant_hardware/util.py index 0e1b56b406e..1afb786369e 100644 --- a/homeassistant/components/homeassistant_hardware/util.py +++ b/homeassistant/components/homeassistant_hardware/util.py @@ -249,10 +249,10 @@ async def guess_firmware_info(hass: HomeAssistant, device_path: str) -> Firmware return guesses[-1][0] -async def probe_silabs_firmware_type( +async def probe_silabs_firmware_info( device: str, *, probe_methods: Iterable[ApplicationType] | None = None -) -> ApplicationType | None: - """Probe the running firmware on a Silabs device.""" +) -> FirmwareInfo | None: + """Probe the running firmware on a SiLabs device.""" flasher = Flasher( device=device, **( @@ -270,4 +270,26 @@ async def probe_silabs_firmware_type( if flasher.app_type is None: return None - return ApplicationType.from_flasher_application_type(flasher.app_type) + return FirmwareInfo( + device=device, + firmware_type=ApplicationType.from_flasher_application_type(flasher.app_type), + firmware_version=( + flasher.app_version.orig_version + if flasher.app_version is not None + else None + ), + source="probe", + owners=[], + ) + + +async def probe_silabs_firmware_type( + device: str, *, probe_methods: Iterable[ApplicationType] | None = None +) -> ApplicationType | None: + """Probe the running firmware type on a SiLabs device.""" + + fw_info = await probe_silabs_firmware_info(device, probe_methods=probe_methods) + if fw_info is None: + return None + + return fw_info.firmware_type diff --git a/homeassistant/components/homeassistant_sky_connect/config_flow.py b/homeassistant/components/homeassistant_sky_connect/config_flow.py index b3b4f68ba96..d8446c2d3f9 100644 --- a/homeassistant/components/homeassistant_sky_connect/config_flow.py +++ b/homeassistant/components/homeassistant_sky_connect/config_flow.py @@ -10,7 +10,10 @@ from homeassistant.components.homeassistant_hardware import ( firmware_config_flow, silabs_multiprotocol_addon, ) -from homeassistant.components.homeassistant_hardware.util import ApplicationType +from homeassistant.components.homeassistant_hardware.util import ( + ApplicationType, + FirmwareInfo, +) from homeassistant.config_entries import ( ConfigEntry, ConfigEntryBaseFlow, @@ -118,7 +121,7 @@ class HomeAssistantSkyConnectConfigFlow( """Create the config entry.""" assert self._usb_info is not None assert self._hw_variant is not None - assert self._probed_firmware_type is not None + assert self._probed_firmware_info is not None return self.async_create_entry( title=self._hw_variant.full_name, @@ -130,7 +133,7 @@ class HomeAssistantSkyConnectConfigFlow( "description": self._usb_info.description, # For backwards compatibility "product": self._usb_info.description, "device": self._usb_info.device, - "firmware": self._probed_firmware_type.value, + "firmware": self._probed_firmware_info.firmware_type.value, }, ) @@ -203,18 +206,26 @@ class HomeAssistantSkyConnectOptionsFlowHandler( self._hardware_name = self._hw_variant.full_name self._device = self._usb_info.device + self._probed_firmware_info = FirmwareInfo( + device=self._device, + firmware_type=ApplicationType(self.config_entry.data["firmware"]), + firmware_version=None, + source="guess", + owners=[], + ) + # Regenerate the translation placeholders self._get_translation_placeholders() def _async_flow_finished(self) -> ConfigFlowResult: """Create the config entry.""" - assert self._probed_firmware_type is not None + assert self._probed_firmware_info is not None self.hass.config_entries.async_update_entry( entry=self.config_entry, data={ **self.config_entry.data, - "firmware": self._probed_firmware_type.value, + "firmware": self._probed_firmware_info.firmware_type.value, }, options=self.config_entry.options, ) diff --git a/homeassistant/components/homeassistant_yellow/config_flow.py b/homeassistant/components/homeassistant_yellow/config_flow.py index 502a20db07c..b916c6e46ca 100644 --- a/homeassistant/components/homeassistant_yellow/config_flow.py +++ b/homeassistant/components/homeassistant_yellow/config_flow.py @@ -24,7 +24,10 @@ from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon OptionsFlowHandler as MultiprotocolOptionsFlowHandler, SerialPortSettings as MultiprotocolSerialPortSettings, ) -from homeassistant.components.homeassistant_hardware.util import ApplicationType +from homeassistant.components.homeassistant_hardware.util import ( + ApplicationType, + FirmwareInfo, +) from homeassistant.config_entries import ( SOURCE_HARDWARE, ConfigEntry, @@ -79,10 +82,13 @@ class HomeAssistantYellowConfigFlow(BaseFirmwareConfigFlow, domain=DOMAIN): ) -> ConfigFlowResult: """Handle the initial step.""" # We do not actually use any portion of `BaseFirmwareConfigFlow` beyond this - await self._probe_firmware_type() + await self._probe_firmware_info() # Kick off ZHA hardware discovery automatically if Zigbee firmware is running - if self._probed_firmware_type is ApplicationType.EZSP: + if ( + self._probed_firmware_info is not None + and self._probed_firmware_info.firmware_type is ApplicationType.EZSP + ): discovery_flow.async_create_flow( self.hass, ZHA_DOMAIN, @@ -98,7 +104,11 @@ class HomeAssistantYellowConfigFlow(BaseFirmwareConfigFlow, domain=DOMAIN): title=BOARD_NAME, data={ # Assume the firmware type is EZSP if we cannot probe it - FIRMWARE: (self._probed_firmware_type or ApplicationType.EZSP).value, + FIRMWARE: ( + self._probed_firmware_info.firmware_type + if self._probed_firmware_info is not None + else ApplicationType.EZSP + ).value, }, ) @@ -264,6 +274,14 @@ class HomeAssistantYellowOptionsFlowHandler( self._hardware_name = BOARD_NAME self._device = RADIO_DEVICE + self._probed_firmware_info = FirmwareInfo( + device=self._device, + firmware_type=ApplicationType(self.config_entry.data["firmware"]), + firmware_version=None, + source="guess", + owners=[], + ) + # Regenerate the translation placeholders self._get_translation_placeholders() @@ -285,13 +303,13 @@ class HomeAssistantYellowOptionsFlowHandler( def _async_flow_finished(self) -> ConfigFlowResult: """Create the config entry.""" - assert self._probed_firmware_type is not None + assert self._probed_firmware_info is not None self.hass.config_entries.async_update_entry( entry=self.config_entry, data={ **self.config_entry.data, - FIRMWARE: self._probed_firmware_type.value, + FIRMWARE: self._probed_firmware_info.firmware_type.value, }, ) diff --git a/tests/components/homeassistant_hardware/test_config_flow.py b/tests/components/homeassistant_hardware/test_config_flow.py index 3696ea66c03..32c5a381233 100644 --- a/tests/components/homeassistant_hardware/test_config_flow.py +++ b/tests/components/homeassistant_hardware/test_config_flow.py @@ -17,6 +17,7 @@ from homeassistant.components.homeassistant_hardware.firmware_config_flow import ) from homeassistant.components.homeassistant_hardware.util import ( ApplicationType, + FirmwareInfo, get_otbr_addon_manager, get_zigbee_flasher_addon_manager, ) @@ -65,13 +66,13 @@ class FakeFirmwareConfigFlow(BaseFirmwareConfigFlow, domain=TEST_DOMAIN): """Create the config entry.""" assert self._device is not None assert self._hardware_name is not None - assert self._probed_firmware_type is not None + assert self._probed_firmware_info is not None return self.async_create_entry( title=self._hardware_name, data={ "device": self._device, - "firmware": self._probed_firmware_type.value, + "firmware": self._probed_firmware_info.firmware_type.value, "hardware": self._hardware_name, }, ) @@ -87,18 +88,26 @@ class FakeFirmwareOptionsFlowHandler(BaseFirmwareOptionsFlow): self._device = self.config_entry.data["device"] self._hardware_name = self.config_entry.data["hardware"] + self._probed_firmware_info = FirmwareInfo( + device=self._device, + firmware_type=ApplicationType(self.config_entry.data["firmware"]), + firmware_version=None, + source="guess", + owners=[], + ) + # Regenerate the translation placeholders self._get_translation_placeholders() def _async_flow_finished(self) -> ConfigFlowResult: """Create the config entry.""" - assert self._probed_firmware_type is not None + assert self._probed_firmware_info is not None self.hass.config_entries.async_update_entry( entry=self.config_entry, data={ **self.config_entry.data, - "firmware": self._probed_firmware_type.value, + "firmware": self._probed_firmware_info.firmware_type.value, }, options=self.config_entry.options, ) @@ -142,7 +151,7 @@ def mock_addon_info( hass: HomeAssistant, *, is_hassio: bool = True, - app_type: ApplicationType = ApplicationType.EZSP, + app_type: ApplicationType | None = ApplicationType.EZSP, otbr_addon_info: AddonInfo = AddonInfo( available=True, hostname=None, @@ -187,6 +196,17 @@ def mock_addon_info( ) mock_otbr_manager.async_get_addon_info.return_value = otbr_addon_info + if app_type is None: + firmware_info_result = None + else: + firmware_info_result = FirmwareInfo( + device="/dev/ttyUSB0", # Not used + firmware_type=app_type, + firmware_version=None, + owners=[], + source="probe", + ) + with ( patch( "homeassistant.components.homeassistant_hardware.firmware_config_flow.get_otbr_addon_manager", @@ -209,8 +229,8 @@ def mock_addon_info( return_value=is_hassio, ), patch( - "homeassistant.components.homeassistant_hardware.firmware_config_flow.probe_silabs_firmware_type", - return_value=app_type, + "homeassistant.components.homeassistant_hardware.firmware_config_flow.probe_silabs_firmware_info", + return_value=firmware_info_result, ), ): yield mock_otbr_manager, mock_flasher_manager @@ -274,10 +294,14 @@ async def test_config_flow_zigbee(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm_zigbee" - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} - ) - assert result["type"] is FlowResultType.CREATE_ENTRY + with mock_addon_info( + hass, + app_type=ApplicationType.EZSP, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] is FlowResultType.CREATE_ENTRY config_entry = result["result"] assert config_entry.data == { @@ -347,10 +371,14 @@ async def test_config_flow_zigbee_skip_step_if_installed(hass: HomeAssistant) -> result = await hass.config_entries.flow.async_configure(result["flow_id"]) # Done - await hass.async_block_till_done(wait_background_tasks=True) - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "confirm_zigbee" + with mock_addon_info( + hass, + app_type=ApplicationType.EZSP, + ): + await hass.async_block_till_done(wait_background_tasks=True) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "confirm_zigbee" async def test_config_flow_thread(hass: HomeAssistant) -> None: @@ -419,17 +447,21 @@ async def test_config_flow_thread(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm_otbr" - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} - ) - assert result["type"] is FlowResultType.CREATE_ENTRY + with mock_addon_info( + hass, + app_type=ApplicationType.SPINEL, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] is FlowResultType.CREATE_ENTRY - config_entry = result["result"] - assert config_entry.data == { - "firmware": "spinel", - "device": TEST_DEVICE, - "hardware": TEST_HARDWARE_NAME, - } + config_entry = result["result"] + assert config_entry.data == { + "firmware": "spinel", + "device": TEST_DEVICE, + "hardware": TEST_HARDWARE_NAME, + } async def test_config_flow_thread_addon_already_installed(hass: HomeAssistant) -> None: @@ -477,10 +509,14 @@ async def test_config_flow_thread_addon_already_installed(hass: HomeAssistant) - assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm_otbr" - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} - ) - assert result["type"] is FlowResultType.CREATE_ENTRY + with mock_addon_info( + hass, + app_type=ApplicationType.SPINEL, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_config_flow_zigbee_not_hassio(hass: HomeAssistant) -> None: @@ -501,10 +537,10 @@ async def test_config_flow_zigbee_not_hassio(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm_zigbee" - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} - ) - assert result["type"] is FlowResultType.CREATE_ENTRY + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] is FlowResultType.CREATE_ENTRY config_entry = result["result"] assert config_entry.data == { @@ -538,17 +574,17 @@ async def test_options_flow_zigbee_to_thread(hass: HomeAssistant) -> None: assert await hass.config_entries.async_setup(config_entry.entry_id) - # First step is confirmation - result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] is FlowResultType.MENU - assert result["step_id"] == "pick_firmware" - assert result["description_placeholders"]["firmware_type"] == "ezsp" - assert result["description_placeholders"]["model"] == TEST_HARDWARE_NAME - with mock_addon_info( hass, app_type=ApplicationType.EZSP, ) as (mock_otbr_manager, mock_flasher_manager): + # First step is confirmation + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "pick_firmware" + assert result["description_placeholders"]["firmware_type"] == "ezsp" + assert result["description_placeholders"]["model"] == TEST_HARDWARE_NAME + result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"next_step_id": STEP_PICK_FIRMWARE_THREAD}, @@ -599,14 +635,18 @@ async def test_options_flow_zigbee_to_thread(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm_otbr" - # We are now done - result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={} - ) - assert result["type"] is FlowResultType.CREATE_ENTRY + with mock_addon_info( + hass, + app_type=ApplicationType.SPINEL, + ): + # We are now done + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] is FlowResultType.CREATE_ENTRY - # The firmware type has been updated - assert config_entry.data["firmware"] == "spinel" + # The firmware type has been updated + assert config_entry.data["firmware"] == "spinel" async def test_options_flow_thread_to_zigbee(hass: HomeAssistant) -> None: @@ -680,11 +720,15 @@ async def test_options_flow_thread_to_zigbee(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm_zigbee" - # We are now done - result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={} - ) - assert result["type"] is FlowResultType.CREATE_ENTRY + with mock_addon_info( + hass, + app_type=ApplicationType.EZSP, + ): + # We are now done + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] is FlowResultType.CREATE_ENTRY - # The firmware type has been updated - assert config_entry.data["firmware"] == "ezsp" + # The firmware type has been updated + assert config_entry.data["firmware"] == "ezsp" diff --git a/tests/components/homeassistant_hardware/test_config_flow_failures.py b/tests/components/homeassistant_hardware/test_config_flow_failures.py index c240d0198ca..8c2ee4b90ba 100644 --- a/tests/components/homeassistant_hardware/test_config_flow_failures.py +++ b/tests/components/homeassistant_hardware/test_config_flow_failures.py @@ -309,6 +309,42 @@ async def test_config_flow_zigbee_flasher_uninstall_fails(hass: HomeAssistant) - assert result["step_id"] == "confirm_zigbee" +@pytest.mark.parametrize( + "ignore_translations", + ["component.test_firmware_domain.config.abort.unsupported_firmware"], +) +async def test_config_flow_zigbee_confirmation_fails(hass: HomeAssistant) -> None: + """Test the config flow failing due to Zigbee firmware not being detected.""" + result = await hass.config_entries.flow.async_init( + TEST_DOMAIN, context={"source": "hardware"} + ) + + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "pick_firmware" + + with mock_addon_info( + hass, + app_type=ApplicationType.EZSP, + ) as (mock_otbr_manager, mock_flasher_manager): + # Pick the menu option: we are now installing the addon + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_ZIGBEE}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "confirm_zigbee" + + with mock_addon_info( + hass, + app_type=None, # Probing fails + ) as (mock_otbr_manager, mock_flasher_manager): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "unsupported_firmware" + + @pytest.mark.parametrize( "ignore_translations", ["component.test_firmware_domain.config.abort.not_hassio_thread"], @@ -530,6 +566,48 @@ async def test_config_flow_thread_flasher_uninstall_fails(hass: HomeAssistant) - assert result["step_id"] == "confirm_otbr" +@pytest.mark.parametrize( + "ignore_translations", + ["component.test_firmware_domain.config.abort.unsupported_firmware"], +) +async def test_config_flow_thread_confirmation_fails(hass: HomeAssistant) -> None: + """Test the config flow failing due to OpenThread firmware not being detected.""" + result = await hass.config_entries.flow.async_init( + TEST_DOMAIN, context={"source": "hardware"} + ) + + with mock_addon_info( + hass, + app_type=ApplicationType.EZSP, + ) as (mock_otbr_manager, mock_flasher_manager): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_THREAD}, + ) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + await hass.async_block_till_done(wait_background_tasks=True) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + await hass.async_block_till_done(wait_background_tasks=True) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "confirm_otbr" + + with mock_addon_info( + hass, + app_type=None, # Probing fails + ) as (mock_otbr_manager, mock_flasher_manager): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "unsupported_firmware" + + @pytest.mark.parametrize( "ignore_translations", ["component.test_firmware_domain.options.abort.zha_still_using_stick"], diff --git a/tests/components/homeassistant_hardware/test_util.py b/tests/components/homeassistant_hardware/test_util.py index 52739f16886..b467380c431 100644 --- a/tests/components/homeassistant_hardware/test_util.py +++ b/tests/components/homeassistant_hardware/test_util.py @@ -2,6 +2,10 @@ from unittest.mock import AsyncMock, MagicMock, patch +import pytest +from universal_silabs_flasher.common import Version as FlasherVersion +from universal_silabs_flasher.const import ApplicationType as FlasherApplicationType + from homeassistant.components.hassio import ( AddonError, AddonInfo, @@ -18,6 +22,8 @@ from homeassistant.components.homeassistant_hardware.util import ( OwningIntegration, get_otbr_addon_firmware_info, guess_firmware_info, + probe_silabs_firmware_info, + probe_silabs_firmware_type, ) from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.core import HomeAssistant @@ -280,3 +286,95 @@ async def test_get_otbr_addon_firmware_info_failure_bad_options( ) assert (await get_otbr_addon_firmware_info(hass, otbr_addon_manager)) is None + + +@pytest.mark.parametrize( + ("app_type", "firmware_version", "expected_fw_info"), + [ + ( + FlasherApplicationType.EZSP, + FlasherVersion("1.0.0"), + FirmwareInfo( + device="/dev/ttyUSB0", + firmware_type=ApplicationType.EZSP, + firmware_version="1.0.0", + source="probe", + owners=[], + ), + ), + ( + FlasherApplicationType.EZSP, + None, + FirmwareInfo( + device="/dev/ttyUSB0", + firmware_type=ApplicationType.EZSP, + firmware_version=None, + source="probe", + owners=[], + ), + ), + ( + FlasherApplicationType.SPINEL, + FlasherVersion("2.0.0"), + FirmwareInfo( + device="/dev/ttyUSB0", + firmware_type=ApplicationType.SPINEL, + firmware_version="2.0.0", + source="probe", + owners=[], + ), + ), + (None, None, None), + ], +) +async def test_probe_silabs_firmware_info( + app_type: FlasherApplicationType | None, + firmware_version: FlasherVersion | None, + expected_fw_info: FirmwareInfo | None, +) -> None: + """Test getting the firmware info.""" + + def probe_app_type() -> None: + mock_flasher.app_type = app_type + mock_flasher.app_version = firmware_version + + mock_flasher = MagicMock() + mock_flasher.app_type = None + mock_flasher.app_version = None + mock_flasher.probe_app_type = AsyncMock(side_effect=probe_app_type) + + with patch( + "homeassistant.components.homeassistant_hardware.util.Flasher", + return_value=mock_flasher, + ): + result = await probe_silabs_firmware_info("/dev/ttyUSB0") + assert result == expected_fw_info + + +@pytest.mark.parametrize( + ("probe_result", "expected"), + [ + ( + FirmwareInfo( + device="/dev/ttyUSB0", + firmware_type=ApplicationType.EZSP, + firmware_version=None, + source="unknown", + owners=[], + ), + ApplicationType.EZSP, + ), + (None, None), + ], +) +async def test_probe_silabs_firmware_type( + probe_result: FirmwareInfo | None, expected: ApplicationType | None +) -> None: + """Test getting the firmware type from the probe result.""" + with patch( + "homeassistant.components.homeassistant_hardware.util.probe_silabs_firmware_info", + autospec=True, + return_value=probe_result, + ): + result = await probe_silabs_firmware_type("/dev/ttyUSB0") + assert result == expected diff --git a/tests/components/homeassistant_sky_connect/test_config_flow.py b/tests/components/homeassistant_sky_connect/test_config_flow.py index 904fcac321c..d8542002ae8 100644 --- a/tests/components/homeassistant_sky_connect/test_config_flow.py +++ b/tests/components/homeassistant_sky_connect/test_config_flow.py @@ -13,6 +13,10 @@ from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon get_flasher_addon_manager, get_multiprotocol_addon_manager, ) +from homeassistant.components.homeassistant_hardware.util import ( + ApplicationType, + FirmwareInfo, +) from homeassistant.components.homeassistant_sky_connect.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -61,10 +65,22 @@ async def test_config_flow( async def mock_async_step_pick_firmware_zigbee(self, data): return await self.async_step_confirm_zigbee(user_input={}) - with patch( - "homeassistant.components.homeassistant_hardware.firmware_config_flow.BaseFirmwareConfigFlow.async_step_pick_firmware_zigbee", - autospec=True, - side_effect=mock_async_step_pick_firmware_zigbee, + with ( + patch( + "homeassistant.components.homeassistant_hardware.firmware_config_flow.BaseFirmwareConfigFlow.async_step_pick_firmware_zigbee", + autospec=True, + side_effect=mock_async_step_pick_firmware_zigbee, + ), + patch( + "homeassistant.components.homeassistant_hardware.firmware_config_flow.probe_silabs_firmware_info", + return_value=FirmwareInfo( + device=usb_data.device, + firmware_type=ApplicationType.EZSP, + firmware_version=None, + owners=[], + source="probe", + ), + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -134,10 +150,22 @@ async def test_options_flow( async def mock_async_step_pick_firmware_zigbee(self, data): return await self.async_step_confirm_zigbee(user_input={}) - with patch( - "homeassistant.components.homeassistant_hardware.firmware_config_flow.BaseFirmwareOptionsFlow.async_step_pick_firmware_zigbee", - autospec=True, - side_effect=mock_async_step_pick_firmware_zigbee, + with ( + patch( + "homeassistant.components.homeassistant_hardware.firmware_config_flow.BaseFirmwareOptionsFlow.async_step_pick_firmware_zigbee", + autospec=True, + side_effect=mock_async_step_pick_firmware_zigbee, + ), + patch( + "homeassistant.components.homeassistant_hardware.firmware_config_flow.probe_silabs_firmware_info", + return_value=FirmwareInfo( + device=usb_data.device, + firmware_type=ApplicationType.EZSP, + firmware_version=None, + owners=[], + source="probe", + ), + ), ): result = await hass.config_entries.options.async_configure( result["flow_id"], diff --git a/tests/components/homeassistant_yellow/test_config_flow.py b/tests/components/homeassistant_yellow/test_config_flow.py index 1067be7b56e..78fd45c6b5b 100644 --- a/tests/components/homeassistant_yellow/test_config_flow.py +++ b/tests/components/homeassistant_yellow/test_config_flow.py @@ -18,7 +18,10 @@ from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon get_flasher_addon_manager, get_multiprotocol_addon_manager, ) -from homeassistant.components.homeassistant_hardware.util import ApplicationType +from homeassistant.components.homeassistant_hardware.util import ( + ApplicationType, + FirmwareInfo, +) from homeassistant.components.homeassistant_yellow.const import DOMAIN, RADIO_DEVICE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -82,8 +85,14 @@ async def test_config_flow(hass: HomeAssistant) -> None: return_value=True, ) as mock_setup_entry, patch( - "homeassistant.components.homeassistant_hardware.firmware_config_flow.probe_silabs_firmware_type", - return_value=ApplicationType.EZSP, + "homeassistant.components.homeassistant_hardware.firmware_config_flow.probe_silabs_firmware_info", + return_value=FirmwareInfo( + device=RADIO_DEVICE, + firmware_type=ApplicationType.EZSP, + firmware_version=None, + owners=[], + source="probe", + ), ), ): result = await hass.config_entries.flow.async_init( @@ -330,10 +339,22 @@ async def test_firmware_options_flow(hass: HomeAssistant) -> None: async def mock_async_step_pick_firmware_zigbee(self, data): return await self.async_step_confirm_zigbee(user_input={}) - with patch( - "homeassistant.components.homeassistant_hardware.firmware_config_flow.BaseFirmwareOptionsFlow.async_step_pick_firmware_zigbee", - autospec=True, - side_effect=mock_async_step_pick_firmware_zigbee, + with ( + patch( + "homeassistant.components.homeassistant_hardware.firmware_config_flow.BaseFirmwareOptionsFlow.async_step_pick_firmware_zigbee", + autospec=True, + side_effect=mock_async_step_pick_firmware_zigbee, + ), + patch( + "homeassistant.components.homeassistant_hardware.firmware_config_flow.probe_silabs_firmware_info", + return_value=FirmwareInfo( + device=RADIO_DEVICE, + firmware_type=ApplicationType.EZSP, + firmware_version=None, + owners=[], + source="probe", + ), + ), ): result = await hass.config_entries.options.async_configure( result["flow_id"],