"""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) -> ApplicationType | None: """Probe the running firmware on a Silabs device.""" flasher = Flasher(device=device) 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 def async_delete_blocking_issues(hass: HomeAssistant) -> None: """Delete repair issues that should disappear on a successful startup.""" ir.async_delete_issue(hass, DOMAIN, ISSUE_WRONG_SILABS_FIRMWARE_INSTALLED)