Skip to content

Commit

Permalink
Name the Yellow-internal radio and multi-PAN addon as ZHA serial ports (
Browse files Browse the repository at this point in the history
home-assistant#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
  • Loading branch information
puddly authored and balloob committed Feb 25, 2023
1 parent 70e1d14 commit 74696a3
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 4 deletions.
46 changes: 43 additions & 3 deletions homeassistant/components/zha/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,21 @@
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

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

Expand Down Expand Up @@ -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."""

Expand Down Expand Up @@ -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
]
Expand All @@ -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 ""
Expand Down
8 changes: 7 additions & 1 deletion homeassistant/components/zha/manifest.json
Original file line number Diff line number Diff line change
@@ -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"],
Expand Down
44 changes: 44 additions & 0 deletions tests/components/zha/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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"
)

0 comments on commit 74696a3

Please sign in to comment.