Yellow firmware selection options flow (#122868)

* Implement Yellow config flow for firmware selection

* Use the probed firmware type when setting up Yellow

* Add translation strings

* Ensure (most) existing `init` tests pass

* Remove multi-PAN setup config flow unit tests

* Get existing config flow unit tests passing

* Add unit tests for uninstalling multi-PAN and such

* Consolidate entity creation for Yellow and clean up steps

* Be explicit with multiple inheritance overrides

* Address review comments
This commit is contained in:
puddly 2024-08-27 17:14:41 -04:00 committed by GitHub
parent 467749eb57
commit 5818e2c2d4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 481 additions and 373 deletions

View File

@ -2,18 +2,24 @@
from __future__ import annotations
import logging
from homeassistant.components.hassio import get_os_info, is_hassio
from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import (
check_multi_pan_addon,
get_zigbee_socket,
multi_pan_addon_using_device,
)
from homeassistant.components.homeassistant_hardware.util import (
ApplicationType,
guess_firmware_type,
)
from homeassistant.config_entries import SOURCE_HARDWARE, ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
from homeassistant.helpers import discovery_flow
from .const import RADIO_DEVICE, ZHA_HW_DISCOVERY_DATA
from .const import FIRMWARE, RADIO_DEVICE, ZHA_HW_DISCOVERY_DATA
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@ -27,34 +33,26 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# The hassio integration has not yet fetched data from the supervisor
raise ConfigEntryNotReady
board: str | None
if (board := os_info.get("board")) is None or board != "yellow":
if os_info.get("board") != "yellow":
# Not running on a Home Assistant Yellow, Home Assistant may have been migrated
hass.async_create_task(hass.config_entries.async_remove(entry.entry_id))
return False
try:
await check_multi_pan_addon(hass)
except HomeAssistantError as err:
raise ConfigEntryNotReady from err
firmware = ApplicationType(entry.data[FIRMWARE])
if not await multi_pan_addon_using_device(hass, RADIO_DEVICE):
hw_discovery_data = ZHA_HW_DISCOVERY_DATA
else:
hw_discovery_data = {
"name": "Yellow Multiprotocol",
"port": {
"path": get_zigbee_socket(),
},
"radio_type": "ezsp",
}
if firmware is ApplicationType.CPC:
try:
await check_multi_pan_addon(hass)
except HomeAssistantError as err:
raise ConfigEntryNotReady from err
discovery_flow.async_create_flow(
hass,
"zha",
context={"source": SOURCE_HARDWARE},
data=hw_discovery_data,
)
if firmware is ApplicationType.EZSP:
discovery_flow.async_create_flow(
hass,
"zha",
context={"source": SOURCE_HARDWARE},
data=ZHA_HW_DISCOVERY_DATA,
)
return True
@ -62,3 +60,39 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
return True
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Migrate old entry."""
_LOGGER.debug(
"Migrating from version %s.%s", config_entry.version, config_entry.minor_version
)
if config_entry.version == 1:
if config_entry.minor_version == 1:
# Add-on startup with type service get started before Core, always (e.g. the
# Multi-Protocol add-on). Probing the firmware would interfere with the add-on,
# so we can't safely probe here. Instead, we must make an educated guess!
firmware_guess = await guess_firmware_type(hass, RADIO_DEVICE)
new_data = {**config_entry.data}
new_data[FIRMWARE] = firmware_guess.firmware_type.value
hass.config_entries.async_update_entry(
config_entry,
data=new_data,
version=1,
minor_version=2,
)
_LOGGER.debug(
"Migration to version %s.%s successful",
config_entry.version,
config_entry.minor_version,
)
return True
# This means the user has downgraded from a future version
return False

View File

@ -2,11 +2,13 @@
from __future__ import annotations
from abc import ABC, abstractmethod
import asyncio
import logging
from typing import Any
from typing import Any, final
import aiohttp
from universal_silabs_flasher.const import ApplicationType
import voluptuous as vol
from homeassistant.components.hassio import (
@ -15,12 +17,25 @@ from homeassistant.components.hassio import (
async_reboot_host,
async_set_yellow_settings,
)
from homeassistant.components.homeassistant_hardware import silabs_multiprotocol_addon
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.components.homeassistant_hardware.firmware_config_flow import (
BaseFirmwareConfigFlow,
BaseFirmwareOptionsFlow,
)
from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import (
OptionsFlowHandler as MultiprotocolOptionsFlowHandler,
SerialPortSettings as MultiprotocolSerialPortSettings,
)
from homeassistant.config_entries import (
SOURCE_HARDWARE,
ConfigEntry,
ConfigFlowResult,
OptionsFlow,
)
from homeassistant.core import callback
from homeassistant.helpers import selector
from homeassistant.helpers import discovery_flow, selector
from .const import DOMAIN, ZHA_HW_DISCOVERY_DATA
from .const import DOMAIN, FIRMWARE, RADIO_DEVICE, ZHA_DOMAIN, ZHA_HW_DISCOVERY_DATA
from .hardware import BOARD_NAME
_LOGGER = logging.getLogger(__name__)
@ -33,18 +48,30 @@ STEP_HW_SETTINGS_SCHEMA = vol.Schema(
)
class HomeAssistantYellowConfigFlow(ConfigFlow, domain=DOMAIN):
class HomeAssistantYellowConfigFlow(BaseFirmwareConfigFlow, domain=DOMAIN):
"""Handle a config flow for Home Assistant Yellow."""
VERSION = 1
MINOR_VERSION = 2
def __init__(self, *args: Any, **kwargs: Any) -> None:
"""Instantiate config flow."""
super().__init__(*args, **kwargs)
self._device = RADIO_DEVICE
@staticmethod
@callback
def async_get_options_flow(
config_entry: ConfigEntry,
) -> HomeAssistantYellowOptionsFlow:
) -> OptionsFlow:
"""Return the options flow."""
return HomeAssistantYellowOptionsFlow(config_entry)
firmware_type = ApplicationType(config_entry.data[FIRMWARE])
if firmware_type is ApplicationType.CPC:
return HomeAssistantYellowMultiPanOptionsFlowHandler(config_entry)
return HomeAssistantYellowOptionsFlowHandler(config_entry)
async def async_step_system(
self, data: dict[str, Any] | None = None
@ -53,30 +80,56 @@ class HomeAssistantYellowConfigFlow(ConfigFlow, domain=DOMAIN):
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
return self.async_create_entry(title="Home Assistant Yellow", data={})
# We do not actually use any portion of `BaseFirmwareConfigFlow` beyond this
await self._probe_firmware_type()
# Kick off ZHA hardware discovery automatically if Zigbee firmware is running
if self._probed_firmware_type is ApplicationType.EZSP:
discovery_flow.async_create_flow(
self.hass,
ZHA_DOMAIN,
context={"source": SOURCE_HARDWARE},
data=ZHA_HW_DISCOVERY_DATA,
)
return self._async_flow_finished()
def _async_flow_finished(self) -> ConfigFlowResult:
"""Create the config entry."""
assert self._probed_firmware_type is not None
return self.async_create_entry(
title=BOARD_NAME,
data={
# Assume the firmware type is EZSP if we cannot probe it
FIRMWARE: (self._probed_firmware_type or ApplicationType.EZSP).value,
},
)
class HomeAssistantYellowOptionsFlow(silabs_multiprotocol_addon.OptionsFlowHandler):
"""Handle an option flow for Home Assistant Yellow."""
class BaseHomeAssistantYellowOptionsFlow(OptionsFlow, ABC):
"""Base Home Assistant Yellow options flow shared between firmware and multi-PAN."""
_hw_settings: dict[str, bool] | None = None
@abstractmethod
async def async_step_main_menu(self, _: None = None) -> ConfigFlowResult:
"""Show the main menu."""
@final
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Manage the options flow."""
return await self.async_step_main_menu()
@final
async def async_step_on_supervisor(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle logic when on Supervisor host."""
return await self.async_step_main_menu()
async def async_step_main_menu(self, _: None = None) -> ConfigFlowResult:
"""Show the main menu."""
return self.async_show_menu(
step_id="main_menu",
menu_options=[
"hardware_settings",
"multipan_settings",
],
)
async def async_step_hardware_settings(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@ -133,18 +186,36 @@ class HomeAssistantYellowOptionsFlow(silabs_multiprotocol_addon.OptionsFlowHandl
"""Reboot later."""
return self.async_create_entry(data={})
class HomeAssistantYellowMultiPanOptionsFlowHandler(
BaseHomeAssistantYellowOptionsFlow, MultiprotocolOptionsFlowHandler
):
"""Handle a multi-PAN options flow for Home Assistant Yellow."""
async def async_step_main_menu(self, _: None = None) -> ConfigFlowResult:
"""Show the main menu."""
return self.async_show_menu(
step_id="main_menu",
menu_options=[
"hardware_settings",
"multipan_settings",
],
)
async def async_step_multipan_settings(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle multipan settings."""
return await super().async_step_on_supervisor(user_input)
return await MultiprotocolOptionsFlowHandler.async_step_on_supervisor(
self, user_input
)
async def _async_serial_port_settings(
self,
) -> silabs_multiprotocol_addon.SerialPortSettings:
) -> MultiprotocolSerialPortSettings:
"""Return the radio serial port settings."""
return silabs_multiprotocol_addon.SerialPortSettings(
device="/dev/ttyAMA1",
return MultiprotocolSerialPortSettings(
device=RADIO_DEVICE,
baudrate="115200",
flow_control=True,
)
@ -163,4 +234,64 @@ class HomeAssistantYellowOptionsFlow(silabs_multiprotocol_addon.OptionsFlowHandl
def _hardware_name(self) -> str:
"""Return the name of the hardware."""
return "Home Assistant Yellow"
return BOARD_NAME
async def async_step_flashing_complete(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Finish flashing and update the config entry."""
self.hass.config_entries.async_update_entry(
entry=self.config_entry,
data={
**self.config_entry.data,
FIRMWARE: ApplicationType.EZSP.value,
},
)
return await super().async_step_flashing_complete(user_input)
class HomeAssistantYellowOptionsFlowHandler(
BaseHomeAssistantYellowOptionsFlow, BaseFirmwareOptionsFlow
):
"""Handle a firmware options flow for Home Assistant Yellow."""
def __init__(self, *args: Any, **kwargs: Any) -> None:
"""Instantiate options flow."""
super().__init__(*args, **kwargs)
self._hardware_name = BOARD_NAME
self._device = RADIO_DEVICE
# Regenerate the translation placeholders
self._get_translation_placeholders()
async def async_step_main_menu(self, _: None = None) -> ConfigFlowResult:
"""Show the main menu."""
return self.async_show_menu(
step_id="main_menu",
menu_options=[
"hardware_settings",
"firmware_settings",
],
)
async def async_step_firmware_settings(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle firmware configuration settings."""
return await super().async_step_pick_firmware()
def _async_flow_finished(self) -> ConfigFlowResult:
"""Create the config entry."""
assert self._probed_firmware_type is not None
self.hass.config_entries.async_update_entry(
entry=self.config_entry,
data={
**self.config_entry.data,
FIRMWARE: self._probed_firmware_type.value,
},
)
return self.async_create_entry(title="", data={})

View File

@ -12,3 +12,6 @@ ZHA_HW_DISCOVERY_DATA = {
},
"radio_type": "efr32",
}
FIRMWARE = "firmware"
ZHA_DOMAIN = "zha"

View File

@ -42,6 +42,7 @@
"main_menu": {
"menu_options": {
"hardware_settings": "[%key:component::homeassistant_yellow::options::step::hardware_settings::title%]",
"firmware_settings": "Switch between Zigbee or Thread firmware.",
"multipan_settings": "Configure IEEE 802.15.4 radio multiprotocol support"
}
},
@ -79,6 +80,46 @@
"start_flasher_addon": {
"title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::start_flasher_addon::title%]",
"description": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::start_flasher_addon::description%]"
},
"pick_firmware": {
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::title%]",
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::description%]",
"menu_options": {
"pick_firmware_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_thread%]",
"pick_firmware_zigbee": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_zigbee%]"
}
},
"install_zigbee_flasher_addon": {
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_zigbee_flasher_addon::title%]",
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_zigbee_flasher_addon::description%]"
},
"run_zigbee_flasher_addon": {
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::run_zigbee_flasher_addon::title%]",
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::run_zigbee_flasher_addon::description%]"
},
"zigbee_flasher_failed": {
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_flasher_failed::title%]",
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_flasher_failed::description%]"
},
"confirm_zigbee": {
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::confirm_zigbee::title%]",
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::confirm_zigbee::description%]"
},
"install_otbr_addon": {
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_otbr_addon::title%]",
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_otbr_addon::description%]"
},
"start_otbr_addon": {
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::start_otbr_addon::title%]",
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::start_otbr_addon::description%]"
},
"otbr_failed": {
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::otbr_failed::title%]",
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::otbr_failed::description%]"
},
"confirm_otbr": {
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::confirm_otbr::title%]",
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::confirm_otbr::description%]"
}
},
"error": {
@ -93,11 +134,19 @@
"not_hassio": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::not_hassio%]",
"read_hw_settings_error": "Failed to read hardware settings",
"write_hw_settings_error": "Failed to write hardware settings",
"zha_migration_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::zha_migration_failed%]"
"zha_migration_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::zha_migration_failed%]",
"not_hassio_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::not_hassio_thread%]",
"otbr_addon_already_running": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_addon_already_running%]",
"zha_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::zha_still_using_stick%]",
"otbr_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_still_using_stick%]"
},
"progress": {
"install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]",
"start_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]"
"start_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]",
"start_otbr_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]",
"install_zigbee_flasher_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_zigbee_flasher_addon%]",
"run_zigbee_flasher_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::run_zigbee_flasher_addon%]",
"uninstall_zigbee_flasher_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::uninstall_zigbee_flasher_addon%]"
}
}
}

