From 74696a3fac3f6e3f2566330877d58750121f4448 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 23 Feb 2023 20:52:53 -0500 Subject: [PATCH] Name the Yellow-internal radio and multi-PAN addon as ZHA serial ports (#88208) * Expose the Yellow-internal radio and multi-PAN addon as named serial ports * Remove the serial number if it isn't available * Use consistent names for the addon and Zigbee radio * Add `homeassistant_hardware` and `_yellow` as `after_dependencies` * Handle `hassio` not existing when listing serial ports * Add unit tests --- homeassistant/components/zha/config_flow.py | 46 +++++++++++++++++++-- homeassistant/components/zha/manifest.json | 8 +++- tests/components/zha/test_config_flow.py | 44 ++++++++++++++++++++ 3 files changed, 94 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index 554a94b8450418..05dc67314ed719 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -7,6 +7,7 @@ from typing import Any import serial.tools.list_ports +from serial.tools.list_ports_common import ListPortInfo import voluptuous as vol import zigpy.backups from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH @@ -14,9 +15,13 @@ from homeassistant import config_entries from homeassistant.components import onboarding, usb, zeroconf from homeassistant.components.file_upload import process_uploaded_file +from homeassistant.components.hassio import AddonError, AddonState +from homeassistant.components.homeassistant_hardware import silabs_multiprotocol_addon +from homeassistant.components.homeassistant_yellow import hardware as yellow_hardware from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowHandler, FlowResult +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.selector import FileSelector, FileSelectorConfig from homeassistant.util import dt @@ -72,6 +77,41 @@ def _format_backup_choice( return f"{dt.as_local(backup.backup_time).strftime('%c')} ({identifier})" +async def list_serial_ports(hass: HomeAssistant) -> list[ListPortInfo]: + """List all serial ports, including the Yellow radio and the multi-PAN addon.""" + ports = await hass.async_add_executor_job(serial.tools.list_ports.comports) + + # Add useful info to the Yellow's serial port selection screen + try: + yellow_hardware.async_info(hass) + except HomeAssistantError: + pass + else: + yellow_radio = next(p for p in ports if p.device == "/dev/ttyAMA1") + yellow_radio.description = "Yellow Zigbee module" + yellow_radio.manufacturer = "Nabu Casa" + + # Present the multi-PAN addon as a setup option, if it's available + addon_manager = silabs_multiprotocol_addon.get_addon_manager(hass) + + try: + addon_info = await addon_manager.async_get_addon_info() + except (AddonError, KeyError): + addon_info = None + + if addon_info is not None and addon_info.state != AddonState.NOT_INSTALLED: + addon_port = ListPortInfo( + device=silabs_multiprotocol_addon.get_zigbee_socket(hass, addon_info), + skip_link_detection=True, + ) + + addon_port.description = "Multiprotocol add-on" + addon_port.manufacturer = "Nabu Casa" + ports.append(addon_port) + + return ports + + class BaseZhaFlow(FlowHandler): """Mixin for common ZHA flow steps and forms.""" @@ -120,9 +160,9 @@ async def async_step_choose_serial_port( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Choose a serial port.""" - ports = await self.hass.async_add_executor_job(serial.tools.list_ports.comports) + ports = await list_serial_ports(self.hass) list_of_ports = [ - f"{p}, s/n: {p.serial_number or 'n/a'}" + f"{p}{', s/n: ' + p.serial_number if p.serial_number else ''}" + (f" - {p.manufacturer}" if p.manufacturer else "") for p in ports ] @@ -146,7 +186,7 @@ async def async_step_choose_serial_port( return await self.async_step_manual_pick_radio_type() self._title = ( - f"{port.description}, s/n: {port.serial_number or 'n/a'}" + f"{port.description}{', s/n: ' + port.serial_number if port.serial_number else ''}" f" - {port.manufacturer}" if port.manufacturer else "" diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 090e171835d2ce..c3aefe5987a33d 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -1,7 +1,13 @@ { "domain": "zha", "name": "Zigbee Home Automation", - "after_dependencies": ["onboarding", "usb", "zeroconf"], + "after_dependencies": [ + "onboarding", + "usb", + "zeroconf", + "homeassistant_hardware", + "homeassistant_yellow" + ], "codeowners": ["@dmulcahey", "@adminiuga", "@puddly"], "config_flow": true, "dependencies": ["file_upload"], diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 0f7363bb011769..d95564519969a6 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -15,6 +15,7 @@ from homeassistant import config_entries from homeassistant.components import ssdp, usb, zeroconf +from homeassistant.components.hassio import AddonState from homeassistant.components.ssdp import ATTR_UPNP_MANUFACTURER_URL, ATTR_UPNP_SERIAL from homeassistant.components.zha import config_flow, radio_manager from homeassistant.components.zha.core.const import ( @@ -1840,3 +1841,46 @@ async def test_options_flow_migration_reset_old_adapter( user_input={}, ) assert result4["step_id"] == "choose_serial_port" + + +async def test_config_flow_port_yellow_port_name(hass: HomeAssistant) -> None: + """Test config flow serial port name for Yellow Zigbee radio.""" + port = com_port(device="/dev/ttyAMA1") + port.serial_number = None + port.manufacturer = None + port.description = None + + with patch( + "homeassistant.components.zha.config_flow.yellow_hardware.async_info" + ), patch("serial.tools.list_ports.comports", MagicMock(return_value=[port])): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + ) + + assert ( + result["data_schema"].schema["path"].container[0] + == "/dev/ttyAMA1 - Yellow Zigbee module - Nabu Casa" + ) + + +async def test_config_flow_port_multiprotocol_port_name(hass: HomeAssistant) -> None: + """Test config flow serial port name for multiprotocol add-on.""" + + with patch( + "homeassistant.components.hassio.addon_manager.AddonManager.async_get_addon_info" + ) as async_get_addon_info, patch( + "serial.tools.list_ports.comports", MagicMock(return_value=[]) + ): + async_get_addon_info.return_value.state = AddonState.RUNNING + async_get_addon_info.return_value.hostname = "core-silabs-multiprotocol" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + ) + + assert ( + result["data_schema"].schema["path"].container[0] + == "socket://core-silabs-multiprotocol:9999 - Multiprotocol add-on - Nabu Casa" + )