mirror of
https://github.com/home-assistant/core.git
synced 2026-05-27 19:25:18 +02:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fce17c8e6f | |||
| 51d1d4aa9e | |||
| 8184b93151 | |||
| 403cb85bc8 | |||
| 4bf3a5b4bd | |||
| 5a73d78c90 | |||
| ebd9934213 | |||
| 73898c29e2 |
@@ -7,7 +7,7 @@ from homeassistant.components.select import (
|
||||
PLATFORM_SCHEMA as SELECT_PLATFORM_SCHEMA,
|
||||
SelectEntity,
|
||||
)
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.const import CONF_NAME, CONF_OPTIONS
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
@@ -19,9 +19,6 @@ from .hub import AdsHub
|
||||
|
||||
DEFAULT_NAME = "ADS select"
|
||||
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
CONF_OPTIONS = "options"
|
||||
|
||||
PLATFORM_SCHEMA = SELECT_PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_ADS_VAR): cv.string,
|
||||
|
||||
@@ -21,5 +21,5 @@
|
||||
"integration_type": "system",
|
||||
"preview_features": { "winter_mode": {} },
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20260429.4"]
|
||||
"requirements": ["home-assistant-frontend==20260527.0"]
|
||||
}
|
||||
|
||||
@@ -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,72 @@
|
||||
"""Repairs for the Home Assistant Hardware integration."""
|
||||
|
||||
from homeassistant.components.repairs import RepairsFlow, RepairsFlowResult
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
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) -> RepairsFlowResult:
|
||||
"""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]
|
||||
@@ -6,6 +6,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
|
||||
|
||||
@@ -25,6 +27,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,
|
||||
@@ -37,15 +40,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"
|
||||
@@ -71,53 +77,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 is 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."""
|
||||
|
||||
@@ -265,18 +224,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."""
|
||||
@@ -339,6 +286,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."""
|
||||
@@ -686,61 +646,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 is AddonState.NOT_INSTALLED:
|
||||
return await self.async_step_install_flasher_addon()
|
||||
|
||||
if addon_info.state is 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=home-assistant-component-root-import
|
||||
from homeassistant.components.zha import DOMAIN as ZHA_DOMAIN # noqa: PLC0415
|
||||
from homeassistant.components.zha.radio_manager import ( # noqa: PLC0415
|
||||
@@ -782,17 +688,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(
|
||||
@@ -821,62 +716,93 @@ 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,
|
||||
progress_callback=lambda offset, total: (
|
||||
self.async_update_progress(offset / total)
|
||||
),
|
||||
)
|
||||
|
||||
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": {
|
||||
|
||||
@@ -37,13 +37,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."""
|
||||
@@ -279,6 +325,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:
|
||||
|
||||
@@ -7,6 +7,13 @@ import os.path
|
||||
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 (
|
||||
multi_pan_addon_using_device,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.util import guess_firmware_info
|
||||
from homeassistant.components.usb import (
|
||||
USBDevice,
|
||||
@@ -92,6 +99,16 @@ async def async_setup_entry(
|
||||
translation_key="device_disconnected",
|
||||
)
|
||||
|
||||
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(
|
||||
|
||||
@@ -248,6 +248,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,48 @@
|
||||
"""Repairs for the Home Assistant SkyConnect integration."""
|
||||
|
||||
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,
|
||||
RepairsFlowResult,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
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( # type: ignore[override]
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> RepairsFlowResult:
|
||||
"""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": {
|
||||
|
||||
@@ -7,8 +7,13 @@ from homeassistant.components.hassio import HassioNotReadyError, 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,
|
||||
@@ -27,6 +32,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,
|
||||
@@ -77,6 +83,16 @@ async def async_setup_entry(
|
||||
except HomeAssistantError as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
try:
|
||||
multipan_using_device = await multi_pan_addon_using_device(hass, RADIO_DEVICE)
|
||||
except HomeAssistantError as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
if 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,
|
||||
|
||||
@@ -319,6 +319,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,48 @@
|
||||
"""Repairs for the Home Assistant Yellow integration."""
|
||||
|
||||
from typing import cast
|
||||
|
||||
from homeassistant.components.homeassistant_hardware.repair_helpers import (
|
||||
ISSUE_MULTI_PAN_MIGRATION,
|
||||
MultiPanMigrationRepairFlow,
|
||||
)
|
||||
from homeassistant.components.repairs import (
|
||||
ConfirmRepairFlow,
|
||||
RepairsFlow,
|
||||
RepairsFlowResult,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
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( # type: ignore[override]
|
||||
self, _: None = None
|
||||
) -> RepairsFlowResult:
|
||||
"""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": {
|
||||
|
||||
@@ -50,8 +50,10 @@ class QbusWeatherDescription(SensorEntityDescription):
|
||||
"""Description for Qbus weather entities."""
|
||||
|
||||
property: str
|
||||
scale_factor: int | None = None
|
||||
|
||||
|
||||
# Qbus reports illuminance in klux, HA only supports lux.
|
||||
_WEATHER_DESCRIPTIONS = (
|
||||
QbusWeatherDescription(
|
||||
key="daylight",
|
||||
@@ -60,6 +62,7 @@ _WEATHER_DESCRIPTIONS = (
|
||||
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=LIGHT_LUX,
|
||||
scale_factor=1000,
|
||||
),
|
||||
QbusWeatherDescription(
|
||||
key="light",
|
||||
@@ -67,6 +70,7 @@ _WEATHER_DESCRIPTIONS = (
|
||||
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=LIGHT_LUX,
|
||||
scale_factor=1000,
|
||||
),
|
||||
QbusWeatherDescription(
|
||||
key="light_east",
|
||||
@@ -75,6 +79,7 @@ _WEATHER_DESCRIPTIONS = (
|
||||
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=LIGHT_LUX,
|
||||
scale_factor=1000,
|
||||
),
|
||||
QbusWeatherDescription(
|
||||
key="light_south",
|
||||
@@ -83,6 +88,7 @@ _WEATHER_DESCRIPTIONS = (
|
||||
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=LIGHT_LUX,
|
||||
scale_factor=1000,
|
||||
),
|
||||
QbusWeatherDescription(
|
||||
key="light_west",
|
||||
@@ -91,6 +97,7 @@ _WEATHER_DESCRIPTIONS = (
|
||||
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=LIGHT_LUX,
|
||||
scale_factor=1000,
|
||||
),
|
||||
QbusWeatherDescription(
|
||||
key="temperature",
|
||||
@@ -400,4 +407,8 @@ class QbusWeatherSensor(QbusEntity, SensorEntity):
|
||||
|
||||
async def _handle_state_received(self, state: QbusMqttWeatherState) -> None:
|
||||
if value := state.read_property(self.entity_description.property, None):
|
||||
self.native_value = value
|
||||
self.native_value = (
|
||||
value * self.entity_description.scale_factor
|
||||
if self.entity_description.scale_factor is not None
|
||||
else value
|
||||
)
|
||||
|
||||
@@ -939,6 +939,7 @@ class DPCode(StrEnum):
|
||||
TEMP_INDOOR = "temp_indoor" # Indoor temperature in °C
|
||||
TEMP_SET = "temp_set" # Set the temperature in °C
|
||||
TEMP_SET_F = "temp_set_f" # Set the temperature in °F
|
||||
TEMP_SETTING_QUICK_C = "temp_setting_quick_c"
|
||||
TEMP_UNIT_CONVERT = "temp_unit_convert" # Temperature unit switching
|
||||
TEMP_VALUE = "temp_value" # Color temperature
|
||||
TEMP_VALUE_V2 = "temp_value_v2"
|
||||
@@ -992,6 +993,7 @@ class DPCode(StrEnum):
|
||||
WORK_POWER = "work_power"
|
||||
WORK_STATE = "work_state"
|
||||
WORK_STATE_E = "work_state_e"
|
||||
WORK_TYPE = "work_type"
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -19,6 +19,18 @@ from .entity import TuyaEntity
|
||||
# All descriptions can be found here. Mostly the Enum data types in the
|
||||
# default instructions set of each category end up being a select.
|
||||
SELECTS: dict[DeviceCategory, tuple[SelectEntityDescription, ...]] = {
|
||||
DeviceCategory.BH: (
|
||||
SelectEntityDescription(
|
||||
key=DPCode.TEMP_SETTING_QUICK_C,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
translation_key="quick_heat_temperature",
|
||||
),
|
||||
SelectEntityDescription(
|
||||
key=DPCode.WORK_TYPE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
translation_key="kettle_work_mode",
|
||||
),
|
||||
),
|
||||
DeviceCategory.CL: (
|
||||
SelectEntityDescription(
|
||||
key=DPCode.CONTROL_BACK_MODE,
|
||||
|
||||
@@ -478,6 +478,15 @@
|
||||
"1": "Continuous working mode"
|
||||
}
|
||||
},
|
||||
"kettle_work_mode": {
|
||||
"name": "Work mode",
|
||||
"state": {
|
||||
"boiling_quick": "Quick boil",
|
||||
"setting_quick": "Quick heat",
|
||||
"temp_boiling": "Boil and keep warm",
|
||||
"temp_setting": "Heat and keep warm"
|
||||
}
|
||||
},
|
||||
"led_type": {
|
||||
"name": "Light source type",
|
||||
"state": {
|
||||
@@ -515,6 +524,16 @@
|
||||
"smart": "Smart"
|
||||
}
|
||||
},
|
||||
"quick_heat_temperature": {
|
||||
"name": "Quick heat temperature",
|
||||
"state": {
|
||||
"80": "80 °C",
|
||||
"85": "85 °C",
|
||||
"90": "90 °C",
|
||||
"95": "95 °C",
|
||||
"100": "100 °C"
|
||||
}
|
||||
},
|
||||
"record_mode": {
|
||||
"name": "Record mode",
|
||||
"state": {
|
||||
|
||||
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2026
|
||||
MINOR_VERSION: Final = 6
|
||||
PATCH_VERSION: Final = "0.dev0"
|
||||
PATCH_VERSION: Final = "0b0"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 14, 2)
|
||||
|
||||
@@ -813,7 +813,7 @@ class EntityNumericalConditionBase(EntityConditionBase):
|
||||
if lower_limit is None or upper_limit is None:
|
||||
# Entity not found or invalid number, don't trigger
|
||||
return False
|
||||
between = lower_limit < value < upper_limit
|
||||
between = lower_limit <= value <= upper_limit
|
||||
if self._threshold_type == NumericThresholdType.BETWEEN:
|
||||
return between
|
||||
return not between
|
||||
|
||||
@@ -760,7 +760,7 @@ class EntityNumericalStateTriggerBase(EntityTriggerBase):
|
||||
if lower_limit is None or upper_limit is None:
|
||||
# Entity not found or invalid number, don't trigger
|
||||
return False
|
||||
between = lower_limit < current_value < upper_limit
|
||||
between = lower_limit <= current_value <= upper_limit
|
||||
if self._threshold_type == NumericThresholdType.BETWEEN:
|
||||
return between
|
||||
return not between
|
||||
|
||||
@@ -39,7 +39,7 @@ habluetooth==6.7.9
|
||||
hass-nabucasa==2.2.0
|
||||
hassil==3.5.0
|
||||
home-assistant-bluetooth==2.0.0
|
||||
home-assistant-frontend==20260429.4
|
||||
home-assistant-frontend==20260527.0
|
||||
home-assistant-intents==2026.5.5
|
||||
httpx==0.28.1
|
||||
ifaddr==0.2.0
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ To update, run python3 -m script.hassfest
|
||||
|
||||
from typing import Final
|
||||
|
||||
FRONTEND_VERSION: Final[str] = "20260429.4"
|
||||
FRONTEND_VERSION: Final[str] = "20260527.0"
|
||||
|
||||
MDI_ICONS: Final[set[str]] = {
|
||||
"ab-testing",
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2026.6.0.dev0"
|
||||
version = "2026.6.0b0"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
|
||||
Generated
+1
-1
@@ -1266,7 +1266,7 @@ hole==0.9.0
|
||||
holidays==0.97
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20260429.4
|
||||
home-assistant-frontend==20260527.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2026.5.5
|
||||
|
||||
+23
-23
@@ -1062,8 +1062,8 @@ def parametrize_numerical_attribute_crossed_threshold_trigger_states(
|
||||
threshold_unit,
|
||||
),
|
||||
target_states=[
|
||||
(state, {attribute: 50 * s} | unit_attributes),
|
||||
(state, {attribute: 60 * s} | unit_attributes),
|
||||
(state, {attribute: 10 * s} | unit_attributes),
|
||||
(state, {attribute: 90 * s} | unit_attributes),
|
||||
],
|
||||
other_states=[
|
||||
other_invalid_attr,
|
||||
@@ -1092,8 +1092,8 @@ def parametrize_numerical_attribute_crossed_threshold_trigger_states(
|
||||
],
|
||||
other_states=[
|
||||
other_invalid_attr,
|
||||
(state, {attribute: 50 * s} | unit_attributes),
|
||||
(state, {attribute: 60 * s} | unit_attributes),
|
||||
(state, {attribute: 10 * s} | unit_attributes),
|
||||
(state, {attribute: 90 * s} | unit_attributes),
|
||||
],
|
||||
extra_excluded_states=extra_excluded_states,
|
||||
required_filter_attributes=required_filter_attributes,
|
||||
@@ -1262,7 +1262,7 @@ def parametrize_numerical_state_value_crossed_threshold_trigger_states(
|
||||
},
|
||||
threshold_unit,
|
||||
),
|
||||
target_states=[("50", unit_attributes), ("60", unit_attributes)],
|
||||
target_states=[("10", unit_attributes), ("90", unit_attributes)],
|
||||
other_states=[
|
||||
("none", unit_attributes),
|
||||
("0", unit_attributes),
|
||||
@@ -1287,8 +1287,8 @@ def parametrize_numerical_state_value_crossed_threshold_trigger_states(
|
||||
target_states=[("0", unit_attributes), ("100", unit_attributes)],
|
||||
other_states=[
|
||||
("none", unit_attributes),
|
||||
("50", unit_attributes),
|
||||
("60", unit_attributes),
|
||||
("10", unit_attributes),
|
||||
("90", unit_attributes),
|
||||
],
|
||||
required_filter_attributes=required_filter_attributes,
|
||||
trigger_from_none=False,
|
||||
@@ -2016,14 +2016,14 @@ def parametrize_numerical_condition_above_below_any(
|
||||
threshold_unit,
|
||||
),
|
||||
target_states=[
|
||||
("21", unit_attributes),
|
||||
("20", unit_attributes),
|
||||
("50", unit_attributes),
|
||||
("79", unit_attributes),
|
||||
("80", unit_attributes),
|
||||
],
|
||||
other_states=[
|
||||
("0", unit_attributes),
|
||||
("20", unit_attributes),
|
||||
("80", unit_attributes),
|
||||
("19", unit_attributes),
|
||||
("81", unit_attributes),
|
||||
("100", unit_attributes),
|
||||
],
|
||||
required_filter_attributes=required_filter_attributes,
|
||||
@@ -2134,14 +2134,14 @@ def parametrize_numerical_condition_above_below_all(
|
||||
threshold_unit,
|
||||
),
|
||||
target_states=[
|
||||
("21", unit_attributes),
|
||||
("20", unit_attributes),
|
||||
("50", unit_attributes),
|
||||
("79", unit_attributes),
|
||||
("80", unit_attributes),
|
||||
],
|
||||
other_states=[
|
||||
("0", unit_attributes),
|
||||
("20", unit_attributes),
|
||||
("80", unit_attributes),
|
||||
("19", unit_attributes),
|
||||
("81", unit_attributes),
|
||||
("100", unit_attributes),
|
||||
],
|
||||
required_filter_attributes=required_filter_attributes,
|
||||
@@ -2281,14 +2281,14 @@ def parametrize_numerical_attribute_condition_above_below_any(
|
||||
threshold_unit,
|
||||
),
|
||||
target_states=[
|
||||
(state, {attribute: 21 * s} | unit_attributes),
|
||||
(state, {attribute: 20 * s} | unit_attributes),
|
||||
(state, {attribute: 50 * s} | unit_attributes),
|
||||
(state, {attribute: 79 * s} | unit_attributes),
|
||||
(state, {attribute: 80 * s} | unit_attributes),
|
||||
],
|
||||
other_states=[
|
||||
(state, {attribute: 0 * s} | unit_attributes),
|
||||
(state, {attribute: 20 * s} | unit_attributes),
|
||||
(state, {attribute: 80 * s} | unit_attributes),
|
||||
(state, {attribute: 19 * s} | unit_attributes),
|
||||
(state, {attribute: 81 * s} | unit_attributes),
|
||||
(state, {attribute: 100 * s} | unit_attributes),
|
||||
],
|
||||
extra_excluded_states=extra_excluded_states,
|
||||
@@ -2428,14 +2428,14 @@ def parametrize_numerical_attribute_condition_above_below_all(
|
||||
threshold_unit,
|
||||
),
|
||||
target_states=[
|
||||
(state, {attribute: 21 * s} | unit_attributes),
|
||||
(state, {attribute: 20 * s} | unit_attributes),
|
||||
(state, {attribute: 50 * s} | unit_attributes),
|
||||
(state, {attribute: 79 * s} | unit_attributes),
|
||||
(state, {attribute: 80 * s} | unit_attributes),
|
||||
],
|
||||
other_states=[
|
||||
(state, {attribute: 0 * s} | unit_attributes),
|
||||
(state, {attribute: 20 * s} | unit_attributes),
|
||||
(state, {attribute: 80 * s} | unit_attributes),
|
||||
(state, {attribute: 19 * s} | unit_attributes),
|
||||
(state, {attribute: 81 * s} | unit_attributes),
|
||||
(state, {attribute: 100 * s} | unit_attributes),
|
||||
],
|
||||
extra_excluded_states=extra_excluded_states,
|
||||
|
||||
@@ -98,8 +98,8 @@ async def test_counter_condition_options_validation(
|
||||
"value_max": {"number": 30},
|
||||
},
|
||||
},
|
||||
target_states=["11", "20", "29"],
|
||||
other_states=["0", "10", "30", "100"],
|
||||
target_states=["10", "11", "20", "29", "30"],
|
||||
other_states=["0", "9", "31", "100"],
|
||||
),
|
||||
*parametrize_condition_states_any(
|
||||
condition="counter.is_value",
|
||||
@@ -110,8 +110,8 @@ async def test_counter_condition_options_validation(
|
||||
"value_max": {"number": 30},
|
||||
},
|
||||
},
|
||||
target_states=["0", "10", "30", "100"],
|
||||
other_states=["11", "20", "29"],
|
||||
target_states=["0", "9", "31", "100"],
|
||||
other_states=["10", "11", "20", "29", "30"],
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -171,8 +171,8 @@ async def test_counter_is_value_condition_behavior_any(
|
||||
"value_max": {"number": 30},
|
||||
},
|
||||
},
|
||||
target_states=["11", "20", "29"],
|
||||
other_states=["0", "10", "30", "100"],
|
||||
target_states=["10", "11", "20", "29", "30"],
|
||||
other_states=["0", "9", "31", "100"],
|
||||
),
|
||||
*parametrize_condition_states_all(
|
||||
condition="counter.is_value",
|
||||
@@ -183,8 +183,8 @@ async def test_counter_is_value_condition_behavior_any(
|
||||
"value_max": {"number": 30},
|
||||
},
|
||||
},
|
||||
target_states=["0", "10", "30", "100"],
|
||||
other_states=["11", "20", "29"],
|
||||
target_states=["0", "9", "31", "100"],
|
||||
other_states=["10", "11", "20", "29", "30"],
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -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
|
||||
@@ -6,6 +6,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
|
||||
@@ -96,6 +97,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(
|
||||
@@ -116,6 +130,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."""
|
||||
@@ -172,7 +211,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(
|
||||
@@ -631,12 +670,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."""
|
||||
|
||||
@@ -673,21 +710,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"
|
||||
@@ -697,12 +723,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
|
||||
@@ -724,11 +748,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."""
|
||||
|
||||
@@ -762,19 +781,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"
|
||||
|
||||
@@ -798,162 +813,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"
|
||||
|
||||
@@ -977,30 +890,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"])
|
||||
@@ -1014,11 +936,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."""
|
||||
@@ -1076,14 +993,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"
|
||||
|
||||
@@ -1127,9 +1041,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,
|
||||
@@ -26,6 +29,7 @@ from homeassistant.config_entries import (
|
||||
)
|
||||
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
|
||||
|
||||
@@ -416,13 +420,14 @@ async def test_usb_device_reactivity(hass: HomeAssistant) -> None:
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
# Wait for a bit for the USB scan debouncer to cool off
|
||||
async_fire_time_changed(hass, dt_util.now() + timedelta(minutes=5))
|
||||
|
||||
# Unplug the stick
|
||||
# Unplug the stick before advancing time: the forced polling watcher rescans on
|
||||
# the time jump used to cool off the request debouncer, so the device must
|
||||
# already be gone or that scan would reload it as still present
|
||||
mock_exists.return_value = False
|
||||
|
||||
with patch_scanned_serial_ports(return_value=[]):
|
||||
# Wait for a bit for the USB scan debouncer to cool off
|
||||
async_fire_time_changed(hass, dt_util.now() + timedelta(minutes=5))
|
||||
await async_request_scan(hass)
|
||||
|
||||
# The integration has reloaded and is now in a failed state
|
||||
@@ -550,3 +555,127 @@ 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_not_created_for_cpc(
|
||||
hass: HomeAssistant, issue_registry: ir.IssueRegistry
|
||||
) -> None:
|
||||
"""Test no repair issue is created for CPC firmware when the addon is not running."""
|
||||
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()
|
||||
|
||||
assert (
|
||||
issue_registry.async_get_issue(
|
||||
domain=DOMAIN,
|
||||
issue_id=f"{ISSUE_MULTI_PAN_MIGRATION}_{config_entry.entry_id}",
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
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.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_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
|
||||
)
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
"""Test the Home Assistant SkyConnect repairs flow."""
|
||||
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.hassio import (
|
||||
DOMAIN as HASSIO_DOMAIN,
|
||||
AddonInfo,
|
||||
AddonState,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.repair_helpers import (
|
||||
ISSUE_MULTI_PAN_MIGRATION,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import (
|
||||
CONF_DISABLE_MULTI_PAN,
|
||||
get_multiprotocol_addon_manager,
|
||||
)
|
||||
from homeassistant.components.homeassistant_sky_connect.const import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import MockConfigEntry, MockModule, mock_integration
|
||||
from tests.components.repairs import (
|
||||
async_process_repairs_platforms,
|
||||
process_repair_fix_flow,
|
||||
start_repair_fix_flow,
|
||||
)
|
||||
from tests.typing import ClientSessionGenerator
|
||||
|
||||
DEVICE = (
|
||||
"/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0"
|
||||
"_9e2adbd75b8beb119fe564a0f320645d-if00-port0"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("supervisor_client")
|
||||
async def test_multi_pan_migration_repair_flow(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
) -> None:
|
||||
"""Test the multi-PAN migration repair flow reverts the firmware with progress."""
|
||||
mock_integration(hass, MockModule("hassio"))
|
||||
await async_setup_component(hass, HASSIO_DOMAIN, {})
|
||||
assert await async_setup_component(hass, "repairs", {})
|
||||
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id="some_unique_id",
|
||||
data={
|
||||
"description": "SkyConnect v1.0",
|
||||
"device": DEVICE,
|
||||
"vid": "10C4",
|
||||
"pid": "EA60",
|
||||
"serial_number": "3c0ed67c628beb11b1cd64a0f320645d",
|
||||
"manufacturer": "Nabu Casa",
|
||||
"product": "SkyConnect v1.0",
|
||||
"firmware": "cpc",
|
||||
"firmware_version": None,
|
||||
},
|
||||
title="Home Assistant SkyConnect",
|
||||
version=1,
|
||||
minor_version=4,
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
# Multi-PAN addon is running and using the radio
|
||||
mock_multipan_manager = Mock(spec_set=await get_multiprotocol_addon_manager(hass))
|
||||
mock_multipan_manager.addon_name = "Silicon Labs Multiprotocol"
|
||||
mock_multipan_manager.async_get_addon_info.return_value = AddonInfo(
|
||||
available=True,
|
||||
hostname=None,
|
||||
options={"device": DEVICE},
|
||||
state=AddonState.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(
|
||||
"homeassistant.components.homeassistant_sky_connect.os.path.exists",
|
||||
return_value=True,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.get_multiprotocol_addon_manager",
|
||||
return_value=mock_multipan_manager,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.async_flash_silabs_firmware",
|
||||
new_callable=AsyncMock,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.async_firmware_flashing_context"
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.FirmwareUpdateClient",
|
||||
return_value=mock_fw_client,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio",
|
||||
return_value=True,
|
||||
),
|
||||
):
|
||||
# Setting up the entry creates the migration issue
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_process_repairs_platforms(hass)
|
||||
client = await hass_client()
|
||||
|
||||
issue_id = f"{ISSUE_MULTI_PAN_MIGRATION}_{config_entry.entry_id}"
|
||||
assert issue_registry.async_get_issue(DOMAIN, issue_id) is not None
|
||||
|
||||
# The repair flow jumps straight into the uninstall confirmation form
|
||||
result = await start_repair_fix_flow(client, DOMAIN, issue_id)
|
||||
flow_id = result["flow_id"]
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "uninstall_addon"
|
||||
|
||||
# Confirm the migration: uninstall the multiprotocol addon (progress)
|
||||
result = await process_repair_fix_flow(
|
||||
client, flow_id, json={CONF_DISABLE_MULTI_PAN: True}
|
||||
)
|
||||
assert result["type"] == FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "uninstall_multiprotocol_addon"
|
||||
assert result["progress_action"] == "uninstall_multiprotocol_addon"
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
# Flash the Zigbee firmware (progress)
|
||||
result = await process_repair_fix_flow(client, flow_id)
|
||||
assert result["type"] == FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "install_zigbee_firmware"
|
||||
assert result["progress_action"] == "install_zigbee_firmware"
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
# Flashing complete, the flow finishes
|
||||
result = await process_repair_fix_flow(client, flow_id)
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
|
||||
# The firmware was reverted back to Zigbee
|
||||
assert config_entry.data["firmware"] == "ezsp"
|
||||
@@ -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 (
|
||||
@@ -550,15 +549,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(
|
||||
@@ -566,8 +561,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",
|
||||
@@ -596,11 +595,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, HassioNotReadyError
|
||||
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,157 @@ async def test_setup_entry_addon_info_fails(
|
||||
assert config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_multi_pan_migration_issue_not_created_for_cpc(
|
||||
hass: HomeAssistant,
|
||||
addon_store_info,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
) -> None:
|
||||
"""Test no repair issue is created for CPC firmware when the addon is not running."""
|
||||
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()
|
||||
|
||||
assert (
|
||||
issue_registry.async_get_issue(
|
||||
domain=DOMAIN,
|
||||
issue_id=f"{ISSUE_MULTI_PAN_MIGRATION}_{config_entry.entry_id}",
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
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.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_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 +534,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()
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
"""Test the Home Assistant Yellow repairs flow."""
|
||||
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
from homeassistant.components.hassio import (
|
||||
DOMAIN as HASSIO_DOMAIN,
|
||||
AddonInfo,
|
||||
AddonState,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.repair_helpers import (
|
||||
ISSUE_MULTI_PAN_MIGRATION,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import (
|
||||
CONF_DISABLE_MULTI_PAN,
|
||||
get_multiprotocol_addon_manager,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.util import ApplicationType
|
||||
from homeassistant.components.homeassistant_yellow.const import DOMAIN, RADIO_DEVICE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import MockConfigEntry, MockModule, mock_integration
|
||||
from tests.components.repairs import (
|
||||
async_process_repairs_platforms,
|
||||
process_repair_fix_flow,
|
||||
start_repair_fix_flow,
|
||||
)
|
||||
from tests.typing import ClientSessionGenerator
|
||||
|
||||
|
||||
async def test_multi_pan_migration_repair_flow(
|
||||
hass: HomeAssistant,
|
||||
supervisor_client: AsyncMock,
|
||||
hass_client: ClientSessionGenerator,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
) -> None:
|
||||
"""Test the multi-PAN migration repair flow reverts the firmware with progress."""
|
||||
mock_integration(hass, MockModule("hassio"))
|
||||
await async_setup_component(hass, HASSIO_DOMAIN, {})
|
||||
assert await async_setup_component(hass, "repairs", {})
|
||||
|
||||
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)
|
||||
|
||||
# Multi-PAN addon is running and using the radio
|
||||
mock_multipan_manager = Mock(spec_set=await get_multiprotocol_addon_manager(hass))
|
||||
mock_multipan_manager.addon_name = "Silicon Labs Multiprotocol"
|
||||
mock_multipan_manager.async_get_addon_info.return_value = AddonInfo(
|
||||
available=True,
|
||||
hostname=None,
|
||||
options={"device": RADIO_DEVICE},
|
||||
state=AddonState.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(
|
||||
"homeassistant.components.homeassistant_yellow.get_os_info",
|
||||
return_value={"board": "yellow"},
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_yellow.config_flow.get_supervisor_client",
|
||||
return_value=supervisor_client,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_yellow.check_multi_pan_addon",
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.get_multiprotocol_addon_manager",
|
||||
return_value=mock_multipan_manager,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.async_flash_silabs_firmware",
|
||||
new_callable=AsyncMock,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.async_firmware_flashing_context"
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.FirmwareUpdateClient",
|
||||
return_value=mock_fw_client,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio",
|
||||
return_value=True,
|
||||
),
|
||||
):
|
||||
# Setting up the entry creates the migration issue
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_process_repairs_platforms(hass)
|
||||
client = await hass_client()
|
||||
|
||||
issue_id = f"{ISSUE_MULTI_PAN_MIGRATION}_{config_entry.entry_id}"
|
||||
assert issue_registry.async_get_issue(DOMAIN, issue_id) is not None
|
||||
|
||||
# The repair flow jumps straight into the uninstall confirmation form
|
||||
result = await start_repair_fix_flow(client, DOMAIN, issue_id)
|
||||
flow_id = result["flow_id"]
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "uninstall_addon"
|
||||
|
||||
# Confirm the migration: uninstall the multiprotocol addon (progress)
|
||||
result = await process_repair_fix_flow(
|
||||
client, flow_id, json={CONF_DISABLE_MULTI_PAN: True}
|
||||
)
|
||||
assert result["type"] == FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "uninstall_multiprotocol_addon"
|
||||
assert result["progress_action"] == "uninstall_multiprotocol_addon"
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
# Flash the Zigbee firmware (progress)
|
||||
result = await process_repair_fix_flow(client, flow_id)
|
||||
assert result["type"] == FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "install_zigbee_firmware"
|
||||
assert result["progress_action"] == "install_zigbee_firmware"
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
# Flashing complete, the flow finishes
|
||||
result = await process_repair_fix_flow(client, flow_id)
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
|
||||
# The firmware was reverted back to Zigbee
|
||||
assert config_entry.data["firmware"] == "ezsp"
|
||||
@@ -9,7 +9,9 @@ from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
from tests.common import MockConfigEntry, async_fire_mqtt_message, snapshot_platform
|
||||
|
||||
_TOPIC_WEATHER_STATE = "cloudapp/QBUSMQTTGW/UL1/UL60/state"
|
||||
|
||||
|
||||
async def test_sensor(
|
||||
@@ -45,3 +47,33 @@ async def test_sensor_gauge_unit_missing(
|
||||
entity = hass.states.get("sensor.tuin_luchtkwaliteit")
|
||||
assert entity
|
||||
assert entity.attributes[ATTR_UNIT_OF_MEASUREMENT] == "ppm"
|
||||
|
||||
|
||||
async def test_weather_lux_uses_scale_factor(
|
||||
hass: HomeAssistant, setup_integration: None
|
||||
) -> None:
|
||||
"""Test whether lux values are using the scale factor."""
|
||||
|
||||
async_fire_mqtt_message(
|
||||
hass,
|
||||
_TOPIC_WEATHER_STATE,
|
||||
'{"id":"UL60","properties":{"lightSouth":21},"type":"state"}',
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get("sensor.tuin_weersensor_illuminance_south").state == "21000"
|
||||
|
||||
|
||||
async def test_weather_wind_skips_scale_factor(
|
||||
hass: HomeAssistant, setup_integration: None
|
||||
) -> None:
|
||||
"""Test whether wind value is not using the scale factor."""
|
||||
|
||||
async_fire_mqtt_message(
|
||||
hass,
|
||||
_TOPIC_WEATHER_STATE,
|
||||
'{"id":"UL60","properties":{"wind":25},"type":"state"}',
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get("sensor.tuin_weersensor_wind_speed").state == "25"
|
||||
|
||||
@@ -171,8 +171,8 @@ def parametrize_incomplete_condition_states_any(
|
||||
"value_max": {"number": 8},
|
||||
}
|
||||
},
|
||||
target_states=["3", "5"],
|
||||
other_states=["0", "1", "2", "8", "10"],
|
||||
target_states=["2", "3", "5", "8"],
|
||||
other_states=["0", "1", "9", "10"],
|
||||
),
|
||||
]
|
||||
|
||||
@@ -203,8 +203,8 @@ def parametrize_incomplete_condition_states_all(
|
||||
"value_max": {"number": 8},
|
||||
}
|
||||
},
|
||||
target_states=["3", "5"],
|
||||
other_states=["0", "1", "2", "8", "10"],
|
||||
target_states=["2", "3", "5", "8"],
|
||||
other_states=["0", "1", "9", "10"],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@@ -5637,6 +5637,128 @@
|
||||
'state': 'low',
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[select.smart_kettle_quick_heat_temperature-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'85',
|
||||
'90',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'select',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'select.smart_kettle_quick_heat_temperature',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Quick heat temperature',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Quick heat temperature',
|
||||
'platform': 'tuya',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'quick_heat_temperature',
|
||||
'unique_id': 'tuya.s5ah3novtabe4tfdhbtemp_setting_quick_c',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[select.smart_kettle_quick_heat_temperature-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Smart Kettle Quick heat temperature',
|
||||
'options': list([
|
||||
'85',
|
||||
'90',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'select.smart_kettle_quick_heat_temperature',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '85',
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[select.smart_kettle_work_mode-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'setting_quick',
|
||||
'boiling_quick',
|
||||
'temp_setting',
|
||||
'temp_boiling',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'select',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'select.smart_kettle_work_mode',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Work mode',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Work mode',
|
||||
'platform': 'tuya',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'kettle_work_mode',
|
||||
'unique_id': 'tuya.s5ah3novtabe4tfdhbwork_type',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[select.smart_kettle_work_mode-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Smart Kettle Work mode',
|
||||
'options': list([
|
||||
'setting_quick',
|
||||
'boiling_quick',
|
||||
'temp_setting',
|
||||
'temp_boiling',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'select.smart_kettle_work_mode',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'setting_quick',
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[select.smart_odor_eliminator_pro_odor_elimination_mode-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
|
||||
@@ -3270,7 +3270,8 @@ async def _setup_numerical_condition(
|
||||
({"threshold": {"type": "below", "value": {"number": 50}}}, "25", True),
|
||||
({"threshold": {"type": "below", "value": {"number": 50}}}, "50", False),
|
||||
({"threshold": {"type": "below", "value": {"number": 50}}}, "75", False),
|
||||
# above and below (range)
|
||||
# between (range) — limits are inclusive, so a value exactly equal
|
||||
# to either bound is treated as "inside" and matches
|
||||
(
|
||||
{
|
||||
"threshold": {
|
||||
@@ -3291,7 +3292,7 @@ async def _setup_numerical_condition(
|
||||
}
|
||||
},
|
||||
"20",
|
||||
False,
|
||||
True,
|
||||
),
|
||||
(
|
||||
{
|
||||
@@ -3302,7 +3303,7 @@ async def _setup_numerical_condition(
|
||||
}
|
||||
},
|
||||
"80",
|
||||
False,
|
||||
True,
|
||||
),
|
||||
(
|
||||
{
|
||||
@@ -3326,8 +3327,9 @@ async def _setup_numerical_condition(
|
||||
"90",
|
||||
False,
|
||||
),
|
||||
# outside (inverse of between) — limits are non-inclusive, so a value
|
||||
# equal to either bound is treated as "not inside" and matches
|
||||
# outside (inverse of between) — limits are inclusive on the between
|
||||
# side, so a value exactly equal to either bound is "inside" and
|
||||
# does NOT match outside
|
||||
(
|
||||
{
|
||||
"threshold": {
|
||||
@@ -3348,7 +3350,7 @@ async def _setup_numerical_condition(
|
||||
}
|
||||
},
|
||||
"20",
|
||||
True,
|
||||
False,
|
||||
),
|
||||
(
|
||||
{
|
||||
@@ -3359,7 +3361,7 @@ async def _setup_numerical_condition(
|
||||
}
|
||||
},
|
||||
"80",
|
||||
True,
|
||||
False,
|
||||
),
|
||||
(
|
||||
{
|
||||
|
||||
@@ -1948,7 +1948,7 @@ async def test_numerical_state_attribute_changed_error_handling(
|
||||
({"threshold": {"type": "below", "value": {"number": 50}}}, 25, True),
|
||||
({"threshold": {"type": "below", "value": {"number": 50}}}, 50, False),
|
||||
({"threshold": {"type": "below", "value": {"number": 50}}}, 75, False),
|
||||
# between — both limits are non-inclusive
|
||||
# between — both limits are inclusive
|
||||
(
|
||||
{
|
||||
"threshold": {
|
||||
@@ -1969,7 +1969,7 @@ async def test_numerical_state_attribute_changed_error_handling(
|
||||
}
|
||||
},
|
||||
20,
|
||||
False,
|
||||
True,
|
||||
),
|
||||
(
|
||||
{
|
||||
@@ -1980,7 +1980,7 @@ async def test_numerical_state_attribute_changed_error_handling(
|
||||
}
|
||||
},
|
||||
80,
|
||||
False,
|
||||
True,
|
||||
),
|
||||
(
|
||||
{
|
||||
@@ -2004,7 +2004,8 @@ async def test_numerical_state_attribute_changed_error_handling(
|
||||
90,
|
||||
False,
|
||||
),
|
||||
# outside — values equal to either bound are treated as "not inside"
|
||||
# outside — values equal to either bound are inside the between range
|
||||
# and therefore do NOT match outside
|
||||
(
|
||||
{
|
||||
"threshold": {
|
||||
@@ -2025,7 +2026,7 @@ async def test_numerical_state_attribute_changed_error_handling(
|
||||
}
|
||||
},
|
||||
20,
|
||||
True,
|
||||
False,
|
||||
),
|
||||
(
|
||||
{
|
||||
@@ -2036,7 +2037,7 @@ async def test_numerical_state_attribute_changed_error_handling(
|
||||
}
|
||||
},
|
||||
80,
|
||||
True,
|
||||
False,
|
||||
),
|
||||
(
|
||||
{
|
||||
@@ -3039,7 +3040,7 @@ async def test_numerical_state_attribute_crossed_threshold_error_handling(
|
||||
({"threshold": {"type": "below", "value": {"number": 50}}}, 25, True),
|
||||
({"threshold": {"type": "below", "value": {"number": 50}}}, 50, False),
|
||||
({"threshold": {"type": "below", "value": {"number": 50}}}, 75, False),
|
||||
# between — both limits are non-inclusive
|
||||
# between — both limits are inclusive
|
||||
(
|
||||
{
|
||||
"threshold": {
|
||||
@@ -3060,7 +3061,7 @@ async def test_numerical_state_attribute_crossed_threshold_error_handling(
|
||||
}
|
||||
},
|
||||
20,
|
||||
False,
|
||||
True,
|
||||
),
|
||||
(
|
||||
{
|
||||
@@ -3071,7 +3072,7 @@ async def test_numerical_state_attribute_crossed_threshold_error_handling(
|
||||
}
|
||||
},
|
||||
80,
|
||||
False,
|
||||
True,
|
||||
),
|
||||
(
|
||||
{
|
||||
@@ -3095,8 +3096,8 @@ async def test_numerical_state_attribute_crossed_threshold_error_handling(
|
||||
90,
|
||||
False,
|
||||
),
|
||||
# outside — values equal to either bound are treated as "not inside"
|
||||
# and therefore enter the "outside" range from the inside seed value
|
||||
# outside — values equal to either bound are inside the between range
|
||||
# and therefore do NOT match outside (no cross from the inside seed)
|
||||
(
|
||||
{
|
||||
"threshold": {
|
||||
@@ -3117,7 +3118,7 @@ async def test_numerical_state_attribute_crossed_threshold_error_handling(
|
||||
}
|
||||
},
|
||||
20,
|
||||
True,
|
||||
False,
|
||||
),
|
||||
(
|
||||
{
|
||||
@@ -3128,7 +3129,7 @@ async def test_numerical_state_attribute_crossed_threshold_error_handling(
|
||||
}
|
||||
},
|
||||
80,
|
||||
True,
|
||||
False,
|
||||
),
|
||||
(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user