Make async_flash_firmware a public helper

This commit is contained in:
puddly
2025-05-12 15:59:30 -04:00
parent d2ef3ca100
commit 323f6fa359
2 changed files with 81 additions and 70 deletions

View File

@@ -2,14 +2,24 @@
from collections import defaultdict from collections import defaultdict
from collections.abc import AsyncIterator, Awaitable, Callable from collections.abc import AsyncIterator, Awaitable, Callable
from contextlib import AsyncExitStack
import logging import logging
from typing import Protocol from typing import Protocol
from universal_silabs_flasher.firmware import parse_firmware_image
from universal_silabs_flasher.flasher import Flasher
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback
from homeassistant.exceptions import HomeAssistantError
from . import DATA_COMPONENT from . import DATA_COMPONENT
from .util import FirmwareInfo from .util import (
ApplicationType,
FirmwareInfo,
guess_firmware_info,
probe_silabs_firmware_info,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -141,3 +151,52 @@ def async_notify_firmware_info(
) -> Awaitable[None]: ) -> Awaitable[None]:
"""Notify the dispatcher of new firmware information.""" """Notify the dispatcher of new firmware information."""
return hass.data[DATA_COMPONENT].notify_firmware_info(domain, firmware_info) return hass.data[DATA_COMPONENT].notify_firmware_info(domain, firmware_info)
async def async_flash_silabs_firmware(
hass: HomeAssistant,
device: str,
fw_data: bytes,
expected_installed_firmware_type: ApplicationType,
bootloader_reset_type: str | None = None,
progress_callback: Callable[[int, int], None] | None = None,
) -> FirmwareInfo:
"""Flash firmware to the SiLabs device."""
firmware_info = await guess_firmware_info(hass, device)
_LOGGER.debug("Identified firmware info: %s", firmware_info)
fw_image = await hass.async_add_executor_job(parse_firmware_image, fw_data)
flasher = Flasher(
device=device,
probe_methods=(
ApplicationType.GECKO_BOOTLOADER.as_flasher_application_type(),
ApplicationType.EZSP.as_flasher_application_type(),
ApplicationType.SPINEL.as_flasher_application_type(),
ApplicationType.CPC.as_flasher_application_type(),
),
bootloader_reset=bootloader_reset_type,
)
async with AsyncExitStack() as stack:
for owner in firmware_info.owners:
await stack.enter_async_context(owner.temporarily_stop(hass))
try:
# Enter the bootloader with indeterminate progress
await flasher.enter_bootloader()
# Flash the firmware, with progress
await flasher.flash_firmware(fw_image, progress_callback=progress_callback)
except Exception as err:
raise HomeAssistantError("Failed to flash firmware") from err
probed_firmware_info = await probe_silabs_firmware_info(
device,
probe_methods=(expected_installed_firmware_type,),
)
if probed_firmware_info is None:
raise HomeAssistantError("Failed to probe the firmware after flashing")
return probed_firmware_info

View File

@@ -2,15 +2,12 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import AsyncIterator, Callable from collections.abc import Callable
from contextlib import AsyncExitStack, asynccontextmanager
from dataclasses import dataclass from dataclasses import dataclass
import logging import logging
from typing import Any, cast from typing import Any, cast
from ha_silabs_firmware_client import FirmwareManifest, FirmwareMetadata from ha_silabs_firmware_client import FirmwareManifest, FirmwareMetadata
from universal_silabs_flasher.firmware import parse_firmware_image
from universal_silabs_flasher.flasher import Flasher
from yarl import URL from yarl import URL
from homeassistant.components.update import ( from homeassistant.components.update import (
@@ -20,18 +17,12 @@ from homeassistant.components.update import (
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.core import CALLBACK_TYPE, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.restore_state import ExtraStoredData from homeassistant.helpers.restore_state import ExtraStoredData
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .coordinator import FirmwareUpdateCoordinator from .coordinator import FirmwareUpdateCoordinator
from .helpers import async_register_firmware_info_callback from .helpers import async_flash_silabs_firmware, async_register_firmware_info_callback
from .util import ( from .util import ApplicationType, FirmwareInfo
ApplicationType,
FirmwareInfo,
guess_firmware_info,
probe_silabs_firmware_info,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -249,19 +240,11 @@ class BaseFirmwareUpdateEntity(
self._attr_update_percentage = round((offset * 100) / total_size) self._attr_update_percentage = round((offset * 100) / total_size)
self.async_write_ha_state() self.async_write_ha_state()
@asynccontextmanager # Switch to an indeterminate progress bar after installation is complete, since
async def _temporarily_stop_hardware_owners( # we probe the firmware after flashing
self, device: str if offset == total_size:
) -> AsyncIterator[None]: self._attr_update_percentage = None
"""Temporarily stop addons and integrations communicating with the device.""" self.async_write_ha_state()
firmware_info = await guess_firmware_info(self.hass, device)
_LOGGER.debug("Identified firmware info: %s", firmware_info)
async with AsyncExitStack() as stack:
for owner in firmware_info.owners:
await stack.enter_async_context(owner.temporarily_stop(self.hass))
yield
async def async_install( async def async_install(
self, version: str | None, backup: bool, **kwargs: Any self, version: str | None, backup: bool, **kwargs: Any
@@ -278,49 +261,18 @@ class BaseFirmwareUpdateEntity(
fw_data = await self.coordinator.client.async_fetch_firmware( fw_data = await self.coordinator.client.async_fetch_firmware(
self._latest_firmware self._latest_firmware
) )
fw_image = await self.hass.async_add_executor_job(parse_firmware_image, fw_data)
device = self._current_device
flasher = Flasher(
device=device,
probe_methods=(
ApplicationType.GECKO_BOOTLOADER.as_flasher_application_type(),
ApplicationType.EZSP.as_flasher_application_type(),
ApplicationType.SPINEL.as_flasher_application_type(),
ApplicationType.CPC.as_flasher_application_type(),
),
bootloader_reset=self.bootloader_reset_type,
)
async with self._temporarily_stop_hardware_owners(device):
try: try:
try: firmware_info = await async_flash_silabs_firmware(
# Enter the bootloader with indeterminate progress hass=self.hass,
await flasher.enter_bootloader() device=self._current_device,
fw_data=fw_data,
# Flash the firmware, with progress expected_installed_firmware_type=self.entity_description.expected_firmware_type,
await flasher.flash_firmware( bootloader_reset_type=self.bootloader_reset_type,
fw_image, progress_callback=self._update_progress progress_callback=self._update_progress,
) )
except Exception as err:
raise HomeAssistantError("Failed to flash firmware") from err
# Probe the running application type with indeterminate progress
self._attr_update_percentage = None
self.async_write_ha_state()
firmware_info = await probe_silabs_firmware_info(
device,
probe_methods=(self.entity_description.expected_firmware_type,),
)
if firmware_info is None:
raise HomeAssistantError(
"Failed to probe the firmware after flashing"
)
self._firmware_info_callback(firmware_info)
finally: finally:
self._attr_in_progress = False self._attr_in_progress = False
self.async_write_ha_state() self.async_write_ha_state()
self._firmware_info_callback(firmware_info)