diff --git a/CODEOWNERS b/CODEOWNERS index 6ff584963dc..374a3d37a81 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -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 diff --git a/homeassistant/components/hassio/addon_manager.py b/homeassistant/components/hassio/addon_manager.py index ff3e9036018..f240937c7f5 100644 --- a/homeassistant/components/hassio/addon_manager.py +++ b/homeassistant/components/hassio/addon_manager.py @@ -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"], diff --git a/homeassistant/components/homeassistant_hardware/__init__.py b/homeassistant/components/homeassistant_hardware/__init__.py new file mode 100644 index 00000000000..f3a63a7f767 --- /dev/null +++ b/homeassistant/components/homeassistant_hardware/__init__.py @@ -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 diff --git a/homeassistant/components/homeassistant_hardware/const.py b/homeassistant/components/homeassistant_hardware/const.py new file mode 100644 index 00000000000..dd3a254d097 --- /dev/null +++ b/homeassistant/components/homeassistant_hardware/const.py @@ -0,0 +1,7 @@ +"""Constants for the Homeassistant Hardware integration.""" + +import logging + +LOGGER = logging.getLogger(__package__) + +SILABS_MULTIPROTOCOL_ADDON_SLUG = "core_silabs_multiprotocol" diff --git a/homeassistant/components/homeassistant_hardware/manifest.json b/homeassistant/components/homeassistant_hardware/manifest.json new file mode 100644 index 00000000000..32472bcdded --- /dev/null +++ b/homeassistant/components/homeassistant_hardware/manifest.json @@ -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" +} diff --git a/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py b/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py new file mode 100644 index 00000000000..b4dcca0e329 --- /dev/null +++ b/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py @@ -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={}) diff --git a/homeassistant/components/homeassistant_yellow/__init__.py b/homeassistant/components/homeassistant_yellow/__init__.py index e6eaa2f7fce..83a3d9dad32 100644 --- a/homeassistant/components/homeassistant_yellow/__init__.py +++ b/homeassistant/components/homeassistant_yellow/__init__.py @@ -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 diff --git a/homeassistant/components/homeassistant_yellow/config_flow.py b/homeassistant/components/homeassistant_yellow/config_flow.py index 191a28f47a4..48a797a2aab 100644 --- a/homeassistant/components/homeassistant_yellow/config_flow.py +++ b/homeassistant/components/homeassistant_yellow/config_flow.py @@ -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, + ) diff --git a/homeassistant/components/homeassistant_yellow/manifest.json b/homeassistant/components/homeassistant_yellow/manifest.json index 47e6c8e2cd8..ef708e9429a 100644 --- a/homeassistant/components/homeassistant_yellow/manifest.json +++ b/homeassistant/components/homeassistant_yellow/manifest.json @@ -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" } diff --git a/homeassistant/components/homeassistant_yellow/strings.json b/homeassistant/components/homeassistant_yellow/strings.json new file mode 100644 index 00000000000..df1eb780a96 --- /dev/null +++ b/homeassistant/components/homeassistant_yellow/strings.json @@ -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." + } + } +} diff --git a/homeassistant/components/homeassistant_yellow/translations/en.json b/homeassistant/components/homeassistant_yellow/translations/en.json new file mode 100644 index 00000000000..252303ea428 --- /dev/null +++ b/homeassistant/components/homeassistant_yellow/translations/en.json @@ -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." + } + } + } +} \ No newline at end of file diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 874fb069818..b4130de1dee 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -59,6 +59,7 @@ NO_IOT_CLASS = [ "history", "homeassistant", "homeassistant_alerts", + "homeassistant_hardware", "homeassistant_sky_connect", "homeassistant_yellow", "image", diff --git a/tests/components/hassio/test_addon_manager.py b/tests/components/hassio/test_addon_manager.py index ffeecd167e6..7135f1ea646 100644 --- a/tests/components/hassio/test_addon_manager.py +++ b/tests/components/hassio/test_addon_manager.py @@ -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, diff --git a/tests/components/homeassistant_hardware/__init__.py b/tests/components/homeassistant_hardware/__init__.py new file mode 100644 index 00000000000..8f2ab13e5d1 --- /dev/null +++ b/tests/components/homeassistant_hardware/__init__.py @@ -0,0 +1 @@ +"""Tests for the Home Assistant Hardware integration.""" diff --git a/tests/components/homeassistant_hardware/conftest.py b/tests/components/homeassistant_hardware/conftest.py new file mode 100644 index 00000000000..d46241b46e2 --- /dev/null +++ b/tests/components/homeassistant_hardware/conftest.py @@ -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 diff --git a/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py b/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py new file mode 100644 index 00000000000..4b966033857 --- /dev/null +++ b/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py @@ -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" diff --git a/tests/components/homeassistant_yellow/conftest.py b/tests/components/homeassistant_yellow/conftest.py index 52759ba6d89..e5224aea05d 100644 --- a/tests/components/homeassistant_yellow/conftest.py +++ b/tests/components/homeassistant_yellow/conftest.py @@ -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 diff --git a/tests/components/homeassistant_yellow/test_config_flow.py b/tests/components/homeassistant_yellow/test_config_flow.py index e6d5da11806..91f15f792d5 100644 --- a/tests/components/homeassistant_yellow/test_config_flow.py +++ b/tests/components/homeassistant_yellow/test_config_flow.py @@ -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 diff --git a/tests/components/homeassistant_yellow/test_hardware.py b/tests/components/homeassistant_yellow/test_hardware.py index 681c031b2e1..add83fde7c1 100644 --- a/tests/components/homeassistant_yellow/test_hardware.py +++ b/tests/components/homeassistant_yellow/test_hardware.py @@ -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")) diff --git a/tests/components/homeassistant_yellow/test_init.py b/tests/components/homeassistant_yellow/test_init.py index bc36ae3cec2..06fe9465d2d 100644 --- a/tests/components/homeassistant_yellow/test_init.py +++ b/tests/components/homeassistant_yellow/test_init.py @@ -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() diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 74b43e8394a..e5a759ba0a0 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -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,