Skip to content

Commit e8acced

Browse files
authored
Disable owning integrations for the entire firmware interaction process (home-assistant#157082)
1 parent 758a30e commit e8acced

File tree

7 files changed

+218
-195
lines changed

7 files changed

+218
-195
lines changed

homeassistant/components/homeassistant_hardware/firmware_config_flow.py

Lines changed: 81 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,14 @@
3333
from homeassistant.helpers.aiohttp_client import async_get_clientsession
3434
from homeassistant.helpers.hassio import is_hassio
3535

36-
from .const import OTBR_DOMAIN, Z2M_EMBER_DOCS_URL, ZHA_DOMAIN
36+
from .const import DOMAIN, OTBR_DOMAIN, Z2M_EMBER_DOCS_URL, ZHA_DOMAIN
3737
from .util import (
3838
ApplicationType,
3939
FirmwareInfo,
4040
OwningAddon,
4141
OwningIntegration,
4242
ResetTarget,
43+
async_firmware_flashing_context,
4344
async_flash_silabs_firmware,
4445
get_otbr_addon_manager,
4546
guess_firmware_info,
@@ -228,83 +229,95 @@ async def _install_firmware(
228229
# Keep track of the firmware we're working with, for error messages
229230
self.installing_firmware_name = firmware_name
230231

231-
# Installing new firmware is only truly required if the wrong type is
232-
# installed: upgrading to the latest release of the current firmware type
233-
# isn't strictly necessary for functionality.
234-
self._probed_firmware_info = await probe_silabs_firmware_info(
235-
self._device,
236-
bootloader_reset_methods=self.BOOTLOADER_RESET_METHODS,
237-
application_probe_methods=self.APPLICATION_PROBE_METHODS,
238-
)
239-
240-
firmware_install_required = self._probed_firmware_info is None or (
241-
self._probed_firmware_info.firmware_type != expected_installed_firmware_type
242-
)
243-
244-
session = async_get_clientsession(self.hass)
245-
client = FirmwareUpdateClient(fw_update_url, session)
246-
247-
try:
248-
manifest = await client.async_update_data()
249-
fw_manifest = next(
250-
fw for fw in manifest.firmwares if fw.filename.startswith(fw_type)
232+
# For the duration of firmware flashing, hint to other integrations (i.e. ZHA)
233+
# that the hardware is in use and should not be accessed. This is separate from
234+
# locking the serial port itself, since a momentary release of the port may
235+
# still allow for ZHA to reclaim the device.
236+
async with async_firmware_flashing_context(self.hass, self._device, DOMAIN):
237+
# Installing new firmware is only truly required if the wrong type is
238+
# installed: upgrading to the latest release of the current firmware type
239+
# isn't strictly necessary for functionality.
240+
self._probed_firmware_info = await probe_silabs_firmware_info(
241+
self._device,
242+
bootloader_reset_methods=self.BOOTLOADER_RESET_METHODS,
243+
application_probe_methods=self.APPLICATION_PROBE_METHODS,
251244
)
252-
except (StopIteration, TimeoutError, ClientError, ManifestMissing) as err:
253-
_LOGGER.warning("Failed to fetch firmware update manifest", exc_info=True)
254245

255-
# Not having internet access should not prevent setup
256-
if not firmware_install_required:
257-
_LOGGER.debug("Skipping firmware upgrade due to index download failure")
258-
return
259-
260-
raise AbortFlow(
261-
reason="fw_download_failed",
262-
description_placeholders=self._get_translation_placeholders(),
263-
) from err
264-
265-
if not firmware_install_required:
266-
assert self._probed_firmware_info is not None
246+
firmware_install_required = self._probed_firmware_info is None or (
247+
self._probed_firmware_info.firmware_type
248+
!= expected_installed_firmware_type
249+
)
267250

268-
# Make sure we do not downgrade the firmware
269-
fw_metadata = NabuCasaMetadata.from_json(fw_manifest.metadata)
270-
fw_version = fw_metadata.get_public_version()
271-
probed_fw_version = Version(self._probed_firmware_info.firmware_version)
251+
session = async_get_clientsession(self.hass)
252+
client = FirmwareUpdateClient(fw_update_url, session)
272253

273-
if probed_fw_version >= fw_version:
274-
_LOGGER.debug(
275-
"Not downgrading firmware, installed %s is newer than available %s",
276-
probed_fw_version,
277-
fw_version,
254+
try:
255+
manifest = await client.async_update_data()
256+
fw_manifest = next(
257+
fw for fw in manifest.firmwares if fw.filename.startswith(fw_type)
258+
)
259+
except (StopIteration, TimeoutError, ClientError, ManifestMissing) as err:
260+
_LOGGER.warning(
261+
"Failed to fetch firmware update manifest", exc_info=True
278262
)
279-
return
280263

281-
try:
282-
fw_data = await client.async_fetch_firmware(fw_manifest)
283-
except (TimeoutError, ClientError, ValueError) as err:
284-
_LOGGER.warning("Failed to fetch firmware update", exc_info=True)
264+
# Not having internet access should not prevent setup
265+
if not firmware_install_required:
266+
_LOGGER.debug(
267+
"Skipping firmware upgrade due to index download failure"
268+
)
269+
return
270+
271+
raise AbortFlow(
272+
reason="fw_download_failed",
273+
description_placeholders=self._get_translation_placeholders(),
274+
) from err
285275

286-
# If we cannot download new firmware, we shouldn't block setup
287276
if not firmware_install_required:
288-
_LOGGER.debug("Skipping firmware upgrade due to image download failure")
289-
return
277+
assert self._probed_firmware_info is not None
278+
279+
# Make sure we do not downgrade the firmware
280+
fw_metadata = NabuCasaMetadata.from_json(fw_manifest.metadata)
281+
fw_version = fw_metadata.get_public_version()
282+
probed_fw_version = Version(self._probed_firmware_info.firmware_version)
283+
284+
if probed_fw_version >= fw_version:
285+
_LOGGER.debug(
286+
"Not downgrading firmware, installed %s is newer than available %s",
287+
probed_fw_version,
288+
fw_version,
289+
)
290+
return
290291

291-
# Otherwise, fail
292-
raise AbortFlow(
293-
reason="fw_download_failed",
294-
description_placeholders=self._get_translation_placeholders(),
295-
) from err
292+
try:
293+
fw_data = await client.async_fetch_firmware(fw_manifest)
294+
except (TimeoutError, ClientError, ValueError) as err:
295+
_LOGGER.warning("Failed to fetch firmware update", exc_info=True)
296296

297-
self._probed_firmware_info = await async_flash_silabs_firmware(
298-
hass=self.hass,
299-
device=self._device,
300-
fw_data=fw_data,
301-
expected_installed_firmware_type=expected_installed_firmware_type,
302-
bootloader_reset_methods=self.BOOTLOADER_RESET_METHODS,
303-
application_probe_methods=self.APPLICATION_PROBE_METHODS,
304-
progress_callback=lambda offset, total: self.async_update_progress(
305-
offset / total
306-
),
307-
)
297+
# If we cannot download new firmware, we shouldn't block setup
298+
if not firmware_install_required:
299+
_LOGGER.debug(
300+
"Skipping firmware upgrade due to image download failure"
301+
)
302+
return
303+
304+
# Otherwise, fail
305+
raise AbortFlow(
306+
reason="fw_download_failed",
307+
description_placeholders=self._get_translation_placeholders(),
308+
) from err
309+
310+
self._probed_firmware_info = await async_flash_silabs_firmware(
311+
hass=self.hass,
312+
device=self._device,
313+
fw_data=fw_data,
314+
expected_installed_firmware_type=expected_installed_firmware_type,
315+
bootloader_reset_methods=self.BOOTLOADER_RESET_METHODS,
316+
application_probe_methods=self.APPLICATION_PROBE_METHODS,
317+
progress_callback=lambda offset, total: self.async_update_progress(
318+
offset / total
319+
),
320+
)
308321

309322
async def _configure_and_start_otbr_addon(self) -> None:
310323
"""Configure and start the OTBR addon."""

homeassistant/components/homeassistant_hardware/update.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
ApplicationType,
2727
FirmwareInfo,
2828
ResetTarget,
29+
async_firmware_flashing_context,
2930
async_flash_silabs_firmware,
3031
)
3132

@@ -274,16 +275,18 @@ async def async_install(
274275
)
275276

276277
try:
277-
firmware_info = await async_flash_silabs_firmware(
278-
hass=self.hass,
279-
device=self._current_device,
280-
fw_data=fw_data,
281-
expected_installed_firmware_type=self.entity_description.expected_firmware_type,
282-
bootloader_reset_methods=self.BOOTLOADER_RESET_METHODS,
283-
application_probe_methods=self.APPLICATION_PROBE_METHODS,
284-
progress_callback=self._update_progress,
285-
domain=self._config_entry.domain,
286-
)
278+
async with async_firmware_flashing_context(
279+
self.hass, self._current_device, self._config_entry.domain
280+
):
281+
firmware_info = await async_flash_silabs_firmware(
282+
hass=self.hass,
283+
device=self._current_device,
284+
fw_data=fw_data,
285+
expected_installed_firmware_type=self.entity_description.expected_firmware_type,
286+
bootloader_reset_methods=self.BOOTLOADER_RESET_METHODS,
287+
application_probe_methods=self.APPLICATION_PROBE_METHODS,
288+
progress_callback=self._update_progress,
289+
)
287290
finally:
288291
self._attr_in_progress = False
289292
self.async_write_ha_state()

homeassistant/components/homeassistant_hardware/util.py

Lines changed: 59 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626

2727
from . import DATA_COMPONENT
2828
from .const import (
29-
DOMAIN,
3029
OTBR_ADDON_MANAGER_DATA,
3130
OTBR_ADDON_NAME,
3231
OTBR_ADDON_SLUG,
@@ -366,6 +365,22 @@ async def probe_silabs_firmware_type(
366365
return fw_info.firmware_type
367366

368367

368+
@asynccontextmanager
369+
async def async_firmware_flashing_context(
370+
hass: HomeAssistant, device: str, source_domain: str
371+
) -> AsyncIterator[None]:
372+
"""Register a device as having its firmware being actively interacted with."""
373+
async with async_firmware_update_context(hass, device, source_domain):
374+
firmware_info = await guess_firmware_info(hass, device)
375+
_LOGGER.debug("Guessed firmware info before update: %s", firmware_info)
376+
377+
async with AsyncExitStack() as stack:
378+
for owner in firmware_info.owners:
379+
await stack.enter_async_context(owner.temporarily_stop(hass))
380+
381+
yield
382+
383+
369384
async def async_flash_silabs_firmware(
370385
hass: HomeAssistant,
371386
device: str,
@@ -374,10 +389,11 @@ async def async_flash_silabs_firmware(
374389
bootloader_reset_methods: Sequence[ResetTarget],
375390
application_probe_methods: Sequence[tuple[ApplicationType, int]],
376391
progress_callback: Callable[[int, int], None] | None = None,
377-
*,
378-
domain: str = DOMAIN,
379392
) -> FirmwareInfo:
380-
"""Flash firmware to the SiLabs device."""
393+
"""Flash firmware to the SiLabs device.
394+
395+
This function is meant to be used within a firmware update context.
396+
"""
381397
if not any(
382398
method == expected_installed_firmware_type
383399
for method, _ in application_probe_methods
@@ -387,54 +403,44 @@ async def async_flash_silabs_firmware(
387403
f" not in application probe methods {application_probe_methods!r}"
388404
)
389405

390-
async with async_firmware_update_context(hass, device, domain):
391-
firmware_info = await guess_firmware_info(hass, device)
392-
_LOGGER.debug("Identified firmware info: %s", firmware_info)
393-
394-
fw_image = await hass.async_add_executor_job(parse_firmware_image, fw_data)
395-
396-
flasher = Flasher(
397-
device=device,
398-
probe_methods=tuple(
399-
(m.as_flasher_application_type(), baudrate)
400-
for m, baudrate in application_probe_methods
401-
),
402-
bootloader_reset=tuple(
403-
m.as_flasher_reset_target() for m in bootloader_reset_methods
404-
),
405-
)
406+
fw_image = await hass.async_add_executor_job(parse_firmware_image, fw_data)
406407

407-
async with AsyncExitStack() as stack:
408-
for owner in firmware_info.owners:
409-
await stack.enter_async_context(owner.temporarily_stop(hass))
408+
flasher = Flasher(
409+
device=device,
410+
probe_methods=tuple(
411+
(m.as_flasher_application_type(), baudrate)
412+
for m, baudrate in application_probe_methods
413+
),
414+
bootloader_reset=tuple(
415+
m.as_flasher_reset_target() for m in bootloader_reset_methods
416+
),
417+
)
418+
419+
try:
420+
# Enter the bootloader with indeterminate progress
421+
await flasher.enter_bootloader()
422+
423+
# Flash the firmware, with progress
424+
await flasher.flash_firmware(fw_image, progress_callback=progress_callback)
425+
except PermissionError as err:
426+
raise HomeAssistantError(
427+
"Failed to flash firmware: Device is used by another application"
428+
) from err
429+
except Exception as err:
430+
raise HomeAssistantError("Failed to flash firmware") from err
431+
432+
probed_firmware_info = await probe_silabs_firmware_info(
433+
device,
434+
bootloader_reset_methods=bootloader_reset_methods,
435+
# Only probe for the expected installed firmware type
436+
application_probe_methods=[
437+
(method, baudrate)
438+
for method, baudrate in application_probe_methods
439+
if method == expected_installed_firmware_type
440+
],
441+
)
442+
443+
if probed_firmware_info is None:
444+
raise HomeAssistantError("Failed to probe the firmware after flashing")
410445

411-
try:
412-
# Enter the bootloader with indeterminate progress
413-
await flasher.enter_bootloader()
414-
415-
# Flash the firmware, with progress
416-
await flasher.flash_firmware(
417-
fw_image, progress_callback=progress_callback
418-
)
419-
except PermissionError as err:
420-
raise HomeAssistantError(
421-
"Failed to flash firmware: Device is used by another application"
422-
) from err
423-
except Exception as err:
424-
raise HomeAssistantError("Failed to flash firmware") from err
425-
426-
probed_firmware_info = await probe_silabs_firmware_info(
427-
device,
428-
bootloader_reset_methods=bootloader_reset_methods,
429-
# Only probe for the expected installed firmware type
430-
application_probe_methods=[
431-
(method, baudrate)
432-
for method, baudrate in application_probe_methods
433-
if method == expected_installed_firmware_type
434-
],
435-
)
436-
437-
if probed_firmware_info is None:
438-
raise HomeAssistantError("Failed to probe the firmware after flashing")
439-
440-
return probed_firmware_info
446+
return probed_firmware_info

0 commit comments

Comments
 (0)