1
0
mirror of https://github.com/home-assistant/core.git synced 2025-12-24 12:59:34 +00:00

Bump universal-silabs-flasher to v0.1.0 (#156291)

Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>
This commit is contained in:
puddly
2025-11-12 04:44:33 -05:00
committed by Franck Nijhof
parent 8fb9d92daf
commit 7073c40385
17 changed files with 189 additions and 38 deletions

View File

@@ -76,9 +76,18 @@ class ZBT2FirmwareMixin(ConfigEntryBaseFlow, FirmwareInstallFlowProtocol):
"""Mixin for Home Assistant Connect ZBT-2 firmware methods."""
context: ConfigFlowContext
BOOTLOADER_RESET_METHODS = [ResetTarget.RTS_DTR]
ZIGBEE_BAUDRATE = 460800
# Early ZBT-2 samples used RTS/DTR to trigger the bootloader, later ones use the
# baudrate method. Since the two are mutually exclusive we just use both.
BOOTLOADER_RESET_METHODS = [ResetTarget.RTS_DTR, ResetTarget.BAUDRATE]
APPLICATION_PROBE_METHODS = [
(ApplicationType.GECKO_BOOTLOADER, 115200),
(ApplicationType.EZSP, ZIGBEE_BAUDRATE),
(ApplicationType.SPINEL, 460800),
]
async def async_step_install_zigbee_firmware(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:

View File

@@ -14,7 +14,6 @@ from homeassistant.components.homeassistant_hardware.update import (
from homeassistant.components.homeassistant_hardware.util import (
ApplicationType,
FirmwareInfo,
ResetTarget,
)
from homeassistant.components.update import UpdateDeviceClass
from homeassistant.const import EntityCategory
@@ -24,6 +23,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import HomeAssistantConnectZBT2ConfigEntry
from .config_flow import ZBT2FirmwareMixin
from .const import DOMAIN, FIRMWARE, FIRMWARE_VERSION, HARDWARE_NAME, SERIAL_NUMBER
_LOGGER = logging.getLogger(__name__)
@@ -134,7 +134,8 @@ async def async_setup_entry(
class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
"""Connect ZBT-2 firmware update entity."""
bootloader_reset_methods = [ResetTarget.RTS_DTR]
BOOTLOADER_RESET_METHODS = ZBT2FirmwareMixin.BOOTLOADER_RESET_METHODS
APPLICATION_PROBE_METHODS = ZBT2FirmwareMixin.APPLICATION_PROBE_METHODS
def __init__(
self,

View File

@@ -81,6 +81,7 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
ZIGBEE_BAUDRATE = 115200 # Default, subclasses may override
BOOTLOADER_RESET_METHODS: list[ResetTarget] = [] # Default, subclasses may override
APPLICATION_PROBE_METHODS: list[tuple[ApplicationType, int]] = []
_picked_firmware_type: PickedFirmwareType
_zigbee_flow_strategy: ZigbeeFlowStrategy = ZigbeeFlowStrategy.RECOMMENDED
@@ -230,7 +231,11 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
# Installing new firmware is only truly required if the wrong type is
# installed: upgrading to the latest release of the current firmware type
# isn't strictly necessary for functionality.
self._probed_firmware_info = await probe_silabs_firmware_info(self._device)
self._probed_firmware_info = await probe_silabs_firmware_info(
self._device,
bootloader_reset_methods=self.BOOTLOADER_RESET_METHODS,
application_probe_methods=self.APPLICATION_PROBE_METHODS,
)
firmware_install_required = self._probed_firmware_info is None or (
self._probed_firmware_info.firmware_type != expected_installed_firmware_type
@@ -295,6 +300,7 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
fw_data=fw_data,
expected_installed_firmware_type=expected_installed_firmware_type,
bootloader_reset_methods=self.BOOTLOADER_RESET_METHODS,
application_probe_methods=self.APPLICATION_PROBE_METHODS,
progress_callback=lambda offset, total: self.async_update_progress(
offset / total
),

View File

@@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/homeassistant_hardware",
"integration_type": "system",
"requirements": [
"universal-silabs-flasher==0.0.37",
"universal-silabs-flasher==0.1.0",
"ha-silabs-firmware-client==0.3.0"
]
}

View File

@@ -86,7 +86,8 @@ class BaseFirmwareUpdateEntity(
# Subclasses provide the mapping between firmware types and entity descriptions
entity_description: FirmwareUpdateEntityDescription
bootloader_reset_methods: list[ResetTarget] = []
BOOTLOADER_RESET_METHODS: list[ResetTarget]
APPLICATION_PROBE_METHODS: list[tuple[ApplicationType, int]]
_attr_supported_features = (
UpdateEntityFeature.INSTALL | UpdateEntityFeature.PROGRESS
@@ -278,7 +279,8 @@ class BaseFirmwareUpdateEntity(
device=self._current_device,
fw_data=fw_data,
expected_installed_firmware_type=self.entity_description.expected_firmware_type,
bootloader_reset_methods=self.bootloader_reset_methods,
bootloader_reset_methods=self.BOOTLOADER_RESET_METHODS,
application_probe_methods=self.APPLICATION_PROBE_METHODS,
progress_callback=self._update_progress,
domain=self._config_entry.domain,
)

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
import asyncio
from collections import defaultdict
from collections.abc import AsyncIterator, Callable, Iterable, Sequence
from collections.abc import AsyncIterator, Callable, Sequence
from contextlib import AsyncExitStack, asynccontextmanager
from dataclasses import dataclass
from enum import StrEnum
@@ -309,15 +309,20 @@ async def guess_firmware_info(hass: HomeAssistant, device_path: str) -> Firmware
async def probe_silabs_firmware_info(
device: str, *, probe_methods: Iterable[ApplicationType] | None = None
device: str,
*,
bootloader_reset_methods: Sequence[ResetTarget],
application_probe_methods: Sequence[tuple[ApplicationType, int]],
) -> FirmwareInfo | None:
"""Probe the running firmware on a SiLabs device."""
flasher = Flasher(
device=device,
**(
{"probe_methods": [m.as_flasher_application_type() for m in probe_methods]}
if probe_methods
else {}
probe_methods=tuple(
(m.as_flasher_application_type(), baudrate)
for m, baudrate in application_probe_methods
),
bootloader_reset=tuple(
m.as_flasher_reset_target() for m in bootloader_reset_methods
),
)
@@ -343,11 +348,18 @@ async def probe_silabs_firmware_info(
async def probe_silabs_firmware_type(
device: str, *, probe_methods: Iterable[ApplicationType] | None = None
device: str,
*,
bootloader_reset_methods: Sequence[ResetTarget],
application_probe_methods: Sequence[tuple[ApplicationType, int]],
) -> ApplicationType | None:
"""Probe the running firmware type on a SiLabs device."""
fw_info = await probe_silabs_firmware_info(device, probe_methods=probe_methods)
fw_info = await probe_silabs_firmware_info(
device,
bootloader_reset_methods=bootloader_reset_methods,
application_probe_methods=application_probe_methods,
)
if fw_info is None:
return None
@@ -359,12 +371,22 @@ async def async_flash_silabs_firmware(
device: str,
fw_data: bytes,
expected_installed_firmware_type: ApplicationType,
bootloader_reset_methods: Sequence[ResetTarget] = (),
bootloader_reset_methods: Sequence[ResetTarget],
application_probe_methods: Sequence[tuple[ApplicationType, int]],
progress_callback: Callable[[int, int], None] | None = None,
*,
domain: str = DOMAIN,
) -> FirmwareInfo:
"""Flash firmware to the SiLabs device."""
if not any(
method == expected_installed_firmware_type
for method, _ in application_probe_methods
):
raise ValueError(
f"Expected installed firmware type {expected_installed_firmware_type!r}"
f" not in application probe methods {application_probe_methods!r}"
)
async with async_firmware_update_context(hass, device, domain):
firmware_info = await guess_firmware_info(hass, device)
_LOGGER.debug("Identified firmware info: %s", firmware_info)
@@ -373,11 +395,9 @@ async def async_flash_silabs_firmware(
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(),
probe_methods=tuple(
(m.as_flasher_application_type(), baudrate)
for m, baudrate in application_probe_methods
),
bootloader_reset=tuple(
m.as_flasher_reset_target() for m in bootloader_reset_methods
@@ -401,7 +421,13 @@ async def async_flash_silabs_firmware(
probed_firmware_info = await probe_silabs_firmware_info(
device,
probe_methods=(expected_installed_firmware_type,),
bootloader_reset_methods=bootloader_reset_methods,
# Only probe for the expected installed firmware type
application_probe_methods=[
(method, baudrate)
for method, baudrate in application_probe_methods
if method == expected_installed_firmware_type
],
)
if probed_firmware_info is None:

View File

@@ -16,6 +16,7 @@ from homeassistant.components.homeassistant_hardware.helpers import (
from homeassistant.components.homeassistant_hardware.util import (
ApplicationType,
FirmwareInfo,
ResetTarget,
)
from homeassistant.components.usb import (
usb_service_info_from_device,
@@ -79,6 +80,20 @@ class SkyConnectFirmwareMixin(ConfigEntryBaseFlow, FirmwareInstallFlowProtocol):
context: ConfigFlowContext
ZIGBEE_BAUDRATE = 115200
# There is no hardware bootloader trigger
BOOTLOADER_RESET_METHODS: list[ResetTarget] = []
APPLICATION_PROBE_METHODS = [
(ApplicationType.GECKO_BOOTLOADER, 115200),
(ApplicationType.EZSP, ZIGBEE_BAUDRATE),
(ApplicationType.SPINEL, 460800),
# CPC baudrates can be removed once multiprotocol is removed
(ApplicationType.CPC, 115200),
(ApplicationType.CPC, 230400),
(ApplicationType.CPC, 460800),
(ApplicationType.ROUTER, 115200),
]
def _get_translation_placeholders(self) -> dict[str, str]:
"""Shared translation placeholders."""
placeholders = {

View File

@@ -23,6 +23,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import HomeAssistantSkyConnectConfigEntry
from .config_flow import SkyConnectFirmwareMixin
from .const import (
DOMAIN,
FIRMWARE,
@@ -151,8 +152,8 @@ async def async_setup_entry(
class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
"""SkyConnect firmware update entity."""
# The ZBT-1 does not have a hardware bootloader trigger
bootloader_reset_methods = []
BOOTLOADER_RESET_METHODS = SkyConnectFirmwareMixin.BOOTLOADER_RESET_METHODS
APPLICATION_PROBE_METHODS = SkyConnectFirmwareMixin.APPLICATION_PROBE_METHODS
def __init__(
self,

View File

@@ -82,7 +82,18 @@ else:
class YellowFirmwareMixin(ConfigEntryBaseFlow, FirmwareInstallFlowProtocol):
"""Mixin for Home Assistant Yellow firmware methods."""
ZIGBEE_BAUDRATE = 115200
BOOTLOADER_RESET_METHODS = [ResetTarget.YELLOW]
APPLICATION_PROBE_METHODS = [
(ApplicationType.GECKO_BOOTLOADER, 115200),
(ApplicationType.EZSP, ZIGBEE_BAUDRATE),
(ApplicationType.SPINEL, 460800),
# CPC baudrates can be removed once multiprotocol is removed
(ApplicationType.CPC, 115200),
(ApplicationType.CPC, 230400),
(ApplicationType.CPC, 460800),
(ApplicationType.ROUTER, 115200),
]
async def async_step_install_zigbee_firmware(
self, user_input: dict[str, Any] | None = None
@@ -146,7 +157,11 @@ class HomeAssistantYellowConfigFlow(
assert self._device is not None
# We do not actually use any portion of `BaseFirmwareConfigFlow` beyond this
self._probed_firmware_info = await probe_silabs_firmware_info(self._device)
self._probed_firmware_info = await probe_silabs_firmware_info(
self._device,
bootloader_reset_methods=self.BOOTLOADER_RESET_METHODS,
application_probe_methods=self.APPLICATION_PROBE_METHODS,
)
# Kick off ZHA hardware discovery automatically if Zigbee firmware is running
if (

View File

@@ -14,7 +14,6 @@ from homeassistant.components.homeassistant_hardware.update import (
from homeassistant.components.homeassistant_hardware.util import (
ApplicationType,
FirmwareInfo,
ResetTarget,
)
from homeassistant.components.update import UpdateDeviceClass
from homeassistant.const import EntityCategory
@@ -24,6 +23,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import HomeAssistantYellowConfigEntry
from .config_flow import YellowFirmwareMixin
from .const import DOMAIN, FIRMWARE, FIRMWARE_VERSION, MANUFACTURER, MODEL, RADIO_DEVICE
_LOGGER = logging.getLogger(__name__)
@@ -150,7 +150,8 @@ async def async_setup_entry(
class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
"""Yellow firmware update entity."""
bootloader_reset_methods = [ResetTarget.YELLOW] # Triggers a GPIO reset
BOOTLOADER_RESET_METHODS = YellowFirmwareMixin.BOOTLOADER_RESET_METHODS
APPLICATION_PROBE_METHODS = YellowFirmwareMixin.APPLICATION_PROBE_METHODS
def __init__(
self,

View File

@@ -71,7 +71,20 @@ async def warn_on_wrong_silabs_firmware(hass: HomeAssistant, device: str) -> boo
if device.startswith("socket://"):
return False
app_type = await probe_silabs_firmware_type(device)
app_type = await probe_silabs_firmware_type(
device,
bootloader_reset_methods=(),
application_probe_methods=[
(ApplicationType.GECKO_BOOTLOADER, 115200),
(ApplicationType.EZSP, 115200),
(ApplicationType.EZSP, 460800),
(ApplicationType.SPINEL, 460800),
(ApplicationType.CPC, 460800),
(ApplicationType.CPC, 230400),
(ApplicationType.CPC, 115200),
(ApplicationType.ROUTER, 115200),
],
)
if app_type is None:
# Failed to probe, we can't tell if the wrong firmware is installed

2
requirements_all.txt generated
View File

@@ -3084,7 +3084,7 @@ unifi_ap==0.0.2
unifiled==0.11
# homeassistant.components.homeassistant_hardware
universal-silabs-flasher==0.0.37
universal-silabs-flasher==0.1.0
# homeassistant.components.upb
upb-lib==0.6.1

View File

@@ -2555,7 +2555,7 @@ ultraheat-api==0.5.7
unifi-discovery==1.2.0
# homeassistant.components.homeassistant_hardware
universal-silabs-flasher==0.0.37
universal-silabs-flasher==0.1.0
# homeassistant.components.upb
upb-lib==0.6.1

View File

@@ -16,6 +16,7 @@ from homeassistant.components.homeassistant_hardware.helpers import (
from homeassistant.components.homeassistant_hardware.util import (
ApplicationType,
FirmwareInfo,
ResetTarget,
)
from homeassistant.components.usb import USBDevice
from homeassistant.config_entries import ConfigFlowResult
@@ -367,7 +368,10 @@ async def test_options_flow(
# Verify async_flash_silabs_firmware was called with ZBT-2's reset methods
assert flash_mock.call_count == 1
assert flash_mock.mock_calls[0].kwargs["bootloader_reset_methods"] == ["rts_dtr"]
assert flash_mock.mock_calls[0].kwargs["bootloader_reset_methods"] == [
ResetTarget.RTS_DTR,
ResetTarget.BAUDRATE,
]
flows = hass.config_entries.flow.async_progress()

View File

@@ -304,6 +304,7 @@ def mock_firmware_info(
fw_data: bytes,
expected_installed_firmware_type: ApplicationType,
bootloader_reset_methods: Sequence[ResetTarget] = (),
application_probe_methods: Sequence[tuple[ApplicationType, int]] = (),
progress_callback: Callable[[int, int], None] | None = None,
*,
domain: str = "homeassistant_hardware",

View File

@@ -173,7 +173,12 @@ async def mock_async_setup_update_entities(
class MockFirmwareUpdateEntity(BaseFirmwareUpdateEntity):
"""Mock SkyConnect firmware update entity."""
bootloader_reset_methods = []
BOOTLOADER_RESET_METHODS = [ResetTarget.RTS_DTR]
APPLICATION_PROBE_METHODS = [
(ApplicationType.GECKO_BOOTLOADER, 115200),
(ApplicationType.EZSP, 115200),
(ApplicationType.SPINEL, 460800),
]
def __init__(
self,
@@ -320,6 +325,7 @@ async def test_update_entity_installation(
fw_data: bytes,
expected_installed_firmware_type: ApplicationType,
bootloader_reset_methods: Sequence[ResetTarget] = (),
application_probe_methods: Sequence[tuple[ApplicationType, int]] = (),
progress_callback: Callable[[int, int], None] | None = None,
*,
domain: str = "homeassistant_hardware",

View File

@@ -23,6 +23,7 @@ from homeassistant.components.homeassistant_hardware.util import (
FirmwareInfo,
OwningAddon,
OwningIntegration,
ResetTarget,
async_flash_silabs_firmware,
get_otbr_addon_firmware_info,
guess_firmware_info,
@@ -502,7 +503,14 @@ async def test_probe_silabs_firmware_info(
"homeassistant.components.homeassistant_hardware.util.Flasher",
return_value=mock_flasher,
):
result = await probe_silabs_firmware_info("/dev/ttyUSB0")
result = await probe_silabs_firmware_info(
"/dev/ttyUSB0",
bootloader_reset_methods=[ResetTarget.RTS_DTR],
application_probe_methods=[
(ApplicationType.EZSP, 460800),
(ApplicationType.SPINEL, 460800),
],
)
assert result == expected_fw_info
@@ -531,7 +539,14 @@ async def test_probe_silabs_firmware_type(
autospec=True,
return_value=probe_result,
):
result = await probe_silabs_firmware_type("/dev/ttyUSB0")
result = await probe_silabs_firmware_type(
"/dev/ttyUSB0",
bootloader_reset_methods=[ResetTarget.RTS_DTR],
application_probe_methods=[
(ApplicationType.EZSP, 460800),
(ApplicationType.SPINEL, 460800),
],
)
assert result == expected
@@ -596,7 +611,11 @@ async def test_async_flash_silabs_firmware(hass: HomeAssistant) -> None:
device="/dev/ttyUSB0",
fw_data=b"firmware contents",
expected_installed_firmware_type=ApplicationType.SPINEL,
bootloader_reset_methods=(),
bootloader_reset_methods=[ResetTarget.RTS_DTR],
application_probe_methods=[
(ApplicationType.EZSP, 460800),
(ApplicationType.SPINEL, 460800),
],
progress_callback=progress_callback,
)
@@ -605,7 +624,9 @@ async def test_async_flash_silabs_firmware(hass: HomeAssistant) -> None:
# Verify Flasher was called with correct bootloader_reset parameter
assert flasher_mock.call_count == 1
assert flasher_mock.mock_calls[0].kwargs["bootloader_reset"] == ()
assert flasher_mock.mock_calls[0].kwargs["bootloader_reset"] == (
ResetTarget.RTS_DTR.as_flasher_reset_target(),
)
# Both owning integrations/addons are stopped and restarted
assert owner1.temporarily_stop.mock_calls == [
@@ -623,6 +644,28 @@ async def test_async_flash_silabs_firmware(hass: HomeAssistant) -> None:
]
async def test_async_flash_silabs_firmware_expected_type_not_probed(
hass: HomeAssistant,
) -> None:
"""Test firmware flashing requires probing config to exist for firmware type."""
with pytest.raises(
ValueError,
match=(
r"Expected installed firmware type .*? not in application probe methods .*?"
),
):
await async_flash_silabs_firmware(
hass=hass,
device="/dev/ttyUSB0",
fw_data=b"firmware contents",
expected_installed_firmware_type=ApplicationType.SPINEL,
bootloader_reset_methods=[ResetTarget.RTS_DTR],
application_probe_methods=[
(ApplicationType.EZSP, 460800),
],
)
async def test_async_flash_silabs_firmware_flash_failure(hass: HomeAssistant) -> None:
"""Test async_flash_silabs_firmware flash failure."""
await async_setup_component(hass, "homeassistant_hardware", {})
@@ -659,7 +702,11 @@ async def test_async_flash_silabs_firmware_flash_failure(hass: HomeAssistant) ->
device="/dev/ttyUSB0",
fw_data=b"firmware contents",
expected_installed_firmware_type=ApplicationType.SPINEL,
bootloader_reset_methods=(),
bootloader_reset_methods=[ResetTarget.RTS_DTR],
application_probe_methods=[
(ApplicationType.EZSP, 460800),
(ApplicationType.SPINEL, 460800),
],
)
# Both owning integrations/addons are stopped and restarted
@@ -719,7 +766,11 @@ async def test_async_flash_silabs_firmware_probe_failure(hass: HomeAssistant) ->
device="/dev/ttyUSB0",
fw_data=b"firmware contents",
expected_installed_firmware_type=ApplicationType.SPINEL,
bootloader_reset_methods=(),
bootloader_reset_methods=[ResetTarget.RTS_DTR],
application_probe_methods=[
(ApplicationType.EZSP, 460800),
(ApplicationType.SPINEL, 460800),
],
)
# Both owning integrations/addons are stopped and restarted