Allow setup of Zigbee/Thread for ZBT-1 and Yellow without internet access (#147549)

Co-authored-by: Norbert Rittel <norbert@rittel.de>
This commit is contained in:
puddly 2025-06-27 03:50:45 -04:00 committed by Franck Nijhof
parent cb359da79e
commit f93ab8d519
No known key found for this signature in database
GPG Key ID: AB33ADACE7101952
9 changed files with 318 additions and 42 deletions

View File

@ -7,7 +7,10 @@ import asyncio
import logging import logging
from typing import Any from typing import Any
from ha_silabs_firmware_client import FirmwareUpdateClient from aiohttp import ClientError
from ha_silabs_firmware_client import FirmwareUpdateClient, ManifestMissing
from universal_silabs_flasher.common import Version
from universal_silabs_flasher.firmware import NabuCasaMetadata
from homeassistant.components.hassio import ( from homeassistant.components.hassio import (
AddonError, AddonError,
@ -149,15 +152,78 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
assert self._device is not None assert self._device is not None
if not self.firmware_install_task: if not self.firmware_install_task:
session = async_get_clientsession(self.hass) # We 100% need to install new firmware only if the wrong firmware is
client = FirmwareUpdateClient(fw_update_url, session) # currently installed
manifest = await client.async_update_data() firmware_install_required = self._probed_firmware_info is None or (
self._probed_firmware_info.firmware_type
fw_meta = next( != expected_installed_firmware_type
fw for fw in manifest.firmwares if fw.filename.startswith(fw_type)
) )
fw_data = await client.async_fetch_firmware(fw_meta) session = async_get_clientsession(self.hass)
client = FirmwareUpdateClient(fw_update_url, session)
try:
manifest = await client.async_update_data()
fw_manifest = next(
fw for fw in manifest.firmwares if fw.filename.startswith(fw_type)
)
except (StopIteration, TimeoutError, ClientError, ManifestMissing) as err:
_LOGGER.warning(
"Failed to fetch firmware update manifest", exc_info=True
)
# Not having internet access should not prevent setup
if not firmware_install_required:
_LOGGER.debug(
"Skipping firmware upgrade due to index download failure"
)
return self.async_show_progress_done(next_step_id=next_step_id)
raise AbortFlow(
"fw_download_failed",
description_placeholders={
**self._get_translation_placeholders(),
"firmware_name": firmware_name,
},
) from err
if not firmware_install_required:
assert self._probed_firmware_info is not None
# Make sure we do not downgrade the firmware
fw_metadata = NabuCasaMetadata.from_json(fw_manifest.metadata)
fw_version = fw_metadata.get_public_version()
probed_fw_version = Version(self._probed_firmware_info.firmware_version)
if probed_fw_version >= fw_version:
_LOGGER.debug(
"Not downgrading firmware, installed %s is newer than available %s",
probed_fw_version,
fw_version,
)
return self.async_show_progress_done(next_step_id=next_step_id)
try:
fw_data = await client.async_fetch_firmware(fw_manifest)
except (TimeoutError, ClientError, ValueError) as err:
_LOGGER.warning("Failed to fetch firmware update", exc_info=True)
# If we cannot download new firmware, we shouldn't block setup
if not firmware_install_required:
_LOGGER.debug(
"Skipping firmware upgrade due to image download failure"
)
return self.async_show_progress_done(next_step_id=next_step_id)
# Otherwise, fail
raise AbortFlow(
"fw_download_failed",
description_placeholders={
**self._get_translation_placeholders(),
"firmware_name": firmware_name,
},
) from err
self.firmware_install_task = self.hass.async_create_task( self.firmware_install_task = self.hass.async_create_task(
async_flash_silabs_firmware( async_flash_silabs_firmware(
hass=self.hass, hass=self.hass,
@ -215,6 +281,14 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
}, },
) )
async def async_step_pre_confirm_zigbee(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Pre-confirm Zigbee setup."""
# This step is necessary to prevent `user_input` from being passed through
return await self.async_step_confirm_zigbee()
async def async_step_confirm_zigbee( async def async_step_confirm_zigbee(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult: ) -> ConfigFlowResult:
@ -409,7 +483,15 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
finally: finally:
self.addon_start_task = None self.addon_start_task = None
return self.async_show_progress_done(next_step_id="confirm_otbr") return self.async_show_progress_done(next_step_id="pre_confirm_otbr")
async def async_step_pre_confirm_otbr(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Pre-confirm OTBR setup."""
# This step is necessary to prevent `user_input` from being passed through
return await self.async_step_confirm_otbr()
async def async_step_confirm_otbr( async def async_step_confirm_otbr(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None

View File

@ -36,7 +36,8 @@
"otbr_addon_already_running": "The OpenThread Border Router add-on is already running, it cannot be installed again.", "otbr_addon_already_running": "The OpenThread Border Router add-on is already running, it cannot be installed again.",
"zha_still_using_stick": "This {model} is in use by the Zigbee Home Automation integration. Please migrate your Zigbee network to another adapter or delete the integration and try again.", "zha_still_using_stick": "This {model} is in use by the Zigbee Home Automation integration. Please migrate your Zigbee network to another adapter or delete the integration and try again.",
"otbr_still_using_stick": "This {model} is in use by the OpenThread Border Router add-on. If you use the Thread network, make sure you have alternative border routers. Uninstall the add-on and try again.", "otbr_still_using_stick": "This {model} is in use by the OpenThread Border Router add-on. If you use the Thread network, make sure you have alternative border routers. Uninstall the add-on and try again.",
"unsupported_firmware": "The radio firmware on your {model} could not be determined. Make sure that no other integration or add-on is currently trying to communicate with the device. If you are running Home Assistant OS in a virtual machine or in Docker, please make sure that permissions are set correctly for the device." "unsupported_firmware": "The radio firmware on your {model} could not be determined. Make sure that no other integration or add-on is currently trying to communicate with the device. If you are running Home Assistant OS in a virtual machine or in Docker, please make sure that permissions are set correctly for the device.",
"fw_download_failed": "{firmware_name} firmware for your {model} failed to download. Make sure Home Assistant has internet access and try again."
}, },
"progress": { "progress": {
"install_firmware": "Please wait while {firmware_name} firmware is installed to your {model}, this will take a few minutes. Do not make any changes to your hardware or software until this finishes." "install_firmware": "Please wait while {firmware_name} firmware is installed to your {model}, this will take a few minutes. Do not make any changes to your hardware or software until this finishes."

View File

@ -93,7 +93,7 @@ class SkyConnectFirmwareMixin(ConfigEntryBaseFlow, FirmwareInstallFlowProtocol):
firmware_name="Zigbee", firmware_name="Zigbee",
expected_installed_firmware_type=ApplicationType.EZSP, expected_installed_firmware_type=ApplicationType.EZSP,
step_id="install_zigbee_firmware", step_id="install_zigbee_firmware",
next_step_id="confirm_zigbee", next_step_id="pre_confirm_zigbee",
) )
async def async_step_install_thread_firmware( async def async_step_install_thread_firmware(

View File

@ -92,7 +92,8 @@
"otbr_addon_already_running": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_addon_already_running%]", "otbr_addon_already_running": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_addon_already_running%]",
"zha_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::zha_still_using_stick%]", "zha_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::zha_still_using_stick%]",
"otbr_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_still_using_stick%]", "otbr_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_still_using_stick%]",
"unsupported_firmware": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::unsupported_firmware%]" "unsupported_firmware": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::unsupported_firmware%]",
"fw_download_failed": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::fw_download_failed%]"
}, },
"progress": { "progress": {
"install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]", "install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]",
@ -145,7 +146,8 @@
"otbr_addon_already_running": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_addon_already_running%]", "otbr_addon_already_running": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_addon_already_running%]",
"zha_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::zha_still_using_stick%]", "zha_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::zha_still_using_stick%]",
"otbr_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_still_using_stick%]", "otbr_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_still_using_stick%]",
"unsupported_firmware": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::unsupported_firmware%]" "unsupported_firmware": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::unsupported_firmware%]",
"fw_download_failed": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::fw_download_failed%]"
}, },
"progress": { "progress": {
"install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]", "install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]",

View File

@ -117,7 +117,8 @@
"otbr_addon_already_running": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_addon_already_running%]", "otbr_addon_already_running": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_addon_already_running%]",
"zha_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::zha_still_using_stick%]", "zha_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::zha_still_using_stick%]",
"otbr_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_still_using_stick%]", "otbr_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_still_using_stick%]",
"unsupported_firmware": "The radio firmware on your {model} could not be determined. Make sure that no other integration or addon is currently trying to communicate with the device." "unsupported_firmware": "The radio firmware on your {model} could not be determined. Make sure that no other integration or add-on is currently trying to communicate with the device.",
"fw_download_failed": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::fw_download_failed%]"
}, },
"progress": { "progress": {
"install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]", "install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]",

View File

@ -6,6 +6,7 @@ import contextlib
from typing import Any from typing import Any
from unittest.mock import AsyncMock, MagicMock, Mock, call, patch from unittest.mock import AsyncMock, MagicMock, Mock, call, patch
from aiohttp import ClientError
from ha_silabs_firmware_client import ( from ha_silabs_firmware_client import (
FirmwareManifest, FirmwareManifest,
FirmwareMetadata, FirmwareMetadata,
@ -80,7 +81,7 @@ class FakeFirmwareConfigFlow(BaseFirmwareConfigFlow, domain=TEST_DOMAIN):
firmware_name="Zigbee", firmware_name="Zigbee",
expected_installed_firmware_type=ApplicationType.EZSP, expected_installed_firmware_type=ApplicationType.EZSP,
step_id="install_zigbee_firmware", step_id="install_zigbee_firmware",
next_step_id="confirm_zigbee", next_step_id="pre_confirm_zigbee",
) )
async def async_step_install_thread_firmware( async def async_step_install_thread_firmware(
@ -137,7 +138,7 @@ class FakeFirmwareOptionsFlowHandler(BaseFirmwareOptionsFlow):
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult: ) -> ConfigFlowResult:
"""Install Zigbee firmware.""" """Install Zigbee firmware."""
return await self.async_step_confirm_zigbee() return await self.async_step_pre_confirm_zigbee()
async def async_step_install_thread_firmware( async def async_step_install_thread_firmware(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
@ -208,6 +209,7 @@ def mock_firmware_info(
*, *,
is_hassio: bool = True, is_hassio: bool = True,
probe_app_type: ApplicationType | None = ApplicationType.EZSP, probe_app_type: ApplicationType | None = ApplicationType.EZSP,
probe_fw_version: str | None = "2.4.4.0",
otbr_addon_info: AddonInfo = AddonInfo( otbr_addon_info: AddonInfo = AddonInfo(
available=True, available=True,
hostname=None, hostname=None,
@ -217,6 +219,7 @@ def mock_firmware_info(
version=None, version=None,
), ),
flash_app_type: ApplicationType = ApplicationType.EZSP, flash_app_type: ApplicationType = ApplicationType.EZSP,
flash_fw_version: str | None = "7.4.4.0",
) -> Iterator[tuple[Mock, Mock]]: ) -> Iterator[tuple[Mock, Mock]]:
"""Mock the main addon states for the config flow.""" """Mock the main addon states for the config flow."""
mock_otbr_manager = Mock(spec_set=get_otbr_addon_manager(hass)) mock_otbr_manager = Mock(spec_set=get_otbr_addon_manager(hass))
@ -243,7 +246,14 @@ def mock_firmware_info(
checksum="sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", checksum="sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
size=123, size=123,
release_notes="Some release notes", release_notes="Some release notes",
metadata={}, metadata={
"baudrate": 460800,
"fw_type": "openthread_rcp",
"fw_variant": None,
"metadata_version": 2,
"ot_rcp_version": "SL-OPENTHREAD/2.4.4.0_GitHub-7074a43e4",
"sdk_version": "4.4.4",
},
url=TEST_RELEASES_URL / "fake_openthread_rcp_7.4.4.0_variant.gbl", url=TEST_RELEASES_URL / "fake_openthread_rcp_7.4.4.0_variant.gbl",
), ),
FirmwareMetadata( FirmwareMetadata(
@ -251,7 +261,14 @@ def mock_firmware_info(
checksum="sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", checksum="sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
size=123, size=123,
release_notes="Some release notes", release_notes="Some release notes",
metadata={}, metadata={
"baudrate": 115200,
"ezsp_version": "7.4.4.0",
"fw_type": "zigbee_ncp",
"fw_variant": None,
"metadata_version": 2,
"sdk_version": "4.4.4",
},
url=TEST_RELEASES_URL / "fake_zigbee_ncp_7.4.4.0_variant.gbl", url=TEST_RELEASES_URL / "fake_zigbee_ncp_7.4.4.0_variant.gbl",
), ),
], ],
@ -263,7 +280,7 @@ def mock_firmware_info(
probed_firmware_info = FirmwareInfo( probed_firmware_info = FirmwareInfo(
device="/dev/ttyUSB0", # Not used device="/dev/ttyUSB0", # Not used
firmware_type=probe_app_type, firmware_type=probe_app_type,
firmware_version=None, firmware_version=probe_fw_version,
owners=[], owners=[],
source="probe", source="probe",
) )
@ -274,7 +291,7 @@ def mock_firmware_info(
flashed_firmware_info = FirmwareInfo( flashed_firmware_info = FirmwareInfo(
device=TEST_DEVICE, device=TEST_DEVICE,
firmware_type=flash_app_type, firmware_type=flash_app_type,
firmware_version="7.4.4.0", firmware_version=flash_fw_version,
owners=[create_mock_owner()], owners=[create_mock_owner()],
source="probe", source="probe",
) )
@ -333,7 +350,7 @@ def mock_firmware_info(
side_effect=mock_flash_firmware, side_effect=mock_flash_firmware,
), ),
): ):
yield mock_otbr_manager yield mock_otbr_manager, mock_update_client
async def consume_progress_flow( async def consume_progress_flow(
@ -411,6 +428,91 @@ async def test_config_flow_zigbee(hass: HomeAssistant) -> None:
assert zha_flow["step_id"] == "confirm" assert zha_flow["step_id"] == "confirm"
async def test_config_flow_firmware_index_download_fails_but_not_required(
hass: HomeAssistant,
) -> None:
"""Test flow continues if index download fails but install is not required."""
init_result = await hass.config_entries.flow.async_init(
TEST_DOMAIN, context={"source": "hardware"}
)
with mock_firmware_info(
hass,
# The correct firmware is already installed
probe_app_type=ApplicationType.EZSP,
# An older version is probed, so an upgrade is attempted
probe_fw_version="7.4.3.0",
) as (_, mock_update_client):
# Mock the firmware download to fail
mock_update_client.async_update_data.side_effect = ClientError()
pick_result = await hass.config_entries.flow.async_configure(
init_result["flow_id"],
user_input={"next_step_id": STEP_PICK_FIRMWARE_ZIGBEE},
)
assert pick_result["type"] is FlowResultType.FORM
assert pick_result["step_id"] == "confirm_zigbee"
async def test_config_flow_firmware_download_fails_but_not_required(
hass: HomeAssistant,
) -> None:
"""Test flow continues if firmware download fails but install is not required."""
init_result = await hass.config_entries.flow.async_init(
TEST_DOMAIN, context={"source": "hardware"}
)
with (
mock_firmware_info(
hass,
# The correct firmware is already installed so installation isn't required
probe_app_type=ApplicationType.EZSP,
# An older version is probed, so an upgrade is attempted
probe_fw_version="7.4.3.0",
) as (_, mock_update_client),
):
mock_update_client.async_fetch_firmware.side_effect = ClientError()
pick_result = await hass.config_entries.flow.async_configure(
init_result["flow_id"],
user_input={"next_step_id": STEP_PICK_FIRMWARE_ZIGBEE},
)
assert pick_result["type"] is FlowResultType.FORM
assert pick_result["step_id"] == "confirm_zigbee"
async def test_config_flow_doesnt_downgrade(
hass: HomeAssistant,
) -> None:
"""Test flow exits early, without downgrading firmware."""
init_result = await hass.config_entries.flow.async_init(
TEST_DOMAIN, context={"source": "hardware"}
)
with (
mock_firmware_info(
hass,
probe_app_type=ApplicationType.EZSP,
# An newer version is probed than what we offer
probe_fw_version="7.5.0.0",
),
patch(
"homeassistant.components.homeassistant_hardware.firmware_config_flow.async_flash_silabs_firmware"
) as mock_async_flash_silabs_firmware,
):
pick_result = await hass.config_entries.flow.async_configure(
init_result["flow_id"],
user_input={"next_step_id": STEP_PICK_FIRMWARE_ZIGBEE},
)
assert pick_result["type"] is FlowResultType.FORM
assert pick_result["step_id"] == "confirm_zigbee"
assert len(mock_async_flash_silabs_firmware.mock_calls) == 0
async def test_config_flow_zigbee_skip_step_if_installed(hass: HomeAssistant) -> None: async def test_config_flow_zigbee_skip_step_if_installed(hass: HomeAssistant) -> None:
"""Test the config flow, skip installing the addon if necessary.""" """Test the config flow, skip installing the addon if necessary."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
@ -480,7 +582,7 @@ async def test_config_flow_thread(hass: HomeAssistant) -> None:
hass, hass,
probe_app_type=ApplicationType.EZSP, probe_app_type=ApplicationType.EZSP,
flash_app_type=ApplicationType.SPINEL, flash_app_type=ApplicationType.SPINEL,
) as mock_otbr_manager: ) as (mock_otbr_manager, _):
# Pick the menu option # Pick the menu option
pick_result = await hass.config_entries.flow.async_configure( pick_result = await hass.config_entries.flow.async_configure(
init_result["flow_id"], init_result["flow_id"],
@ -564,7 +666,7 @@ async def test_config_flow_thread_addon_already_installed(hass: HomeAssistant) -
update_available=False, update_available=False,
version=None, version=None,
), ),
) as mock_otbr_manager: ) as (mock_otbr_manager, _):
# Pick the menu option # Pick the menu option
pick_result = await hass.config_entries.flow.async_configure( pick_result = await hass.config_entries.flow.async_configure(
init_result["flow_id"], init_result["flow_id"],
@ -631,7 +733,7 @@ async def test_options_flow_zigbee_to_thread(hass: HomeAssistant) -> None:
hass, hass,
probe_app_type=ApplicationType.EZSP, probe_app_type=ApplicationType.EZSP,
flash_app_type=ApplicationType.SPINEL, flash_app_type=ApplicationType.SPINEL,
) as mock_otbr_manager: ) as (mock_otbr_manager, _):
# First step is confirmation # First step is confirmation
result = await hass.config_entries.options.async_init(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id)
assert result["type"] is FlowResultType.MENU assert result["type"] is FlowResultType.MENU

View File

@ -2,6 +2,7 @@
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
from aiohttp import ClientError
import pytest import pytest
from homeassistant.components.hassio import AddonError, AddonInfo, AddonState from homeassistant.components.hassio import AddonError, AddonInfo, AddonState
@ -109,7 +110,7 @@ async def test_config_flow_thread_addon_info_fails(hass: HomeAssistant) -> None:
with mock_firmware_info( with mock_firmware_info(
hass, hass,
probe_app_type=ApplicationType.EZSP, probe_app_type=ApplicationType.EZSP,
) as mock_otbr_manager: ) as (mock_otbr_manager, _):
mock_otbr_manager.async_get_addon_info.side_effect = AddonError() mock_otbr_manager.async_get_addon_info.side_effect = AddonError()
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={} result["flow_id"], user_input={}
@ -147,7 +148,7 @@ async def test_config_flow_thread_addon_already_configured(hass: HomeAssistant)
update_available=False, update_available=False,
version="1.0.0", version="1.0.0",
), ),
) as mock_otbr_manager: ) as (mock_otbr_manager, _):
mock_otbr_manager.async_install_addon_waiting = AsyncMock( mock_otbr_manager.async_install_addon_waiting = AsyncMock(
side_effect=AddonError() side_effect=AddonError()
) )
@ -178,7 +179,7 @@ async def test_config_flow_thread_addon_install_fails(hass: HomeAssistant) -> No
with mock_firmware_info( with mock_firmware_info(
hass, hass,
probe_app_type=ApplicationType.EZSP, probe_app_type=ApplicationType.EZSP,
) as mock_otbr_manager: ) as (mock_otbr_manager, _):
mock_otbr_manager.async_install_addon_waiting = AsyncMock( mock_otbr_manager.async_install_addon_waiting = AsyncMock(
side_effect=AddonError() side_effect=AddonError()
) )
@ -209,7 +210,7 @@ async def test_config_flow_thread_addon_set_config_fails(hass: HomeAssistant) ->
with mock_firmware_info( with mock_firmware_info(
hass, hass,
probe_app_type=ApplicationType.EZSP, probe_app_type=ApplicationType.EZSP,
) as mock_otbr_manager: ) as (mock_otbr_manager, _):
async def install_addon() -> None: async def install_addon() -> None:
mock_otbr_manager.async_get_addon_info.return_value = AddonInfo( mock_otbr_manager.async_get_addon_info.return_value = AddonInfo(
@ -270,7 +271,7 @@ async def test_config_flow_thread_flasher_run_fails(hass: HomeAssistant) -> None
update_available=False, update_available=False,
version="1.0.0", version="1.0.0",
), ),
) as mock_otbr_manager: ) as (mock_otbr_manager, _):
mock_otbr_manager.async_start_addon_waiting = AsyncMock( mock_otbr_manager.async_start_addon_waiting = AsyncMock(
side_effect=AddonError() side_effect=AddonError()
) )
@ -341,6 +342,64 @@ async def test_config_flow_thread_confirmation_fails(hass: HomeAssistant) -> Non
assert pick_thread_progress_result["reason"] == "unsupported_firmware" assert pick_thread_progress_result["reason"] == "unsupported_firmware"
@pytest.mark.parametrize(
"ignore_translations_for_mock_domains", ["test_firmware_domain"]
)
async def test_config_flow_firmware_index_download_fails_and_required(
hass: HomeAssistant,
) -> None:
"""Test flow aborts if OTA index download fails and install is required."""
init_result = await hass.config_entries.flow.async_init(
TEST_DOMAIN, context={"source": "hardware"}
)
with (
mock_firmware_info(
hass,
# The wrong firmware is installed, so a new install is required
probe_app_type=ApplicationType.SPINEL,
) as (_, mock_update_client),
):
mock_update_client.async_update_data.side_effect = ClientError()
pick_result = await hass.config_entries.flow.async_configure(
init_result["flow_id"],
user_input={"next_step_id": STEP_PICK_FIRMWARE_ZIGBEE},
)
assert pick_result["type"] is FlowResultType.ABORT
assert pick_result["reason"] == "fw_download_failed"
@pytest.mark.parametrize(
"ignore_translations_for_mock_domains", ["test_firmware_domain"]
)
async def test_config_flow_firmware_download_fails_and_required(
hass: HomeAssistant,
) -> None:
"""Test flow aborts if firmware download fails and install is required."""
init_result = await hass.config_entries.flow.async_init(
TEST_DOMAIN, context={"source": "hardware"}
)
with (
mock_firmware_info(
hass,
# The wrong firmware is installed, so a new install is required
probe_app_type=ApplicationType.SPINEL,
) as (_, mock_update_client),
):
mock_update_client.async_fetch_firmware.side_effect = ClientError()
pick_result = await hass.config_entries.flow.async_configure(
init_result["flow_id"],
user_input={"next_step_id": STEP_PICK_FIRMWARE_ZIGBEE},
)
assert pick_result["type"] is FlowResultType.ABORT
assert pick_result["reason"] == "fw_download_failed"
@pytest.mark.parametrize( @pytest.mark.parametrize(
"ignore_translations_for_mock_domains", "ignore_translations_for_mock_domains",
["test_firmware_domain"], ["test_firmware_domain"],

View File

@ -75,7 +75,7 @@ async def test_config_flow(
next_step_id: str, next_step_id: str,
) -> ConfigFlowResult: ) -> ConfigFlowResult:
if next_step_id == "start_otbr_addon": if next_step_id == "start_otbr_addon":
next_step_id = "confirm_otbr" next_step_id = "pre_confirm_otbr"
return await getattr(self, f"async_step_{next_step_id}")(user_input={}) return await getattr(self, f"async_step_{next_step_id}")(user_input={})
@ -100,14 +100,22 @@ async def test_config_flow(
), ),
), ),
): ):
result = await hass.config_entries.flow.async_configure( confirm_result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
user_input={"next_step_id": step}, user_input={"next_step_id": step},
) )
assert result["type"] is FlowResultType.CREATE_ENTRY assert confirm_result["type"] is FlowResultType.FORM
assert confirm_result["step_id"] == (
"confirm_zigbee" if step == STEP_PICK_FIRMWARE_ZIGBEE else "confirm_otbr"
)
config_entry = result["result"] create_result = await hass.config_entries.flow.async_configure(
confirm_result["flow_id"], user_input={}
)
assert create_result["type"] is FlowResultType.CREATE_ENTRY
config_entry = create_result["result"]
assert config_entry.data == { assert config_entry.data == {
"firmware": fw_type.value, "firmware": fw_type.value,
"firmware_version": fw_version, "firmware_version": fw_version,
@ -171,7 +179,7 @@ async def test_options_flow(
assert result["description_placeholders"]["model"] == model assert result["description_placeholders"]["model"] == model
async def mock_async_step_pick_firmware_zigbee(self, data): async def mock_async_step_pick_firmware_zigbee(self, data):
return await self.async_step_confirm_zigbee(user_input={}) return await self.async_step_pre_confirm_zigbee()
with ( with (
patch( patch(
@ -190,13 +198,20 @@ async def test_options_flow(
), ),
), ),
): ):
result = await hass.config_entries.options.async_configure( confirm_result = await hass.config_entries.options.async_configure(
result["flow_id"], result["flow_id"],
user_input={"next_step_id": STEP_PICK_FIRMWARE_ZIGBEE}, user_input={"next_step_id": STEP_PICK_FIRMWARE_ZIGBEE},
) )
assert result["type"] is FlowResultType.CREATE_ENTRY assert confirm_result["type"] is FlowResultType.FORM
assert result["result"] is True assert confirm_result["step_id"] == "confirm_zigbee"
create_result = await hass.config_entries.options.async_configure(
confirm_result["flow_id"], user_input={}
)
assert create_result["type"] is FlowResultType.CREATE_ENTRY
assert create_result["result"] is True
assert config_entry.data == { assert config_entry.data == {
"firmware": "ezsp", "firmware": "ezsp",

View File

@ -348,7 +348,7 @@ async def test_firmware_options_flow(
assert result["description_placeholders"]["model"] == "Home Assistant Yellow" assert result["description_placeholders"]["model"] == "Home Assistant Yellow"
async def mock_async_step_pick_firmware_zigbee(self, data): async def mock_async_step_pick_firmware_zigbee(self, data):
return await self.async_step_confirm_zigbee(user_input={}) return await self.async_step_pre_confirm_zigbee()
async def mock_install_firmware_step( async def mock_install_firmware_step(
self, self,
@ -360,11 +360,16 @@ async def test_firmware_options_flow(
next_step_id: str, next_step_id: str,
) -> ConfigFlowResult: ) -> ConfigFlowResult:
if next_step_id == "start_otbr_addon": if next_step_id == "start_otbr_addon":
next_step_id = "confirm_otbr" next_step_id = "pre_confirm_otbr"
return await getattr(self, f"async_step_{next_step_id}")(user_input={}) return await getattr(self, f"async_step_{next_step_id}")(user_input={})
with ( 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( patch(
"homeassistant.components.homeassistant_hardware.firmware_config_flow.BaseFirmwareConfigFlow._ensure_thread_addon_setup", "homeassistant.components.homeassistant_hardware.firmware_config_flow.BaseFirmwareConfigFlow._ensure_thread_addon_setup",
return_value=None, return_value=None,
@ -385,13 +390,22 @@ async def test_firmware_options_flow(
), ),
), ),
): ):
result = await hass.config_entries.options.async_configure( confirm_result = await hass.config_entries.options.async_configure(
result["flow_id"], result["flow_id"],
user_input={"next_step_id": step}, user_input={"next_step_id": step},
) )
assert result["type"] is FlowResultType.CREATE_ENTRY assert confirm_result["type"] is FlowResultType.FORM
assert result["result"] is True assert confirm_result["step_id"] == (
"confirm_zigbee" if step == STEP_PICK_FIRMWARE_ZIGBEE else "confirm_otbr"
)
create_result = await hass.config_entries.options.async_configure(
confirm_result["flow_id"], user_input={}
)
assert create_result["type"] is FlowResultType.CREATE_ENTRY
assert create_result["result"] is True
assert config_entry.data == { assert config_entry.data == {
"firmware": fw_type.value, "firmware": fw_type.value,