ha-core/homeassistant/components/zha/repairs/wrong_silabs_firmware.py

128 lines
3.8 KiB
Python

"""ZHA repairs for common environmental and device problems."""
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_sky_connect import (
hardware as skyconnect_hardware,
)
from homeassistant.components.homeassistant_yellow import (
RADIO_DEVICE as YELLOW_RADIO_DEVICE,
hardware as yellow_hardware,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import issue_registry as ir
from ..core.const import DOMAIN
_LOGGER = logging.getLogger(__name__)
class AlreadyRunningEZSP(Exception):
"""The device is already running EZSP firmware."""
class HardwareType(enum.StrEnum):
"""Detected Zigbee hardware type."""
SKYCONNECT = "skyconnect"
YELLOW = "yellow"
OTHER = "other"
DISABLE_MULTIPAN_URL = {
HardwareType.YELLOW: (
"https://yellow.home-assistant.io/guides/disable-multiprotocol/#flash-the-silicon-labs-radio-firmware"
),
HardwareType.SKYCONNECT: (
"https://skyconnect.home-assistant.io/procedures/disable-multiprotocol/#step-flash-the-silicon-labs-radio-firmware"
),
HardwareType.OTHER: None,
}
ISSUE_WRONG_SILABS_FIRMWARE_INSTALLED = "wrong_silabs_firmware_installed"
def _detect_radio_hardware(hass: HomeAssistant, device: str) -> HardwareType:
"""Identify the radio hardware with the given serial port."""
try:
yellow_hardware.async_info(hass)
except HomeAssistantError:
pass
else:
if device == YELLOW_RADIO_DEVICE:
return HardwareType.YELLOW
try:
info = skyconnect_hardware.async_info(hass)
except HomeAssistantError:
pass
else:
for hardware_info in info:
for entry_id in hardware_info.config_entries or []:
entry = hass.config_entries.async_get_entry(entry_id)
if entry is not None and entry.data["device"] == device:
return HardwareType.SKYCONNECT
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: # pylint: disable=broad-except
_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
if device.startswith("socket://"):
return False
app_type = await probe_silabs_firmware_type(device)
if app_type is None:
# Failed to probe, we can't tell if the wrong firmware is installed
return False
if app_type == ApplicationType.EZSP:
# If connecting fails but we somehow probe EZSP (e.g. stuck in bootloader),
# reconnect, it should work
raise AlreadyRunningEZSP
hardware_type = _detect_radio_hardware(hass, device)
ir.async_create_issue(
hass,
domain=DOMAIN,
issue_id=ISSUE_WRONG_SILABS_FIRMWARE_INSTALLED,
is_fixable=False,
is_persistent=True,
learn_more_url=DISABLE_MULTIPAN_URL[hardware_type],
severity=ir.IssueSeverity.ERROR,
translation_key=(
ISSUE_WRONG_SILABS_FIRMWARE_INSTALLED
+ ("_nabucasa" if hardware_type != HardwareType.OTHER else "_other")
),
translation_placeholders={"firmware_type": app_type.name},
)
return True