mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
Move SiLabs firmware probing helper from ZHA into homeassistant_hardware
(#131586)
* Move firmware probing helper out of ZHA and into hardware * Add a unit test
This commit is contained in:
parent
a53554dad3
commit
90265e2afd
@ -7,17 +7,12 @@ import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from universal_silabs_flasher.const import ApplicationType
|
||||
|
||||
from homeassistant.components.hassio import (
|
||||
AddonError,
|
||||
AddonInfo,
|
||||
AddonManager,
|
||||
AddonState,
|
||||
)
|
||||
from homeassistant.components.zha.repairs.wrong_silabs_firmware import (
|
||||
probe_silabs_firmware_type,
|
||||
)
|
||||
from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigEntryBaseFlow,
|
||||
@ -32,9 +27,11 @@ from homeassistant.helpers.hassio import is_hassio
|
||||
from . import silabs_multiprotocol_addon
|
||||
from .const import ZHA_DOMAIN
|
||||
from .util import (
|
||||
ApplicationType,
|
||||
get_otbr_addon_manager,
|
||||
get_zha_device_path,
|
||||
get_zigbee_flasher_addon_manager,
|
||||
probe_silabs_firmware_type,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -1,8 +1,9 @@
|
||||
{
|
||||
"domain": "homeassistant_hardware",
|
||||
"name": "Home Assistant Hardware",
|
||||
"after_dependencies": ["hassio", "zha"],
|
||||
"after_dependencies": ["hassio"],
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/homeassistant_hardware",
|
||||
"integration_type": "system"
|
||||
"integration_type": "system",
|
||||
"requirements": ["universal-silabs-flasher==0.0.25"]
|
||||
}
|
||||
|
@ -3,11 +3,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
import logging
|
||||
from typing import cast
|
||||
|
||||
from universal_silabs_flasher.const import ApplicationType
|
||||
from universal_silabs_flasher.const import ApplicationType as FlasherApplicationType
|
||||
from universal_silabs_flasher.flasher import Flasher
|
||||
|
||||
from homeassistant.components.hassio import AddonError, AddonState
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
@ -32,6 +35,26 @@ from .silabs_multiprotocol_addon import (
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ApplicationType(StrEnum):
|
||||
"""Application type running on a device."""
|
||||
|
||||
GECKO_BOOTLOADER = "bootloader"
|
||||
CPC = "cpc"
|
||||
EZSP = "ezsp"
|
||||
SPINEL = "spinel"
|
||||
|
||||
@classmethod
|
||||
def from_flasher_application_type(
|
||||
cls, app_type: FlasherApplicationType
|
||||
) -> ApplicationType:
|
||||
"""Convert a USF application type enum."""
|
||||
return cls(app_type.value)
|
||||
|
||||
def as_flasher_application_type(self) -> FlasherApplicationType:
|
||||
"""Convert the application type enum into one compatible with USF."""
|
||||
return FlasherApplicationType(self.value)
|
||||
|
||||
|
||||
def get_zha_device_path(config_entry: ConfigEntry) -> str | None:
|
||||
"""Get the device path from a ZHA config entry."""
|
||||
return cast(str | None, config_entry.data.get("device", {}).get("path", None))
|
||||
@ -137,3 +160,27 @@ async def guess_firmware_type(hass: HomeAssistant, device_path: str) -> Firmware
|
||||
assert guesses
|
||||
|
||||
return guesses[-1]
|
||||
|
||||
|
||||
async def probe_silabs_firmware_type(
|
||||
device: str, *, probe_methods: Iterable[ApplicationType] | None = None
|
||||
) -> ApplicationType | 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 {}
|
||||
),
|
||||
)
|
||||
|
||||
try:
|
||||
await flasher.probe_app_type()
|
||||
except Exception: # noqa: BLE001
|
||||
_LOGGER.debug("Failed to probe application type", exc_info=True)
|
||||
|
||||
if flasher.app_type is None:
|
||||
return None
|
||||
|
||||
return ApplicationType.from_flasher_application_type(flasher.app_type)
|
||||
|
@ -5,13 +5,12 @@ from __future__ import annotations
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Protocol
|
||||
|
||||
from universal_silabs_flasher.const import ApplicationType
|
||||
|
||||
from homeassistant.components import usb
|
||||
from homeassistant.components.homeassistant_hardware import (
|
||||
firmware_config_flow,
|
||||
silabs_multiprotocol_addon,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.util import ApplicationType
|
||||
from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigEntryBaseFlow,
|
||||
|
@ -8,7 +8,6 @@ import logging
|
||||
from typing import Any, final
|
||||
|
||||
import aiohttp
|
||||
from universal_silabs_flasher.const import ApplicationType
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.hassio import (
|
||||
@ -25,6 +24,7 @@ from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon
|
||||
OptionsFlowHandler as MultiprotocolOptionsFlowHandler,
|
||||
SerialPortSettings as MultiprotocolSerialPortSettings,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.util import ApplicationType
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_HARDWARE,
|
||||
ConfigEntry,
|
||||
|
@ -4,7 +4,7 @@
|
||||
"after_dependencies": ["hassio", "onboarding", "usb"],
|
||||
"codeowners": ["@dmulcahey", "@adminiuga", "@puddly", "@TheJulianJES"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["file_upload"],
|
||||
"dependencies": ["file_upload", "homeassistant_hardware"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/zha",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": [
|
||||
@ -21,7 +21,7 @@
|
||||
"zha",
|
||||
"universal_silabs_flasher"
|
||||
],
|
||||
"requirements": ["universal-silabs-flasher==0.0.25", "zha==0.0.44"],
|
||||
"requirements": ["zha==0.0.44"],
|
||||
"usb": [
|
||||
{
|
||||
"vid": "10C4",
|
||||
|
@ -5,9 +5,10 @@ from __future__ import annotations
|
||||
import enum
|
||||
import logging
|
||||
|
||||
from universal_silabs_flasher.const import ApplicationType
|
||||
from universal_silabs_flasher.flasher import Flasher
|
||||
|
||||
from homeassistant.components.homeassistant_hardware.util import (
|
||||
ApplicationType,
|
||||
probe_silabs_firmware_type,
|
||||
)
|
||||
from homeassistant.components.homeassistant_sky_connect import (
|
||||
hardware as skyconnect_hardware,
|
||||
)
|
||||
@ -74,23 +75,6 @@ def _detect_radio_hardware(hass: HomeAssistant, device: str) -> HardwareType:
|
||||
return HardwareType.OTHER
|
||||
|
||||
|
||||
async def probe_silabs_firmware_type(
|
||||
device: str, *, probe_methods: ApplicationType | None = None
|
||||
) -> ApplicationType | None:
|
||||
"""Probe the running firmware on a Silabs device."""
|
||||
flasher = Flasher(
|
||||
device=device,
|
||||
**({"probe_methods": probe_methods} if probe_methods else {}),
|
||||
)
|
||||
|
||||
try:
|
||||
await flasher.probe_app_type()
|
||||
except Exception: # noqa: BLE001
|
||||
_LOGGER.debug("Failed to probe application type", exc_info=True)
|
||||
|
||||
return flasher.app_type
|
||||
|
||||
|
||||
async def warn_on_wrong_silabs_firmware(hass: HomeAssistant, device: str) -> bool:
|
||||
"""Create a repair issue if the wrong type of SiLabs firmware is detected."""
|
||||
# Only consider actual serial ports
|
||||
|
@ -2942,7 +2942,7 @@ unifi_ap==0.0.2
|
||||
# homeassistant.components.unifiled
|
||||
unifiled==0.11
|
||||
|
||||
# homeassistant.components.zha
|
||||
# homeassistant.components.homeassistant_hardware
|
||||
universal-silabs-flasher==0.0.25
|
||||
|
||||
# homeassistant.components.upb
|
||||
|
@ -2358,9 +2358,6 @@ ultraheat-api==0.5.7
|
||||
# homeassistant.components.unifiprotect
|
||||
unifi-discovery==1.2.0
|
||||
|
||||
# homeassistant.components.zha
|
||||
universal-silabs-flasher==0.0.25
|
||||
|
||||
# homeassistant.components.upb
|
||||
upb-lib==0.5.9
|
||||
|
||||
|
@ -168,6 +168,7 @@ IGNORE_VIOLATIONS = {
|
||||
("zha", "homeassistant_sky_connect"),
|
||||
("zha", "homeassistant_yellow"),
|
||||
("homeassistant_sky_connect", "zha"),
|
||||
("homeassistant_hardware", "zha"),
|
||||
# This should become a helper method that integrations can submit data to
|
||||
("websocket_api", "lovelace"),
|
||||
("websocket_api", "shopping_list"),
|
||||
|
@ -7,7 +7,6 @@ from typing import Any
|
||||
from unittest.mock import AsyncMock, Mock, call, patch
|
||||
|
||||
import pytest
|
||||
from universal_silabs_flasher.const import ApplicationType
|
||||
|
||||
from homeassistant.components.hassio import AddonInfo, AddonState
|
||||
from homeassistant.components.homeassistant_hardware.firmware_config_flow import (
|
||||
@ -17,6 +16,7 @@ from homeassistant.components.homeassistant_hardware.firmware_config_flow import
|
||||
BaseFirmwareOptionsFlow,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.util import (
|
||||
ApplicationType,
|
||||
get_otbr_addon_manager,
|
||||
get_zigbee_flasher_addon_manager,
|
||||
)
|
||||
|
@ -3,13 +3,13 @@
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
from universal_silabs_flasher.const import ApplicationType
|
||||
|
||||
from homeassistant.components.hassio import AddonError, AddonInfo, AddonState
|
||||
from homeassistant.components.homeassistant_hardware.firmware_config_flow import (
|
||||
STEP_PICK_FIRMWARE_THREAD,
|
||||
STEP_PICK_FIRMWARE_ZIGBEE,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.util import ApplicationType
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
|
@ -2,13 +2,14 @@
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from universal_silabs_flasher.const import ApplicationType
|
||||
|
||||
from homeassistant.components.hassio import AddonError, AddonInfo, AddonState
|
||||
from homeassistant.components.homeassistant_hardware.util import (
|
||||
ApplicationType,
|
||||
FirmwareGuess,
|
||||
FlasherApplicationType,
|
||||
get_zha_device_path,
|
||||
guess_firmware_type,
|
||||
probe_silabs_firmware_type,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
@ -156,3 +157,27 @@ async def test_guess_firmware_type(hass: HomeAssistant) -> None:
|
||||
assert (await guess_firmware_type(hass, path)) == FirmwareGuess(
|
||||
is_running=True, firmware_type=ApplicationType.CPC, source="multiprotocol"
|
||||
)
|
||||
|
||||
|
||||
async def test_probe_silabs_firmware_type() -> None:
|
||||
"""Test probing Silabs firmware type."""
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.homeassistant_hardware.util.Flasher.probe_app_type",
|
||||
side_effect=RuntimeError,
|
||||
):
|
||||
assert (await probe_silabs_firmware_type("/dev/ttyUSB0")) is None
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.homeassistant_hardware.util.Flasher.probe_app_type",
|
||||
side_effect=lambda self: setattr(self, "app_type", FlasherApplicationType.EZSP),
|
||||
autospec=True,
|
||||
) as mock_probe_app_type:
|
||||
# The application type constant is converted back and forth transparently
|
||||
result = await probe_silabs_firmware_type(
|
||||
"/dev/ttyUSB0", probe_methods=[ApplicationType.EZSP]
|
||||
)
|
||||
assert result is ApplicationType.EZSP
|
||||
|
||||
flasher = mock_probe_app_type.mock_calls[0].args[0]
|
||||
assert flasher._probe_methods == [FlasherApplicationType.EZSP]
|
||||
|
@ -2,9 +2,10 @@
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from universal_silabs_flasher.const import ApplicationType
|
||||
|
||||
from homeassistant.components.homeassistant_hardware.util import FirmwareGuess
|
||||
from homeassistant.components.homeassistant_hardware.util import (
|
||||
ApplicationType,
|
||||
FirmwareGuess,
|
||||
)
|
||||
from homeassistant.components.homeassistant_sky_connect.const import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
|
@ -1,17 +1,14 @@
|
||||
"""Test ZHA repairs."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
from unittest.mock import Mock, call, patch
|
||||
|
||||
import pytest
|
||||
from universal_silabs_flasher.const import ApplicationType
|
||||
from universal_silabs_flasher.flasher import Flasher
|
||||
from zigpy.application import ControllerApplication
|
||||
import zigpy.backups
|
||||
from zigpy.exceptions import NetworkSettingsInconsistent
|
||||
|
||||
from homeassistant.components.homeassistant_hardware.util import ApplicationType
|
||||
from homeassistant.components.homeassistant_sky_connect.const import ( # pylint: disable=hass-component-root-import
|
||||
DOMAIN as SKYCONNECT_DOMAIN,
|
||||
)
|
||||
@ -25,7 +22,6 @@ from homeassistant.components.zha.repairs.wrong_silabs_firmware import (
|
||||
ISSUE_WRONG_SILABS_FIRMWARE_INSTALLED,
|
||||
HardwareType,
|
||||
_detect_radio_hardware,
|
||||
probe_silabs_firmware_type,
|
||||
warn_on_wrong_silabs_firmware,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
@ -41,15 +37,6 @@ SKYCONNECT_DEVICE = "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_9e2adbd75b8
|
||||
CONNECT_ZBT1_DEVICE = "/dev/serial/by-id/usb-Nabu_Casa_Home_Assistant_Connect_ZBT-1_9e2adbd75b8beb119fe564a0f320645d-if00-port0"
|
||||
|
||||
|
||||
def set_flasher_app_type(app_type: ApplicationType) -> Callable[[Flasher], None]:
|
||||
"""Set the app type on the flasher."""
|
||||
|
||||
def replacement(self: Flasher) -> None:
|
||||
self.app_type = app_type
|
||||
|
||||
return replacement
|
||||
|
||||
|
||||
def test_detect_radio_hardware(hass: HomeAssistant) -> None:
|
||||
"""Test logic to detect radio hardware."""
|
||||
skyconnect_config_entry = MockConfigEntry(
|
||||
@ -143,9 +130,8 @@ async def test_multipan_firmware_repair(
|
||||
# ZHA fails to set up
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.zha.repairs.wrong_silabs_firmware.Flasher.probe_app_type",
|
||||
side_effect=set_flasher_app_type(ApplicationType.CPC),
|
||||
autospec=True,
|
||||
"homeassistant.components.zha.repairs.wrong_silabs_firmware.probe_silabs_firmware_type",
|
||||
return_value=ApplicationType.CPC,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.zha.Gateway.async_initialize",
|
||||
@ -194,9 +180,8 @@ async def test_multipan_firmware_no_repair_on_probe_failure(
|
||||
# ZHA fails to set up
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.zha.repairs.wrong_silabs_firmware.Flasher.probe_app_type",
|
||||
side_effect=set_flasher_app_type(None),
|
||||
autospec=True,
|
||||
"homeassistant.components.zha.repairs.wrong_silabs_firmware.probe_silabs_firmware_type",
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.zha.Gateway.async_initialize",
|
||||
@ -231,9 +216,8 @@ async def test_multipan_firmware_retry_on_probe_ezsp(
|
||||
# ZHA fails to set up
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.zha.repairs.wrong_silabs_firmware.Flasher.probe_app_type",
|
||||
side_effect=set_flasher_app_type(ApplicationType.EZSP),
|
||||
autospec=True,
|
||||
"homeassistant.components.zha.repairs.wrong_silabs_firmware.probe_silabs_firmware_type",
|
||||
return_value=ApplicationType.EZSP,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.zha.Gateway.async_initialize",
|
||||
@ -260,37 +244,12 @@ async def test_no_warn_on_socket(hass: HomeAssistant) -> None:
|
||||
"""Test that no warning is issued when the device is a socket."""
|
||||
with patch(
|
||||
"homeassistant.components.zha.repairs.wrong_silabs_firmware.probe_silabs_firmware_type",
|
||||
autospec=True,
|
||||
) as mock_probe:
|
||||
await warn_on_wrong_silabs_firmware(hass, device="socket://1.2.3.4:5678")
|
||||
|
||||
mock_probe.assert_not_called()
|
||||
|
||||
|
||||
async def test_probe_failure_exception_handling(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test that probe failures are handled gracefully."""
|
||||
logger = logging.getLogger(
|
||||
"homeassistant.components.zha.repairs.wrong_silabs_firmware"
|
||||
)
|
||||
orig_level = logger.level
|
||||
|
||||
with (
|
||||
caplog.at_level(logging.DEBUG),
|
||||
patch(
|
||||
"homeassistant.components.zha.repairs.wrong_silabs_firmware.Flasher.probe_app_type",
|
||||
side_effect=RuntimeError(),
|
||||
) as mock_probe_app_type,
|
||||
):
|
||||
logger.setLevel(logging.DEBUG)
|
||||
await probe_silabs_firmware_type("/dev/ttyZigbee")
|
||||
logger.setLevel(orig_level)
|
||||
|
||||
mock_probe_app_type.assert_awaited()
|
||||
assert "Failed to probe application type" in caplog.text
|
||||
|
||||
|
||||
async def test_inconsistent_settings_keep_new(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
|
Loading…
x
Reference in New Issue
Block a user