Skip to content

Commit 9e7c7ec

Browse files
authored
Flash ZBT-1 and Yellow firmwares from Core instead of using addons (home-assistant#145019)
* Make `async_flash_firmware` a public helper * [ZBT-1] Implement flashing for Zigbee and Thread within the config flow * WIP: Begin fixing unit tests * WIP: Unit tests, pass 2 * WIP: pass 3 * Fix hardware unit tests * Have the individual hardware integrations depend on the firmware flasher * Break out firmware filter into its own helper * Mirror to Yellow * Simplify * Simplify * Revert "Have the individual hardware integrations depend on the firmware flasher" This reverts commit 096f429. * Move `async_flash_silabs_firmware` into `util` * Fix existing unit tests * Unconditionally upgrade Zigbee firmware during installation * Fix failing error case unit tests * Fix remaining failing unit tests * Increase test coverage * 100% test coverage * Remove old translation strings * Add new translation strings * Do not probe OTBR firmware when completing the flow * More translation strings * Probe OTBR firmware info before starting the addon
1 parent f735331 commit 9e7c7ec

File tree

14 files changed

+1066
-1161
lines changed

14 files changed

+1066
-1161
lines changed

homeassistant/components/homeassistant_hardware/firmware_config_flow.py

Lines changed: 182 additions & 246 deletions
Large diffs are not rendered by default.

homeassistant/components/homeassistant_hardware/strings.json

Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,6 @@
1010
"pick_firmware_thread": "Thread"
1111
}
1212
},
13-
"install_zigbee_flasher_addon": {
14-
"title": "Installing flasher",
15-
"description": "Installing the Silicon Labs Flasher add-on."
16-
},
17-
"run_zigbee_flasher_addon": {
18-
"title": "Installing Zigbee firmware",
19-
"description": "Installing Zigbee firmware. This will take about a minute."
20-
},
21-
"uninstall_zigbee_flasher_addon": {
22-
"title": "Removing flasher",
23-
"description": "Removing the Silicon Labs Flasher add-on."
24-
},
25-
"zigbee_flasher_failed": {
26-
"title": "Zigbee installation failed",
27-
"description": "The Zigbee firmware installation process was unsuccessful. Ensure no other software is trying to communicate with the {model} and try again."
28-
},
2913
"confirm_zigbee": {
3014
"title": "Zigbee setup complete",
3115
"description": "Your {model} is now a Zigbee coordinator and will be shown as discovered by the Zigbee Home Automation integration."
@@ -55,9 +39,7 @@
5539
"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."
5640
},
5741
"progress": {
58-
"install_zigbee_flasher_addon": "The Silicon Labs Flasher add-on is installed, this may take a few minutes.",
59-
"run_zigbee_flasher_addon": "Please wait while Zigbee 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.",
60-
"uninstall_zigbee_flasher_addon": "The Silicon Labs Flasher add-on is being removed."
42+
"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."
6143
}
6244
}
6345
},
@@ -110,16 +92,6 @@
11092
"data": {
11193
"disable_multi_pan": "Disable multiprotocol support"
11294
}
113-
},
114-
"install_flasher_addon": {
115-
"title": "The Silicon Labs Flasher add-on installation has started"
116-
},
117-
"configure_flasher_addon": {
118-
"title": "The Silicon Labs Flasher add-on installation has started"
119-
},
120-
"start_flasher_addon": {
121-
"title": "Installing firmware",
122-
"description": "Zigbee firmware is now being installed. This will take a few minutes."
12395
}
12496
},
12597
"error": {

homeassistant/components/homeassistant_hardware/update.py

Lines changed: 20 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,12 @@
22

33
from __future__ import annotations
44

5-
from collections.abc import AsyncIterator, Callable
6-
from contextlib import AsyncExitStack, asynccontextmanager
5+
from collections.abc import Callable
76
from dataclasses import dataclass
87
import logging
98
from typing import Any, cast
109

1110
from ha_silabs_firmware_client import FirmwareManifest, FirmwareMetadata
12-
from universal_silabs_flasher.firmware import parse_firmware_image
13-
from universal_silabs_flasher.flasher import Flasher
1411
from yarl import URL
1512

1613
from homeassistant.components.update import (
@@ -20,18 +17,12 @@
2017
)
2118
from homeassistant.config_entries import ConfigEntry
2219
from homeassistant.core import CALLBACK_TYPE, callback
23-
from homeassistant.exceptions import HomeAssistantError
2420
from homeassistant.helpers.restore_state import ExtraStoredData
2521
from homeassistant.helpers.update_coordinator import CoordinatorEntity
2622

2723
from .coordinator import FirmwareUpdateCoordinator
2824
from .helpers import async_register_firmware_info_callback
29-
from .util import (
30-
ApplicationType,
31-
FirmwareInfo,
32-
guess_firmware_info,
33-
probe_silabs_firmware_info,
34-
)
25+
from .util import ApplicationType, FirmwareInfo, async_flash_silabs_firmware
3526

3627
_LOGGER = logging.getLogger(__name__)
3728

@@ -249,19 +240,11 @@ def _update_progress(self, offset: int, total_size: int) -> None:
249240
self._attr_update_percentage = round((offset * 100) / total_size)
250241
self.async_write_ha_state()
251242

252-
@asynccontextmanager
253-
async def _temporarily_stop_hardware_owners(
254-
self, device: str
255-
) -> AsyncIterator[None]:
256-
"""Temporarily stop addons and integrations communicating with the device."""
257-
firmware_info = await guess_firmware_info(self.hass, device)
258-
_LOGGER.debug("Identified firmware info: %s", firmware_info)
259-
260-
async with AsyncExitStack() as stack:
261-
for owner in firmware_info.owners:
262-
await stack.enter_async_context(owner.temporarily_stop(self.hass))
263-
264-
yield
243+
# Switch to an indeterminate progress bar after installation is complete, since
244+
# we probe the firmware after flashing
245+
if offset == total_size:
246+
self._attr_update_percentage = None
247+
self.async_write_ha_state()
265248

266249
async def async_install(
267250
self, version: str | None, backup: bool, **kwargs: Any
@@ -278,49 +261,18 @@ async def async_install(
278261
fw_data = await self.coordinator.client.async_fetch_firmware(
279262
self._latest_firmware
280263
)
281-
fw_image = await self.hass.async_add_executor_job(parse_firmware_image, fw_data)
282-
283-
device = self._current_device
284-
285-
flasher = Flasher(
286-
device=device,
287-
probe_methods=(
288-
ApplicationType.GECKO_BOOTLOADER.as_flasher_application_type(),
289-
ApplicationType.EZSP.as_flasher_application_type(),
290-
ApplicationType.SPINEL.as_flasher_application_type(),
291-
ApplicationType.CPC.as_flasher_application_type(),
292-
),
293-
bootloader_reset=self.bootloader_reset_type,
294-
)
295264

296-
async with self._temporarily_stop_hardware_owners(device):
297-
try:
298-
try:
299-
# Enter the bootloader with indeterminate progress
300-
await flasher.enter_bootloader()
301-
302-
# Flash the firmware, with progress
303-
await flasher.flash_firmware(
304-
fw_image, progress_callback=self._update_progress
305-
)
306-
except Exception as err:
307-
raise HomeAssistantError("Failed to flash firmware") from err
308-
309-
# Probe the running application type with indeterminate progress
310-
self._attr_update_percentage = None
311-
self.async_write_ha_state()
312-
313-
firmware_info = await probe_silabs_firmware_info(
314-
device,
315-
probe_methods=(self.entity_description.expected_firmware_type,),
316-
)
317-
318-
if firmware_info is None:
319-
raise HomeAssistantError(
320-
"Failed to probe the firmware after flashing"
321-
)
265+
try:
266+
firmware_info = await async_flash_silabs_firmware(
267+
hass=self.hass,
268+
device=self._current_device,
269+
fw_data=fw_data,
270+
expected_installed_firmware_type=self.entity_description.expected_firmware_type,
271+
bootloader_reset_type=self.bootloader_reset_type,
272+
progress_callback=self._update_progress,
273+
)
274+
finally:
275+
self._attr_in_progress = False
276+
self.async_write_ha_state()
322277

323-
self._firmware_info_callback(firmware_info)
324-
finally:
325-
self._attr_in_progress = False
326-
self.async_write_ha_state()
278+
self._firmware_info_callback(firmware_info)

homeassistant/components/homeassistant_hardware/util.py

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,20 @@
44

55
import asyncio
66
from collections import defaultdict
7-
from collections.abc import AsyncIterator, Iterable
8-
from contextlib import asynccontextmanager
7+
from collections.abc import AsyncIterator, Callable, Iterable
8+
from contextlib import AsyncExitStack, asynccontextmanager
99
from dataclasses import dataclass
1010
from enum import StrEnum
1111
import logging
1212

1313
from universal_silabs_flasher.const import ApplicationType as FlasherApplicationType
14+
from universal_silabs_flasher.firmware import parse_firmware_image
1415
from universal_silabs_flasher.flasher import Flasher
1516

1617
from homeassistant.components.hassio import AddonError, AddonManager, AddonState
1718
from homeassistant.config_entries import ConfigEntryState
1819
from homeassistant.core import HomeAssistant, callback
20+
from homeassistant.exceptions import HomeAssistantError
1921
from homeassistant.helpers.hassio import is_hassio
2022
from homeassistant.helpers.singleton import singleton
2123

@@ -333,3 +335,52 @@ async def probe_silabs_firmware_type(
333335
return None
334336

335337
return fw_info.firmware_type
338+
339+
340+
async def async_flash_silabs_firmware(
341+
hass: HomeAssistant,
342+
device: str,
343+
fw_data: bytes,
344+
expected_installed_firmware_type: ApplicationType,
345+
bootloader_reset_type: str | None = None,
346+
progress_callback: Callable[[int, int], None] | None = None,
347+
) -> FirmwareInfo:
348+
"""Flash firmware to the SiLabs device."""
349+
firmware_info = await guess_firmware_info(hass, device)
350+
_LOGGER.debug("Identified firmware info: %s", firmware_info)
351+
352+
fw_image = await hass.async_add_executor_job(parse_firmware_image, fw_data)
353+
354+
flasher = Flasher(
355+
device=device,
356+
probe_methods=(
357+
ApplicationType.GECKO_BOOTLOADER.as_flasher_application_type(),
358+
ApplicationType.EZSP.as_flasher_application_type(),
359+
ApplicationType.SPINEL.as_flasher_application_type(),
360+
ApplicationType.CPC.as_flasher_application_type(),
361+
),
362+
bootloader_reset=bootloader_reset_type,
363+
)
364+
365+
async with AsyncExitStack() as stack:
366+
for owner in firmware_info.owners:
367+
await stack.enter_async_context(owner.temporarily_stop(hass))
368+
369+
try:
370+
# Enter the bootloader with indeterminate progress
371+
await flasher.enter_bootloader()
372+
373+
# Flash the firmware, with progress
374+
await flasher.flash_firmware(fw_image, progress_callback=progress_callback)
375+
except Exception as err:
376+
raise HomeAssistantError("Failed to flash firmware") from err
377+
378+
probed_firmware_info = await probe_silabs_firmware_info(
379+
device,
380+
probe_methods=(expected_installed_firmware_type,),
381+
)
382+
383+
if probed_firmware_info is None:
384+
raise HomeAssistantError("Failed to probe the firmware after flashing")
385+
386+
return probed_firmware_info

homeassistant/components/homeassistant_sky_connect/config_flow.py

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
FIRMWARE,
3333
FIRMWARE_VERSION,
3434
MANUFACTURER,
35+
NABU_CASA_FIRMWARE_RELEASES_URL,
3536
PID,
3637
PRODUCT,
3738
SERIAL_NUMBER,
@@ -45,19 +46,29 @@
4546

4647
if TYPE_CHECKING:
4748

48-
class TranslationPlaceholderProtocol(Protocol):
49-
"""Protocol describing `BaseFirmwareInstallFlow`'s translation placeholders."""
49+
class FirmwareInstallFlowProtocol(Protocol):
50+
"""Protocol describing `BaseFirmwareInstallFlow` for a mixin."""
5051

5152
def _get_translation_placeholders(self) -> dict[str, str]:
5253
return {}
5354

55+
async def _install_firmware_step(
56+
self,
57+
fw_update_url: str,
58+
fw_type: str,
59+
firmware_name: str,
60+
expected_installed_firmware_type: ApplicationType,
61+
step_id: str,
62+
next_step_id: str,
63+
) -> ConfigFlowResult: ...
64+
5465
else:
5566
# Multiple inheritance with `Protocol` seems to break
56-
TranslationPlaceholderProtocol = object
67+
FirmwareInstallFlowProtocol = object
5768

5869

59-
class SkyConnectTranslationMixin(ConfigEntryBaseFlow, TranslationPlaceholderProtocol):
60-
"""Translation placeholder mixin for Home Assistant SkyConnect."""
70+
class SkyConnectFirmwareMixin(ConfigEntryBaseFlow, FirmwareInstallFlowProtocol):
71+
"""Mixin for Home Assistant SkyConnect firmware methods."""
6172

6273
context: ConfigFlowContext
6374

@@ -72,9 +83,35 @@ def _get_translation_placeholders(self) -> dict[str, str]:
7283

7384
return placeholders
7485

86+
async def async_step_install_zigbee_firmware(
87+
self, user_input: dict[str, Any] | None = None
88+
) -> ConfigFlowResult:
89+
"""Install Zigbee firmware."""
90+
return await self._install_firmware_step(
91+
fw_update_url=NABU_CASA_FIRMWARE_RELEASES_URL,
92+
fw_type="skyconnect_zigbee_ncp",
93+
firmware_name="Zigbee",
94+
expected_installed_firmware_type=ApplicationType.EZSP,
95+
step_id="install_zigbee_firmware",
96+
next_step_id="confirm_zigbee",
97+
)
98+
99+
async def async_step_install_thread_firmware(
100+
self, user_input: dict[str, Any] | None = None
101+
) -> ConfigFlowResult:
102+
"""Install Thread firmware."""
103+
return await self._install_firmware_step(
104+
fw_update_url=NABU_CASA_FIRMWARE_RELEASES_URL,
105+
fw_type="skyconnect_openthread_rcp",
106+
firmware_name="OpenThread",
107+
expected_installed_firmware_type=ApplicationType.SPINEL,
108+
step_id="install_thread_firmware",
109+
next_step_id="start_otbr_addon",
110+
)
111+
75112

76113
class HomeAssistantSkyConnectConfigFlow(
77-
SkyConnectTranslationMixin,
114+
SkyConnectFirmwareMixin,
78115
firmware_config_flow.BaseFirmwareConfigFlow,
79116
domain=DOMAIN,
80117
):
@@ -207,7 +244,7 @@ async def async_step_flashing_complete(
207244

208245

209246
class HomeAssistantSkyConnectOptionsFlowHandler(
210-
SkyConnectTranslationMixin, firmware_config_flow.BaseFirmwareOptionsFlow
247+
SkyConnectFirmwareMixin, firmware_config_flow.BaseFirmwareOptionsFlow
211248
):
212249
"""Zigbee and Thread options flow handlers."""
213250

0 commit comments

Comments
 (0)