From b7652c78eeb5f759115cf439bf5be8c2e6c5bee4 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 29 Nov 2022 22:34:55 +0100 Subject: [PATCH] Add options flow to enable multiprotocol support on sky connect (#82525) --- .../silabs_multiprotocol_addon.py | 9 +- .../homeassistant_hardware/strings.json | 43 ++++ .../translations/en.json | 43 ++++ .../homeassistant_sky_connect/__init__.py | 80 ++++++- .../homeassistant_sky_connect/config_flow.py | 47 ++++- .../homeassistant_sky_connect/manifest.json | 2 +- .../homeassistant_sky_connect/strings.json | 41 ++++ .../translations/en.json | 41 ++++ .../homeassistant_sky_connect/util.py | 17 ++ .../homeassistant_yellow/__init__.py | 4 +- .../homeassistant_yellow/config_flow.py | 6 +- .../homeassistant_yellow/strings.json | 34 +-- .../homeassistant_yellow/translations/en.json | 2 +- homeassistant/components/zha/radio_manager.py | 34 +-- script/hassfest/translations.py | 18 ++ .../test_silabs_multiprotocol_addon.py | 20 +- .../homeassistant_sky_connect/conftest.py | 126 ++++++++++- .../test_config_flow.py | 196 +++++++++++++++++- .../test_hardware.py | 8 +- .../homeassistant_sky_connect/test_init.py | 171 ++++++++++++++- .../homeassistant_yellow/test_config_flow.py | 4 +- .../homeassistant_yellow/test_init.py | 4 +- tests/components/zha/test_radio_manager.py | 107 ++++++++-- 23 files changed, 962 insertions(+), 95 deletions(-) create mode 100644 homeassistant/components/homeassistant_hardware/strings.json create mode 100644 homeassistant/components/homeassistant_hardware/translations/en.json create mode 100644 homeassistant/components/homeassistant_sky_connect/strings.json create mode 100644 homeassistant/components/homeassistant_sky_connect/translations/en.json create mode 100644 homeassistant/components/homeassistant_sky_connect/util.py diff --git a/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py b/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py index 0724eee9ed5..500555cc6ae 100644 --- a/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py +++ b/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py @@ -98,6 +98,10 @@ class BaseMultiPanFlow(FlowHandler): being migrated. """ + @abstractmethod + def _hardware_name(self) -> str: + """Return the name of the hardware.""" + @abstractmethod def _zha_name(self) -> str: """Return the ZHA name.""" @@ -254,6 +258,7 @@ class OptionsFlowHandler(BaseMultiPanFlow, config_entries.OptionsFlow): data_schema=vol.Schema( {vol.Required(CONF_ENABLE_MULTI_PAN, default=False): bool} ), + description_placeholders={"hardware_name": self._hardware_name()}, ) if not user_input[CONF_ENABLE_MULTI_PAN]: return self.async_create_entry(title="", data={}) @@ -285,10 +290,8 @@ class OptionsFlowHandler(BaseMultiPanFlow, config_entries.OptionsFlow): "name": self._zha_name(), "port": { "path": get_zigbee_socket(self.hass, addon_info), - "baudrate": 115200, - "flow_control": "hardware", }, - "radio_type": "efr32", + "radio_type": "ezsp", }, "old_discovery_info": await self._async_zha_physical_discovery(), } diff --git a/homeassistant/components/homeassistant_hardware/strings.json b/homeassistant/components/homeassistant_hardware/strings.json new file mode 100644 index 00000000000..47549794fc8 --- /dev/null +++ b/homeassistant/components/homeassistant_hardware/strings.json @@ -0,0 +1,43 @@ +{ + "silabs_multiprotocol_hardware": { + "options": { + "step": { + "addon_not_installed": { + "title": "Enable multiprotocol support on the IEEE 802.15.4 radio", + "description": "When multiprotocol support is enabled, the {hardware_name}'s IEEE 802.15.4 radio can be used for both Zigbee and Thread (used by Matter) at the same time. If the radio is already used by the ZHA Zigbee integration, ZHA will be reconfigured to use the multiprotocol firmware.\n\nNote: 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.", + "zha_migration_failed": "The ZHA migration did not succeed." + }, + "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_hardware/translations/en.json b/homeassistant/components/homeassistant_hardware/translations/en.json new file mode 100644 index 00000000000..ec75e234c4d --- /dev/null +++ b/homeassistant/components/homeassistant_hardware/translations/en.json @@ -0,0 +1,43 @@ +{ + "silabs_multiprotocol_hardware": { + "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.", + "zha_migration_failed": "The ZHA migration did not succeed." + }, + "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 {hardware_name}'s IEEE 802.15.4 radio can be used for both Zigbee and Thread (used by Matter) at the same time. If the radio is already used by the ZHA Zigbee integration, ZHA will be reconfigured to use the multiprotocol firmware.\n\nNote: 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/homeassistant/components/homeassistant_sky_connect/__init__.py b/homeassistant/components/homeassistant_sky_connect/__init__.py index cb00f0e32ec..e65394ca15c 100644 --- a/homeassistant/components/homeassistant_sky_connect/__init__.py +++ b/homeassistant/components/homeassistant_sky_connect/__init__.py @@ -1,12 +1,63 @@ """The Home Assistant Sky Connect integration.""" from __future__ import annotations +import logging + from homeassistant.components import usb +from homeassistant.components.hassio import ( + AddonError, + AddonInfo, + AddonManager, + AddonState, + is_hassio, +) +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 from .const import DOMAIN +from .util import get_usb_service_info + +_LOGGER = logging.getLogger(__name__) + + +async def _multi_pan_addon_info(hass, entry: ConfigEntry) -> AddonInfo | None: + """Return AddonInfo if the multi-PAN addon is enabled for our SkyConnect.""" + if not is_hassio(hass): + return None + + 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: + return None + + usb_dev = entry.data["device"] + dev_path = await hass.async_add_executor_job(usb.get_serial_by_id, usb_dev) + + if addon_info.options["device"] != dev_path: + return None + + return addon_info async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -24,19 +75,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # The USB dongle is not plugged in raise ConfigEntryNotReady - usb_info = usb.UsbServiceInfo( - device=entry.data["device"], - vid=entry.data["vid"], - pid=entry.data["pid"], - serial_number=entry.data["serial_number"], - manufacturer=entry.data["manufacturer"], - description=entry.data["description"], - ) + addon_info = await _multi_pan_addon_info(hass, entry) + if not addon_info: + usb_info = get_usb_service_info(entry) + await hass.config_entries.flow.async_init( + "zha", + context={"source": "usb"}, + data=usb_info, + ) + return True + + hw_discovery_data = { + "name": "Sky Connect Multi-PAN", + "port": { + "path": get_zigbee_socket(hass, addon_info), + }, + "radio_type": "ezsp", + } await hass.config_entries.flow.async_init( "zha", - context={"source": "usb"}, - data=usb_info, + context={"source": "hardware"}, + data=hw_discovery_data, ) return True diff --git a/homeassistant/components/homeassistant_sky_connect/config_flow.py b/homeassistant/components/homeassistant_sky_connect/config_flow.py index 21cc5e3ace4..1e4fd8701cd 100644 --- a/homeassistant/components/homeassistant_sky_connect/config_flow.py +++ b/homeassistant/components/homeassistant_sky_connect/config_flow.py @@ -1,11 +1,16 @@ """Config flow for the Home Assistant Sky Connect integration.""" from __future__ import annotations +from typing import Any + from homeassistant.components import usb -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 +from .util import get_usb_service_info class HomeAssistantSkyConnectConfigFlow(ConfigFlow, domain=DOMAIN): @@ -13,6 +18,14 @@ class HomeAssistantSkyConnectConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 + @staticmethod + @callback + def async_get_options_flow( + config_entry: ConfigEntry, + ) -> HomeAssistantSkyConnectOptionsFlow: + """Return the options flow.""" + return HomeAssistantSkyConnectOptionsFlow(config_entry) + async def async_step_usb(self, discovery_info: usb.UsbServiceInfo) -> FlowResult: """Handle usb discovery.""" device = discovery_info.device @@ -35,3 +48,35 @@ class HomeAssistantSkyConnectConfigFlow(ConfigFlow, domain=DOMAIN): "description": description, }, ) + + +class HomeAssistantSkyConnectOptionsFlow(silabs_multiprotocol_addon.OptionsFlowHandler): + """Handle an option flow for Home Assistant Sky Connect.""" + + async def _async_serial_port_settings( + self, + ) -> silabs_multiprotocol_addon.SerialPortSettings: + """Return the radio serial port settings.""" + usb_dev = self.config_entry.data["device"] + dev_path = await self.hass.async_add_executor_job(usb.get_serial_by_id, usb_dev) + return silabs_multiprotocol_addon.SerialPortSettings( + device=dev_path, + baudrate="115200", + flow_control=True, + ) + + async def _async_zha_physical_discovery(self) -> dict[str, Any]: + """Return ZHA discovery data when multiprotocol FW is not used. + + Passed to ZHA do determine if the ZHA config entry is connected to the radio + being migrated. + """ + return {"usb": get_usb_service_info(self.config_entry)} + + def _zha_name(self) -> str: + """Return the ZHA name.""" + return "Sky Connect Multi-PAN" + + def _hardware_name(self) -> str: + """Return the name of the hardware.""" + return "Home Assistant Sky Connect" diff --git a/homeassistant/components/homeassistant_sky_connect/manifest.json b/homeassistant/components/homeassistant_sky_connect/manifest.json index 8fdc1d3c1d1..34bb2ad701c 100644 --- a/homeassistant/components/homeassistant_sky_connect/manifest.json +++ b/homeassistant/components/homeassistant_sky_connect/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Sky Connect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homeassistant_sky_connect", - "dependencies": ["hardware", "usb"], + "dependencies": ["hardware", "usb", "homeassistant_hardware"], "codeowners": ["@home-assistant/core"], "integration_type": "hardware", "usb": [ diff --git a/homeassistant/components/homeassistant_sky_connect/strings.json b/homeassistant/components/homeassistant_sky_connect/strings.json new file mode 100644 index 00000000000..970f9d97a4c --- /dev/null +++ b/homeassistant/components/homeassistant_sky_connect/strings.json @@ -0,0 +1,41 @@ +{ + "options": { + "step": { + "addon_not_installed": { + "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::addon_not_installed::title%]", + "description": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::addon_not_installed::description%]", + "data": { + "enable_multi_pan": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::addon_not_installed::data::enable_multi_pan%]" + } + }, + "addon_installed_other_device": { + "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::addon_installed_other_device::title%]" + }, + "install_addon": { + "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::install_addon::title%]" + }, + "show_revert_guide": { + "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::show_revert_guide::title%]", + "description": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::show_revert_guide::description%]" + }, + "start_addon": { + "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::start_addon::title%]" + } + }, + "error": { + "unknown": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::error::unknown%]" + }, + "abort": { + "addon_info_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_info_failed%]", + "addon_install_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_install_failed%]", + "addon_set_config_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_set_config_failed%]", + "addon_start_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_start_failed%]", + "not_hassio": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::not_hassio%]", + "zha_migration_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::zha_migration_failed%]" + }, + "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%]" + } + } +} diff --git a/homeassistant/components/homeassistant_sky_connect/translations/en.json b/homeassistant/components/homeassistant_sky_connect/translations/en.json new file mode 100644 index 00000000000..8e12173f86a --- /dev/null +++ b/homeassistant/components/homeassistant_sky_connect/translations/en.json @@ -0,0 +1,41 @@ +{ + "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.", + "zha_migration_failed": "The ZHA migration did not succeed." + }, + "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 {hardware_name}'s IEEE 802.15.4 radio can be used for both Zigbee and Thread (used by Matter) at the same time. If the radio is already used by the ZHA Zigbee integration, ZHA will be reconfigured to use the multiprotocol firmware.\n\nNote: 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/homeassistant/components/homeassistant_sky_connect/util.py b/homeassistant/components/homeassistant_sky_connect/util.py new file mode 100644 index 00000000000..804ce83d063 --- /dev/null +++ b/homeassistant/components/homeassistant_sky_connect/util.py @@ -0,0 +1,17 @@ +"""Utility functions for Home Assistant Sky Connect integration.""" +from __future__ import annotations + +from homeassistant.components import usb +from homeassistant.config_entries import ConfigEntry + + +def get_usb_service_info(config_entry: ConfigEntry) -> usb.UsbServiceInfo: + """Return UsbServiceInfo.""" + return usb.UsbServiceInfo( + device=config_entry.data["device"], + vid=config_entry.data["vid"], + pid=config_entry.data["pid"], + serial_number=config_entry.data["serial_number"], + manufacturer=config_entry.data["manufacturer"], + description=config_entry.data["description"], + ) diff --git a/homeassistant/components/homeassistant_yellow/__init__.py b/homeassistant/components/homeassistant_yellow/__init__.py index 1b61fe7d47f..6099dc014df 100644 --- a/homeassistant/components/homeassistant_yellow/__init__.py +++ b/homeassistant/components/homeassistant_yellow/__init__.py @@ -73,10 +73,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "name": "Yellow Multi-PAN", "port": { "path": get_zigbee_socket(hass, addon_info), - "baudrate": 115200, - "flow_control": "hardware", }, - "radio_type": "efr32", + "radio_type": "ezsp", } await hass.config_entries.flow.async_init( diff --git a/homeassistant/components/homeassistant_yellow/config_flow.py b/homeassistant/components/homeassistant_yellow/config_flow.py index e5e6bb3b2c6..09cdcc1469a 100644 --- a/homeassistant/components/homeassistant_yellow/config_flow.py +++ b/homeassistant/components/homeassistant_yellow/config_flow.py @@ -51,8 +51,12 @@ class HomeAssistantYellowOptionsFlow(silabs_multiprotocol_addon.OptionsFlowHandl Passed to ZHA do determine if the ZHA config entry is connected to the radio being migrated. """ - return ZHA_HW_DISCOVERY_DATA + return {"hw": ZHA_HW_DISCOVERY_DATA} def _zha_name(self) -> str: """Return the ZHA name.""" return "Yellow Multi-PAN" + + def _hardware_name(self) -> str: + """Return the name of the hardware.""" + return "Home Assistant Yellow" diff --git a/homeassistant/components/homeassistant_yellow/strings.json b/homeassistant/components/homeassistant_yellow/strings.json index 1810597b242..970f9d97a4c 100644 --- a/homeassistant/components/homeassistant_yellow/strings.json +++ b/homeassistant/components/homeassistant_yellow/strings.json @@ -2,40 +2,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. If the radio is already used by the ZHA Zigbee integration, ZHA will be reconfigured to use the multiprotocol firmware.\n\nNote: This is an experimental feature.", + "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::addon_not_installed::title%]", + "description": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::addon_not_installed::description%]", "data": { - "enable_multi_pan": "Enable multiprotocol support" + "enable_multi_pan": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::addon_not_installed::data::enable_multi_pan%]" } }, "addon_installed_other_device": { - "title": "Multiprotocol support is already enabled for another device" + "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::addon_installed_other_device::title%]" }, "install_addon": { - "title": "The Silicon Labs Multiprotocol add-on installation has started" + "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::install_addon::title%]" }, "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" + "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::show_revert_guide::title%]", + "description": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::show_revert_guide::description%]" }, "start_addon": { - "title": "The Silicon Labs Multiprotocol add-on is starting." + "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::start_addon::title%]" } }, "error": { - "unknown": "[%key:common::config_flow::error::unknown%]" + "unknown": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::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.", - "zha_migration_failed": "The ZHA migration did not succeed." + "addon_info_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_info_failed%]", + "addon_install_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_install_failed%]", + "addon_set_config_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_set_config_failed%]", + "addon_start_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_start_failed%]", + "not_hassio": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::not_hassio%]", + "zha_migration_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::zha_migration_failed%]" }, "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." + "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%]" } } } diff --git a/homeassistant/components/homeassistant_yellow/translations/en.json b/homeassistant/components/homeassistant_yellow/translations/en.json index 0b074452652..8e12173f86a 100644 --- a/homeassistant/components/homeassistant_yellow/translations/en.json +++ b/homeassistant/components/homeassistant_yellow/translations/en.json @@ -23,7 +23,7 @@ "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. If the radio is already used by the ZHA Zigbee integration, ZHA will be reconfigured to use the multiprotocol firmware.\n\nNote: This is an experimental feature.", + "description": "When multiprotocol support is enabled, the {hardware_name}'s IEEE 802.15.4 radio can be used for both Zigbee and Thread (used by Matter) at the same time. If the radio is already used by the ZHA Zigbee integration, ZHA will be reconfigured to use the multiprotocol firmware.\n\nNote: This is an experimental feature.", "title": "Enable multiprotocol support on the IEEE 802.15.4 radio" }, "install_addon": { diff --git a/homeassistant/components/zha/radio_manager.py b/homeassistant/components/zha/radio_manager.py index 512f26a139b..2a914dee1d7 100644 --- a/homeassistant/components/zha/radio_manager.py +++ b/homeassistant/components/zha/radio_manager.py @@ -15,6 +15,7 @@ from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH from zigpy.exceptions import NetworkNotFormed from homeassistant import config_entries +from homeassistant.components import usb from homeassistant.core import HomeAssistant from .core.const import ( @@ -53,7 +54,12 @@ HARDWARE_DISCOVERY_SCHEMA = vol.Schema( HARDWARE_MIGRATION_SCHEMA = vol.Schema( { vol.Required("new_discovery_info"): HARDWARE_DISCOVERY_SCHEMA, - vol.Required("old_discovery_info"): HARDWARE_DISCOVERY_SCHEMA, + vol.Required("old_discovery_info"): vol.Schema( + { + vol.Exclusive("hw", "discovery"): HARDWARE_DISCOVERY_SCHEMA, + vol.Exclusive("usb", "discovery"): usb.UsbServiceInfo, + } + ), } ) @@ -297,21 +303,20 @@ class ZhaMultiPANMigrationHelper: new_radio_type = ZhaRadioManager.parse_radio_type( migration_data["new_discovery_info"]["radio_type"] ) - old_radio_type = ZhaRadioManager.parse_radio_type( - migration_data["old_discovery_info"]["radio_type"] - ) new_device_settings = new_radio_type.controller.SCHEMA_DEVICE( migration_data["new_discovery_info"]["port"] ) - old_device_settings = old_radio_type.controller.SCHEMA_DEVICE( - migration_data["old_discovery_info"]["port"] - ) - if ( - self._config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH] - != old_device_settings[CONF_DEVICE_PATH] - ): + if "hw" in migration_data["old_discovery_info"]: + old_device_path = migration_data["old_discovery_info"]["hw"]["port"]["path"] + else: # usb + device = migration_data["old_discovery_info"]["usb"].device + old_device_path = await self._hass.async_add_executor_job( + usb.get_serial_by_id, device + ) + + if self._config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH] != old_device_path: # ZHA is using another radio, do nothing return False @@ -322,11 +327,12 @@ class ZhaMultiPANMigrationHelper: pass # Temporarily connect to the old radio to read its settings + config_entry_data = self._config_entry.data old_radio_mgr = ZhaRadioManager() old_radio_mgr.hass = self._hass - old_radio_mgr.radio_type = old_radio_type - old_radio_mgr.device_path = old_device_settings[CONF_DEVICE_PATH] - old_radio_mgr.device_settings = old_device_settings + old_radio_mgr.device_path = config_entry_data[CONF_DEVICE][CONF_DEVICE_PATH] + old_radio_mgr.device_settings = config_entry_data[CONF_DEVICE] + old_radio_mgr.radio_type = RadioType[config_entry_data[CONF_RADIO_TYPE]] backup = await old_radio_mgr.async_load_network_settings(create_backup=True) # Then configure the radio manager for the new radio to use the new settings diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index 8f2816e503c..824ebc6b825 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -275,6 +275,22 @@ def gen_auth_schema(config: Config, integration: Integration) -> vol.Schema: ) +def gen_ha_hardware_schema(config: Config, integration: Integration): + """Generate auth schema.""" + return vol.Schema( + { + str: { + vol.Optional("options"): gen_data_entry_schema( + config=config, + integration=integration, + flow_title=UNDEFINED, + require_step_title=False, + ) + } + } + ) + + def gen_platform_strings_schema(config: Config, integration: Integration) -> vol.Schema: """Generate platform strings schema like strings.sensor.json. @@ -351,6 +367,8 @@ def validate_translation_file( # noqa: C901 ) } ) + elif integration.domain == "homeassistant_hardware": + strings_schema = gen_ha_hardware_schema(config, integration) else: strings_schema = gen_strings_schema(config, integration) diff --git a/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py b/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py index f61fd5e8d76..577ac25eb82 100644 --- a/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py +++ b/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py @@ -61,13 +61,15 @@ class TestOptionsFlow(silabs_multiprotocol_addon.OptionsFlowHandler): being migrated. """ return { - "name": "Test", - "port": { - "path": "/dev/ttyTEST123", - "baudrate": 115200, - "flow_control": "hardware", - }, - "radio_type": "efr32", + "hw": { + "name": "Test", + "port": { + "path": "/dev/ttyTEST123", + "baudrate": 115200, + "flow_control": "hardware", + }, + "radio_type": "efr32", + } } def _zha_name(self) -> str: @@ -223,8 +225,8 @@ async def test_option_flow_install_multi_pan_addon_zha( assert zha_config_entry.data == { "device": { "path": "socket://core-silabs-multiprotocol:9999", - "baudrate": 115200, - "flow_control": "hardware", + "baudrate": 57600, # ZHA default + "flow_control": "software", # ZHA default }, "radio_type": "ezsp", } diff --git a/tests/components/homeassistant_sky_connect/conftest.py b/tests/components/homeassistant_sky_connect/conftest.py index cc606c9b988..2d333c62b2d 100644 --- a/tests/components/homeassistant_sky_connect/conftest.py +++ b/tests/components/homeassistant_sky_connect/conftest.py @@ -1,14 +1,138 @@ """Test fixtures for the Home Assistant Sky Connect integration.""" -from unittest.mock import patch +from collections.abc import Generator +from unittest.mock import MagicMock, patch import pytest +@pytest.fixture(name="mock_usb_serial_by_id", autouse=True) +def mock_usb_serial_by_id_fixture() -> Generator[MagicMock, None, None]: + """Mock usb serial by id.""" + with patch( + "homeassistant.components.zwave_js.config_flow.usb.get_serial_by_id" + ) as mock_usb_serial_by_id: + mock_usb_serial_by_id.side_effect = lambda x: x + yield mock_usb_serial_by_id + + @pytest.fixture(autouse=True) def mock_zha(): """Mock the zha integration.""" + mock_connect_app = MagicMock() + mock_connect_app.__aenter__.return_value.backups.backups = [MagicMock()] + mock_connect_app.__aenter__.return_value.backups.create_backup.return_value = ( + MagicMock() + ) + with patch( + "homeassistant.components.zha.radio_manager.ZhaRadioManager._connect_zigpy_app", + return_value=mock_connect_app, + ), patch( "homeassistant.components.zha.async_setup_entry", 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_sky_connect/test_config_flow.py b/tests/components/homeassistant_sky_connect/test_config_flow.py index bbde732d201..c38edf00fa7 100644 --- a/tests/components/homeassistant_sky_connect/test_config_flow.py +++ b/tests/components/homeassistant_sky_connect/test_config_flow.py @@ -1,13 +1,18 @@ """Test the Home Assistant Sky Connect config flow.""" import copy -from unittest.mock import patch +from unittest.mock import Mock, patch from homeassistant.components import homeassistant_sky_connect, usb from homeassistant.components.homeassistant_sky_connect.const import DOMAIN +from homeassistant.components.zha.core.const import ( + CONF_DEVICE_PATH, + DOMAIN as ZHA_DOMAIN, + RadioType, +) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, MockModule, mock_integration USB_DATA = usb.UsbServiceInfo( device="bla_device", @@ -143,3 +148,190 @@ async def test_config_flow_update_device(hass: HomeAssistant) -> None: assert result["reason"] == "already_configured" assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_unload_entry.mock_calls) == 1 + + +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={ + "device": USB_DATA.device, + "vid": USB_DATA.vid, + "pid": USB_DATA.pid, + "serial_number": USB_DATA.serial_number, + "manufacturer": USB_DATA.manufacturer, + "description": USB_DATA.description, + }, + domain=DOMAIN, + options={}, + title="Home Assistant Sky Connect", + unique_id=f"{USB_DATA.vid}:{USB_DATA.pid}_{USB_DATA.serial_number}_{USB_DATA.manufacturer}_{USB_DATA.description}", + ) + 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": "bla_device", + "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 + + +def mock_detect_radio_type(radio_type=RadioType.ezsp, ret=True): + """Mock `detect_radio_type` that just sets the appropriate attributes.""" + + async def detect(self): + self.radio_type = radio_type + self.device_settings = radio_type.controller.SCHEMA_DEVICE( + {CONF_DEVICE_PATH: self.device_path} + ) + + return ret + + return detect + + +@patch( + "homeassistant.components.zha.radio_manager.ZhaRadioManager.detect_radio_type", + mock_detect_radio_type(), +) +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")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={ + "device": USB_DATA.device, + "vid": USB_DATA.vid, + "pid": USB_DATA.pid, + "serial_number": USB_DATA.serial_number, + "manufacturer": USB_DATA.manufacturer, + "description": USB_DATA.description, + }, + domain=DOMAIN, + options={}, + title="Home Assistant Sky Connect", + unique_id=f"{USB_DATA.vid}:{USB_DATA.pid}_{USB_DATA.serial_number}_{USB_DATA.manufacturer}_{USB_DATA.description}", + ) + config_entry.add_to_hass(hass) + + zha_config_entry = MockConfigEntry( + data={"device": {"path": "bla_device"}, "radio_type": "ezsp"}, + domain=ZHA_DOMAIN, + options={}, + title="Yellow", + ) + zha_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": "bla_device", + "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": 57600, # ZHA default + "flow_control": "software", # ZHA default + }, + "radio_type": "ezsp", + } + + 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_sky_connect/test_hardware.py b/tests/components/homeassistant_sky_connect/test_hardware.py index 2ba305afb4a..01f0e6ac5d7 100644 --- a/tests/components/homeassistant_sky_connect/test_hardware.py +++ b/tests/components/homeassistant_sky_connect/test_hardware.py @@ -4,7 +4,7 @@ from unittest.mock import patch from homeassistant.components.homeassistant_sky_connect.const import DOMAIN from homeassistant.core import HomeAssistant -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, MockModule, mock_integration CONFIG_ENTRY_DATA = { "device": "bla_device", @@ -25,8 +25,12 @@ CONFIG_ENTRY_DATA_2 = { } -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("usb")) + # Setup the config entry config_entry = MockConfigEntry( data=CONFIG_ENTRY_DATA, diff --git a/tests/components/homeassistant_sky_connect/test_init.py b/tests/components/homeassistant_sky_connect/test_init.py index 5a9d2064a24..ebf1c74d9e0 100644 --- a/tests/components/homeassistant_sky_connect/test_init.py +++ b/tests/components/homeassistant_sky_connect/test_init.py @@ -1,11 +1,12 @@ """Test the Home Assistant Sky Connect integration.""" from collections.abc import Generator from typing import Any -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock, Mock, patch import pytest from homeassistant.components import zha +from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.homeassistant_sky_connect.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant @@ -46,7 +47,12 @@ def mock_zha_config_flow_setup() -> Generator[None, None, None]: "onboarded, num_entries, num_flows", ((False, 1, 0), (True, 0, 1)) ) async def test_setup_entry( - mock_zha_config_flow_setup, hass: HomeAssistant, onboarded, num_entries, num_flows + mock_zha_config_flow_setup, + hass: HomeAssistant, + addon_store_info, + onboarded, + num_entries, + num_flows, ) -> None: """Test setup of a config entry, including setup of zha.""" # Setup the config entry @@ -90,7 +96,9 @@ async def test_setup_entry( assert len(hass.config_entries.async_entries("zha")) == num_entries -async def test_setup_zha(mock_zha_config_flow_setup, hass: HomeAssistant) -> None: +async def test_setup_zha( + mock_zha_config_flow_setup, hass: HomeAssistant, addon_store_info +) -> None: """Test zha gets the right config.""" # Setup the config entry config_entry = MockConfigEntry( @@ -134,6 +142,108 @@ async def test_setup_zha(mock_zha_config_flow_setup, hass: HomeAssistant) -> Non assert config_entry.title == CONFIG_ENTRY_DATA["description"] +async def test_setup_zha_multipan( + hass: HomeAssistant, addon_info, addon_running +) -> None: + """Test zha gets the right config.""" + addon_info.return_value["options"]["device"] = CONFIG_ENTRY_DATA["device"] + + # Setup the config entry + config_entry = MockConfigEntry( + data=CONFIG_ENTRY_DATA, + domain=DOMAIN, + options={}, + title="Home Assistant Sky Connect", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.homeassistant_sky_connect.usb.async_is_plugged_in", + return_value=True, + ) as mock_is_plugged_in, patch( + "homeassistant.components.onboarding.async_is_onboarded", return_value=False + ), patch( + "homeassistant.components.homeassistant_sky_connect.is_hassio", + side_effect=Mock(return_value=True), + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(mock_is_plugged_in.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": 57600, # ZHA default + "flow_control": "software", # ZHA default + "path": "socket://core-silabs-multiprotocol:9999", + }, + "radio_type": "ezsp", + } + assert config_entry.options == {} + assert config_entry.title == "Sky Connect Multi-PAN" + + +async def test_setup_zha_multipan_other_device( + mock_zha_config_flow_setup, hass: HomeAssistant, addon_info, addon_running +) -> None: + """Test zha gets the right config.""" + addon_info.return_value["options"]["device"] = "/dev/not_our_sky_connect" + + # Setup the config entry + config_entry = MockConfigEntry( + data=CONFIG_ENTRY_DATA, + domain=DOMAIN, + options={}, + title="Home Assistant Yellow", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.homeassistant_sky_connect.usb.async_is_plugged_in", + return_value=True, + ) as mock_is_plugged_in, patch( + "homeassistant.components.onboarding.async_is_onboarded", return_value=False + ), patch( + "homeassistant.components.homeassistant_sky_connect.is_hassio", + side_effect=Mock(return_value=True), + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(mock_is_plugged_in.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": "software", + "path": CONFIG_ENTRY_DATA["device"], + }, + "radio_type": "ezsp", + } + assert config_entry.options == {} + assert config_entry.title == CONFIG_ENTRY_DATA["description"] + + async def test_setup_entry_wait_usb(hass: HomeAssistant) -> None: """Test setup of a config entry when the dongle is not plugged in.""" # Setup the config entry @@ -152,3 +262,58 @@ async def test_setup_entry_wait_usb(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(mock_is_plugged_in.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.""" + addon_store_info.side_effect = HassioAPIError("Boom") + + # Setup the config entry + config_entry = MockConfigEntry( + data=CONFIG_ENTRY_DATA, + domain=DOMAIN, + options={}, + title="Home Assistant Sky Connect", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.homeassistant_sky_connect.usb.async_is_plugged_in", + return_value=True, + ), patch( + "homeassistant.components.onboarding.async_is_onboarded", return_value=False + ), patch( + "homeassistant.components.homeassistant_sky_connect.is_hassio", + side_effect=Mock(return_value=True), + ): + 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.""" + # Setup the config entry + config_entry = MockConfigEntry( + data=CONFIG_ENTRY_DATA, + domain=DOMAIN, + options={}, + title="Home Assistant Sky Connect", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.homeassistant_sky_connect.usb.async_is_plugged_in", + return_value=True, + ), patch( + "homeassistant.components.onboarding.async_is_onboarded", return_value=False + ), patch( + "homeassistant.components.homeassistant_sky_connect.is_hassio", + side_effect=Mock(return_value=True), + ): + 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/homeassistant_yellow/test_config_flow.py b/tests/components/homeassistant_yellow/test_config_flow.py index a956f812d8a..53d1c5e974d 100644 --- a/tests/components/homeassistant_yellow/test_config_flow.py +++ b/tests/components/homeassistant_yellow/test_config_flow.py @@ -197,8 +197,8 @@ async def test_option_flow_install_multi_pan_addon_zha( assert zha_config_entry.data == { "device": { "path": "socket://core-silabs-multiprotocol:9999", - "baudrate": 115200, - "flow_control": "hardware", + "baudrate": 57600, # ZHA default + "flow_control": "software", # ZHA default }, "radio_type": "ezsp", } diff --git a/tests/components/homeassistant_yellow/test_init.py b/tests/components/homeassistant_yellow/test_init.py index 85b027e82ed..4118c6dc654 100644 --- a/tests/components/homeassistant_yellow/test_init.py +++ b/tests/components/homeassistant_yellow/test_init.py @@ -144,8 +144,8 @@ async def test_setup_zha_multipan( config_entry = hass.config_entries.async_entries("zha")[0] assert config_entry.data == { "device": { - "baudrate": 115200, - "flow_control": "hardware", + "baudrate": 57600, # ZHA default + "flow_control": "software", # ZHA default "path": "socket://core-silabs-multiprotocol:9999", }, "radio_type": "ezsp", diff --git a/tests/components/zha/test_radio_manager.py b/tests/components/zha/test_radio_manager.py index 505118524b6..671f831fcce 100644 --- a/tests/components/zha/test_radio_manager.py +++ b/tests/components/zha/test_radio_manager.py @@ -11,6 +11,7 @@ from zigpy.config import CONF_DEVICE_PATH import zigpy.types from homeassistant import config_entries +from homeassistant.components.usb import UsbServiceInfo from homeassistant.components.zha import radio_manager from homeassistant.components.zha.core.const import DOMAIN, RadioType from homeassistant.core import HomeAssistant @@ -125,14 +126,68 @@ async def test_migrate_matching_port( "radio_type": "efr32", }, "old_discovery_info": { - "name": "Test", + "hw": { + "name": "Test", + "port": { + "path": "/dev/ttyTEST123", + "baudrate": 115200, + "flow_control": "hardware", + }, + "radio_type": "efr32", + } + }, + } + + migration_helper = radio_manager.ZhaMultiPANMigrationHelper(hass, config_entry) + assert await migration_helper.async_initiate_migration(migration_data) + + # Check the ZHA config entry data is updated + assert config_entry.data == { + "device": { + "path": "socket://some/virtual_port", + "baudrate": 115200, + "flow_control": "hardware", + }, + "radio_type": "ezsp", + } + assert config_entry.title == "Test Updated" + + await migration_helper.async_finish_migration() + + +@patch( + "homeassistant.components.zha.radio_manager.ZhaRadioManager.detect_radio_type", + mock_detect_radio_type(), +) +@patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) +async def test_migrate_matching_port_usb( + hass: HomeAssistant, + mock_connect_zigpy_app, +) -> None: + """Test automatic migration.""" + # Setup the config entry + config_entry = MockConfigEntry( + data={"device": {"path": "/dev/ttyTEST123"}, "radio_type": "ezsp"}, + domain=DOMAIN, + options={}, + title="Test", + version=3, + ) + config_entry.add_to_hass(hass) + + migration_data = { + "new_discovery_info": { + "name": "Test Updated", "port": { - "path": "/dev/ttyTEST123", + "path": "socket://some/virtual_port", "baudrate": 115200, "flow_control": "hardware", }, "radio_type": "efr32", }, + "old_discovery_info": { + "usb": UsbServiceInfo("/dev/ttyTEST123", "blah", "blah", None, None, None) + }, } migration_helper = radio_manager.ZhaMultiPANMigrationHelper(hass, config_entry) @@ -178,13 +233,15 @@ async def test_migrate_matching_port_config_entry_not_loaded( "radio_type": "efr32", }, "old_discovery_info": { - "name": "Test", - "port": { - "path": "/dev/ttyTEST123", - "baudrate": 115200, - "flow_control": "hardware", - }, - "radio_type": "efr32", + "hw": { + "name": "Test", + "port": { + "path": "/dev/ttyTEST123", + "baudrate": 115200, + "flow_control": "hardware", + }, + "radio_type": "efr32", + } }, } @@ -236,13 +293,15 @@ async def test_migrate_matching_port_retry( "radio_type": "efr32", }, "old_discovery_info": { - "name": "Test", - "port": { - "path": "/dev/ttyTEST123", - "baudrate": 115200, - "flow_control": "hardware", - }, - "radio_type": "efr32", + "hw": { + "name": "Test", + "port": { + "path": "/dev/ttyTEST123", + "baudrate": 115200, + "flow_control": "hardware", + }, + "radio_type": "efr32", + } }, } @@ -290,13 +349,15 @@ async def test_migrate_non_matching_port( "radio_type": "efr32", }, "old_discovery_info": { - "name": "Test", - "port": { - "path": "/dev/ttyTEST456", - "baudrate": 115200, - "flow_control": "hardware", - }, - "radio_type": "efr32", + "hw": { + "name": "Test", + "port": { + "path": "/dev/ttyTEST456", + "baudrate": 115200, + "flow_control": "hardware", + }, + "radio_type": "efr32", + } }, }