View File

@ -6,8 +6,17 @@ from unittest.mock import Mock, patch
import pytest
from homeassistant.components.hassio import DOMAIN as HASSIO_DOMAIN
from homeassistant.components.homeassistant_yellow.const import DOMAIN
from homeassistant.components.zha import DOMAIN as ZHA_DOMAIN
from homeassistant.components.hassio.addon_manager import AddonInfo, AddonState
from homeassistant.components.homeassistant_hardware.firmware_config_flow import (
STEP_PICK_FIRMWARE_ZIGBEE,
)
from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import (
CONF_DISABLE_MULTI_PAN,
get_flasher_addon_manager,
get_multiprotocol_addon_manager,
)
from homeassistant.components.homeassistant_hardware.util import ApplicationType
from homeassistant.components.homeassistant_yellow.const import DOMAIN, RADIO_DEVICE
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.setup import async_setup_component
@ -57,22 +66,28 @@ async def test_config_flow(hass: HomeAssistant) -> None:
mock_integration(hass, MockModule("hassio"))
await async_setup_component(hass, HASSIO_DOMAIN, {})
with patch(
"homeassistant.components.homeassistant_yellow.async_setup_entry",
return_value=True,
) as mock_setup_entry:
with (
patch(
"homeassistant.components.homeassistant_yellow.async_setup_entry",
return_value=True,
) as mock_setup_entry,
patch(
"homeassistant.components.homeassistant_hardware.firmware_config_flow.probe_silabs_firmware_type",
return_value=ApplicationType.EZSP,
),
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "system"}
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "Home Assistant Yellow"
assert result["data"] == {}
assert result["data"] == {"firmware": "ezsp"}
assert result["options"] == {}
assert len(mock_setup_entry.mock_calls) == 1
config_entry = hass.config_entries.async_entries(DOMAIN)[0]
assert config_entry.data == {}
assert config_entry.data == {"firmware": "ezsp"}
assert config_entry.options == {}
assert config_entry.title == "Home Assistant Yellow"
@ -84,10 +99,12 @@ async def test_config_flow_single_entry(hass: HomeAssistant) -> None:
# Setup the config entry
config_entry = MockConfigEntry(
data={},
data={"firmware": ApplicationType.EZSP},
domain=DOMAIN,
options={},
title="Home Assistant Yellow",
version=1,
minor_version=2,
)
config_entry.add_to_hass(hass)
@ -104,165 +121,6 @@ async def test_config_flow_single_entry(hass: HomeAssistant) -> None:
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"))
await async_setup_component(hass, HASSIO_DOMAIN, {})
# Setup the config entry
config_entry = MockConfigEntry(
data={},
domain=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"] is FlowResultType.MENU
with patch(
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio",
side_effect=Mock(return_value=True),
):
result = await hass.config_entries.options.async_configure(
result["flow_id"],
{"next_step_id": "multipan_settings"},
)
assert result["type"] is 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"] is FlowResultType.SHOW_PROGRESS
assert result["step_id"] == "install_addon"
assert result["progress_action"] == "install_addon"
await hass.async_block_till_done()
install_addon.assert_called_once_with(hass, "core_silabs_multiprotocol")
result = await hass.config_entries.options.async_configure(result["flow_id"])
assert result["type"] is FlowResultType.SHOW_PROGRESS
assert result["step_id"] == "start_addon"
set_addon_options.assert_called_once_with(
hass,
"core_silabs_multiprotocol",
{
"options": {
"autoflash_firmware": True,
"device": "/dev/ttyAMA1",
"baudrate": "115200",
"flow_control": True,
}
},
)
await hass.async_block_till_done()
start_addon.assert_called_once_with(hass, "core_silabs_multiprotocol")
result = await hass.config_entries.options.async_configure(result["flow_id"])
assert result["type"] is FlowResultType.CREATE_ENTRY
async def test_option_flow_install_multi_pan_addon_zha(
hass: HomeAssistant,
addon_store_info,
addon_info,
install_addon,
set_addon_options,
start_addon,
) -> None:
"""Test installing the multi pan addon when a zha config entry exists."""
mock_integration(hass, MockModule("hassio"))
await async_setup_component(hass, HASSIO_DOMAIN, {})
# Setup the config entry
config_entry = MockConfigEntry(
data={},
domain=DOMAIN,
options={},
title="Home Assistant Yellow",
)
config_entry.add_to_hass(hass)
zha_config_entry = MockConfigEntry(
data={"device": {"path": "/dev/ttyAMA1"}, "radio_type": "ezsp"},
domain=ZHA_DOMAIN,
options={},
title="Yellow",
)
zha_config_entry.add_to_hass(hass)
result = await hass.config_entries.options.async_init(config_entry.entry_id)
assert result["type"] is FlowResultType.MENU
with patch(
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio",
side_effect=Mock(return_value=True),
):
result = await hass.config_entries.options.async_configure(
result["flow_id"],
{"next_step_id": "multipan_settings"},
)
assert result["type"] is 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"] is FlowResultType.SHOW_PROGRESS
assert result["step_id"] == "install_addon"
assert result["progress_action"] == "install_addon"
await hass.async_block_till_done()
install_addon.assert_called_once_with(hass, "core_silabs_multiprotocol")
result = await hass.config_entries.options.async_configure(result["flow_id"])
assert result["type"] is FlowResultType.SHOW_PROGRESS
assert result["step_id"] == "start_addon"
set_addon_options.assert_called_once_with(
hass,
"core_silabs_multiprotocol",
{
"options": {
"autoflash_firmware": True,
"device": "/dev/ttyAMA1",
"baudrate": "115200",
"flow_control": True,
}
},
)
# Check the ZHA config entry data is updated
assert zha_config_entry.data == {
"device": {
"path": "socket://core-silabs-multiprotocol:9999",
"baudrate": 115200,
"flow_control": None,
},
"radio_type": "ezsp",
}
await hass.async_block_till_done()
start_addon.assert_called_once_with(hass, "core_silabs_multiprotocol")
result = await hass.config_entries.options.async_configure(result["flow_id"])
assert result["type"] is FlowResultType.CREATE_ENTRY
@pytest.mark.parametrize(
("reboot_menu_choice", "reboot_calls"),
[("reboot_now", 1), ("reboot_later", 0)],
@ -281,10 +139,12 @@ async def test_option_flow_led_settings(
# Setup the config entry
config_entry = MockConfigEntry(
data={},
data={"firmware": ApplicationType.EZSP},
domain=DOMAIN,
options={},
title="Home Assistant Yellow",
version=1,
minor_version=2,
)
config_entry.add_to_hass(hass)
@ -327,10 +187,12 @@ async def test_option_flow_led_settings_unchanged(
# Setup the config entry
config_entry = MockConfigEntry(
data={},
data={"firmware": ApplicationType.EZSP},
domain=DOMAIN,
options={},
title="Home Assistant Yellow",
version=1,
minor_version=2,
)
config_entry.add_to_hass(hass)
@ -359,10 +221,12 @@ async def test_option_flow_led_settings_fail_1(hass: HomeAssistant) -> None:
# Setup the config entry
config_entry = MockConfigEntry(
data={},
data={"firmware": ApplicationType.EZSP},
domain=DOMAIN,
options={},
title="Home Assistant Yellow",
version=1,
minor_version=2,
)
config_entry.add_to_hass(hass)
@ -391,10 +255,12 @@ async def test_option_flow_led_settings_fail_2(
# Setup the config entry
config_entry = MockConfigEntry(
data={},
data={"firmware": ApplicationType.EZSP},
domain=DOMAIN,
options={},
title="Home Assistant Yellow",
version=1,
minor_version=2,
)
config_entry.add_to_hass(hass)
@ -418,3 +284,139 @@ async def test_option_flow_led_settings_fail_2(
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "write_hw_settings_error"
async def test_firmware_options_flow(hass: HomeAssistant) -> None:
"""Test the firmware options flow for Yellow."""
mock_integration(hass, MockModule("hassio"))
await async_setup_component(hass, HASSIO_DOMAIN, {})
config_entry = MockConfigEntry(
data={"firmware": ApplicationType.SPINEL},
domain=DOMAIN,
options={},
title="Home Assistant Yellow",
version=1,
minor_version=2,
)
config_entry.add_to_hass(hass)
# First step is confirmation
result = await hass.config_entries.options.async_init(config_entry.entry_id)
assert result["type"] is FlowResultType.MENU
assert result["step_id"] == "main_menu"
assert "firmware_settings" in result["menu_options"]
# Pick firmware settings
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={"next_step_id": "firmware_settings"},
)
assert result["step_id"] == "pick_firmware"
assert result["description_placeholders"]["firmware_type"] == "spinel"
assert result["description_placeholders"]["model"] == "Home Assistant Yellow"
async def mock_async_step_pick_firmware_zigbee(self, data):
return await self.async_step_confirm_zigbee(user_input={})
with patch(
"homeassistant.components.homeassistant_hardware.firmware_config_flow.BaseFirmwareOptionsFlow.async_step_pick_firmware_zigbee",
autospec=True,
side_effect=mock_async_step_pick_firmware_zigbee,
):
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={"next_step_id": STEP_PICK_FIRMWARE_ZIGBEE},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["result"] is True
assert config_entry.data == {
"firmware": "ezsp",
}
async def test_options_flow_multipan_uninstall(hass: HomeAssistant) -> None:
"""Test options flow for when multi-PAN firmware is installed."""
mock_integration(hass, MockModule("hassio"))
await async_setup_component(hass, HASSIO_DOMAIN, {})
config_entry = MockConfigEntry(
data={"firmware": ApplicationType.CPC},
domain=DOMAIN,
options={},
title="Home Assistant Yellow",
version=1,
minor_version=2,
)
config_entry.add_to_hass(hass)
# Multi-PAN addon is running
mock_multipan_manager = Mock(spec_set=await get_multiprotocol_addon_manager(hass))
mock_multipan_manager.async_get_addon_info.return_value = AddonInfo(
available=True,
hostname=None,
options={"device": RADIO_DEVICE},
state=AddonState.RUNNING,
update_available=False,
version="1.0.0",
)
mock_flasher_manager = Mock(spec_set=get_flasher_addon_manager(hass))
mock_flasher_manager.async_get_addon_info.return_value = AddonInfo(
available=True,
hostname=None,
options={},
state=AddonState.NOT_RUNNING,
update_available=False,
version="1.0.0",
)
with (
patch(
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.get_multiprotocol_addon_manager",
return_value=mock_multipan_manager,
),
patch(
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.get_flasher_addon_manager",
return_value=mock_flasher_manager,
),
patch(
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio",
return_value=True,
),
):
result = await hass.config_entries.options.async_init(config_entry.entry_id)
assert result["type"] is FlowResultType.MENU
assert result["step_id"] == "main_menu"
assert "multipan_settings" in result["menu_options"]
# Pick multi-PAN settings
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={"next_step_id": "multipan_settings"},
)
# Pick the uninstall option
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={"next_step_id": "uninstall_addon"},
)
# Check the box
result = await hass.config_entries.options.async_configure(
result["flow_id"], user_input={CONF_DISABLE_MULTI_PAN: True}
)
# Finish the flow
result = await hass.config_entries.options.async_configure(result["flow_id"])
await hass.async_block_till_done(wait_background_tasks=True)
result = await hass.config_entries.options.async_configure(result["flow_id"])
await hass.async_block_till_done(wait_background_tasks=True)
result = await hass.config_entries.options.async_configure(result["flow_id"])
assert result["type"] is FlowResultType.CREATE_ENTRY
# We've reverted the firmware back to Zigbee
assert config_entry.data["firmware"] == "ezsp"

View File

@ -6,10 +6,14 @@ import pytest
from homeassistant.components import zha
from homeassistant.components.hassio import DOMAIN as HASSIO_DOMAIN
from homeassistant.components.hassio.handler import HassioAPIError
from homeassistant.components.homeassistant_hardware.util import (
ApplicationType,
FirmwareGuess,
)
from homeassistant.components.homeassistant_yellow.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry, MockModule, mock_integration
@ -27,10 +31,12 @@ async def test_setup_entry(
# Setup the config entry
config_entry = MockConfigEntry(
data={},
data={"firmware": ApplicationType.EZSP},
domain=DOMAIN,
options={},
title="Home Assistant Yellow",
version=1,
minor_version=2,
)
config_entry.add_to_hass(hass)
with (
@ -42,6 +48,14 @@ async def test_setup_entry(
"homeassistant.components.onboarding.async_is_onboarded",
return_value=onboarded,
),
patch(
"homeassistant.components.homeassistant_yellow.guess_firmware_type",
return_value=FirmwareGuess( # Nothing is setup
is_running=False,
firmware_type=ApplicationType.EZSP,
source="unknown",
),
),
):
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done(wait_background_tasks=True)
@ -74,118 +88,12 @@ async def test_setup_zha(hass: HomeAssistant, addon_store_info) -> None:
# 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(wait_background_tasks=True)
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": "/dev/ttyAMA1",
},
"radio_type": "ezsp",
}
assert config_entry.options == {}
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"))
await async_setup_component(hass, HASSIO_DOMAIN, {})
addon_info.return_value["options"]["device"] = "/dev/ttyAMA1"
# 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(wait_background_tasks=True)
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": None,
"path": "socket://core-silabs-multiprotocol:9999",
},
"radio_type": "ezsp",
}
assert config_entry.options == {}
assert config_entry.title == "Yellow Multiprotocol"
async def test_setup_zha_multipan_other_device(
hass: HomeAssistant, addon_info, addon_running
) -> None:
"""Test zha gets the right config."""
mock_integration(hass, MockModule("hassio"))
await async_setup_component(hass, HASSIO_DOMAIN, {})
addon_info.return_value["options"]["device"] = "/dev/not_yellow_radio"
# Setup the config entry
config_entry = MockConfigEntry(
data={},
data={"firmware": ApplicationType.EZSP},
domain=DOMAIN,
options={},
title="Home Assistant Yellow",
version=1,
minor_version=2,
)
config_entry.add_to_hass(hass)
with (
@ -229,10 +137,12 @@ async def test_setup_entry_no_hassio(hass: HomeAssistant) -> None:
"""Test setup of a config entry without hassio."""
# Setup the config entry
config_entry = MockConfigEntry(
data={},
data={"firmware": ApplicationType.EZSP},
domain=DOMAIN,
options={},
title="Home Assistant Yellow",
version=1,
minor_version=2,
)
config_entry.add_to_hass(hass)
assert len(hass.config_entries.async_entries()) == 1
@ -254,10 +164,12 @@ async def test_setup_entry_wrong_board(hass: HomeAssistant) -> None:
# Setup the config entry
config_entry = MockConfigEntry(
data={},
data={"firmware": ApplicationType.EZSP},
domain=DOMAIN,
options={},
title="Home Assistant Yellow",
version=1,
minor_version=2,
)
config_entry.add_to_hass(hass)
assert len(hass.config_entries.async_entries()) == 1
@ -280,10 +192,12 @@ async def test_setup_entry_wait_hassio(hass: HomeAssistant) -> None:
# Setup the config entry
config_entry = MockConfigEntry(
data={},
data={"firmware": ApplicationType.EZSP},
domain=DOMAIN,
options={},
title="Home Assistant Yellow",
version=1,
minor_version=2,
)
config_entry.add_to_hass(hass)
with patch(
@ -303,14 +217,15 @@ async def test_setup_entry_addon_info_fails(
"""Test setup of a config entry when fetching addon info fails."""
mock_integration(hass, MockModule("hassio"))
await async_setup_component(hass, HASSIO_DOMAIN, {})
addon_store_info.side_effect = HassioAPIError("Boom")
# Setup the config entry
config_entry = MockConfigEntry(
data={},
data={"firmware": ApplicationType.CPC},
domain=DOMAIN,
options={},
title="Home Assistant Yellow",
version=1,
minor_version=2,
)
config_entry.add_to_hass(hass)
with (
@ -319,41 +234,15 @@ async def test_setup_entry_addon_info_fails(
return_value={"board": "yellow"},
),
patch(
"homeassistant.components.onboarding.async_is_onboarded", return_value=False
"homeassistant.components.onboarding.async_is_onboarded",
return_value=False,
),
patch(
"homeassistant.components.homeassistant_yellow.check_multi_pan_addon",
side_effect=HomeAssistantError("Boom"),
),
):
assert not await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is 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"))
await async_setup_component(hass, HASSIO_DOMAIN, {})
# 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 is ConfigEntryState.SETUP_RETRY
start_addon.assert_called_once()