Add support for managing the silabs multiprotocol add-on (#82170)
* Add support for managing the silabs multiprotocol add-on * Fix passing context when starting option flow * Allow unloading a ha yellow config entry * Fix tests * Log data passed to ZHA option flow * Improve ZHA migration logic * Move tests * Improve test coverage * Remove dead code * Drop automatic ZHA migration
This commit is contained in:
parent
607a0e7697
commit
aaec464627
21 changed files with 1329 additions and 9 deletions
|
@ -481,6 +481,8 @@ build.json @home-assistant/supervisor
|
|||
/tests/components/homeassistant/ @home-assistant/core
|
||||
/homeassistant/components/homeassistant_alerts/ @home-assistant/core
|
||||
/tests/components/homeassistant_alerts/ @home-assistant/core
|
||||
/homeassistant/components/homeassistant_hardware/ @home-assistant/core
|
||||
/tests/components/homeassistant_hardware/ @home-assistant/core
|
||||
/homeassistant/components/homeassistant_sky_connect/ @home-assistant/core
|
||||
/tests/components/homeassistant_sky_connect/ @home-assistant/core
|
||||
/homeassistant/components/homeassistant_yellow/ @home-assistant/core
|
||||
|
|
|
@ -70,6 +70,7 @@ def api_error(
|
|||
class AddonInfo:
|
||||
"""Represent the current add-on info state."""
|
||||
|
||||
hostname: str | None
|
||||
options: dict[str, Any]
|
||||
state: AddonState
|
||||
update_available: bool
|
||||
|
@ -143,6 +144,7 @@ class AddonManager:
|
|||
self._logger.debug("Add-on store info: %s", addon_store_info)
|
||||
if not addon_store_info["installed"]:
|
||||
return AddonInfo(
|
||||
hostname=None,
|
||||
options={},
|
||||
state=AddonState.NOT_INSTALLED,
|
||||
update_available=False,
|
||||
|
@ -152,6 +154,7 @@ class AddonManager:
|
|||
addon_info = await async_get_addon_info(self._hass, self.addon_slug)
|
||||
addon_state = self.async_get_addon_state(addon_info)
|
||||
return AddonInfo(
|
||||
hostname=addon_info["hostname"],
|
||||
options=addon_info["options"],
|
||||
state=addon_state,
|
||||
update_available=addon_info["update_available"],
|
||||
|
|
10
homeassistant/components/homeassistant_hardware/__init__.py
Normal file
10
homeassistant/components/homeassistant_hardware/__init__.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
"""The Home Assistant Hardware integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the component."""
|
||||
return True
|
7
homeassistant/components/homeassistant_hardware/const.py
Normal file
7
homeassistant/components/homeassistant_hardware/const.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
"""Constants for the Homeassistant Hardware integration."""
|
||||
|
||||
import logging
|
||||
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
SILABS_MULTIPROTOCOL_ADDON_SLUG = "core_silabs_multiprotocol"
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"domain": "homeassistant_hardware",
|
||||
"name": "Home Assistant Hardware",
|
||||
"documentation": "https://www.home-assistant.io/integrations/homeassistant_hardware",
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
"integration_type": "system"
|
||||
}
|
|
@ -0,0 +1,307 @@
|
|||
"""Manage the Silicon Labs Multiprotocol add-on."""
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import abstractmethod
|
||||
import asyncio
|
||||
import dataclasses
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.hassio import (
|
||||
AddonError,
|
||||
AddonInfo,
|
||||
AddonManager,
|
||||
AddonState,
|
||||
is_hassio,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.data_entry_flow import (
|
||||
AbortFlow,
|
||||
FlowHandler,
|
||||
FlowManager,
|
||||
FlowResult,
|
||||
)
|
||||
from homeassistant.helpers.singleton import singleton
|
||||
|
||||
from .const import LOGGER, SILABS_MULTIPROTOCOL_ADDON_SLUG
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DATA_ADDON_MANAGER = "silabs_multiprotocol_addon_manager"
|
||||
|
||||
ADDON_SETUP_TIMEOUT = 5
|
||||
ADDON_SETUP_TIMEOUT_ROUNDS = 40
|
||||
|
||||
CONF_ADDON_AUTOFLASH_FW = "autoflash_firmware"
|
||||
CONF_ADDON_DEVICE = "device"
|
||||
CONF_ENABLE_MULTI_PAN = "enable_multi_pan"
|
||||
|
||||
|
||||
@singleton(DATA_ADDON_MANAGER)
|
||||
@callback
|
||||
def get_addon_manager(hass: HomeAssistant) -> AddonManager:
|
||||
"""Get the add-on manager."""
|
||||
return AddonManager(
|
||||
hass,
|
||||
LOGGER,
|
||||
"Silicon Labs Multiprotocol",
|
||||
SILABS_MULTIPROTOCOL_ADDON_SLUG,
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class SerialPortSettings:
|
||||
"""Serial port settings."""
|
||||
|
||||
device: str
|
||||
baudrate: str
|
||||
flow_control: bool
|
||||
|
||||
|
||||
def get_zigbee_socket(hass, addon_info: AddonInfo) -> str:
|
||||
"""Return the zigbee socket.
|
||||
|
||||
Raises AddonError on error
|
||||
"""
|
||||
return f"socket://{addon_info.hostname}:9999"
|
||||
|
||||
|
||||
class BaseMultiPanFlow(FlowHandler):
|
||||
"""Support configuring the Silicon Labs Multiprotocol add-on."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Set up flow instance."""
|
||||
# If we install the add-on we should uninstall it on entry remove.
|
||||
self.install_task: asyncio.Task | None = None
|
||||
self.start_task: asyncio.Task | None = None
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def flow_manager(self) -> FlowManager:
|
||||
"""Return the flow manager of the flow."""
|
||||
|
||||
@abstractmethod
|
||||
async def _async_serial_port_settings(self) -> SerialPortSettings:
|
||||
"""Return the radio serial port settings."""
|
||||
|
||||
async def async_step_install_addon(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Install Silicon Labs Multiprotocol add-on."""
|
||||
if not self.install_task:
|
||||
self.install_task = self.hass.async_create_task(self._async_install_addon())
|
||||
return self.async_show_progress(
|
||||
step_id="install_addon", progress_action="install_addon"
|
||||
)
|
||||
|
||||
try:
|
||||
await self.install_task
|
||||
except AddonError as err:
|
||||
self.install_task = None
|
||||
_LOGGER.error(err)
|
||||
return self.async_show_progress_done(next_step_id="install_failed")
|
||||
|
||||
self.install_task = None
|
||||
|
||||
return self.async_show_progress_done(next_step_id="configure_addon")
|
||||
|
||||
async def async_step_install_failed(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Add-on installation failed."""
|
||||
return self.async_abort(reason="addon_install_failed")
|
||||
|
||||
async def async_step_start_addon(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Start Silicon Labs Multiprotocol add-on."""
|
||||
if not self.start_task:
|
||||
self.start_task = self.hass.async_create_task(self._async_start_addon())
|
||||
return self.async_show_progress(
|
||||
step_id="start_addon", progress_action="start_addon"
|
||||
)
|
||||
|
||||
try:
|
||||
await self.start_task
|
||||
except (AddonError, AbortFlow) as err:
|
||||
self.start_task = None
|
||||
_LOGGER.error(err)
|
||||
return self.async_show_progress_done(next_step_id="start_failed")
|
||||
|
||||
self.start_task = None
|
||||
return self.async_show_progress_done(next_step_id="finish_addon_setup")
|
||||
|
||||
async def async_step_start_failed(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Add-on start failed."""
|
||||
return self.async_abort(reason="addon_start_failed")
|
||||
|
||||
async def _async_start_addon(self) -> None:
|
||||
"""Start Silicon Labs Multiprotocol add-on."""
|
||||
addon_manager: AddonManager = get_addon_manager(self.hass)
|
||||
try:
|
||||
await addon_manager.async_schedule_start_addon()
|
||||
finally:
|
||||
# Continue the flow after show progress when the task is done.
|
||||
self.hass.async_create_task(
|
||||
self.flow_manager.async_configure(flow_id=self.flow_id)
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
async def async_step_configure_addon(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Configure the Silicon Labs Multiprotocol add-on."""
|
||||
|
||||
@abstractmethod
|
||||
async def async_step_finish_addon_setup(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Finish setup of the Silicon Labs Multiprotocol add-on."""
|
||||
|
||||
async def _async_get_addon_info(self) -> AddonInfo:
|
||||
"""Return and cache Silicon Labs Multiprotocol add-on info."""
|
||||
addon_manager: AddonManager = get_addon_manager(self.hass)
|
||||
try:
|
||||
addon_info: AddonInfo = await addon_manager.async_get_addon_info()
|
||||
except AddonError as err:
|
||||
_LOGGER.error(err)
|
||||
raise AbortFlow("addon_info_failed") from err
|
||||
|
||||
return addon_info
|
||||
|
||||
async def _async_set_addon_config(self, config: dict) -> None:
|
||||
"""Set Silicon Labs Multiprotocol add-on config."""
|
||||
addon_manager: AddonManager = get_addon_manager(self.hass)
|
||||
try:
|
||||
await addon_manager.async_set_addon_options(config)
|
||||
except AddonError as err:
|
||||
_LOGGER.error(err)
|
||||
raise AbortFlow("addon_set_config_failed") from err
|
||||
|
||||
async def _async_install_addon(self) -> None:
|
||||
"""Install the Silicon Labs Multiprotocol add-on."""
|
||||
addon_manager: AddonManager = get_addon_manager(self.hass)
|
||||
try:
|
||||
await addon_manager.async_schedule_install_addon()
|
||||
finally:
|
||||
# Continue the flow after show progress when the task is done.
|
||||
self.hass.async_create_task(
|
||||
self.flow_manager.async_configure(flow_id=self.flow_id)
|
||||
)
|
||||
|
||||
|
||||
class OptionsFlowHandler(BaseMultiPanFlow, config_entries.OptionsFlow):
|
||||
"""Handle an options flow for the Silicon Labs Multiprotocol add-on."""
|
||||
|
||||
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
|
||||
"""Set up the options flow."""
|
||||
super().__init__()
|
||||
self.config_entry = config_entry
|
||||
self.original_addon_config: dict[str, Any] | None = None
|
||||
self.revert_reason: str | None = None
|
||||
|
||||
@property
|
||||
def flow_manager(self) -> config_entries.OptionsFlowManager:
|
||||
"""Return the correct flow manager."""
|
||||
return self.hass.config_entries.options
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Manage the options."""
|
||||
if not is_hassio(self.hass):
|
||||
return self.async_abort(reason="not_hassio")
|
||||
|
||||
return await self.async_step_on_supervisor()
|
||||
|
||||
async def async_step_on_supervisor(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle logic when on Supervisor host."""
|
||||
addon_info = await self._async_get_addon_info()
|
||||
|
||||
if addon_info.state == AddonState.NOT_INSTALLED:
|
||||
return await self.async_step_addon_not_installed()
|
||||
return await self.async_step_addon_installed()
|
||||
|
||||
async def async_step_addon_not_installed(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle logic when the addon is not yet installed."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="addon_not_installed",
|
||||
data_schema=vol.Schema(
|
||||
{vol.Required(CONF_ENABLE_MULTI_PAN, default=False): bool}
|
||||
),
|
||||
)
|
||||
if not user_input[CONF_ENABLE_MULTI_PAN]:
|
||||
return self.async_create_entry(title="", data={})
|
||||
|
||||
return await self.async_step_install_addon()
|
||||
|
||||
async def async_step_configure_addon(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Configure the Silicon Labs Multiprotocol add-on."""
|
||||
addon_info = await self._async_get_addon_info()
|
||||
|
||||
addon_config = addon_info.options
|
||||
|
||||
serial_port_settings = await self._async_serial_port_settings()
|
||||
new_addon_config = {
|
||||
**addon_config,
|
||||
CONF_ADDON_AUTOFLASH_FW: True,
|
||||
**dataclasses.asdict(serial_port_settings),
|
||||
}
|
||||
|
||||
if new_addon_config != addon_config:
|
||||
# Copy the add-on config to keep the objects separate.
|
||||
self.original_addon_config = dict(addon_config)
|
||||
_LOGGER.debug("Reconfiguring addon with %s", new_addon_config)
|
||||
await self._async_set_addon_config(new_addon_config)
|
||||
|
||||
return await self.async_step_start_addon()
|
||||
|
||||
async def async_step_finish_addon_setup(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Prepare info needed to complete the config entry update."""
|
||||
# Always reload entry after installing the addon.
|
||||
self.hass.async_create_task(
|
||||
self.hass.config_entries.async_reload(self.config_entry.entry_id)
|
||||
)
|
||||
|
||||
return self.async_create_entry(title="", data={})
|
||||
|
||||
async def async_step_addon_installed(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle logic when the addon is already installed."""
|
||||
addon_info = await self._async_get_addon_info()
|
||||
|
||||
serial_device = (await self._async_serial_port_settings()).device
|
||||
if addon_info.options.get(CONF_ADDON_DEVICE) == serial_device:
|
||||
return await self.async_step_show_revert_guide()
|
||||
return await self.async_step_addon_installed_other_device()
|
||||
|
||||
async def async_step_show_revert_guide(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Link to a guide for reverting to Zigbee firmware."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(step_id="show_revert_guide")
|
||||
return self.async_create_entry(title="", data={})
|
||||
|
||||
async def async_step_addon_installed_other_device(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Show dialog explaining the addon is in use by another device."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(step_id="addon_installed_other_device")
|
||||
return self.async_create_entry(title="", data={})
|
|
@ -1,11 +1,25 @@
|
|||
"""The Home Assistant Yellow integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.hassio import get_os_info
|
||||
import logging
|
||||
|
||||
from homeassistant.components.hassio import (
|
||||
AddonError,
|
||||
AddonInfo,
|
||||
AddonManager,
|
||||
AddonState,
|
||||
get_os_info,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import (
|
||||
get_addon_manager,
|
||||
get_zigbee_socket,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up a Home Assistant Yellow config entry."""
|
||||
|
@ -19,13 +33,36 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
hass.async_create_task(hass.config_entries.async_remove(entry.entry_id))
|
||||
return False
|
||||
|
||||
addon_manager: AddonManager = get_addon_manager(hass)
|
||||
try:
|
||||
addon_info: AddonInfo = await addon_manager.async_get_addon_info()
|
||||
except AddonError as err:
|
||||
_LOGGER.error(err)
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
# Start the addon if it's not started
|
||||
if addon_info.state == AddonState.NOT_RUNNING:
|
||||
await addon_manager.async_start_addon()
|
||||
|
||||
if addon_info.state not in (AddonState.NOT_INSTALLED, AddonState.RUNNING):
|
||||
_LOGGER.debug(
|
||||
"Multi pan addon in state %s, delaying yellow config entry setup",
|
||||
addon_info.state,
|
||||
)
|
||||
raise ConfigEntryNotReady
|
||||
|
||||
if addon_info.state == AddonState.NOT_INSTALLED:
|
||||
path = "/dev/ttyAMA1"
|
||||
else:
|
||||
path = get_zigbee_socket(hass, addon_info)
|
||||
|
||||
await hass.config_entries.flow.async_init(
|
||||
"zha",
|
||||
context={"source": "hardware"},
|
||||
data={
|
||||
"name": "Yellow",
|
||||
"port": {
|
||||
"path": "/dev/ttyAMA1",
|
||||
"path": path,
|
||||
"baudrate": 115200,
|
||||
"flow_control": "hardware",
|
||||
},
|
||||
|
@ -34,3 +71,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return True
|
||||
|
|
|
@ -3,7 +3,9 @@ from __future__ import annotations
|
|||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow
|
||||
from homeassistant.components.homeassistant_hardware import silabs_multiprotocol_addon
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigFlow
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
|
||||
from .const import DOMAIN
|
||||
|
@ -14,9 +16,31 @@ class HomeAssistantYellowConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
|
||||
VERSION = 1
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: ConfigEntry,
|
||||
) -> HomeAssistantYellowOptionsFlow:
|
||||
"""Return the options flow."""
|
||||
return HomeAssistantYellowOptionsFlow(config_entry)
|
||||
|
||||
async def async_step_system(self, data: dict[str, Any] | None = None) -> FlowResult:
|
||||
"""Handle the initial step."""
|
||||
if self._async_current_entries():
|
||||
return self.async_abort(reason="single_instance_allowed")
|
||||
|
||||
return self.async_create_entry(title="Home Assistant Yellow", data={})
|
||||
|
||||
|
||||
class HomeAssistantYellowOptionsFlow(silabs_multiprotocol_addon.OptionsFlowHandler):
|
||||
"""Handle an option flow for Home Assistant Yellow."""
|
||||
|
||||
async def _async_serial_port_settings(
|
||||
self,
|
||||
) -> silabs_multiprotocol_addon.SerialPortSettings:
|
||||
"""Return the radio serial port settings."""
|
||||
return silabs_multiprotocol_addon.SerialPortSettings(
|
||||
device="/dev/ttyAMA1",
|
||||
baudrate="115200",
|
||||
flow_control=True,
|
||||
)
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"name": "Home Assistant Yellow",
|
||||
"config_flow": false,
|
||||
"documentation": "https://www.home-assistant.io/integrations/homeassistant_yellow",
|
||||
"dependencies": ["hardware", "hassio"],
|
||||
"dependencies": ["hardware", "hassio", "homeassistant_hardware"],
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
"integration_type": "hardware"
|
||||
}
|
||||
|
|
40
homeassistant/components/homeassistant_yellow/strings.json
Normal file
40
homeassistant/components/homeassistant_yellow/strings.json
Normal file
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"options": {
|
||||
"step": {
|
||||
"addon_not_installed": {
|
||||
"title": "Enable multiprotocol support on the IEEE 802.15.4 radio",
|
||||
"description": "When multiprotocol support is enabled, the Home Assistant Yellow's IEEE 802.15.4 radio can be used for both Zigbee and Thread (used by Matter) at the same time. Note: This is an experimental feature.",
|
||||
"data": {
|
||||
"enable_multi_pan": "Enable multiprotocol support"
|
||||
}
|
||||
},
|
||||
"addon_installed_other_device": {
|
||||
"title": "Multiprotocol support is already enabled for another device"
|
||||
},
|
||||
"install_addon": {
|
||||
"title": "The Silicon Labs Multiprotocol add-on installation has started"
|
||||
},
|
||||
"show_revert_guide": {
|
||||
"title": "Multiprotocol support is enabled for this device",
|
||||
"description": "If you want to change to Zigbee only firmware, please complete the following manual steps:\n\n * Remove the Silicon Labs Multiprotocol addon\n\n * Flash the Zigbee only firmware, follow the guide at https://github.com/NabuCasa/silabs-firmware/wiki/Flash-Silicon-Labs-radio-firmware-manually.\n\n * Reconfigure ZHA to migrate settings to the reflashed radio"
|
||||
},
|
||||
"start_addon": {
|
||||
"title": "The Silicon Labs Multiprotocol add-on is starting."
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"abort": {
|
||||
"addon_info_failed": "Failed to get Silicon Labs Multiprotocol add-on info.",
|
||||
"addon_install_failed": "Failed to install the Silicon Labs Multiprotocol add-on.",
|
||||
"addon_set_config_failed": "Failed to set Silicon Labs Multiprotocol configuration.",
|
||||
"addon_start_failed": "Failed to start the Silicon Labs Multiprotocol add-on.",
|
||||
"not_hassio": "The hardware options can only be configured on HassOS installations."
|
||||
},
|
||||
"progress": {
|
||||
"install_addon": "Please wait while the Silicon Labs Multiprotocol add-on installation finishes. This can take several minutes.",
|
||||
"start_addon": "Please wait while the Silicon Labs Multiprotocol add-on start completes. This may take some seconds."
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"options": {
|
||||
"abort": {
|
||||
"addon_info_failed": "Failed to get Silicon Labs Multiprotocol add-on info.",
|
||||
"addon_install_failed": "Failed to install the Silicon Labs Multiprotocol add-on.",
|
||||
"addon_set_config_failed": "Failed to set Silicon Labs Multiprotocol configuration.",
|
||||
"addon_start_failed": "Failed to start the Silicon Labs Multiprotocol add-on.",
|
||||
"not_hassio": "The hardware options can only be configured on HassOS installations."
|
||||
},
|
||||
"error": {
|
||||
"unknown": "Unexpected error"
|
||||
},
|
||||
"progress": {
|
||||
"install_addon": "Please wait while the Silicon Labs Multiprotocol add-on installation finishes. This can take several minutes.",
|
||||
"start_addon": "Please wait while the Silicon Labs Multiprotocol add-on start completes. This may take some seconds."
|
||||
},
|
||||
"step": {
|
||||
"addon_installed_other_device": {
|
||||
"title": "Multiprotocol support is already enabled for another device"
|
||||
},
|
||||
"addon_not_installed": {
|
||||
"data": {
|
||||
"enable_multi_pan": "Enable multiprotocol support"
|
||||
},
|
||||
"description": "When multiprotocol support is enabled, the Home Assistant Yellow's IEEE 802.15.4 radio can be used for both Zigbee and Thread (used by Matter) at the same time. Note: This is an experimental feature.",
|
||||
"title": "Enable multiprotocol support on the IEEE 802.15.4 radio"
|
||||
},
|
||||
"install_addon": {
|
||||
"title": "The Silicon Labs Multiprotocol add-on installation has started"
|
||||
},
|
||||
"show_revert_guide": {
|
||||
"description": "If you want to change to Zigbee only firmware, please complete the following manual steps:\n\n * Remove the Silicon Labs Multiprotocol addon\n\n * Flash the Zigbee only firmware, follow the guide at https://github.com/NabuCasa/silabs-firmware/wiki/Flash-Silicon-Labs-radio-firmware-manually.\n\n * Reconfigure ZHA to migrate settings to the reflashed radio",
|
||||
"title": "Multiprotocol support is enabled for this device"
|
||||
},
|
||||
"start_addon": {
|
||||
"title": "The Silicon Labs Multiprotocol add-on is starting."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -59,6 +59,7 @@ NO_IOT_CLASS = [
|
|||
"history",
|
||||
"homeassistant",
|
||||
"homeassistant_alerts",
|
||||
"homeassistant_hardware",
|
||||
"homeassistant_sky_connect",
|
||||
"homeassistant_yellow",
|
||||
"image",
|
||||
|
|
|
@ -45,6 +45,7 @@ def mock_addon_installed(
|
|||
"state": "stopped",
|
||||
"version": "1.0.0",
|
||||
}
|
||||
addon_info.return_value["hostname"] = "core-test-addon"
|
||||
addon_info.return_value["state"] = "stopped"
|
||||
addon_info.return_value["version"] = "1.0.0"
|
||||
return addon_info
|
||||
|
@ -80,6 +81,7 @@ def addon_info_fixture() -> Generator[AsyncMock, None, None]:
|
|||
"homeassistant.components.hassio.addon_manager.async_get_addon_info",
|
||||
) as addon_info:
|
||||
addon_info.return_value = {
|
||||
"hostname": None,
|
||||
"options": {},
|
||||
"state": None,
|
||||
"update_available": False,
|
||||
|
@ -220,6 +222,7 @@ async def test_get_addon_info_not_installed(
|
|||
) -> None:
|
||||
"""Test get addon info when addon is not installed.."""
|
||||
assert await addon_manager.async_get_addon_info() == AddonInfo(
|
||||
hostname=None,
|
||||
options={},
|
||||
state=AddonState.NOT_INSTALLED,
|
||||
update_available=False,
|
||||
|
@ -240,6 +243,7 @@ async def test_get_addon_info(
|
|||
"""Test get addon info when addon is installed."""
|
||||
addon_installed.return_value["state"] = addon_info_state
|
||||
assert await addon_manager.async_get_addon_info() == AddonInfo(
|
||||
hostname="core-test-addon",
|
||||
options={},
|
||||
state=addon_state,
|
||||
update_available=False,
|
||||
|
@ -337,6 +341,7 @@ async def test_schedule_install_addon(
|
|||
assert addon_manager.task_in_progress() is True
|
||||
|
||||
assert await addon_manager.async_get_addon_info() == AddonInfo(
|
||||
hostname="core-test-addon",
|
||||
options={},
|
||||
state=AddonState.INSTALLING,
|
||||
update_available=False,
|
||||
|
@ -671,6 +676,7 @@ async def test_schedule_update_addon(
|
|||
assert addon_manager.task_in_progress() is True
|
||||
|
||||
assert await addon_manager.async_get_addon_info() == AddonInfo(
|
||||
hostname="core-test-addon",
|
||||
options={},
|
||||
state=AddonState.UPDATING,
|
||||
update_available=True,
|
||||
|
|
1
tests/components/homeassistant_hardware/__init__.py
Normal file
1
tests/components/homeassistant_hardware/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
"""Tests for the Home Assistant Hardware integration."""
|
108
tests/components/homeassistant_hardware/conftest.py
Normal file
108
tests/components/homeassistant_hardware/conftest.py
Normal file
|
@ -0,0 +1,108 @@
|
|||
"""Test fixtures for the Home Assistant Hardware integration."""
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(name="addon_running")
|
||||
def mock_addon_running(addon_store_info, addon_info):
|
||||
"""Mock add-on already running."""
|
||||
addon_store_info.return_value = {
|
||||
"installed": "1.0.0",
|
||||
"state": "started",
|
||||
"version": "1.0.0",
|
||||
}
|
||||
addon_info.return_value["hostname"] = "core-silabs-multiprotocol"
|
||||
addon_info.return_value["state"] = "started"
|
||||
addon_info.return_value["version"] = "1.0.0"
|
||||
return addon_info
|
||||
|
||||
|
||||
@pytest.fixture(name="addon_installed")
|
||||
def mock_addon_installed(addon_store_info, addon_info):
|
||||
"""Mock add-on already installed but not running."""
|
||||
addon_store_info.return_value = {
|
||||
"installed": "1.0.0",
|
||||
"state": "stopped",
|
||||
"version": "1.0.0",
|
||||
}
|
||||
addon_info.return_value["hostname"] = "core-silabs-multiprotocol"
|
||||
addon_info.return_value["state"] = "stopped"
|
||||
addon_info.return_value["version"] = "1.0.0"
|
||||
return addon_info
|
||||
|
||||
|
||||
@pytest.fixture(name="addon_store_info")
|
||||
def addon_store_info_fixture():
|
||||
"""Mock Supervisor add-on store info."""
|
||||
with patch(
|
||||
"homeassistant.components.hassio.addon_manager.async_get_addon_store_info"
|
||||
) as addon_store_info:
|
||||
addon_store_info.return_value = {
|
||||
"installed": None,
|
||||
"state": None,
|
||||
"version": "1.0.0",
|
||||
}
|
||||
yield addon_store_info
|
||||
|
||||
|
||||
@pytest.fixture(name="addon_info")
|
||||
def addon_info_fixture():
|
||||
"""Mock Supervisor add-on info."""
|
||||
with patch(
|
||||
"homeassistant.components.hassio.addon_manager.async_get_addon_info",
|
||||
) as addon_info:
|
||||
addon_info.return_value = {
|
||||
"hostname": None,
|
||||
"options": {},
|
||||
"state": None,
|
||||
"update_available": False,
|
||||
"version": None,
|
||||
}
|
||||
yield addon_info
|
||||
|
||||
|
||||
@pytest.fixture(name="set_addon_options")
|
||||
def set_addon_options_fixture():
|
||||
"""Mock set add-on options."""
|
||||
with patch(
|
||||
"homeassistant.components.hassio.addon_manager.async_set_addon_options"
|
||||
) as set_options:
|
||||
yield set_options
|
||||
|
||||
|
||||
@pytest.fixture(name="install_addon_side_effect")
|
||||
def install_addon_side_effect_fixture(addon_store_info, addon_info):
|
||||
"""Return the install add-on side effect."""
|
||||
|
||||
async def install_addon(hass, slug):
|
||||
"""Mock install add-on."""
|
||||
addon_store_info.return_value = {
|
||||
"installed": "1.0.0",
|
||||
"state": "stopped",
|
||||
"version": "1.0.0",
|
||||
}
|
||||
addon_info.return_value["hostname"] = "core-silabs-multiprotocol"
|
||||
addon_info.return_value["state"] = "stopped"
|
||||
addon_info.return_value["version"] = "1.0.0"
|
||||
|
||||
return install_addon
|
||||
|
||||
|
||||
@pytest.fixture(name="install_addon")
|
||||
def mock_install_addon(install_addon_side_effect):
|
||||
"""Mock install add-on."""
|
||||
with patch(
|
||||
"homeassistant.components.hassio.addon_manager.async_install_addon",
|
||||
side_effect=install_addon_side_effect,
|
||||
) as install_addon:
|
||||
yield install_addon
|
||||
|
||||
|
||||
@pytest.fixture(name="start_addon")
|
||||
def start_addon_fixture():
|
||||
"""Mock start add-on."""
|
||||
with patch(
|
||||
"homeassistant.components.hassio.addon_manager.async_start_addon"
|
||||
) as start_addon:
|
||||
yield start_addon
|
|
@ -0,0 +1,441 @@
|
|||
"""Test the Home Assistant Yellow config flow."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Generator
|
||||
from typing import Any
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.hassio.handler import HassioAPIError
|
||||
from homeassistant.components.homeassistant_hardware import silabs_multiprotocol_addon
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigFlow
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.data_entry_flow import FlowResult, FlowResultType
|
||||
|
||||
from tests.common import MockConfigEntry, MockModule, mock_integration, mock_platform
|
||||
|
||||
TEST_DOMAIN = "test"
|
||||
|
||||
|
||||
class TestConfigFlow(ConfigFlow, domain=TEST_DOMAIN):
|
||||
"""Handle a config flow for Home Assistant Yellow."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: ConfigEntry,
|
||||
) -> TestOptionsFlow:
|
||||
"""Return the options flow."""
|
||||
return TestOptionsFlow(config_entry)
|
||||
|
||||
async def async_step_system(self, data: dict[str, Any] | None = None) -> FlowResult:
|
||||
"""Handle the initial step."""
|
||||
if self._async_current_entries():
|
||||
return self.async_abort(reason="single_instance_allowed")
|
||||
|
||||
return self.async_create_entry(title="Home Assistant Yellow", data={})
|
||||
|
||||
|
||||
class TestOptionsFlow(silabs_multiprotocol_addon.OptionsFlowHandler):
|
||||
"""Handle an option flow for Home Assistant Yellow."""
|
||||
|
||||
async def _async_serial_port_settings(
|
||||
self,
|
||||
) -> silabs_multiprotocol_addon.SerialPortSettings:
|
||||
"""Return the radio serial port settings."""
|
||||
return silabs_multiprotocol_addon.SerialPortSettings(
|
||||
device="/dev/ttyTEST123",
|
||||
baudrate="115200",
|
||||
flow_control=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def config_flow_handler(
|
||||
hass: HomeAssistant, current_request_with_host: Any
|
||||
) -> Generator[TestConfigFlow, None, None]:
|
||||
"""Fixture for a test config flow."""
|
||||
mock_platform(hass, f"{TEST_DOMAIN}.config_flow")
|
||||
with patch.dict(config_entries.HANDLERS, {TEST_DOMAIN: TestConfigFlow}):
|
||||
yield TestConfigFlow
|
||||
|
||||
|
||||
async def test_option_flow_install_multi_pan_addon(
|
||||
hass: HomeAssistant,
|
||||
addon_store_info,
|
||||
addon_info,
|
||||
install_addon,
|
||||
set_addon_options,
|
||||
start_addon,
|
||||
) -> None:
|
||||
"""Test installing the multi pan addon."""
|
||||
mock_integration(hass, MockModule("hassio"))
|
||||
|
||||
# Setup the config entry
|
||||
config_entry = MockConfigEntry(
|
||||
data={},
|
||||
domain=TEST_DOMAIN,
|
||||
options={},
|
||||
title="Home Assistant Yellow",
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio",
|
||||
side_effect=Mock(return_value=True),
|
||||
):
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "addon_not_installed"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
"enable_multi_pan": True,
|
||||
},
|
||||
)
|
||||
assert result["type"] == FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "install_addon"
|
||||
assert result["progress_action"] == "install_addon"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] == FlowResultType.SHOW_PROGRESS_DONE
|
||||
assert result["step_id"] == "configure_addon"
|
||||
install_addon.assert_called_once_with(hass, "core_silabs_multiprotocol")
|
||||
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] == FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "start_addon"
|
||||
set_addon_options.assert_called_once_with(
|
||||
hass,
|
||||
"core_silabs_multiprotocol",
|
||||
{
|
||||
"options": {
|
||||
"autoflash_firmware": True,
|
||||
"device": "/dev/ttyTEST123",
|
||||
"baudrate": "115200",
|
||||
"flow_control": True,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] == FlowResultType.SHOW_PROGRESS_DONE
|
||||
assert result["step_id"] == "finish_addon_setup"
|
||||
start_addon.assert_called_once_with(hass, "core_silabs_multiprotocol")
|
||||
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
|
||||
|
||||
async def test_option_flow_non_hassio(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test installing the multi pan addon."""
|
||||
mock_integration(hass, MockModule("hassio"))
|
||||
|
||||
# Setup the config entry
|
||||
config_entry = MockConfigEntry(
|
||||
data={},
|
||||
domain=TEST_DOMAIN,
|
||||
options={},
|
||||
title="Home Assistant Yellow",
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "not_hassio"
|
||||
|
||||
|
||||
async def test_option_flow_addon_installed_other_device(
|
||||
hass: HomeAssistant,
|
||||
addon_store_info,
|
||||
addon_installed,
|
||||
) -> None:
|
||||
"""Test installing the multi pan addon."""
|
||||
mock_integration(hass, MockModule("hassio"))
|
||||
|
||||
# Setup the config entry
|
||||
config_entry = MockConfigEntry(
|
||||
data={},
|
||||
domain=TEST_DOMAIN,
|
||||
options={},
|
||||
title="Home Assistant Yellow",
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio",
|
||||
side_effect=Mock(return_value=True),
|
||||
):
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "addon_installed_other_device"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"], {})
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
|
||||
|
||||
async def test_option_flow_addon_installed_same_device(
|
||||
hass: HomeAssistant,
|
||||
addon_info,
|
||||
addon_store_info,
|
||||
addon_installed,
|
||||
) -> None:
|
||||
"""Test installing the multi pan addon."""
|
||||
mock_integration(hass, MockModule("hassio"))
|
||||
addon_info.return_value["options"]["device"] = "/dev/ttyTEST123"
|
||||
|
||||
# Setup the config entry
|
||||
config_entry = MockConfigEntry(
|
||||
data={},
|
||||
domain=TEST_DOMAIN,
|
||||
options={},
|
||||
title="Home Assistant Yellow",
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio",
|
||||
side_effect=Mock(return_value=True),
|
||||
):
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "show_revert_guide"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"], {})
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
|
||||
|
||||
async def test_option_flow_do_not_install_multi_pan_addon(
|
||||
hass: HomeAssistant,
|
||||
addon_info,
|
||||
addon_store_info,
|
||||
) -> None:
|
||||
"""Test installing the multi pan addon."""
|
||||
mock_integration(hass, MockModule("hassio"))
|
||||
|
||||
# Setup the config entry
|
||||
config_entry = MockConfigEntry(
|
||||
data={},
|
||||
domain=TEST_DOMAIN,
|
||||
options={},
|
||||
title="Home Assistant Yellow",
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio",
|
||||
side_effect=Mock(return_value=True),
|
||||
):
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "addon_not_installed"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
"enable_multi_pan": False,
|
||||
},
|
||||
)
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
|
||||
|
||||
async def test_option_flow_install_multi_pan_addon_install_fails(
|
||||
hass: HomeAssistant,
|
||||
addon_store_info,
|
||||
addon_info,
|
||||
install_addon,
|
||||
set_addon_options,
|
||||
start_addon,
|
||||
) -> None:
|
||||
"""Test installing the multi pan addon."""
|
||||
mock_integration(hass, MockModule("hassio"))
|
||||
install_addon.side_effect = HassioAPIError("Boom")
|
||||
|
||||
# Setup the config entry
|
||||
config_entry = MockConfigEntry(
|
||||
data={},
|
||||
domain=TEST_DOMAIN,
|
||||
options={},
|
||||
title="Home Assistant Yellow",
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio",
|
||||
side_effect=Mock(return_value=True),
|
||||
):
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "addon_not_installed"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
"enable_multi_pan": True,
|
||||
},
|
||||
)
|
||||
assert result["type"] == FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "install_addon"
|
||||
assert result["progress_action"] == "install_addon"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] == FlowResultType.SHOW_PROGRESS_DONE
|
||||
assert result["step_id"] == "install_failed"
|
||||
install_addon.assert_called_once_with(hass, "core_silabs_multiprotocol")
|
||||
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "addon_install_failed"
|
||||
|
||||
|
||||
async def test_option_flow_install_multi_pan_addon_start_fails(
|
||||
hass: HomeAssistant,
|
||||
addon_store_info,
|
||||
addon_info,
|
||||
install_addon,
|
||||
set_addon_options,
|
||||
start_addon,
|
||||
) -> None:
|
||||
"""Test installing the multi pan addon."""
|
||||
mock_integration(hass, MockModule("hassio"))
|
||||
start_addon.side_effect = HassioAPIError("Boom")
|
||||
|
||||
# Setup the config entry
|
||||
config_entry = MockConfigEntry(
|
||||
data={},
|
||||
domain=TEST_DOMAIN,
|
||||
options={},
|
||||
title="Home Assistant Yellow",
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio",
|
||||
side_effect=Mock(return_value=True),
|
||||
):
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "addon_not_installed"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
"enable_multi_pan": True,
|
||||
},
|
||||
)
|
||||
assert result["type"] == FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "install_addon"
|
||||
assert result["progress_action"] == "install_addon"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] == FlowResultType.SHOW_PROGRESS_DONE
|
||||
assert result["step_id"] == "configure_addon"
|
||||
install_addon.assert_called_once_with(hass, "core_silabs_multiprotocol")
|
||||
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] == FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "start_addon"
|
||||
set_addon_options.assert_called_once_with(
|
||||
hass,
|
||||
"core_silabs_multiprotocol",
|
||||
{
|
||||
"options": {
|
||||
"autoflash_firmware": True,
|
||||
"device": "/dev/ttyTEST123",
|
||||
"baudrate": "115200",
|
||||
"flow_control": True,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] == FlowResultType.SHOW_PROGRESS_DONE
|
||||
assert result["step_id"] == "start_failed"
|
||||
start_addon.assert_called_once_with(hass, "core_silabs_multiprotocol")
|
||||
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "addon_start_failed"
|
||||
|
||||
|
||||
async def test_option_flow_install_multi_pan_addon_set_options_fails(
|
||||
hass: HomeAssistant,
|
||||
addon_store_info,
|
||||
addon_info,
|
||||
install_addon,
|
||||
set_addon_options,
|
||||
start_addon,
|
||||
) -> None:
|
||||
"""Test installing the multi pan addon."""
|
||||
mock_integration(hass, MockModule("hassio"))
|
||||
set_addon_options.side_effect = HassioAPIError("Boom")
|
||||
|
||||
# Setup the config entry
|
||||
config_entry = MockConfigEntry(
|
||||
data={},
|
||||
domain=TEST_DOMAIN,
|
||||
options={},
|
||||
title="Home Assistant Yellow",
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio",
|
||||
side_effect=Mock(return_value=True),
|
||||
):
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "addon_not_installed"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
"enable_multi_pan": True,
|
||||
},
|
||||
)
|
||||
assert result["type"] == FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "install_addon"
|
||||
assert result["progress_action"] == "install_addon"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] == FlowResultType.SHOW_PROGRESS_DONE
|
||||
assert result["step_id"] == "configure_addon"
|
||||
install_addon.assert_called_once_with(hass, "core_silabs_multiprotocol")
|
||||
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "addon_set_config_failed"
|
||||
|
||||
|
||||
async def test_option_flow_addon_info_fails(
|
||||
hass: HomeAssistant,
|
||||
addon_store_info,
|
||||
addon_info,
|
||||
) -> None:
|
||||
"""Test installing the multi pan addon."""
|
||||
mock_integration(hass, MockModule("hassio"))
|
||||
addon_store_info.side_effect = HassioAPIError("Boom")
|
||||
|
||||
# Setup the config entry
|
||||
config_entry = MockConfigEntry(
|
||||
data={},
|
||||
domain=TEST_DOMAIN,
|
||||
options={},
|
||||
title="Home Assistant Yellow",
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio",
|
||||
side_effect=Mock(return_value=True),
|
||||
):
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "addon_info_failed"
|
|
@ -27,3 +27,107 @@ def mock_zha_config_flow_setup() -> Generator[None, None, None]:
|
|||
return_value=True,
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(name="addon_running")
|
||||
def mock_addon_running(addon_store_info, addon_info):
|
||||
"""Mock add-on already running."""
|
||||
addon_store_info.return_value = {
|
||||
"installed": "1.0.0",
|
||||
"state": "started",
|
||||
"version": "1.0.0",
|
||||
}
|
||||
addon_info.return_value["hostname"] = "core-silabs-multiprotocol"
|
||||
addon_info.return_value["state"] = "started"
|
||||
addon_info.return_value["version"] = "1.0.0"
|
||||
return addon_info
|
||||
|
||||
|
||||
@pytest.fixture(name="addon_installed")
|
||||
def mock_addon_installed(addon_store_info, addon_info):
|
||||
"""Mock add-on already installed but not running."""
|
||||
addon_store_info.return_value = {
|
||||
"installed": "1.0.0",
|
||||
"state": "stopped",
|
||||
"version": "1.0.0",
|
||||
}
|
||||
addon_info.return_value["hostname"] = "core-silabs-multiprotocol"
|
||||
addon_info.return_value["state"] = "stopped"
|
||||
addon_info.return_value["version"] = "1.0.0"
|
||||
return addon_info
|
||||
|
||||
|
||||
@pytest.fixture(name="addon_store_info")
|
||||
def addon_store_info_fixture():
|
||||
"""Mock Supervisor add-on store info."""
|
||||
with patch(
|
||||
"homeassistant.components.hassio.addon_manager.async_get_addon_store_info"
|
||||
) as addon_store_info:
|
||||
addon_store_info.return_value = {
|
||||
"installed": None,
|
||||
"state": None,
|
||||
"version": "1.0.0",
|
||||
}
|
||||
yield addon_store_info
|
||||
|
||||
|
||||
@pytest.fixture(name="addon_info")
|
||||
def addon_info_fixture():
|
||||
"""Mock Supervisor add-on info."""
|
||||
with patch(
|
||||
"homeassistant.components.hassio.addon_manager.async_get_addon_info",
|
||||
) as addon_info:
|
||||
addon_info.return_value = {
|
||||
"hostname": None,
|
||||
"options": {},
|
||||
"state": None,
|
||||
"update_available": False,
|
||||
"version": None,
|
||||
}
|
||||
yield addon_info
|
||||
|
||||
|
||||
@pytest.fixture(name="set_addon_options")
|
||||
def set_addon_options_fixture():
|
||||
"""Mock set add-on options."""
|
||||
with patch(
|
||||
"homeassistant.components.hassio.addon_manager.async_set_addon_options"
|
||||
) as set_options:
|
||||
yield set_options
|
||||
|
||||
|
||||
@pytest.fixture(name="install_addon_side_effect")
|
||||
def install_addon_side_effect_fixture(addon_store_info, addon_info):
|
||||
"""Return the install add-on side effect."""
|
||||
|
||||
async def install_addon(hass, slug):
|
||||
"""Mock install add-on."""
|
||||
addon_store_info.return_value = {
|
||||
"installed": "1.0.0",
|
||||
"state": "stopped",
|
||||
"version": "1.0.0",
|
||||
}
|
||||
addon_info.return_value["hostname"] = "core-silabs-multiprotocol"
|
||||
addon_info.return_value["state"] = "stopped"
|
||||
addon_info.return_value["version"] = "1.0.0"
|
||||
|
||||
return install_addon
|
||||
|
||||
|
||||
@pytest.fixture(name="install_addon")
|
||||
def mock_install_addon(install_addon_side_effect):
|
||||
"""Mock install add-on."""
|
||||
with patch(
|
||||
"homeassistant.components.hassio.addon_manager.async_install_addon",
|
||||
side_effect=install_addon_side_effect,
|
||||
) as install_addon:
|
||||
yield install_addon
|
||||
|
||||
|
||||
@pytest.fixture(name="start_addon")
|
||||
def start_addon_fixture():
|
||||
"""Mock start add-on."""
|
||||
with patch(
|
||||
"homeassistant.components.hassio.addon_manager.async_start_addon"
|
||||
) as start_addon:
|
||||
yield start_addon
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
"""Test the Home Assistant Yellow config flow."""
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from homeassistant.components.homeassistant_yellow.const import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
@ -56,3 +56,71 @@ async def test_config_flow_single_entry(hass: HomeAssistant) -> None:
|
|||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "single_instance_allowed"
|
||||
mock_setup_entry.assert_not_called()
|
||||
|
||||
|
||||
async def test_option_flow_install_multi_pan_addon(
|
||||
hass: HomeAssistant,
|
||||
addon_store_info,
|
||||
addon_info,
|
||||
install_addon,
|
||||
set_addon_options,
|
||||
start_addon,
|
||||
) -> None:
|
||||
"""Test installing the multi pan addon."""
|
||||
mock_integration(hass, MockModule("hassio"))
|
||||
|
||||
# Setup the config entry
|
||||
config_entry = MockConfigEntry(
|
||||
data={},
|
||||
domain=DOMAIN,
|
||||
options={},
|
||||
title="Home Assistant Yellow",
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio",
|
||||
side_effect=Mock(return_value=True),
|
||||
):
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "addon_not_installed"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
"enable_multi_pan": True,
|
||||
},
|
||||
)
|
||||
assert result["type"] == FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "install_addon"
|
||||
assert result["progress_action"] == "install_addon"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] == FlowResultType.SHOW_PROGRESS_DONE
|
||||
assert result["step_id"] == "configure_addon"
|
||||
install_addon.assert_called_once_with(hass, "core_silabs_multiprotocol")
|
||||
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] == FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "start_addon"
|
||||
set_addon_options.assert_called_once_with(
|
||||
hass,
|
||||
"core_silabs_multiprotocol",
|
||||
{
|
||||
"options": {
|
||||
"autoflash_firmware": True,
|
||||
"device": "/dev/ttyAMA1",
|
||||
"baudrate": "115200",
|
||||
"flow_control": True,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] == FlowResultType.SHOW_PROGRESS_DONE
|
||||
assert result["step_id"] == "finish_addon_setup"
|
||||
start_addon.assert_called_once_with(hass, "core_silabs_multiprotocol")
|
||||
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
|
|
|
@ -9,7 +9,9 @@ from homeassistant.core import HomeAssistant
|
|||
from tests.common import MockConfigEntry, MockModule, mock_integration
|
||||
|
||||
|
||||
async def test_hardware_info(hass: HomeAssistant, hass_ws_client) -> None:
|
||||
async def test_hardware_info(
|
||||
hass: HomeAssistant, hass_ws_client, addon_store_info
|
||||
) -> None:
|
||||
"""Test we can get the board info."""
|
||||
mock_integration(hass, MockModule("hassio"))
|
||||
|
||||
|
@ -58,7 +60,9 @@ async def test_hardware_info(hass: HomeAssistant, hass_ws_client) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("os_info", [None, {"board": None}, {"board": "other"}])
|
||||
async def test_hardware_info_fail(hass: HomeAssistant, hass_ws_client, os_info) -> None:
|
||||
async def test_hardware_info_fail(
|
||||
hass: HomeAssistant, hass_ws_client, os_info, addon_store_info
|
||||
) -> None:
|
||||
"""Test async_info raises if os_info is not as expected."""
|
||||
mock_integration(hass, MockModule("hassio"))
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ from unittest.mock import patch
|
|||
import pytest
|
||||
|
||||
from homeassistant.components import zha
|
||||
from homeassistant.components.hassio.handler import HassioAPIError
|
||||
from homeassistant.components.homeassistant_yellow.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
@ -15,7 +16,7 @@ from tests.common import MockConfigEntry, MockModule, mock_integration
|
|||
"onboarded, num_entries, num_flows", ((False, 1, 0), (True, 0, 1))
|
||||
)
|
||||
async def test_setup_entry(
|
||||
hass: HomeAssistant, onboarded, num_entries, num_flows
|
||||
hass: HomeAssistant, onboarded, num_entries, num_flows, addon_store_info
|
||||
) -> None:
|
||||
"""Test setup of a config entry, including setup of zha."""
|
||||
mock_integration(hass, MockModule("hassio"))
|
||||
|
@ -53,8 +54,11 @@ async def test_setup_entry(
|
|||
assert len(hass.config_entries.flow.async_progress_by_handler("zha")) == num_flows
|
||||
assert len(hass.config_entries.async_entries("zha")) == num_entries
|
||||
|
||||
# Test unloading the config entry
|
||||
assert await hass.config_entries.async_unload(config_entry.entry_id)
|
||||
|
||||
async def test_setup_zha(hass: HomeAssistant) -> None:
|
||||
|
||||
async def test_setup_zha(hass: HomeAssistant, addon_store_info) -> None:
|
||||
"""Test zha gets the right config."""
|
||||
mock_integration(hass, MockModule("hassio"))
|
||||
|
||||
|
@ -100,6 +104,54 @@ async def test_setup_zha(hass: HomeAssistant) -> None:
|
|||
assert config_entry.title == "Yellow"
|
||||
|
||||
|
||||
async def test_setup_zha_multipan(
|
||||
hass: HomeAssistant, addon_info, addon_running
|
||||
) -> None:
|
||||
"""Test zha gets the right config."""
|
||||
mock_integration(hass, MockModule("hassio"))
|
||||
|
||||
# Setup the config entry
|
||||
config_entry = MockConfigEntry(
|
||||
data={},
|
||||
domain=DOMAIN,
|
||||
options={},
|
||||
title="Home Assistant Yellow",
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
with patch(
|
||||
"homeassistant.components.homeassistant_yellow.get_os_info",
|
||||
return_value={"board": "yellow"},
|
||||
) as mock_get_os_info, patch(
|
||||
"homeassistant.components.onboarding.async_is_onboarded", return_value=False
|
||||
):
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert len(mock_get_os_info.mock_calls) == 1
|
||||
|
||||
# Finish setting up ZHA
|
||||
zha_flows = hass.config_entries.flow.async_progress_by_handler("zha")
|
||||
assert len(zha_flows) == 1
|
||||
assert zha_flows[0]["step_id"] == "choose_formation_strategy"
|
||||
|
||||
await hass.config_entries.flow.async_configure(
|
||||
zha_flows[0]["flow_id"],
|
||||
user_input={"next_step_id": zha.config_flow.FORMATION_REUSE_SETTINGS},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
config_entry = hass.config_entries.async_entries("zha")[0]
|
||||
assert config_entry.data == {
|
||||
"device": {
|
||||
"baudrate": 115200,
|
||||
"flow_control": "hardware",
|
||||
"path": "socket://core-silabs-multiprotocol:9999",
|
||||
},
|
||||
"radio_type": "ezsp",
|
||||
}
|
||||
assert config_entry.options == {}
|
||||
assert config_entry.title == "Yellow"
|
||||
|
||||
|
||||
async def test_setup_entry_wrong_board(hass: HomeAssistant) -> None:
|
||||
"""Test setup of a config entry with wrong board type."""
|
||||
mock_integration(hass, MockModule("hassio"))
|
||||
|
@ -141,3 +193,55 @@ async def test_setup_entry_wait_hassio(hass: HomeAssistant) -> None:
|
|||
await hass.async_block_till_done()
|
||||
assert len(mock_get_os_info.mock_calls) == 1
|
||||
assert config_entry.state == ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_setup_entry_addon_info_fails(
|
||||
hass: HomeAssistant, addon_store_info
|
||||
) -> None:
|
||||
"""Test setup of a config entry when fetching addon info fails."""
|
||||
mock_integration(hass, MockModule("hassio"))
|
||||
addon_store_info.side_effect = HassioAPIError("Boom")
|
||||
|
||||
# Setup the config entry
|
||||
config_entry = MockConfigEntry(
|
||||
data={},
|
||||
domain=DOMAIN,
|
||||
options={},
|
||||
title="Home Assistant Yellow",
|
||||
)
|
||||
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
|
||||
):
|
||||
assert not await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert config_entry.state == ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_setup_entry_addon_not_running(
|
||||
hass: HomeAssistant, addon_installed, start_addon
|
||||
) -> None:
|
||||
"""Test the addon is started if it is not running."""
|
||||
mock_integration(hass, MockModule("hassio"))
|
||||
|
||||
# Setup the config entry
|
||||
config_entry = MockConfigEntry(
|
||||
data={},
|
||||
domain=DOMAIN,
|
||||
options={},
|
||||
title="Home Assistant Yellow",
|
||||
)
|
||||
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
|
||||
):
|
||||
assert not await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert config_entry.state == ConfigEntryState.SETUP_RETRY
|
||||
start_addon.assert_called_once()
|
||||
|
|
|
@ -30,6 +30,7 @@ def mock_addon_info(addon_info_side_effect):
|
|||
side_effect=addon_info_side_effect,
|
||||
) as addon_info:
|
||||
addon_info.return_value = {
|
||||
"hostname": None,
|
||||
"options": {},
|
||||
"state": None,
|
||||
"update_available": False,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue