mirror of
https://github.com/home-assistant/core.git
synced 2026-05-22 08:45:16 +02:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 90e8ed0210 | |||
| fd049891ff | |||
| e1b2de20d9 | |||
| ab52d02da9 | |||
| 98be2356ca | |||
| 9eddc58754 | |||
| e709189dab | |||
| b2ca7f4eef | |||
| 0a14e93cb3 | |||
| 352e89812f | |||
| 2f1730ed51 | |||
| 110ba33e43 | |||
| 9d96626e93 | |||
| 26df3f1966 | |||
| dc4f4ad5fe | |||
| 523f5dac48 | |||
| 19aecf2f1c | |||
| 6170156408 |
@@ -3,7 +3,7 @@
|
||||
"name": "Home Assistant Hardware",
|
||||
"after_dependencies": ["hassio"],
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
"dependencies": ["usb"],
|
||||
"dependencies": ["repairs", "usb"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/homeassistant_hardware",
|
||||
"integration_type": "system",
|
||||
"requirements": [
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
"""Repairs for the Home Assistant Hardware integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.repairs import RepairsFlow
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigFlowResult
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
|
||||
ISSUE_MULTI_PAN_MIGRATION = "multi_pan_migration"
|
||||
|
||||
|
||||
@callback
|
||||
def _multi_pan_issue_id(config_entry: ConfigEntry) -> str:
|
||||
"""Return the issue id for the multi-PAN migration issue of an entry."""
|
||||
return f"{ISSUE_MULTI_PAN_MIGRATION}_{config_entry.entry_id}"
|
||||
|
||||
|
||||
@callback
|
||||
def async_create_multi_pan_migration_issue(
|
||||
hass: HomeAssistant,
|
||||
domain: str,
|
||||
config_entry: ConfigEntry,
|
||||
) -> None:
|
||||
"""Create a repair issue to guide migration away from Multi-PAN."""
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
domain=domain,
|
||||
issue_id=_multi_pan_issue_id(config_entry),
|
||||
is_fixable=True,
|
||||
is_persistent=False,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key=ISSUE_MULTI_PAN_MIGRATION,
|
||||
translation_placeholders={"hardware_name": config_entry.title},
|
||||
data={"entry_id": config_entry.entry_id},
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_delete_multi_pan_migration_issue(
|
||||
hass: HomeAssistant,
|
||||
domain: str,
|
||||
config_entry: ConfigEntry,
|
||||
) -> None:
|
||||
"""Delete the multi-PAN migration repair issue for this entry."""
|
||||
ir.async_delete_issue(hass, domain, _multi_pan_issue_id(config_entry))
|
||||
|
||||
|
||||
class MultiPanMigrationRepairFlow(RepairsFlow):
|
||||
"""Reuse the multi-PAN options flow uninstall steps as a repair flow.
|
||||
|
||||
Subclass this together with the hardware-specific
|
||||
``MultiPanOptionsFlowHandler`` in each hardware integration's repairs
|
||||
module.
|
||||
|
||||
The repair flow runs in the repairs flow manager where ``self.handler``
|
||||
is the integration domain rather than the hardware config entry id, so
|
||||
the ``config_entry`` accessor of ``OptionsFlow`` must be overridden.
|
||||
"""
|
||||
|
||||
_repair_config_entry: ConfigEntry
|
||||
|
||||
@property
|
||||
def config_entry(self) -> ConfigEntry:
|
||||
"""Return the hardware config entry to migrate."""
|
||||
return self._repair_config_entry
|
||||
|
||||
async def _async_step_start_migration(self) -> ConfigFlowResult:
|
||||
"""Jump straight into the uninstall step of the migration flow.
|
||||
|
||||
The repair flow's init data is the issue context, not user form input,
|
||||
so pass None to render the uninstall confirmation form.
|
||||
"""
|
||||
return await self.async_step_uninstall_addon() # type: ignore[attr-defined, no-any-return]
|
||||
@@ -8,6 +8,8 @@ import dataclasses
|
||||
import logging
|
||||
from typing import Any, Protocol
|
||||
|
||||
from aiohttp import ClientError
|
||||
from ha_silabs_firmware_client import FirmwareUpdateClient, ManifestMissing
|
||||
import voluptuous as vol
|
||||
import yarl
|
||||
|
||||
@@ -27,6 +29,7 @@ from homeassistant.config_entries import (
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.data_entry_flow import AbortFlow
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.hassio import is_hassio
|
||||
from homeassistant.helpers.integration_platform import (
|
||||
async_process_integration_platforms,
|
||||
@@ -39,15 +42,18 @@ from homeassistant.helpers.selector import (
|
||||
from homeassistant.helpers.singleton import singleton
|
||||
from homeassistant.helpers.storage import Store
|
||||
|
||||
from .const import LOGGER, SILABS_FLASHER_ADDON_SLUG, SILABS_MULTIPROTOCOL_ADDON_SLUG
|
||||
from .const import DOMAIN, LOGGER, SILABS_MULTIPROTOCOL_ADDON_SLUG
|
||||
from .util import (
|
||||
ApplicationType,
|
||||
WaitingAddonManager,
|
||||
async_firmware_flashing_context,
|
||||
async_flash_silabs_firmware,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DATA_MULTIPROTOCOL_ADDON_MANAGER = "silabs_multiprotocol_addon_manager"
|
||||
DATA_FLASHER_ADDON_MANAGER = "silabs_flasher"
|
||||
|
||||
ADDON_STATE_POLL_INTERVAL = 3
|
||||
ADDON_INFO_POLL_TIMEOUT = 15 * 60
|
||||
|
||||
CONF_ADDON_AUTOFLASH_FW = "autoflash_firmware"
|
||||
CONF_ADDON_DEVICE = "device"
|
||||
@@ -73,53 +79,6 @@ async def get_multiprotocol_addon_manager(
|
||||
return manager
|
||||
|
||||
|
||||
class WaitingAddonManager(AddonManager):
|
||||
"""Addon manager which supports waiting operations for managing an addon."""
|
||||
|
||||
async def async_wait_until_addon_state(self, *states: AddonState) -> None:
|
||||
"""Poll an addon's info until it is in a specific state."""
|
||||
async with asyncio.timeout(ADDON_INFO_POLL_TIMEOUT):
|
||||
while True:
|
||||
try:
|
||||
info = await self.async_get_addon_info()
|
||||
except AddonError:
|
||||
info = None
|
||||
|
||||
_LOGGER.debug("Waiting for addon to be in state %s: %s", states, info)
|
||||
|
||||
if info is not None and info.state in states:
|
||||
break
|
||||
|
||||
await asyncio.sleep(ADDON_STATE_POLL_INTERVAL)
|
||||
|
||||
async def async_start_addon_waiting(self) -> None:
|
||||
"""Start an add-on."""
|
||||
await self.async_schedule_start_addon()
|
||||
await self.async_wait_until_addon_state(AddonState.RUNNING)
|
||||
|
||||
async def async_install_addon_waiting(self) -> None:
|
||||
"""Install an add-on."""
|
||||
await self.async_schedule_install_addon()
|
||||
await self.async_wait_until_addon_state(
|
||||
AddonState.RUNNING,
|
||||
AddonState.NOT_RUNNING,
|
||||
)
|
||||
|
||||
async def async_uninstall_addon_waiting(self) -> None:
|
||||
"""Uninstall an add-on."""
|
||||
try:
|
||||
info = await self.async_get_addon_info()
|
||||
except AddonError:
|
||||
info = None
|
||||
|
||||
# Do not try to uninstall an addon if it is already uninstalled
|
||||
if info is not None and info.state == AddonState.NOT_INSTALLED:
|
||||
return
|
||||
|
||||
await self.async_uninstall_addon()
|
||||
await self.async_wait_until_addon_state(AddonState.NOT_INSTALLED)
|
||||
|
||||
|
||||
class MultiprotocolAddonManager(WaitingAddonManager):
|
||||
"""Silicon Labs Multiprotocol add-on manager."""
|
||||
|
||||
@@ -267,18 +226,6 @@ class MultipanProtocol(Protocol):
|
||||
"""
|
||||
|
||||
|
||||
@singleton(DATA_FLASHER_ADDON_MANAGER)
|
||||
@callback
|
||||
def get_flasher_addon_manager(hass: HomeAssistant) -> WaitingAddonManager:
|
||||
"""Get the flasher add-on manager."""
|
||||
return WaitingAddonManager(
|
||||
hass,
|
||||
LOGGER,
|
||||
"Silicon Labs Flasher",
|
||||
SILABS_FLASHER_ADDON_SLUG,
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class SerialPortSettings:
|
||||
"""Serial port settings."""
|
||||
@@ -341,6 +288,19 @@ class OptionsFlowHandler(OptionsFlow, ABC):
|
||||
def _zha_name(self) -> str:
|
||||
"""Return the ZHA name."""
|
||||
|
||||
@abstractmethod
|
||||
def _firmware_update_url(self) -> str:
|
||||
"""Return the firmware update manifest URL."""
|
||||
|
||||
@abstractmethod
|
||||
def _zigbee_firmware_type(self) -> str:
|
||||
"""Return the zigbee firmware type identifier (e.g. 'yellow_zigbee_ncp')."""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def _flasher_cls(self) -> type:
|
||||
"""Return the hardware-specific flasher class."""
|
||||
|
||||
@property
|
||||
def flow_manager(self) -> OptionsFlowManager:
|
||||
"""Return the correct flow manager."""
|
||||
@@ -688,61 +648,7 @@ class OptionsFlowHandler(OptionsFlow, ABC):
|
||||
async def async_step_firmware_revert(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Install the flasher addon, if necessary."""
|
||||
|
||||
flasher_manager = get_flasher_addon_manager(self.hass)
|
||||
addon_info = await self._async_get_addon_info(flasher_manager)
|
||||
|
||||
if addon_info.state == AddonState.NOT_INSTALLED:
|
||||
return await self.async_step_install_flasher_addon()
|
||||
|
||||
if addon_info.state == AddonState.NOT_RUNNING:
|
||||
return await self.async_step_configure_flasher_addon()
|
||||
|
||||
# If the addon is already installed and running, fail
|
||||
return self.async_abort(
|
||||
reason="addon_already_running",
|
||||
description_placeholders={"addon_name": flasher_manager.addon_name},
|
||||
)
|
||||
|
||||
async def async_step_install_flasher_addon(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Show progress dialog for installing flasher addon."""
|
||||
flasher_manager = get_flasher_addon_manager(self.hass)
|
||||
addon_info = await self._async_get_addon_info(flasher_manager)
|
||||
|
||||
_LOGGER.debug("Flasher addon state: %s", addon_info)
|
||||
|
||||
if not self.install_task:
|
||||
self.install_task = self.hass.async_create_task(
|
||||
flasher_manager.async_install_addon_waiting(),
|
||||
"SiLabs Flasher addon install",
|
||||
eager_start=False,
|
||||
)
|
||||
|
||||
if not self.install_task.done():
|
||||
return self.async_show_progress(
|
||||
step_id="install_flasher_addon",
|
||||
progress_action="install_addon",
|
||||
description_placeholders={"addon_name": flasher_manager.addon_name},
|
||||
progress_task=self.install_task,
|
||||
)
|
||||
|
||||
try:
|
||||
await self.install_task
|
||||
except AddonError as err:
|
||||
_LOGGER.error(err)
|
||||
return self.async_show_progress_done(next_step_id="install_failed")
|
||||
finally:
|
||||
self.install_task = None
|
||||
|
||||
return self.async_show_progress_done(next_step_id="configure_flasher_addon")
|
||||
|
||||
async def async_step_configure_flasher_addon(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Perform initial backup and reconfigure ZHA."""
|
||||
"""Initiate ZHA backup and start multiprotocol addon uninstall."""
|
||||
# pylint: disable=hass-component-root-import
|
||||
from homeassistant.components.zha import DOMAIN as ZHA_DOMAIN # noqa: PLC0415
|
||||
from homeassistant.components.zha.radio_manager import ( # noqa: PLC0415
|
||||
@@ -784,17 +690,6 @@ class OptionsFlowHandler(OptionsFlow, ABC):
|
||||
_LOGGER.exception("Unexpected exception during ZHA migration")
|
||||
raise AbortFlow("zha_migration_failed") from err
|
||||
|
||||
flasher_manager = get_flasher_addon_manager(self.hass)
|
||||
addon_info = await self._async_get_addon_info(flasher_manager)
|
||||
new_addon_config = {
|
||||
**addon_info.options,
|
||||
"device": new_settings.device,
|
||||
"flow_control": new_settings.flow_control,
|
||||
}
|
||||
|
||||
_LOGGER.debug("Reconfiguring flasher addon with %s", new_addon_config)
|
||||
await self._async_set_addon_config(new_addon_config, flasher_manager)
|
||||
|
||||
return await self.async_step_uninstall_multiprotocol_addon()
|
||||
|
||||
async def async_step_uninstall_multiprotocol_addon(
|
||||
@@ -823,62 +718,90 @@ class OptionsFlowHandler(OptionsFlow, ABC):
|
||||
finally:
|
||||
self.stop_task = None
|
||||
|
||||
return self.async_show_progress_done(next_step_id="start_flasher_addon")
|
||||
return self.async_show_progress_done(next_step_id="install_zigbee_firmware")
|
||||
|
||||
async def async_step_start_flasher_addon(
|
||||
async def async_step_install_zigbee_firmware(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Start Silicon Labs Flasher add-on."""
|
||||
flasher_manager = get_flasher_addon_manager(self.hass)
|
||||
"""Flash Zigbee firmware directly onto the radio."""
|
||||
if not self.install_task:
|
||||
|
||||
if not self.start_task:
|
||||
async def _flash_firmware() -> None:
|
||||
serial_port_settings = await self._async_serial_port_settings()
|
||||
device = serial_port_settings.device
|
||||
|
||||
async def start_and_wait_until_done() -> None:
|
||||
await flasher_manager.async_start_addon_waiting()
|
||||
# Now that the addon is running, wait for it to finish
|
||||
await flasher_manager.async_wait_until_addon_state(
|
||||
AddonState.NOT_RUNNING
|
||||
)
|
||||
# For the duration of firmware flashing, hint to other integrations
|
||||
# (i.e. ZHA) that the hardware is in use and should not be accessed.
|
||||
async with async_firmware_flashing_context(self.hass, device, DOMAIN):
|
||||
session = async_get_clientsession(self.hass)
|
||||
client = FirmwareUpdateClient(self._firmware_update_url(), session)
|
||||
|
||||
self.start_task = self.hass.async_create_task(
|
||||
start_and_wait_until_done(), eager_start=False
|
||||
try:
|
||||
manifest = await client.async_update_data()
|
||||
fw_manifest = next(
|
||||
fw
|
||||
for fw in manifest.firmwares
|
||||
if fw.filename.startswith(self._zigbee_firmware_type())
|
||||
)
|
||||
fw_data = await client.async_fetch_firmware(fw_manifest)
|
||||
except (
|
||||
StopIteration,
|
||||
TimeoutError,
|
||||
ClientError,
|
||||
ManifestMissing,
|
||||
ValueError,
|
||||
) as err:
|
||||
raise HomeAssistantError(
|
||||
"Failed to fetch Zigbee firmware"
|
||||
) from err
|
||||
|
||||
await async_flash_silabs_firmware(
|
||||
hass=self.hass,
|
||||
device=device,
|
||||
fw_data=fw_data,
|
||||
flasher_cls=self._flasher_cls,
|
||||
expected_installed_firmware_type=ApplicationType.EZSP,
|
||||
)
|
||||
|
||||
self.install_task = self.hass.async_create_task(
|
||||
_flash_firmware(),
|
||||
"Flash Zigbee firmware",
|
||||
eager_start=False,
|
||||
)
|
||||
|
||||
if not self.start_task.done():
|
||||
if not self.install_task.done():
|
||||
return self.async_show_progress(
|
||||
step_id="start_flasher_addon",
|
||||
progress_action="start_flasher_addon",
|
||||
description_placeholders={"addon_name": flasher_manager.addon_name},
|
||||
progress_task=self.start_task,
|
||||
step_id="install_zigbee_firmware",
|
||||
progress_action="install_zigbee_firmware",
|
||||
description_placeholders={
|
||||
"hardware_name": self._hardware_name(),
|
||||
},
|
||||
progress_task=self.install_task,
|
||||
)
|
||||
|
||||
try:
|
||||
await self.start_task
|
||||
except (AddonError, AbortFlow) as err:
|
||||
_LOGGER.error(err)
|
||||
return self.async_show_progress_done(next_step_id="flasher_failed")
|
||||
await self.install_task
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error("Failed to flash Zigbee firmware: %s", err)
|
||||
return self.async_show_progress_done(next_step_id="firmware_flash_failed")
|
||||
finally:
|
||||
self.start_task = None
|
||||
self.install_task = None
|
||||
|
||||
return self.async_show_progress_done(next_step_id="flashing_complete")
|
||||
|
||||
async def async_step_flasher_failed(
|
||||
async def async_step_firmware_flash_failed(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Flasher add-on start failed."""
|
||||
flasher_manager = get_flasher_addon_manager(self.hass)
|
||||
"""Firmware flashing failed."""
|
||||
return self.async_abort(
|
||||
reason="addon_start_failed",
|
||||
description_placeholders={"addon_name": flasher_manager.addon_name},
|
||||
reason="fw_install_failed",
|
||||
description_placeholders={"firmware_name": "Zigbee"},
|
||||
)
|
||||
|
||||
async def async_step_flashing_complete(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Finish flashing and update the config entry."""
|
||||
flasher_manager = get_flasher_addon_manager(self.hass)
|
||||
await flasher_manager.async_uninstall_addon_waiting()
|
||||
|
||||
# Finish ZHA migration if needed
|
||||
if self._zha_migration_mgr:
|
||||
try:
|
||||
|
||||
@@ -102,7 +102,9 @@
|
||||
},
|
||||
"progress": {
|
||||
"install_addon": "Please wait while the {addon_name} app installation finishes. This can take several minutes.",
|
||||
"start_addon": "Please wait while the {addon_name} app start completes. This may take some seconds."
|
||||
"install_zigbee_firmware": "Please wait while Zigbee-only firmware is installed on your {hardware_name}. This can take several minutes.",
|
||||
"start_addon": "Please wait while the {addon_name} app start completes. This may take some seconds.",
|
||||
"uninstall_multiprotocol_addon": "Please wait while the {addon_name} app is uninstalled."
|
||||
},
|
||||
"step": {
|
||||
"addon_installed_other_device": {
|
||||
|
||||
@@ -38,13 +38,59 @@ from .const import (
|
||||
ZIGBEE_FLASHER_ADDON_SLUG,
|
||||
)
|
||||
from .helpers import async_firmware_update_context
|
||||
from .silabs_multiprotocol_addon import (
|
||||
WaitingAddonManager,
|
||||
get_multiprotocol_addon_manager,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ADDON_STATE_POLL_INTERVAL = 3
|
||||
ADDON_INFO_POLL_TIMEOUT = 15 * 60
|
||||
|
||||
|
||||
class WaitingAddonManager(AddonManager):
|
||||
"""Addon manager which supports waiting operations for managing an addon."""
|
||||
|
||||
async def async_wait_until_addon_state(self, *states: AddonState) -> None:
|
||||
"""Poll an addon's info until it is in a specific state."""
|
||||
async with asyncio.timeout(ADDON_INFO_POLL_TIMEOUT):
|
||||
while True:
|
||||
try:
|
||||
info = await self.async_get_addon_info()
|
||||
except AddonError:
|
||||
info = None
|
||||
|
||||
_LOGGER.debug("Waiting for addon to be in state %s: %s", states, info)
|
||||
|
||||
if info is not None and info.state in states:
|
||||
break
|
||||
|
||||
await asyncio.sleep(ADDON_STATE_POLL_INTERVAL)
|
||||
|
||||
async def async_start_addon_waiting(self) -> None:
|
||||
"""Start an add-on."""
|
||||
await self.async_schedule_start_addon()
|
||||
await self.async_wait_until_addon_state(AddonState.RUNNING)
|
||||
|
||||
async def async_install_addon_waiting(self) -> None:
|
||||
"""Install an add-on."""
|
||||
await self.async_schedule_install_addon()
|
||||
await self.async_wait_until_addon_state(
|
||||
AddonState.RUNNING,
|
||||
AddonState.NOT_RUNNING,
|
||||
)
|
||||
|
||||
async def async_uninstall_addon_waiting(self) -> None:
|
||||
"""Uninstall an add-on."""
|
||||
try:
|
||||
info = await self.async_get_addon_info()
|
||||
except AddonError:
|
||||
info = None
|
||||
|
||||
# Do not try to uninstall an addon if it is already uninstalled
|
||||
if info is not None and info.state == AddonState.NOT_INSTALLED:
|
||||
return
|
||||
|
||||
await self.async_uninstall_addon()
|
||||
await self.async_wait_until_addon_state(AddonState.NOT_INSTALLED)
|
||||
|
||||
|
||||
class ApplicationType(StrEnum):
|
||||
"""Application type running on a device."""
|
||||
@@ -280,6 +326,11 @@ async def guess_hardware_owners(
|
||||
assert otbr_addon_fw_info is not None
|
||||
device_guesses[otbr_path].append(otbr_addon_fw_info)
|
||||
|
||||
# Lazy import to avoid circular dependency
|
||||
from .silabs_multiprotocol_addon import ( # noqa: PLC0415
|
||||
get_multiprotocol_addon_manager,
|
||||
)
|
||||
|
||||
multipan_addon_manager = await get_multiprotocol_addon_manager(hass)
|
||||
|
||||
try:
|
||||
|
||||
@@ -9,7 +9,17 @@ import os.path
|
||||
from homeassistant.components.homeassistant_hardware.coordinator import (
|
||||
FirmwareUpdateCoordinator,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.util import guess_firmware_info
|
||||
from homeassistant.components.homeassistant_hardware.repair_helpers import (
|
||||
async_create_multi_pan_migration_issue,
|
||||
async_delete_multi_pan_migration_issue,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import (
|
||||
multi_pan_addon_using_device,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.util import (
|
||||
ApplicationType,
|
||||
guess_firmware_info,
|
||||
)
|
||||
from homeassistant.components.usb import (
|
||||
USBDevice,
|
||||
async_register_port_event_callback,
|
||||
@@ -94,6 +104,18 @@ async def async_setup_entry(
|
||||
translation_key="device_disconnected",
|
||||
)
|
||||
|
||||
uses_multi_pan = ApplicationType(entry.data[FIRMWARE]) is ApplicationType.CPC
|
||||
if not uses_multi_pan:
|
||||
try:
|
||||
uses_multi_pan = await multi_pan_addon_using_device(hass, device_path)
|
||||
except HomeAssistantError as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
if uses_multi_pan:
|
||||
async_create_multi_pan_migration_issue(hass, DOMAIN, entry)
|
||||
else:
|
||||
async_delete_multi_pan_migration_issue(hass, DOMAIN, entry)
|
||||
|
||||
# Create and store the firmware update coordinator in runtime_data
|
||||
session = async_get_clientsession(hass)
|
||||
coordinator = FirmwareUpdateCoordinator(
|
||||
|
||||
@@ -254,6 +254,19 @@ class HomeAssistantSkyConnectMultiPanOptionsFlowHandler(
|
||||
"""Return the name of the hardware."""
|
||||
return self._hw_variant.full_name
|
||||
|
||||
def _firmware_update_url(self) -> str:
|
||||
"""Return the firmware update manifest URL."""
|
||||
return NABU_CASA_FIRMWARE_RELEASES_URL
|
||||
|
||||
def _zigbee_firmware_type(self) -> str:
|
||||
"""Return the zigbee firmware type identifier."""
|
||||
return "skyconnect_zigbee_ncp"
|
||||
|
||||
@property
|
||||
def _flasher_cls(self) -> type:
|
||||
"""Return the hardware-specific flasher class."""
|
||||
return Zbt1Flasher # type: ignore[no-any-return]
|
||||
|
||||
async def async_step_flashing_complete(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
"""Repairs for the Home Assistant SkyConnect integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, cast
|
||||
|
||||
from homeassistant.components.homeassistant_hardware.repair_helpers import (
|
||||
ISSUE_MULTI_PAN_MIGRATION,
|
||||
MultiPanMigrationRepairFlow,
|
||||
)
|
||||
from homeassistant.components.repairs import ConfirmRepairFlow, RepairsFlow
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigFlowResult
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .config_flow import HomeAssistantSkyConnectMultiPanOptionsFlowHandler
|
||||
|
||||
|
||||
class SkyConnectMultiPanMigrationRepairFlow(
|
||||
MultiPanMigrationRepairFlow, HomeAssistantSkyConnectMultiPanOptionsFlowHandler
|
||||
):
|
||||
"""Multi-PAN migration repair flow for Home Assistant SkyConnect."""
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize the repair flow."""
|
||||
HomeAssistantSkyConnectMultiPanOptionsFlowHandler.__init__(self, config_entry)
|
||||
self._repair_config_entry = config_entry
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Jump straight into the uninstall step."""
|
||||
return await self._async_step_start_migration()
|
||||
|
||||
|
||||
async def async_create_fix_flow(
|
||||
hass: HomeAssistant,
|
||||
issue_id: str,
|
||||
data: dict[str, str | int | float | None] | None,
|
||||
) -> RepairsFlow:
|
||||
"""Create a fix flow for a SkyConnect repair issue."""
|
||||
if issue_id.startswith(ISSUE_MULTI_PAN_MIGRATION) and data is not None:
|
||||
entry_id = cast(str, data["entry_id"])
|
||||
if (entry := hass.config_entries.async_get_entry(entry_id)) is not None:
|
||||
return SkyConnectMultiPanMigrationRepairFlow(entry)
|
||||
|
||||
return ConfirmRepairFlow()
|
||||
@@ -106,6 +106,37 @@
|
||||
"message": "The device is not plugged in"
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"multi_pan_migration": {
|
||||
"fix_flow": {
|
||||
"abort": {
|
||||
"addon_already_running": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_already_running%]",
|
||||
"addon_info_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_info_failed%]",
|
||||
"addon_install_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_install_failed%]",
|
||||
"addon_set_config_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_set_config_failed%]",
|
||||
"addon_start_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_start_failed%]",
|
||||
"fw_install_failed": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::fw_install_failed%]",
|
||||
"not_hassio": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::not_hassio%]",
|
||||
"zha_migration_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::zha_migration_failed%]"
|
||||
},
|
||||
"progress": {
|
||||
"install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]",
|
||||
"install_zigbee_firmware": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_zigbee_firmware%]",
|
||||
"uninstall_multiprotocol_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::uninstall_multiprotocol_addon%]"
|
||||
},
|
||||
"step": {
|
||||
"uninstall_addon": {
|
||||
"data": {
|
||||
"disable_multi_pan": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::uninstall_addon::data::disable_multi_pan%]"
|
||||
},
|
||||
"description": "Multiprotocol support for the IEEE 802.15.4 radio in your {hardware_name} has been deprecated. Migrate the radio back to Zigbee-only firmware to keep it working in the future.\n\nDisabling multiprotocol support will disable Thread support provided by the {hardware_name}. Your Thread devices will continue working only if you have another Thread border router nearby.\n\nIt will take a few minutes to install the Zigbee firmware and restore a backup.",
|
||||
"title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::uninstall_addon::title%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Multiprotocol support is deprecated"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"abort": {
|
||||
"addon_already_running": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_already_running%]",
|
||||
@@ -130,8 +161,10 @@
|
||||
"install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]",
|
||||
"install_firmware": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_firmware%]",
|
||||
"install_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_otbr_addon%]",
|
||||
"install_zigbee_firmware": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_zigbee_firmware%]",
|
||||
"start_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]",
|
||||
"start_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::start_otbr_addon%]"
|
||||
"start_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::start_otbr_addon%]",
|
||||
"uninstall_multiprotocol_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::uninstall_multiprotocol_addon%]"
|
||||
},
|
||||
"step": {
|
||||
"addon_installed_other_device": {
|
||||
|
||||
@@ -9,8 +9,13 @@ from homeassistant.components.hassio import get_os_info
|
||||
from homeassistant.components.homeassistant_hardware.coordinator import (
|
||||
FirmwareUpdateCoordinator,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.repair_helpers import (
|
||||
async_create_multi_pan_migration_issue,
|
||||
async_delete_multi_pan_migration_issue,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import (
|
||||
check_multi_pan_addon,
|
||||
multi_pan_addon_using_device,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.util import (
|
||||
ApplicationType,
|
||||
@@ -29,6 +34,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.hassio import is_hassio
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
FIRMWARE,
|
||||
FIRMWARE_VERSION,
|
||||
MANUFACTURER,
|
||||
@@ -72,12 +78,21 @@ async def async_setup_entry(
|
||||
firmware = ApplicationType(entry.data[FIRMWARE])
|
||||
|
||||
# Auto start the multiprotocol addon if it is in use
|
||||
try:
|
||||
multipan_using_device = await multi_pan_addon_using_device(hass, RADIO_DEVICE)
|
||||
except HomeAssistantError as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
if firmware is ApplicationType.CPC:
|
||||
try:
|
||||
await check_multi_pan_addon(hass)
|
||||
except HomeAssistantError as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
if firmware is ApplicationType.CPC or multipan_using_device:
|
||||
async_create_multi_pan_migration_issue(hass, DOMAIN, entry)
|
||||
else:
|
||||
async_delete_multi_pan_migration_issue(hass, DOMAIN, entry)
|
||||
|
||||
if firmware is ApplicationType.EZSP:
|
||||
discovery_flow.async_create_flow(
|
||||
hass,
|
||||
|
||||
@@ -321,6 +321,19 @@ class HomeAssistantYellowMultiPanOptionsFlowHandler(
|
||||
"""Return the name of the hardware."""
|
||||
return BOARD_NAME
|
||||
|
||||
def _firmware_update_url(self) -> str:
|
||||
"""Return the firmware update manifest URL."""
|
||||
return NABU_CASA_FIRMWARE_RELEASES_URL
|
||||
|
||||
def _zigbee_firmware_type(self) -> str:
|
||||
"""Return the zigbee firmware type identifier."""
|
||||
return "yellow_zigbee_ncp"
|
||||
|
||||
@property
|
||||
def _flasher_cls(self) -> type:
|
||||
"""Return the hardware-specific flasher class."""
|
||||
return YellowFlasher # type: ignore[no-any-return]
|
||||
|
||||
async def async_step_flashing_complete(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Repairs for the Home Assistant Yellow integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import cast
|
||||
|
||||
from homeassistant.components.homeassistant_hardware.repair_helpers import (
|
||||
ISSUE_MULTI_PAN_MIGRATION,
|
||||
MultiPanMigrationRepairFlow,
|
||||
)
|
||||
from homeassistant.components.repairs import ConfirmRepairFlow, RepairsFlow
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigFlowResult
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .config_flow import HomeAssistantYellowMultiPanOptionsFlowHandler
|
||||
|
||||
|
||||
class YellowMultiPanMigrationRepairFlow(
|
||||
MultiPanMigrationRepairFlow, HomeAssistantYellowMultiPanOptionsFlowHandler
|
||||
):
|
||||
"""Multi-PAN migration repair flow for Home Assistant Yellow."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize the repair flow."""
|
||||
HomeAssistantYellowMultiPanOptionsFlowHandler.__init__(self, hass, config_entry)
|
||||
self._repair_config_entry = config_entry
|
||||
|
||||
async def async_step_main_menu(self, _: None = None) -> ConfigFlowResult:
|
||||
"""Jump straight into the uninstall step."""
|
||||
return await self._async_step_start_migration()
|
||||
|
||||
|
||||
async def async_create_fix_flow(
|
||||
hass: HomeAssistant,
|
||||
issue_id: str,
|
||||
data: dict[str, str | int | float | None] | None,
|
||||
) -> RepairsFlow:
|
||||
"""Create a fix flow for a Yellow repair issue."""
|
||||
if issue_id.startswith(ISSUE_MULTI_PAN_MIGRATION) and data is not None:
|
||||
entry_id = cast(str, data["entry_id"])
|
||||
if (entry := hass.config_entries.async_get_entry(entry_id)) is not None:
|
||||
return YellowMultiPanMigrationRepairFlow(hass, entry)
|
||||
|
||||
return ConfirmRepairFlow()
|
||||
@@ -11,6 +11,37 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"multi_pan_migration": {
|
||||
"fix_flow": {
|
||||
"abort": {
|
||||
"addon_already_running": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_already_running%]",
|
||||
"addon_info_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_info_failed%]",
|
||||
"addon_install_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_install_failed%]",
|
||||
"addon_set_config_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_set_config_failed%]",
|
||||
"addon_start_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_start_failed%]",
|
||||
"fw_install_failed": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::fw_install_failed%]",
|
||||
"not_hassio": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::not_hassio%]",
|
||||
"zha_migration_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::zha_migration_failed%]"
|
||||
},
|
||||
"progress": {
|
||||
"install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]",
|
||||
"install_zigbee_firmware": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_zigbee_firmware%]",
|
||||
"uninstall_multiprotocol_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::uninstall_multiprotocol_addon%]"
|
||||
},
|
||||
"step": {
|
||||
"uninstall_addon": {
|
||||
"data": {
|
||||
"disable_multi_pan": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::uninstall_addon::data::disable_multi_pan%]"
|
||||
},
|
||||
"description": "Multiprotocol support for the IEEE 802.15.4 radio in your {hardware_name} has been deprecated. Migrate the radio back to Zigbee-only firmware to keep it working in the future.\n\nDisabling multiprotocol support will disable Thread support provided by the {hardware_name}. Your Thread devices will continue working only if you have another Thread border router nearby.\n\nIt will take a few minutes to install the Zigbee firmware and restore a backup.",
|
||||
"title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::uninstall_addon::title%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Multiprotocol support is deprecated"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"abort": {
|
||||
"addon_already_running": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_already_running%]",
|
||||
@@ -37,8 +68,10 @@
|
||||
"install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]",
|
||||
"install_firmware": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_firmware%]",
|
||||
"install_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_otbr_addon%]",
|
||||
"install_zigbee_firmware": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_zigbee_firmware%]",
|
||||
"start_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]",
|
||||
"start_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::start_otbr_addon%]"
|
||||
"start_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::start_otbr_addon%]",
|
||||
"uninstall_multiprotocol_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::uninstall_multiprotocol_addon%]"
|
||||
},
|
||||
"step": {
|
||||
"addon_installed_other_device": {
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Tests for the homeassistant_hardware repairs helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.homeassistant_hardware.repair_helpers import (
|
||||
ISSUE_MULTI_PAN_MIGRATION,
|
||||
async_create_multi_pan_migration_issue,
|
||||
async_delete_multi_pan_migration_issue,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
TEST_DOMAIN = "test_hardware"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ignore_translations_for_mock_domains() -> str:
|
||||
"""Ignore translation check for the fake test_hardware domain."""
|
||||
return TEST_DOMAIN
|
||||
|
||||
|
||||
async def test_create_and_delete_multi_pan_migration_issue(
|
||||
hass: HomeAssistant, issue_registry: ir.IssueRegistry
|
||||
) -> None:
|
||||
"""Test the helpers create and delete the migration issue per entry."""
|
||||
entry = MockConfigEntry(domain=TEST_DOMAIN, title="Test HW", data={})
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
async_create_multi_pan_migration_issue(hass, TEST_DOMAIN, entry)
|
||||
issue_id = f"{ISSUE_MULTI_PAN_MIGRATION}_{entry.entry_id}"
|
||||
issue = issue_registry.async_get_issue(domain=TEST_DOMAIN, issue_id=issue_id)
|
||||
assert issue is not None
|
||||
assert issue.translation_key == ISSUE_MULTI_PAN_MIGRATION
|
||||
assert issue.translation_placeholders == {"hardware_name": "Test HW"}
|
||||
assert issue.data == {"entry_id": entry.entry_id}
|
||||
assert issue.is_fixable
|
||||
assert issue.severity is ir.IssueSeverity.WARNING
|
||||
|
||||
async_delete_multi_pan_migration_issue(hass, TEST_DOMAIN, entry)
|
||||
assert issue_registry.async_get_issue(domain=TEST_DOMAIN, issue_id=issue_id) is None
|
||||
@@ -8,6 +8,7 @@ from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
from aiohasupervisor import SupervisorError
|
||||
from aiohasupervisor.models import AddonsOptions
|
||||
from aiohttp import ClientError
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.hassio import AddonError, AddonInfo, AddonState, HassIO
|
||||
@@ -98,6 +99,19 @@ class FakeOptionsFlow(silabs_multiprotocol_addon.OptionsFlowHandler):
|
||||
"""Return the name of the hardware."""
|
||||
return "Test"
|
||||
|
||||
def _firmware_update_url(self) -> str:
|
||||
"""Return the firmware update manifest URL."""
|
||||
return "https://example.com/firmware"
|
||||
|
||||
def _zigbee_firmware_type(self) -> str:
|
||||
"""Return the zigbee firmware type identifier."""
|
||||
return "test_zigbee_ncp"
|
||||
|
||||
@property
|
||||
def _flasher_cls(self) -> type:
|
||||
"""Return the hardware-specific flasher class."""
|
||||
return Mock
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def config_flow_handler(
|
||||
@@ -118,6 +132,31 @@ def options_flow_poll_addon_state() -> Generator[None]:
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_firmware_client() -> Generator[tuple[AsyncMock, AsyncMock]]:
|
||||
"""Fixture to mock FirmwareUpdateClient and async_flash_silabs_firmware."""
|
||||
mock_fw_manifest = Mock()
|
||||
mock_fw_manifest.filename = "test_zigbee_ncp_7.4.4.0.gbl"
|
||||
mock_fw_client = AsyncMock()
|
||||
mock_fw_client.async_update_data.return_value = Mock(firmwares=[mock_fw_manifest])
|
||||
mock_fw_client.async_fetch_firmware.return_value = b"fake_firmware"
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.FirmwareUpdateClient",
|
||||
return_value=mock_fw_client,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.async_firmware_flashing_context"
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.async_flash_silabs_firmware",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_flash,
|
||||
):
|
||||
yield mock_fw_client, mock_flash
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def hassio_integration(hass: HomeAssistant) -> Generator[None]:
|
||||
"""Fixture to mock the `hassio` integration."""
|
||||
@@ -174,7 +213,7 @@ def get_suggested(schema, key):
|
||||
|
||||
|
||||
@patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.ADDON_STATE_POLL_INTERVAL",
|
||||
"homeassistant.components.homeassistant_hardware.util.ADDON_STATE_POLL_INTERVAL",
|
||||
0,
|
||||
)
|
||||
@pytest.mark.usefixtures(
|
||||
@@ -633,12 +672,10 @@ async def test_option_flow_addon_installed_same_device_uninstall(
|
||||
addon_info,
|
||||
addon_store_info,
|
||||
addon_installed,
|
||||
install_addon,
|
||||
start_addon,
|
||||
stop_addon,
|
||||
uninstall_addon,
|
||||
set_addon_options,
|
||||
options_flow_poll_addon_state,
|
||||
mock_firmware_client: tuple[AsyncMock, AsyncMock],
|
||||
) -> None:
|
||||
"""Test uninstalling the multi pan addon."""
|
||||
|
||||
@@ -675,21 +712,10 @@ async def test_option_flow_addon_installed_same_device_uninstall(
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "uninstall_addon"
|
||||
|
||||
# Make sure the flasher addon is installed
|
||||
addon_store_info.return_value.installed = False
|
||||
addon_store_info.return_Value.available = True
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"], {silabs_multiprotocol_addon.CONF_DISABLE_MULTI_PAN: True}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "install_flasher_addon"
|
||||
assert result["progress_action"] == "install_addon"
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "uninstall_multiprotocol_addon"
|
||||
assert result["progress_action"] == "uninstall_multiprotocol_addon"
|
||||
@@ -699,12 +725,10 @@ async def test_option_flow_addon_installed_same_device_uninstall(
|
||||
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "start_flasher_addon"
|
||||
assert result["progress_action"] == "start_flasher_addon"
|
||||
assert result["description_placeholders"] == {"addon_name": "Silicon Labs Flasher"}
|
||||
assert result["step_id"] == "install_zigbee_firmware"
|
||||
assert result["progress_action"] == "install_zigbee_firmware"
|
||||
|
||||
await hass.async_block_till_done()
|
||||
install_addon.assert_called_once_with("core_silabs_flasher")
|
||||
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
@@ -726,11 +750,6 @@ async def test_option_flow_addon_installed_same_device_do_not_uninstall_multi_pa
|
||||
addon_info,
|
||||
addon_store_info,
|
||||
addon_installed,
|
||||
install_addon,
|
||||
start_addon,
|
||||
stop_addon,
|
||||
uninstall_addon,
|
||||
set_addon_options,
|
||||
) -> None:
|
||||
"""Test uninstalling the multi pan addon."""
|
||||
|
||||
@@ -764,19 +783,15 @@ async def test_option_flow_addon_installed_same_device_do_not_uninstall_multi_pa
|
||||
|
||||
|
||||
@pytest.mark.parametrize("ignore_translations_for_mock_domains", ["test"])
|
||||
async def test_option_flow_flasher_already_running_failure(
|
||||
async def test_option_flow_firmware_flash_failure(
|
||||
hass: HomeAssistant,
|
||||
addon_info,
|
||||
addon_store_info,
|
||||
addon_installed,
|
||||
install_addon,
|
||||
start_addon,
|
||||
stop_addon,
|
||||
uninstall_addon,
|
||||
set_addon_options,
|
||||
options_flow_poll_addon_state,
|
||||
) -> None:
|
||||
"""Test uninstalling the multi pan addon but with the flasher addon running."""
|
||||
"""Test uninstalling the multi pan addon, case where firmware flash fails."""
|
||||
|
||||
addon_info.return_value.options["device"] = "/dev/ttyTEST123"
|
||||
|
||||
@@ -800,162 +815,60 @@ async def test_option_flow_flasher_already_running_failure(
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "uninstall_addon"
|
||||
|
||||
# The flasher addon is already installed and running, this is bad
|
||||
addon_store_info.return_value.installed = True
|
||||
addon_info.return_value.state = "started"
|
||||
mock_fw_manifest = Mock()
|
||||
mock_fw_manifest.filename = "test_zigbee_ncp_7.4.4.0.gbl"
|
||||
mock_fw_client = AsyncMock()
|
||||
mock_fw_client.async_update_data.return_value = Mock(firmwares=[mock_fw_manifest])
|
||||
mock_fw_client.async_fetch_firmware.return_value = b"fake_firmware"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"], {silabs_multiprotocol_addon.CONF_DISABLE_MULTI_PAN: True}
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "addon_already_running"
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.FirmwareUpdateClient",
|
||||
return_value=mock_fw_client,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.async_firmware_flashing_context"
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.async_flash_silabs_firmware",
|
||||
new_callable=AsyncMock,
|
||||
side_effect=HomeAssistantError("Flash failed"),
|
||||
),
|
||||
):
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
{silabs_multiprotocol_addon.CONF_DISABLE_MULTI_PAN: True},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "uninstall_multiprotocol_addon"
|
||||
assert result["progress_action"] == "uninstall_multiprotocol_addon"
|
||||
|
||||
async def test_option_flow_addon_installed_same_device_flasher_already_installed(
|
||||
hass: HomeAssistant,
|
||||
addon_info,
|
||||
addon_store_info,
|
||||
addon_installed,
|
||||
install_addon,
|
||||
start_addon,
|
||||
stop_addon,
|
||||
uninstall_addon,
|
||||
set_addon_options,
|
||||
options_flow_poll_addon_state,
|
||||
) -> None:
|
||||
"""Test uninstalling the multi pan addon."""
|
||||
await hass.async_block_till_done()
|
||||
uninstall_addon.assert_called_once_with("core_silabs_multiprotocol")
|
||||
|
||||
addon_info.return_value.options["device"] = "/dev/ttyTEST123"
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "install_zigbee_firmware"
|
||||
assert result["progress_action"] == "install_zigbee_firmware"
|
||||
|
||||
# Setup the config entry
|
||||
config_entry = MockConfigEntry(
|
||||
data={},
|
||||
domain=TEST_DOMAIN,
|
||||
options={},
|
||||
title="Test HW",
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "addon_menu"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "uninstall_addon"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "uninstall_addon"
|
||||
|
||||
addon_store_info.return_value.installed = True
|
||||
addon_store_info.return_value.available = True
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"], {silabs_multiprotocol_addon.CONF_DISABLE_MULTI_PAN: True}
|
||||
)
|
||||
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "uninstall_multiprotocol_addon"
|
||||
assert result["progress_action"] == "uninstall_multiprotocol_addon"
|
||||
|
||||
await hass.async_block_till_done()
|
||||
uninstall_addon.assert_called_once_with("core_silabs_multiprotocol")
|
||||
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "start_flasher_addon"
|
||||
assert result["progress_action"] == "start_flasher_addon"
|
||||
assert result["description_placeholders"] == {"addon_name": "Silicon Labs Flasher"}
|
||||
|
||||
addon_store_info.return_value.installed = True
|
||||
addon_store_info.return_value.available = True
|
||||
await hass.async_block_till_done()
|
||||
install_addon.assert_not_called()
|
||||
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "fw_install_failed"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("ignore_translations_for_mock_domains", ["test"])
|
||||
async def test_option_flow_flasher_install_failure(
|
||||
async def test_option_flow_zigbee_firmware_fetch_failure(
|
||||
hass: HomeAssistant,
|
||||
addon_info,
|
||||
addon_store_info,
|
||||
addon_installed,
|
||||
install_addon,
|
||||
start_addon,
|
||||
stop_addon,
|
||||
uninstall_addon,
|
||||
set_addon_options,
|
||||
options_flow_poll_addon_state,
|
||||
) -> None:
|
||||
"""Test uninstalling the multi pan addon, case where flasher addon fails."""
|
||||
|
||||
addon_info.return_value.options["device"] = "/dev/ttyTEST123"
|
||||
|
||||
# Setup the config entry
|
||||
config_entry = MockConfigEntry(
|
||||
data={},
|
||||
domain=TEST_DOMAIN,
|
||||
options={},
|
||||
title="Test HW",
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
zha_config_entry = MockConfigEntry(
|
||||
data={
|
||||
"device": {"path": "socket://core-silabs-multiprotocol:9999"},
|
||||
"radio_type": "ezsp",
|
||||
},
|
||||
domain=ZHA_DOMAIN,
|
||||
options={},
|
||||
title="Test Multiprotocol",
|
||||
)
|
||||
zha_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "addon_menu"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "uninstall_addon"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "uninstall_addon"
|
||||
|
||||
addon_store_info.return_value.installed = False
|
||||
addon_store_info.return_value.available = True
|
||||
install_addon.side_effect = [AddonError()]
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"], {silabs_multiprotocol_addon.CONF_DISABLE_MULTI_PAN: True}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "install_flasher_addon"
|
||||
assert result["progress_action"] == "install_addon"
|
||||
|
||||
await hass.async_block_till_done()
|
||||
install_addon.assert_called_once_with("core_silabs_flasher")
|
||||
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "addon_install_failed"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("ignore_translations_for_mock_domains", ["test"])
|
||||
async def test_option_flow_flasher_addon_flash_failure(
|
||||
hass: HomeAssistant,
|
||||
addon_info,
|
||||
addon_store_info,
|
||||
addon_installed,
|
||||
install_addon,
|
||||
start_addon,
|
||||
stop_addon,
|
||||
uninstall_addon,
|
||||
set_addon_options,
|
||||
options_flow_poll_addon_state,
|
||||
) -> None:
|
||||
"""Test where flasher addon fails to flash Zigbee firmware."""
|
||||
"""Test where fetching Zigbee firmware fails."""
|
||||
|
||||
addon_info.return_value.options["device"] = "/dev/ttyTEST123"
|
||||
|
||||
@@ -979,30 +892,39 @@ async def test_option_flow_flasher_addon_flash_failure(
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "uninstall_addon"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"], {silabs_multiprotocol_addon.CONF_DISABLE_MULTI_PAN: True}
|
||||
)
|
||||
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "uninstall_multiprotocol_addon"
|
||||
assert result["progress_action"] == "uninstall_multiprotocol_addon"
|
||||
mock_fw_client = AsyncMock()
|
||||
mock_fw_client.async_update_data.side_effect = ClientError("Network error")
|
||||
|
||||
start_addon.side_effect = SupervisorError("Boom")
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.FirmwareUpdateClient",
|
||||
return_value=mock_fw_client,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.async_firmware_flashing_context"
|
||||
),
|
||||
):
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
{silabs_multiprotocol_addon.CONF_DISABLE_MULTI_PAN: True},
|
||||
)
|
||||
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "uninstall_multiprotocol_addon"
|
||||
assert result["progress_action"] == "uninstall_multiprotocol_addon"
|
||||
|
||||
await hass.async_block_till_done()
|
||||
uninstall_addon.assert_called_once_with("core_silabs_multiprotocol")
|
||||
await hass.async_block_till_done()
|
||||
uninstall_addon.assert_called_once_with("core_silabs_multiprotocol")
|
||||
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "start_flasher_addon"
|
||||
assert result["progress_action"] == "start_flasher_addon"
|
||||
assert result["description_placeholders"] == {"addon_name": "Silicon Labs Flasher"}
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "install_zigbee_firmware"
|
||||
assert result["progress_action"] == "install_zigbee_firmware"
|
||||
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "addon_start_failed"
|
||||
assert result["description_placeholders"]["addon_name"] == "Silicon Labs Flasher"
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "fw_install_failed"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("ignore_translations_for_mock_domains", ["test"])
|
||||
@@ -1016,11 +938,6 @@ async def test_option_flow_uninstall_migration_initiate_failure(
|
||||
addon_info,
|
||||
addon_store_info,
|
||||
addon_installed,
|
||||
install_addon,
|
||||
start_addon,
|
||||
stop_addon,
|
||||
uninstall_addon,
|
||||
set_addon_options,
|
||||
options_flow_poll_addon_state,
|
||||
) -> None:
|
||||
"""Test uninstalling the multi pan addon, case where ZHA migration init fails."""
|
||||
@@ -1078,14 +995,11 @@ async def test_option_flow_uninstall_migration_finish_failure(
|
||||
addon_info,
|
||||
addon_store_info,
|
||||
addon_installed,
|
||||
install_addon,
|
||||
start_addon,
|
||||
stop_addon,
|
||||
uninstall_addon,
|
||||
set_addon_options,
|
||||
options_flow_poll_addon_state,
|
||||
mock_firmware_client: tuple[AsyncMock, AsyncMock],
|
||||
) -> None:
|
||||
"""Test uninstalling the multi pan addon, case where ZHA migration init fails."""
|
||||
"""Test uninstalling the multi pan addon, case where ZHA migration finish fails."""
|
||||
|
||||
addon_info.return_value.options["device"] = "/dev/ttyTEST123"
|
||||
|
||||
@@ -1129,9 +1043,8 @@ async def test_option_flow_uninstall_migration_finish_failure(
|
||||
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "start_flasher_addon"
|
||||
assert result["progress_action"] == "start_flasher_addon"
|
||||
assert result["description_placeholders"] == {"addon_name": "Silicon Labs Flasher"}
|
||||
assert result["step_id"] == "install_zigbee_firmware"
|
||||
assert result["progress_action"] == "install_zigbee_firmware"
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
@@ -219,7 +219,7 @@ async def test_guess_hardware_owners_z2m(
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.util.get_multiprotocol_addon_manager",
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.get_multiprotocol_addon_manager",
|
||||
return_value=multipan_addon_manager,
|
||||
),
|
||||
patch(
|
||||
@@ -290,7 +290,7 @@ async def test_guess_hardware_owners_otbr(hass: HomeAssistant) -> None:
|
||||
return_value=otbr_addon_fw_info,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.util.get_multiprotocol_addon_manager",
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.get_multiprotocol_addon_manager",
|
||||
return_value=multipan_addon_manager,
|
||||
),
|
||||
patch(
|
||||
@@ -334,7 +334,7 @@ async def test_guess_hardware_owners_multipan(hass: HomeAssistant) -> None:
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.util.get_multiprotocol_addon_manager",
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.get_multiprotocol_addon_manager",
|
||||
return_value=multipan_addon_manager,
|
||||
),
|
||||
patch(
|
||||
|
||||
@@ -18,7 +18,6 @@ from homeassistant.components.homeassistant_hardware.helpers import (
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import (
|
||||
CONF_DISABLE_MULTI_PAN,
|
||||
get_flasher_addon_manager,
|
||||
get_multiprotocol_addon_manager,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.util import (
|
||||
@@ -384,15 +383,11 @@ async def test_options_flow_multipan_uninstall(
|
||||
version="1.0.0",
|
||||
)
|
||||
|
||||
mock_flasher_manager = Mock(spec_set=get_flasher_addon_manager(hass))
|
||||
mock_flasher_manager.async_get_addon_info.return_value = AddonInfo(
|
||||
available=True,
|
||||
hostname=None,
|
||||
options={},
|
||||
state=AddonState.NOT_RUNNING,
|
||||
update_available=False,
|
||||
version="1.0.0",
|
||||
)
|
||||
mock_fw_manifest = Mock()
|
||||
mock_fw_manifest.filename = "skyconnect_zigbee_ncp_7.4.4.0.gbl"
|
||||
mock_fw_client = AsyncMock()
|
||||
mock_fw_client.async_update_data.return_value = Mock(firmwares=[mock_fw_manifest])
|
||||
mock_fw_client.async_fetch_firmware.return_value = b"fake_firmware"
|
||||
|
||||
with (
|
||||
patch(
|
||||
@@ -400,8 +395,12 @@ async def test_options_flow_multipan_uninstall(
|
||||
return_value=mock_multipan_manager,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.get_flasher_addon_manager",
|
||||
return_value=mock_flasher_manager,
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.async_flash_silabs_firmware",
|
||||
new_callable=AsyncMock,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.FirmwareUpdateClient",
|
||||
return_value=mock_fw_client,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio",
|
||||
@@ -424,11 +423,15 @@ async def test_options_flow_multipan_uninstall(
|
||||
result["flow_id"], user_input={CONF_DISABLE_MULTI_PAN: True}
|
||||
)
|
||||
|
||||
# Finish the flow
|
||||
# Uninstall multiprotocol addon
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
# Flash zigbee firmware
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
# Flashing complete
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
|
||||
|
||||
@@ -5,6 +5,9 @@ from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.homeassistant_hardware.repair_helpers import (
|
||||
ISSUE_MULTI_PAN_MIGRATION,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.util import (
|
||||
ApplicationType,
|
||||
FirmwareInfo,
|
||||
@@ -22,6 +25,7 @@ from homeassistant.components.usb import DOMAIN as USB_DOMAIN, USBDevice
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
@@ -291,3 +295,126 @@ async def test_bad_config_entry_fixing(hass: HomeAssistant) -> None:
|
||||
|
||||
untouched_bad_entry = hass.config_entries.async_get_entry(bad_entry.entry_id)
|
||||
assert untouched_bad_entry.minor_version == 3
|
||||
|
||||
|
||||
def _multi_pan_sky_connect_entry(firmware: str) -> MockConfigEntry:
|
||||
"""Return a SkyConnect config entry with the given firmware type."""
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id="some_unique_id",
|
||||
data={
|
||||
"description": "SkyConnect v1.0",
|
||||
"device": "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_9e2adbd75b8beb119fe564a0f320645d-if00-port0",
|
||||
"vid": "10C4",
|
||||
"pid": "EA60",
|
||||
"serial_number": "3c0ed67c628beb11b1cd64a0f320645d",
|
||||
"manufacturer": "Nabu Casa",
|
||||
"product": "SkyConnect v1.0",
|
||||
"firmware": firmware,
|
||||
"firmware_version": None,
|
||||
},
|
||||
title="Home Assistant SkyConnect",
|
||||
version=1,
|
||||
minor_version=4,
|
||||
)
|
||||
|
||||
|
||||
async def test_multi_pan_migration_issue_created_for_cpc(
|
||||
hass: HomeAssistant, issue_registry: ir.IssueRegistry
|
||||
) -> None:
|
||||
"""Test the multi-PAN migration repair issue is created when firmware is CPC."""
|
||||
config_entry = _multi_pan_sky_connect_entry(ApplicationType.CPC.value)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_sky_connect.os.path.exists",
|
||||
return_value=True,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_sky_connect.multi_pan_addon_using_device",
|
||||
return_value=False,
|
||||
),
|
||||
):
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
issue = issue_registry.async_get_issue(
|
||||
domain=DOMAIN,
|
||||
issue_id=f"{ISSUE_MULTI_PAN_MIGRATION}_{config_entry.entry_id}",
|
||||
)
|
||||
assert issue is not None
|
||||
assert issue.translation_key == ISSUE_MULTI_PAN_MIGRATION
|
||||
assert issue.translation_placeholders == {
|
||||
"hardware_name": "Home Assistant SkyConnect"
|
||||
}
|
||||
assert issue.data == {"entry_id": config_entry.entry_id}
|
||||
assert issue.is_fixable
|
||||
|
||||
|
||||
async def test_multi_pan_migration_issue_created_for_addon(
|
||||
hass: HomeAssistant, issue_registry: ir.IssueRegistry
|
||||
) -> None:
|
||||
"""Test the repair issue is created when the multi-PAN addon is running."""
|
||||
config_entry = _multi_pan_sky_connect_entry(ApplicationType.SPINEL.value)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_sky_connect.os.path.exists",
|
||||
return_value=True,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_sky_connect.multi_pan_addon_using_device",
|
||||
return_value=True,
|
||||
),
|
||||
):
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
issue = issue_registry.async_get_issue(
|
||||
domain=DOMAIN,
|
||||
issue_id=f"{ISSUE_MULTI_PAN_MIGRATION}_{config_entry.entry_id}",
|
||||
)
|
||||
assert issue is not None
|
||||
assert issue.is_fixable
|
||||
|
||||
|
||||
async def test_multi_pan_migration_issue_deleted_for_ezsp(
|
||||
hass: HomeAssistant, issue_registry: ir.IssueRegistry
|
||||
) -> None:
|
||||
"""Test the multi-PAN migration repair issue is removed when not using multi-PAN."""
|
||||
config_entry = _multi_pan_sky_connect_entry(ApplicationType.EZSP.value)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
domain=DOMAIN,
|
||||
issue_id=f"{ISSUE_MULTI_PAN_MIGRATION}_{config_entry.entry_id}",
|
||||
is_fixable=True,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key=ISSUE_MULTI_PAN_MIGRATION,
|
||||
translation_placeholders={"hardware_name": "Home Assistant SkyConnect"},
|
||||
data={"entry_id": config_entry.entry_id},
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_sky_connect.os.path.exists",
|
||||
return_value=True,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_sky_connect.multi_pan_addon_using_device",
|
||||
return_value=False,
|
||||
),
|
||||
):
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (
|
||||
issue_registry.async_get_issue(
|
||||
domain=DOMAIN,
|
||||
issue_id=f"{ISSUE_MULTI_PAN_MIGRATION}_{config_entry.entry_id}",
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
||||
@@ -19,7 +19,6 @@ from homeassistant.components.homeassistant_hardware.firmware_config_flow import
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import (
|
||||
CONF_DISABLE_MULTI_PAN,
|
||||
get_flasher_addon_manager,
|
||||
get_multiprotocol_addon_manager,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.util import (
|
||||
@@ -549,15 +548,11 @@ async def test_options_flow_multipan_uninstall(hass: HomeAssistant) -> None:
|
||||
version="1.0.0",
|
||||
)
|
||||
|
||||
mock_flasher_manager = Mock(spec_set=get_flasher_addon_manager(hass))
|
||||
mock_flasher_manager.async_get_addon_info.return_value = AddonInfo(
|
||||
available=True,
|
||||
hostname=None,
|
||||
options={},
|
||||
state=AddonState.NOT_RUNNING,
|
||||
update_available=False,
|
||||
version="1.0.0",
|
||||
)
|
||||
mock_fw_manifest = Mock()
|
||||
mock_fw_manifest.filename = "yellow_zigbee_ncp_7.4.4.0.gbl"
|
||||
mock_fw_client = AsyncMock()
|
||||
mock_fw_client.async_update_data.return_value = Mock(firmwares=[mock_fw_manifest])
|
||||
mock_fw_client.async_fetch_firmware.return_value = b"fake_firmware"
|
||||
|
||||
with (
|
||||
patch(
|
||||
@@ -565,8 +560,12 @@ async def test_options_flow_multipan_uninstall(hass: HomeAssistant) -> None:
|
||||
return_value=mock_multipan_manager,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.get_flasher_addon_manager",
|
||||
return_value=mock_flasher_manager,
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.async_flash_silabs_firmware",
|
||||
new_callable=AsyncMock,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.FirmwareUpdateClient",
|
||||
return_value=mock_fw_client,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio",
|
||||
@@ -595,11 +594,15 @@ async def test_options_flow_multipan_uninstall(hass: HomeAssistant) -> None:
|
||||
result["flow_id"], user_input={CONF_DISABLE_MULTI_PAN: True}
|
||||
)
|
||||
|
||||
# Finish the flow
|
||||
# Uninstall multiprotocol addon
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
# Flash zigbee firmware
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
# Flashing complete
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ import pytest
|
||||
|
||||
from homeassistant.components import zha
|
||||
from homeassistant.components.hassio import DOMAIN as HASSIO_DOMAIN
|
||||
from homeassistant.components.homeassistant_hardware.repair_helpers import (
|
||||
ISSUE_MULTI_PAN_MIGRATION,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.util import (
|
||||
ApplicationType,
|
||||
FirmwareInfo,
|
||||
@@ -18,6 +21,7 @@ from homeassistant.components.usb import SerialDevice, async_scan_serial_ports
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import MockConfigEntry, MockModule, mock_integration
|
||||
@@ -321,6 +325,156 @@ async def test_setup_entry_addon_info_fails(
|
||||
assert config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_multi_pan_migration_issue_created_for_cpc(
|
||||
hass: HomeAssistant,
|
||||
addon_store_info,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
) -> None:
|
||||
"""Test the multi-PAN migration repair issue is created when firmware is CPC."""
|
||||
mock_integration(hass, MockModule("hassio"))
|
||||
await async_setup_component(hass, HASSIO_DOMAIN, {})
|
||||
|
||||
config_entry = MockConfigEntry(
|
||||
data={"firmware": ApplicationType.CPC},
|
||||
domain=DOMAIN,
|
||||
options={},
|
||||
title="Home Assistant Yellow",
|
||||
version=1,
|
||||
minor_version=2,
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_yellow.get_os_info",
|
||||
return_value={"board": "yellow"},
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.onboarding.async_is_onboarded",
|
||||
return_value=False,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_yellow.check_multi_pan_addon",
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_yellow.multi_pan_addon_using_device",
|
||||
return_value=False,
|
||||
),
|
||||
):
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
issue = issue_registry.async_get_issue(
|
||||
domain=DOMAIN,
|
||||
issue_id=f"{ISSUE_MULTI_PAN_MIGRATION}_{config_entry.entry_id}",
|
||||
)
|
||||
assert issue is not None
|
||||
assert issue.translation_key == ISSUE_MULTI_PAN_MIGRATION
|
||||
assert issue.translation_placeholders == {"hardware_name": "Home Assistant Yellow"}
|
||||
assert issue.data == {"entry_id": config_entry.entry_id}
|
||||
assert issue.is_fixable
|
||||
|
||||
|
||||
async def test_multi_pan_migration_issue_created_for_addon(
|
||||
hass: HomeAssistant,
|
||||
addon_store_info,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
) -> None:
|
||||
"""Test the repair issue is created when the multi-PAN addon is running."""
|
||||
mock_integration(hass, MockModule("hassio"))
|
||||
await async_setup_component(hass, HASSIO_DOMAIN, {})
|
||||
|
||||
config_entry = MockConfigEntry(
|
||||
data={"firmware": ApplicationType.SPINEL},
|
||||
domain=DOMAIN,
|
||||
options={},
|
||||
title="Home Assistant Yellow",
|
||||
version=1,
|
||||
minor_version=2,
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_yellow.get_os_info",
|
||||
return_value={"board": "yellow"},
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.onboarding.async_is_onboarded",
|
||||
return_value=False,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_yellow.multi_pan_addon_using_device",
|
||||
return_value=True,
|
||||
),
|
||||
):
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
issue = issue_registry.async_get_issue(
|
||||
domain=DOMAIN,
|
||||
issue_id=f"{ISSUE_MULTI_PAN_MIGRATION}_{config_entry.entry_id}",
|
||||
)
|
||||
assert issue is not None
|
||||
assert issue.is_fixable
|
||||
|
||||
|
||||
async def test_multi_pan_migration_issue_deleted_for_ezsp(
|
||||
hass: HomeAssistant,
|
||||
addon_store_info,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
) -> None:
|
||||
"""Test the multi-PAN migration repair issue is removed when not using multi-PAN."""
|
||||
mock_integration(hass, MockModule("hassio"))
|
||||
await async_setup_component(hass, HASSIO_DOMAIN, {})
|
||||
|
||||
config_entry = MockConfigEntry(
|
||||
data={"firmware": ApplicationType.EZSP},
|
||||
domain=DOMAIN,
|
||||
options={},
|
||||
title="Home Assistant Yellow",
|
||||
version=1,
|
||||
minor_version=2,
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
# Pre-existing issue from a previous CPC run
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
domain=DOMAIN,
|
||||
issue_id=f"{ISSUE_MULTI_PAN_MIGRATION}_{config_entry.entry_id}",
|
||||
is_fixable=True,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key=ISSUE_MULTI_PAN_MIGRATION,
|
||||
translation_placeholders={"hardware_name": "Home Assistant Yellow"},
|
||||
data={"entry_id": config_entry.entry_id},
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_yellow.get_os_info",
|
||||
return_value={"board": "yellow"},
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.onboarding.async_is_onboarded",
|
||||
return_value=False,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_yellow.multi_pan_addon_using_device",
|
||||
return_value=False,
|
||||
),
|
||||
):
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (
|
||||
issue_registry.async_get_issue(
|
||||
domain=DOMAIN,
|
||||
issue_id=f"{ISSUE_MULTI_PAN_MIGRATION}_{config_entry.entry_id}",
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("start_version", "data", "migrated_data"),
|
||||
[
|
||||
@@ -379,6 +533,10 @@ async def test_migrate_entry(
|
||||
owners=[],
|
||||
),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_yellow.multi_pan_addon_using_device",
|
||||
return_value=False,
|
||||
),
|
||||
):
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
Reference in New Issue
Block a user