Compare commits

..

2 Commits

Author SHA1 Message Date
abmantis 5e4f0e143d Reword 2026-05-27 15:39:51 +01:00
abmantis 218c3ffd11 Add skill instruction on not duplicating entity base class behavior 2026-05-27 15:36:11 +01:00
44 changed files with 474 additions and 1485 deletions
@@ -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
+4 -1
View File
@@ -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": {
+1 -12
View File
@@ -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
-2
View File
@@ -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
-12
View File
@@ -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": {
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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."
+1 -1
View File
@@ -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
View File
@@ -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,
+8 -8
View File
@@ -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"
+1 -33
View File
@@ -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"
+4 -4
View File
@@ -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([
+7 -9
View File
@@ -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,
),
(
{
+13 -14
View File
@@ -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,
),
(
{