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 from __future__ import annotations
import logging
from homeassistant.components.hassio import get_os_info, is_hassio from homeassistant.components.hassio import get_os_info, is_hassio
from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import ( from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import (
check_multi_pan_addon, 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.config_entries import SOURCE_HARDWARE, ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
from homeassistant.helpers import discovery_flow 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: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@ -27,33 +33,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# The hassio integration has not yet fetched data from the supervisor # The hassio integration has not yet fetched data from the supervisor
raise ConfigEntryNotReady raise ConfigEntryNotReady
board: str | None if os_info.get("board") != "yellow":
if (board := os_info.get("board")) is None or board != "yellow":
# Not running on a Home Assistant Yellow, Home Assistant may have been migrated # 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)) hass.async_create_task(hass.config_entries.async_remove(entry.entry_id))
return False return False
firmware = ApplicationType(entry.data[FIRMWARE])
if firmware is ApplicationType.CPC:
try: try:
await check_multi_pan_addon(hass) await check_multi_pan_addon(hass)
except HomeAssistantError as err: except HomeAssistantError as err:
raise ConfigEntryNotReady from err raise ConfigEntryNotReady from err
if not await multi_pan_addon_using_device(hass, RADIO_DEVICE): if firmware is ApplicationType.EZSP:
hw_discovery_data = ZHA_HW_DISCOVERY_DATA
else:
hw_discovery_data = {
"name": "Yellow Multiprotocol",
"port": {
"path": get_zigbee_socket(),
},
"radio_type": "ezsp",
}
discovery_flow.async_create_flow( discovery_flow.async_create_flow(
hass, hass,
"zha", "zha",
context={"source": SOURCE_HARDWARE}, context={"source": SOURCE_HARDWARE},
data=hw_discovery_data, data=ZHA_HW_DISCOVERY_DATA,
) )
return True 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: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
return True 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 __future__ import annotations
from abc import ABC, abstractmethod
import asyncio import asyncio
import logging import logging
from typing import Any from typing import Any, final
import aiohttp import aiohttp
from universal_silabs_flasher.const import ApplicationType
import voluptuous as vol import voluptuous as vol
from homeassistant.components.hassio import ( from homeassistant.components.hassio import (
@ -15,12 +17,25 @@ from homeassistant.components.hassio import (
async_reboot_host, async_reboot_host,
async_set_yellow_settings, async_set_yellow_settings,
) )
from homeassistant.components.homeassistant_hardware import silabs_multiprotocol_addon from homeassistant.components.homeassistant_hardware.firmware_config_flow import (
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult 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.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__) _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.""" """Handle a config flow for Home Assistant Yellow."""
VERSION = 1 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 @staticmethod
@callback @callback
def async_get_options_flow( def async_get_options_flow(
config_entry: ConfigEntry, config_entry: ConfigEntry,
) -> HomeAssistantYellowOptionsFlow: ) -> OptionsFlow:
"""Return the options flow.""" """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( async def async_step_system(
self, data: dict[str, Any] | None = None self, data: dict[str, Any] | None = None
@ -53,30 +80,56 @@ class HomeAssistantYellowConfigFlow(ConfigFlow, domain=DOMAIN):
if self._async_current_entries(): if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed") 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): class BaseHomeAssistantYellowOptionsFlow(OptionsFlow, ABC):
"""Handle an option flow for Home Assistant Yellow.""" """Base Home Assistant Yellow options flow shared between firmware and multi-PAN."""
_hw_settings: dict[str, bool] | None = None _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( async def async_step_on_supervisor(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult: ) -> ConfigFlowResult:
"""Handle logic when on Supervisor host.""" """Handle logic when on Supervisor host."""
return await self.async_step_main_menu() 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( async def async_step_hardware_settings(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult: ) -> ConfigFlowResult:
@ -133,18 +186,36 @@ class HomeAssistantYellowOptionsFlow(silabs_multiprotocol_addon.OptionsFlowHandl
"""Reboot later.""" """Reboot later."""
return self.async_create_entry(data={}) 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( async def async_step_multipan_settings(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult: ) -> ConfigFlowResult:
"""Handle multipan settings.""" """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( async def _async_serial_port_settings(
self, self,
) -> silabs_multiprotocol_addon.SerialPortSettings: ) -> MultiprotocolSerialPortSettings:
"""Return the radio serial port settings.""" """Return the radio serial port settings."""
return silabs_multiprotocol_addon.SerialPortSettings( return MultiprotocolSerialPortSettings(
device="/dev/ttyAMA1", device=RADIO_DEVICE,
baudrate="115200", baudrate="115200",
flow_control=True, flow_control=True,
) )
@ -163,4 +234,64 @@ class HomeAssistantYellowOptionsFlow(silabs_multiprotocol_addon.OptionsFlowHandl
def _hardware_name(self) -> str: def _hardware_name(self) -> str:
"""Return the name of the hardware.""" """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", "radio_type": "efr32",
} }
FIRMWARE = "firmware"
ZHA_DOMAIN = "zha"

View File

@ -42,6 +42,7 @@
"main_menu": { "main_menu": {
"menu_options": { "menu_options": {
"hardware_settings": "[%key:component::homeassistant_yellow::options::step::hardware_settings::title%]", "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" "multipan_settings": "Configure IEEE 802.15.4 radio multiprotocol support"
} }
}, },
@ -79,6 +80,46 @@
"start_flasher_addon": { "start_flasher_addon": {
"title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::start_flasher_addon::title%]", "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%]" "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": { "error": {
@ -93,11 +134,19 @@
"not_hassio": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::not_hassio%]", "not_hassio": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::not_hassio%]",
"read_hw_settings_error": "Failed to read hardware settings", "read_hw_settings_error": "Failed to read hardware settings",
"write_hw_settings_error": "Failed to write 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": { "progress": {
"install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]", "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 import pytest
from homeassistant.components.hassio import DOMAIN as HASSIO_DOMAIN from homeassistant.components.hassio import DOMAIN as HASSIO_DOMAIN
from homeassistant.components.homeassistant_yellow.const import DOMAIN from homeassistant.components.hassio.addon_manager import AddonInfo, AddonState
from homeassistant.components.zha import DOMAIN as ZHA_DOMAIN 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.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType from homeassistant.data_entry_flow import FlowResultType
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
@ -57,22 +66,28 @@ async def test_config_flow(hass: HomeAssistant) -> None:
mock_integration(hass, MockModule("hassio")) mock_integration(hass, MockModule("hassio"))
await async_setup_component(hass, HASSIO_DOMAIN, {}) await async_setup_component(hass, HASSIO_DOMAIN, {})
with patch( with (
patch(
"homeassistant.components.homeassistant_yellow.async_setup_entry", "homeassistant.components.homeassistant_yellow.async_setup_entry",
return_value=True, return_value=True,
) as mock_setup_entry: ) 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( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "system"} DOMAIN, context={"source": "system"}
) )
assert result["type"] is FlowResultType.CREATE_ENTRY assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "Home Assistant Yellow" assert result["title"] == "Home Assistant Yellow"
assert result["data"] == {} assert result["data"] == {"firmware": "ezsp"}
assert result["options"] == {} assert result["options"] == {}
assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1
config_entry = hass.config_entries.async_entries(DOMAIN)[0] 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.options == {}
assert config_entry.title == "Home Assistant Yellow" 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 # Setup the config entry
config_entry = MockConfigEntry( config_entry = MockConfigEntry(
data={}, data={"firmware": ApplicationType.EZSP},
domain=DOMAIN, domain=DOMAIN,
options={}, options={},
title="Home Assistant Yellow", title="Home Assistant Yellow",
version=1,
minor_version=2,
) )
config_entry.add_to_hass(hass) 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() 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( @pytest.mark.parametrize(
("reboot_menu_choice", "reboot_calls"), ("reboot_menu_choice", "reboot_calls"),
[("reboot_now", 1), ("reboot_later", 0)], [("reboot_now", 1), ("reboot_later", 0)],
@ -281,10 +139,12 @@ async def test_option_flow_led_settings(
# Setup the config entry # Setup the config entry
config_entry = MockConfigEntry( config_entry = MockConfigEntry(
data={}, data={"firmware": ApplicationType.EZSP},
domain=DOMAIN, domain=DOMAIN,
options={}, options={},
title="Home Assistant Yellow", title="Home Assistant Yellow",
version=1,
minor_version=2,
) )
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
@ -327,10 +187,12 @@ async def test_option_flow_led_settings_unchanged(
# Setup the config entry # Setup the config entry
config_entry = MockConfigEntry( config_entry = MockConfigEntry(
data={}, data={"firmware": ApplicationType.EZSP},
domain=DOMAIN, domain=DOMAIN,
options={}, options={},
title="Home Assistant Yellow", title="Home Assistant Yellow",
version=1,
minor_version=2,
) )
config_entry.add_to_hass(hass) 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 # Setup the config entry
config_entry = MockConfigEntry( config_entry = MockConfigEntry(
data={}, data={"firmware": ApplicationType.EZSP},
domain=DOMAIN, domain=DOMAIN,
options={}, options={},
title="Home Assistant Yellow", title="Home Assistant Yellow",
version=1,
minor_version=2,
) )
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
@ -391,10 +255,12 @@ async def test_option_flow_led_settings_fail_2(
# Setup the config entry # Setup the config entry
config_entry = MockConfigEntry( config_entry = MockConfigEntry(
data={}, data={"firmware": ApplicationType.EZSP},
domain=DOMAIN, domain=DOMAIN,
options={}, options={},
title="Home Assistant Yellow", title="Home Assistant Yellow",
version=1,
minor_version=2,
) )
config_entry.add_to_hass(hass) 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["type"] is FlowResultType.ABORT
assert result["reason"] == "write_hw_settings_error" 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 import zha
from homeassistant.components.hassio import DOMAIN as HASSIO_DOMAIN 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.components.homeassistant_yellow.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry, MockModule, mock_integration from tests.common import MockConfigEntry, MockModule, mock_integration
@ -27,10 +31,12 @@ async def test_setup_entry(
# Setup the config entry # Setup the config entry
config_entry = MockConfigEntry( config_entry = MockConfigEntry(
data={}, data={"firmware": ApplicationType.EZSP},
domain=DOMAIN, domain=DOMAIN,
options={}, options={},
title="Home Assistant Yellow", title="Home Assistant Yellow",
version=1,
minor_version=2,
) )
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
with ( with (
@ -42,6 +48,14 @@ async def test_setup_entry(
"homeassistant.components.onboarding.async_is_onboarded", "homeassistant.components.onboarding.async_is_onboarded",
return_value=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) assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done(wait_background_tasks=True) 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 # Setup the config entry
config_entry = MockConfigEntry( config_entry = MockConfigEntry(
data={}, data={"firmware": ApplicationType.EZSP},
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={},
domain=DOMAIN, domain=DOMAIN,
options={}, options={},
title="Home Assistant Yellow", title="Home Assistant Yellow",
version=1,
minor_version=2,
) )
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
with ( with (
@ -229,10 +137,12 @@ async def test_setup_entry_no_hassio(hass: HomeAssistant) -> None:
"""Test setup of a config entry without hassio.""" """Test setup of a config entry without hassio."""
# Setup the config entry # Setup the config entry
config_entry = MockConfigEntry( config_entry = MockConfigEntry(
data={}, data={"firmware": ApplicationType.EZSP},
domain=DOMAIN, domain=DOMAIN,
options={}, options={},
title="Home Assistant Yellow", title="Home Assistant Yellow",
version=1,
minor_version=2,
) )
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
assert len(hass.config_entries.async_entries()) == 1 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 # Setup the config entry
config_entry = MockConfigEntry( config_entry = MockConfigEntry(
data={}, data={"firmware": ApplicationType.EZSP},
domain=DOMAIN, domain=DOMAIN,
options={}, options={},
title="Home Assistant Yellow", title="Home Assistant Yellow",
version=1,
minor_version=2,
) )
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
assert len(hass.config_entries.async_entries()) == 1 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 # Setup the config entry
config_entry = MockConfigEntry( config_entry = MockConfigEntry(
data={}, data={"firmware": ApplicationType.EZSP},
domain=DOMAIN, domain=DOMAIN,
options={}, options={},
title="Home Assistant Yellow", title="Home Assistant Yellow",
version=1,
minor_version=2,
) )
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
with patch( 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.""" """Test setup of a config entry when fetching addon info fails."""
mock_integration(hass, MockModule("hassio")) mock_integration(hass, MockModule("hassio"))
await async_setup_component(hass, HASSIO_DOMAIN, {}) await async_setup_component(hass, HASSIO_DOMAIN, {})
addon_store_info.side_effect = HassioAPIError("Boom")
# Setup the config entry # Setup the config entry
config_entry = MockConfigEntry( config_entry = MockConfigEntry(
data={}, data={"firmware": ApplicationType.CPC},
domain=DOMAIN, domain=DOMAIN,
options={}, options={},
title="Home Assistant Yellow", title="Home Assistant Yellow",
version=1,
minor_version=2,
) )
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
with ( with (
@ -319,41 +234,15 @@ async def test_setup_entry_addon_info_fails(
return_value={"board": "yellow"}, return_value={"board": "yellow"},
), ),
patch( 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) assert not await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.SETUP_RETRY 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()