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:
Erik Montnemery 2022-11-16 17:38:07 +01:00 committed by GitHub
parent 607a0e7697
commit aaec464627
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 1329 additions and 9 deletions

View file

@ -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

View file

@ -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"],

View 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

View file

@ -0,0 +1,7 @@
"""Constants for the Homeassistant Hardware integration."""
import logging
LOGGER = logging.getLogger(__package__)
SILABS_MULTIPROTOCOL_ADDON_SLUG = "core_silabs_multiprotocol"

View file

@ -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"
}

View file

@ -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={})

View file

@ -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

View file

@ -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,
)

View file

@ -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"
}

View 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."
}
}
}

View file

@ -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."
}
}
}
}

View file

@ -59,6 +59,7 @@ NO_IOT_CLASS = [
"history",
"homeassistant",
"homeassistant_alerts",
"homeassistant_hardware",
"homeassistant_sky_connect",
"homeassistant_yellow",
"image",

View file

@ -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,

View file

@ -0,0 +1 @@
"""Tests for the Home Assistant Hardware integration."""

View 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

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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"))

View file

@ -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()

View file

@ -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,