mirror of
https://github.com/home-assistant/core.git
synced 2026-05-27 19:25:18 +02:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5e4f0e143d | |||
| 218c3ffd11 |
@@ -24,6 +24,7 @@ The following platforms have extra guidelines:
|
||||
## Entity platforms
|
||||
|
||||
- Ensure `async_added_to_hass()` and `async_will_remove_from_hass()` have symmetrical behavior. For example, if a subscription is created in `async_added_to_hass()`, it should be unsubscribed in `async_will_remove_from_hass()`. Also, if something is torn down in `async_will_remove_from_hass()`, it should be set up in `async_added_to_hass()`.
|
||||
- Entity base class (e.g. `SensorEntity`, `TrackerEntity`) provide a stable API for child classes to inherit from. Do not suggest redeclaring or duplicating attributes, properties, or methods the base class already provides, and do not add guards against the parent's behavior changing — rely on the base class instead.
|
||||
|
||||
## Integration Quality Scale
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ The following platforms have extra guidelines:
|
||||
## Entity platforms
|
||||
|
||||
- Ensure `async_added_to_hass()` and `async_will_remove_from_hass()` have symmetrical behavior. For example, if a subscription is created in `async_added_to_hass()`, it should be unsubscribed in `async_will_remove_from_hass()`. Also, if something is torn down in `async_will_remove_from_hass()`, it should be set up in `async_added_to_hass()`.
|
||||
- Entity base class (e.g. `SensorEntity`, `TrackerEntity`) provide a stable API for child classes to inherit from. Do not suggest redeclaring or duplicating attributes, properties, or methods the base class already provides, and do not add guards against the parent's behavior changing — rely on the base class instead.
|
||||
|
||||
## Integration Quality Scale
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from homeassistant.components.select import (
|
||||
PLATFORM_SCHEMA as SELECT_PLATFORM_SCHEMA,
|
||||
SelectEntity,
|
||||
)
|
||||
from homeassistant.const import CONF_NAME, CONF_OPTIONS
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
@@ -19,6 +19,9 @@ 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==20260527.0"]
|
||||
"requirements": ["home-assistant-frontend==20260429.4"]
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Home Assistant Hardware",
|
||||
"after_dependencies": ["hassio"],
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
"dependencies": ["repairs", "usb"],
|
||||
"dependencies": ["usb"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/homeassistant_hardware",
|
||||
"integration_type": "system",
|
||||
"requirements": [
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
"""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,8 +6,6 @@ import dataclasses
|
||||
import logging
|
||||
from typing import Any, Protocol
|
||||
|
||||
from aiohttp import ClientError
|
||||
from ha_silabs_firmware_client import FirmwareUpdateClient, ManifestMissing
|
||||
import voluptuous as vol
|
||||
import yarl
|
||||
|
||||
@@ -27,7 +25,6 @@ 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,
|
||||
@@ -40,18 +37,15 @@ from homeassistant.helpers.selector import (
|
||||
from homeassistant.helpers.singleton import singleton
|
||||
from homeassistant.helpers.storage import Store
|
||||
|
||||
from .const import DOMAIN, LOGGER, SILABS_MULTIPROTOCOL_ADDON_SLUG
|
||||
from .util import (
|
||||
ApplicationType,
|
||||
WaitingAddonManager,
|
||||
async_firmware_flashing_context,
|
||||
async_flash_silabs_firmware,
|
||||
)
|
||||
from .const import LOGGER, SILABS_FLASHER_ADDON_SLUG, SILABS_MULTIPROTOCOL_ADDON_SLUG
|
||||
|
||||
_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"
|
||||
@@ -77,6 +71,53 @@ 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."""
|
||||
|
||||
@@ -224,6 +265,18 @@ 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."""
|
||||
@@ -286,19 +339,6 @@ 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."""
|
||||
@@ -646,7 +686,61 @@ class OptionsFlowHandler(OptionsFlow, ABC):
|
||||
async def async_step_firmware_revert(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Initiate ZHA backup and start multiprotocol addon uninstall."""
|
||||
"""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."""
|
||||
# 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
|
||||
@@ -688,6 +782,17 @@ 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(
|
||||
@@ -716,93 +821,62 @@ class OptionsFlowHandler(OptionsFlow, ABC):
|
||||
finally:
|
||||
self.stop_task = None
|
||||
|
||||
return self.async_show_progress_done(next_step_id="install_zigbee_firmware")
|
||||
return self.async_show_progress_done(next_step_id="start_flasher_addon")
|
||||
|
||||
async def async_step_install_zigbee_firmware(
|
||||
async def async_step_start_flasher_addon(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Flash Zigbee firmware directly onto the radio."""
|
||||
if not self.install_task:
|
||||
"""Start Silicon Labs Flasher add-on."""
|
||||
flasher_manager = get_flasher_addon_manager(self.hass)
|
||||
|
||||
async def _flash_firmware() -> None:
|
||||
serial_port_settings = await self._async_serial_port_settings()
|
||||
device = serial_port_settings.device
|
||||
if not self.start_task:
|
||||
|
||||
# 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)
|
||||
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
|
||||
)
|
||||
|
||||
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,
|
||||
self.start_task = self.hass.async_create_task(
|
||||
start_and_wait_until_done(), eager_start=False
|
||||
)
|
||||
|
||||
if not self.install_task.done():
|
||||
if not self.start_task.done():
|
||||
return self.async_show_progress(
|
||||
step_id="install_zigbee_firmware",
|
||||
progress_action="install_zigbee_firmware",
|
||||
description_placeholders={
|
||||
"hardware_name": self._hardware_name(),
|
||||
},
|
||||
progress_task=self.install_task,
|
||||
step_id="start_flasher_addon",
|
||||
progress_action="start_flasher_addon",
|
||||
description_placeholders={"addon_name": flasher_manager.addon_name},
|
||||
progress_task=self.start_task,
|
||||
)
|
||||
|
||||
try:
|
||||
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")
|
||||
await self.start_task
|
||||
except (AddonError, AbortFlow) as err:
|
||||
_LOGGER.error(err)
|
||||
return self.async_show_progress_done(next_step_id="flasher_failed")
|
||||
finally:
|
||||
self.install_task = None
|
||||
self.start_task = None
|
||||
|
||||
return self.async_show_progress_done(next_step_id="flashing_complete")
|
||||
|
||||
async def async_step_firmware_flash_failed(
|
||||
async def async_step_flasher_failed(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Firmware flashing failed."""
|
||||
"""Flasher add-on start failed."""
|
||||
flasher_manager = get_flasher_addon_manager(self.hass)
|
||||
return self.async_abort(
|
||||
reason="fw_install_failed",
|
||||
description_placeholders={"firmware_name": "Zigbee"},
|
||||
reason="addon_start_failed",
|
||||
description_placeholders={"addon_name": flasher_manager.addon_name},
|
||||
)
|
||||
|
||||
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,9 +102,7 @@
|
||||
},
|
||||
"progress": {
|
||||
"install_addon": "Please wait while the {addon_name} app installation finishes. This can take several minutes.",
|
||||
"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."
|
||||
"start_addon": "Please wait while the {addon_name} app start completes. This may take some seconds."
|
||||
},
|
||||
"step": {
|
||||
"addon_installed_other_device": {
|
||||
|
||||
@@ -37,59 +37,13 @@ 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."""
|
||||
@@ -325,11 +279,6 @@ 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,13 +7,6 @@ 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,
|
||||
@@ -99,16 +92,6 @@ 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,19 +248,6 @@ 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:
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
"""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,37 +106,6 @@
|
||||
"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%]",
|
||||
@@ -161,10 +130,8 @@
|
||||
"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%]",
|
||||
"uninstall_multiprotocol_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::uninstall_multiprotocol_addon%]"
|
||||
"start_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::start_otbr_addon%]"
|
||||
},
|
||||
"step": {
|
||||
"addon_installed_other_device": {
|
||||
|
||||
@@ -7,13 +7,8 @@ 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,
|
||||
@@ -32,7 +27,6 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.hassio import is_hassio
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
FIRMWARE,
|
||||
FIRMWARE_VERSION,
|
||||
MANUFACTURER,
|
||||
@@ -83,16 +77,6 @@ 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,19 +319,6 @@ 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:
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
"""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,37 +11,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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%]",
|
||||
@@ -68,10 +37,8 @@
|
||||
"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%]",
|
||||
"uninstall_multiprotocol_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::uninstall_multiprotocol_addon%]"
|
||||
"start_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::start_otbr_addon%]"
|
||||
},
|
||||
"step": {
|
||||
"addon_installed_other_device": {
|
||||
|
||||
@@ -50,10 +50,8 @@ 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",
|
||||
@@ -62,7 +60,6 @@ _WEATHER_DESCRIPTIONS = (
|
||||
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=LIGHT_LUX,
|
||||
scale_factor=1000,
|
||||
),
|
||||
QbusWeatherDescription(
|
||||
key="light",
|
||||
@@ -70,7 +67,6 @@ _WEATHER_DESCRIPTIONS = (
|
||||
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=LIGHT_LUX,
|
||||
scale_factor=1000,
|
||||
),
|
||||
QbusWeatherDescription(
|
||||
key="light_east",
|
||||
@@ -79,7 +75,6 @@ _WEATHER_DESCRIPTIONS = (
|
||||
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=LIGHT_LUX,
|
||||
scale_factor=1000,
|
||||
),
|
||||
QbusWeatherDescription(
|
||||
key="light_south",
|
||||
@@ -88,7 +83,6 @@ _WEATHER_DESCRIPTIONS = (
|
||||
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=LIGHT_LUX,
|
||||
scale_factor=1000,
|
||||
),
|
||||
QbusWeatherDescription(
|
||||
key="light_west",
|
||||
@@ -97,7 +91,6 @@ _WEATHER_DESCRIPTIONS = (
|
||||
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=LIGHT_LUX,
|
||||
scale_factor=1000,
|
||||
),
|
||||
QbusWeatherDescription(
|
||||
key="temperature",
|
||||
@@ -407,8 +400,4 @@ 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.entity_description.scale_factor
|
||||
if self.entity_description.scale_factor is not None
|
||||
else value
|
||||
)
|
||||
self.native_value = value
|
||||
|
||||
@@ -939,7 +939,6 @@ 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"
|
||||
@@ -993,7 +992,6 @@ class DPCode(StrEnum):
|
||||
WORK_POWER = "work_power"
|
||||
WORK_STATE = "work_state"
|
||||
WORK_STATE_E = "work_state_e"
|
||||
WORK_TYPE = "work_type"
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -19,18 +19,6 @@ 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,15 +478,6 @@
|
||||
"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": {
|
||||
@@ -524,16 +515,6 @@
|
||||
"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 = "0b0"
|
||||
PATCH_VERSION: Final = "0.dev0"
|
||||
__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==20260527.0
|
||||
home-assistant-frontend==20260429.4
|
||||
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] = "20260527.0"
|
||||
FRONTEND_VERSION: Final[str] = "20260429.4"
|
||||
|
||||
MDI_ICONS: Final[set[str]] = {
|
||||
"ab-testing",
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2026.6.0b0"
|
||||
version = "2026.6.0.dev0"
|
||||
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==20260527.0
|
||||
home-assistant-frontend==20260429.4
|
||||
|
||||
# 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: 10 * s} | unit_attributes),
|
||||
(state, {attribute: 90 * s} | unit_attributes),
|
||||
(state, {attribute: 50 * s} | unit_attributes),
|
||||
(state, {attribute: 60 * 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: 10 * s} | unit_attributes),
|
||||
(state, {attribute: 90 * s} | unit_attributes),
|
||||
(state, {attribute: 50 * s} | unit_attributes),
|
||||
(state, {attribute: 60 * 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=[("10", unit_attributes), ("90", unit_attributes)],
|
||||
target_states=[("50", unit_attributes), ("60", 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),
|
||||
("10", unit_attributes),
|
||||
("90", unit_attributes),
|
||||
("50", unit_attributes),
|
||||
("60", 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=[
|
||||
("20", unit_attributes),
|
||||
("21", unit_attributes),
|
||||
("50", unit_attributes),
|
||||
("80", unit_attributes),
|
||||
("79", unit_attributes),
|
||||
],
|
||||
other_states=[
|
||||
("0", unit_attributes),
|
||||
("19", unit_attributes),
|
||||
("81", unit_attributes),
|
||||
("20", unit_attributes),
|
||||
("80", 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=[
|
||||
("20", unit_attributes),
|
||||
("21", unit_attributes),
|
||||
("50", unit_attributes),
|
||||
("80", unit_attributes),
|
||||
("79", unit_attributes),
|
||||
],
|
||||
other_states=[
|
||||
("0", unit_attributes),
|
||||
("19", unit_attributes),
|
||||
("81", unit_attributes),
|
||||
("20", unit_attributes),
|
||||
("80", 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: 20 * s} | unit_attributes),
|
||||
(state, {attribute: 21 * s} | unit_attributes),
|
||||
(state, {attribute: 50 * s} | unit_attributes),
|
||||
(state, {attribute: 80 * s} | unit_attributes),
|
||||
(state, {attribute: 79 * s} | unit_attributes),
|
||||
],
|
||||
other_states=[
|
||||
(state, {attribute: 0 * s} | unit_attributes),
|
||||
(state, {attribute: 19 * s} | unit_attributes),
|
||||
(state, {attribute: 81 * s} | unit_attributes),
|
||||
(state, {attribute: 20 * s} | unit_attributes),
|
||||
(state, {attribute: 80 * 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: 20 * s} | unit_attributes),
|
||||
(state, {attribute: 21 * s} | unit_attributes),
|
||||
(state, {attribute: 50 * s} | unit_attributes),
|
||||
(state, {attribute: 80 * s} | unit_attributes),
|
||||
(state, {attribute: 79 * s} | unit_attributes),
|
||||
],
|
||||
other_states=[
|
||||
(state, {attribute: 0 * s} | unit_attributes),
|
||||
(state, {attribute: 19 * s} | unit_attributes),
|
||||
(state, {attribute: 81 * s} | unit_attributes),
|
||||
(state, {attribute: 20 * s} | unit_attributes),
|
||||
(state, {attribute: 80 * 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=["10", "11", "20", "29", "30"],
|
||||
other_states=["0", "9", "31", "100"],
|
||||
target_states=["11", "20", "29"],
|
||||
other_states=["0", "10", "30", "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", "9", "31", "100"],
|
||||
other_states=["10", "11", "20", "29", "30"],
|
||||
target_states=["0", "10", "30", "100"],
|
||||
other_states=["11", "20", "29"],
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -171,8 +171,8 @@ async def test_counter_is_value_condition_behavior_any(
|
||||
"value_max": {"number": 30},
|
||||
},
|
||||
},
|
||||
target_states=["10", "11", "20", "29", "30"],
|
||||
other_states=["0", "9", "31", "100"],
|
||||
target_states=["11", "20", "29"],
|
||||
other_states=["0", "10", "30", "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", "9", "31", "100"],
|
||||
other_states=["10", "11", "20", "29", "30"],
|
||||
target_states=["0", "10", "30", "100"],
|
||||
other_states=["11", "20", "29"],
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
"""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,7 +6,6 @@ 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
|
||||
@@ -97,19 +96,6 @@ 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(
|
||||
@@ -130,31 +116,6 @@ 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."""
|
||||
@@ -211,7 +172,7 @@ def get_suggested(schema, key):
|
||||
|
||||
|
||||
@patch(
|
||||
"homeassistant.components.homeassistant_hardware.util.ADDON_STATE_POLL_INTERVAL",
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.ADDON_STATE_POLL_INTERVAL",
|
||||
0,
|
||||
)
|
||||
@pytest.mark.usefixtures(
|
||||
@@ -670,10 +631,12 @@ 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."""
|
||||
|
||||
@@ -710,10 +673,21 @@ 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"
|
||||
@@ -723,10 +697,12 @@ 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"] == "install_zigbee_firmware"
|
||||
assert result["progress_action"] == "install_zigbee_firmware"
|
||||
assert result["step_id"] == "start_flasher_addon"
|
||||
assert result["progress_action"] == "start_flasher_addon"
|
||||
assert result["description_placeholders"] == {"addon_name": "Silicon Labs Flasher"}
|
||||
|
||||
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
|
||||
@@ -748,6 +724,11 @@ 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."""
|
||||
|
||||
@@ -781,15 +762,19 @@ 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_firmware_flash_failure(
|
||||
async def test_option_flow_flasher_already_running_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 firmware flash fails."""
|
||||
"""Test uninstalling the multi pan addon but with the flasher addon running."""
|
||||
|
||||
addon_info.return_value.options["device"] = "/dev/ttyTEST123"
|
||||
|
||||
@@ -813,60 +798,162 @@ async def test_option_flow_firmware_flash_failure(
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "uninstall_addon"
|
||||
|
||||
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"
|
||||
# The flasher addon is already installed and running, this is bad
|
||||
addon_store_info.return_value.installed = True
|
||||
addon_info.return_value.state = "started"
|
||||
|
||||
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},
|
||||
)
|
||||
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"
|
||||
|
||||
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")
|
||||
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."""
|
||||
|
||||
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"
|
||||
addon_info.return_value.options["device"] = "/dev/ttyTEST123"
|
||||
|
||||
await hass.async_block_till_done()
|
||||
# Setup the config entry
|
||||
config_entry = MockConfigEntry(
|
||||
data={},
|
||||
domain=TEST_DOMAIN,
|
||||
options={},
|
||||
title="Test HW",
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "fw_install_failed"
|
||||
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
|
||||
|
||||
|
||||
@pytest.mark.parametrize("ignore_translations_for_mock_domains", ["test"])
|
||||
async def test_option_flow_zigbee_firmware_fetch_failure(
|
||||
async def test_option_flow_flasher_install_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 fetching Zigbee firmware fails."""
|
||||
"""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."""
|
||||
|
||||
addon_info.return_value.options["device"] = "/dev/ttyTEST123"
|
||||
|
||||
@@ -890,39 +977,30 @@ async def test_option_flow_zigbee_firmware_fetch_failure(
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "uninstall_addon"
|
||||
|
||||
mock_fw_client = AsyncMock()
|
||||
mock_fw_client.async_update_data.side_effect = ClientError("Network error")
|
||||
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"
|
||||
|
||||
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"
|
||||
start_addon.side_effect = SupervisorError("Boom")
|
||||
|
||||
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"] == "install_zigbee_firmware"
|
||||
assert result["progress_action"] == "install_zigbee_firmware"
|
||||
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"}
|
||||
|
||||
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"] == "fw_install_failed"
|
||||
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"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("ignore_translations_for_mock_domains", ["test"])
|
||||
@@ -936,6 +1014,11 @@ 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."""
|
||||
@@ -993,11 +1076,14 @@ 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 finish fails."""
|
||||
"""Test uninstalling the multi pan addon, case where ZHA migration init fails."""
|
||||
|
||||
addon_info.return_value.options["device"] = "/dev/ttyTEST123"
|
||||
|
||||
@@ -1041,8 +1127,9 @@ 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"] == "install_zigbee_firmware"
|
||||
assert result["progress_action"] == "install_zigbee_firmware"
|
||||
assert result["step_id"] == "start_flasher_addon"
|
||||
assert result["progress_action"] == "start_flasher_addon"
|
||||
assert result["description_placeholders"] == {"addon_name": "Silicon Labs Flasher"}
|
||||
|
||||
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.silabs_multiprotocol_addon.get_multiprotocol_addon_manager",
|
||||
"homeassistant.components.homeassistant_hardware.util.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.silabs_multiprotocol_addon.get_multiprotocol_addon_manager",
|
||||
"homeassistant.components.homeassistant_hardware.util.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.silabs_multiprotocol_addon.get_multiprotocol_addon_manager",
|
||||
"homeassistant.components.homeassistant_hardware.util.get_multiprotocol_addon_manager",
|
||||
return_value=multipan_addon_manager,
|
||||
),
|
||||
patch(
|
||||
|
||||
@@ -18,6 +18,7 @@ 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 (
|
||||
@@ -383,11 +384,15 @@ async def test_options_flow_multipan_uninstall(
|
||||
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"
|
||||
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",
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
@@ -395,12 +400,8 @@ async def test_options_flow_multipan_uninstall(
|
||||
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.FirmwareUpdateClient",
|
||||
return_value=mock_fw_client,
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.get_flasher_addon_manager",
|
||||
return_value=mock_flasher_manager,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio",
|
||||
@@ -423,15 +424,11 @@ async def test_options_flow_multipan_uninstall(
|
||||
result["flow_id"], user_input={CONF_DISABLE_MULTI_PAN: True}
|
||||
)
|
||||
|
||||
# Uninstall multiprotocol addon
|
||||
# Finish the flow
|
||||
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,9 +5,6 @@ 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,
|
||||
@@ -29,7 +26,6 @@ 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
|
||||
|
||||
@@ -420,14 +416,13 @@ 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
|
||||
|
||||
# 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
|
||||
# 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
|
||||
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
|
||||
@@ -555,127 +550,3 @@ 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
|
||||
)
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
"""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,6 +19,7 @@ from homeassistant.components.homeassistant_hardware.firmware_config_flow import
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import (
|
||||
CONF_DISABLE_MULTI_PAN,
|
||||
get_flasher_addon_manager,
|
||||
get_multiprotocol_addon_manager,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.util import (
|
||||
@@ -549,11 +550,15 @@ async def test_options_flow_multipan_uninstall(hass: HomeAssistant) -> None:
|
||||
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"
|
||||
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",
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
@@ -561,12 +566,8 @@ async def test_options_flow_multipan_uninstall(hass: HomeAssistant) -> None:
|
||||
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.FirmwareUpdateClient",
|
||||
return_value=mock_fw_client,
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.get_flasher_addon_manager",
|
||||
return_value=mock_flasher_manager,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio",
|
||||
@@ -595,15 +596,11 @@ async def test_options_flow_multipan_uninstall(hass: HomeAssistant) -> None:
|
||||
result["flow_id"], user_input={CONF_DISABLE_MULTI_PAN: True}
|
||||
)
|
||||
|
||||
# Uninstall multiprotocol addon
|
||||
# Finish the flow
|
||||
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,9 +6,6 @@ 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,
|
||||
@@ -21,7 +18,6 @@ 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
|
||||
@@ -325,157 +321,6 @@ 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"),
|
||||
[
|
||||
@@ -534,10 +379,6 @@ 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()
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
"""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,9 +9,7 @@ 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, async_fire_mqtt_message, snapshot_platform
|
||||
|
||||
_TOPIC_WEATHER_STATE = "cloudapp/QBUSMQTTGW/UL1/UL60/state"
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
|
||||
async def test_sensor(
|
||||
@@ -47,33 +45,3 @@ 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=["2", "3", "5", "8"],
|
||||
other_states=["0", "1", "9", "10"],
|
||||
target_states=["3", "5"],
|
||||
other_states=["0", "1", "2", "8", "10"],
|
||||
),
|
||||
]
|
||||
|
||||
@@ -203,8 +203,8 @@ def parametrize_incomplete_condition_states_all(
|
||||
"value_max": {"number": 8},
|
||||
}
|
||||
},
|
||||
target_states=["2", "3", "5", "8"],
|
||||
other_states=["0", "1", "9", "10"],
|
||||
target_states=["3", "5"],
|
||||
other_states=["0", "1", "2", "8", "10"],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@@ -5637,128 +5637,6 @@
|
||||
'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,8 +3270,7 @@ 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),
|
||||
# between (range) — limits are inclusive, so a value exactly equal
|
||||
# to either bound is treated as "inside" and matches
|
||||
# above and below (range)
|
||||
(
|
||||
{
|
||||
"threshold": {
|
||||
@@ -3292,7 +3291,7 @@ async def _setup_numerical_condition(
|
||||
}
|
||||
},
|
||||
"20",
|
||||
True,
|
||||
False,
|
||||
),
|
||||
(
|
||||
{
|
||||
@@ -3303,7 +3302,7 @@ async def _setup_numerical_condition(
|
||||
}
|
||||
},
|
||||
"80",
|
||||
True,
|
||||
False,
|
||||
),
|
||||
(
|
||||
{
|
||||
@@ -3327,9 +3326,8 @@ async def _setup_numerical_condition(
|
||||
"90",
|
||||
False,
|
||||
),
|
||||
# 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
|
||||
# outside (inverse of between) — limits are non-inclusive, so a value
|
||||
# equal to either bound is treated as "not inside" and matches
|
||||
(
|
||||
{
|
||||
"threshold": {
|
||||
@@ -3350,7 +3348,7 @@ async def _setup_numerical_condition(
|
||||
}
|
||||
},
|
||||
"20",
|
||||
False,
|
||||
True,
|
||||
),
|
||||
(
|
||||
{
|
||||
@@ -3361,7 +3359,7 @@ async def _setup_numerical_condition(
|
||||
}
|
||||
},
|
||||
"80",
|
||||
False,
|
||||
True,
|
||||
),
|
||||
(
|
||||
{
|
||||
|
||||
@@ -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 inclusive
|
||||
# between — both limits are non-inclusive
|
||||
(
|
||||
{
|
||||
"threshold": {
|
||||
@@ -1969,7 +1969,7 @@ async def test_numerical_state_attribute_changed_error_handling(
|
||||
}
|
||||
},
|
||||
20,
|
||||
True,
|
||||
False,
|
||||
),
|
||||
(
|
||||
{
|
||||
@@ -1980,7 +1980,7 @@ async def test_numerical_state_attribute_changed_error_handling(
|
||||
}
|
||||
},
|
||||
80,
|
||||
True,
|
||||
False,
|
||||
),
|
||||
(
|
||||
{
|
||||
@@ -2004,8 +2004,7 @@ async def test_numerical_state_attribute_changed_error_handling(
|
||||
90,
|
||||
False,
|
||||
),
|
||||
# outside — values equal to either bound are inside the between range
|
||||
# and therefore do NOT match outside
|
||||
# outside — values equal to either bound are treated as "not inside"
|
||||
(
|
||||
{
|
||||
"threshold": {
|
||||
@@ -2026,7 +2025,7 @@ async def test_numerical_state_attribute_changed_error_handling(
|
||||
}
|
||||
},
|
||||
20,
|
||||
False,
|
||||
True,
|
||||
),
|
||||
(
|
||||
{
|
||||
@@ -2037,7 +2036,7 @@ async def test_numerical_state_attribute_changed_error_handling(
|
||||
}
|
||||
},
|
||||
80,
|
||||
False,
|
||||
True,
|
||||
),
|
||||
(
|
||||
{
|
||||
@@ -3040,7 +3039,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 inclusive
|
||||
# between — both limits are non-inclusive
|
||||
(
|
||||
{
|
||||
"threshold": {
|
||||
@@ -3061,7 +3060,7 @@ async def test_numerical_state_attribute_crossed_threshold_error_handling(
|
||||
}
|
||||
},
|
||||
20,
|
||||
True,
|
||||
False,
|
||||
),
|
||||
(
|
||||
{
|
||||
@@ -3072,7 +3071,7 @@ async def test_numerical_state_attribute_crossed_threshold_error_handling(
|
||||
}
|
||||
},
|
||||
80,
|
||||
True,
|
||||
False,
|
||||
),
|
||||
(
|
||||
{
|
||||
@@ -3096,8 +3095,8 @@ async def test_numerical_state_attribute_crossed_threshold_error_handling(
|
||||
90,
|
||||
False,
|
||||
),
|
||||
# outside — values equal to either bound are inside the between range
|
||||
# and therefore do NOT match outside (no cross from the inside seed)
|
||||
# outside — values equal to either bound are treated as "not inside"
|
||||
# and therefore enter the "outside" range from the inside seed value
|
||||
(
|
||||
{
|
||||
"threshold": {
|
||||
@@ -3118,7 +3117,7 @@ async def test_numerical_state_attribute_crossed_threshold_error_handling(
|
||||
}
|
||||
},
|
||||
20,
|
||||
False,
|
||||
True,
|
||||
),
|
||||
(
|
||||
{
|
||||
@@ -3129,7 +3128,7 @@ async def test_numerical_state_attribute_crossed_threshold_error_handling(
|
||||
}
|
||||
},
|
||||
80,
|
||||
False,
|
||||
True,
|
||||
),
|
||||
(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user