Expose async serial port scanning helper in USB integration (#167706)

This commit is contained in:
puddly
2026-04-08 14:29:27 -04:00
committed by GitHub
parent 038b583888
commit b0511519a1
8 changed files with 43 additions and 23 deletions

View File

@@ -13,7 +13,7 @@ from homeassistant.components.homeassistant_hardware.util import guess_firmware_
from homeassistant.components.usb import (
USBDevice,
async_register_port_event_callback,
scan_serial_ports,
async_scan_serial_ports,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
@@ -163,7 +163,7 @@ async def async_migrate_entry(
key not in config_entry.data
for key in (VID, PID, MANUFACTURER, PRODUCT, SERIAL_NUMBER)
):
serial_ports = await hass.async_add_executor_job(scan_serial_ports)
serial_ports = await async_scan_serial_ports(hass)
serial_ports_info = {port.device: port for port in serial_ports}
device = config_entry.data[DEVICE]

View File

@@ -37,7 +37,8 @@ from .models import (
USBDevice,
)
from .utils import (
scan_serial_ports,
async_scan_serial_ports,
scan_serial_ports, # noqa: F401
usb_device_from_path, # noqa: F401
usb_device_from_port, # noqa: F401
usb_device_matches_matcher,
@@ -433,7 +434,7 @@ class USBDiscovery:
# Only consider USB-serial ports for discovery
usb_ports = [
p
for p in await self.hass.async_add_executor_job(scan_serial_ports)
for p in await async_scan_serial_ports(self.hass)
if isinstance(p, USBDevice)
]

View File

@@ -10,6 +10,7 @@ import os
from serial.tools.list_ports import comports
from serial.tools.list_ports_common import ListPortInfo
from homeassistant.core import HomeAssistant
from homeassistant.helpers.service_info.usb import UsbServiceInfo
from homeassistant.loader import USBMatcher
@@ -76,6 +77,13 @@ def scan_serial_ports() -> Sequence[USBDevice | SerialDevice]:
return serial_ports
async def async_scan_serial_ports(
hass: HomeAssistant,
) -> Sequence[USBDevice | SerialDevice]:
"""Scan serial ports and return USB and other serial devices, async."""
return await hass.async_add_executor_job(scan_serial_ports)
def usb_device_from_path(device_path: str) -> USBDevice | None:
"""Get USB device info from a device path."""

View File

@@ -26,7 +26,11 @@ from homeassistant.components.homeassistant_hardware.firmware_config_flow import
ZigbeeFlowStrategy,
)
from homeassistant.components.homeassistant_yellow import hardware as yellow_hardware
from homeassistant.components.usb import SerialDevice, USBDevice, scan_serial_ports
from homeassistant.components.usb import (
SerialDevice,
USBDevice,
async_scan_serial_ports,
)
from homeassistant.config_entries import (
SOURCE_IGNORE,
SOURCE_ZEROCONF,
@@ -155,7 +159,7 @@ def _format_serial_port_choice(
async def list_serial_ports(hass: HomeAssistant) -> list[USBDevice | SerialDevice]:
"""List all serial ports, including the Yellow radio and the multi-PAN addon."""
ports: list[USBDevice | SerialDevice] = []
ports.extend(await hass.async_add_executor_job(scan_serial_ports))
ports.extend(await async_scan_serial_ports(hass))
# Add useful info to the Yellow's serial port selection screen
try:

View File

@@ -265,7 +265,7 @@ async def test_bad_config_entry_fixing(hass: HomeAssistant) -> None:
fixable_entry.add_to_hass(hass)
with patch(
"homeassistant.components.homeassistant_sky_connect.scan_serial_ports",
"homeassistant.components.homeassistant_sky_connect.async_scan_serial_ports",
return_value=[
USBDevice(
device="/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_4f5f3b26d59f8714a78b599690741999-if00-port0",

View File

@@ -21,7 +21,7 @@ def force_usb_polling_watcher():
def patch_scanned_serial_ports(**kwargs) -> None:
"""Patch the USB integration's list of scanned serial ports."""
return patch("homeassistant.components.usb.scan_serial_ports", **kwargs)
return patch("homeassistant.components.usb.utils.scan_serial_ports", **kwargs)
async def async_request_scan(hass: HomeAssistant) -> None:

View File

@@ -12,7 +12,10 @@ from homeassistant import config_entries
from homeassistant.components import usb
from homeassistant.components.usb import DOMAIN
from homeassistant.components.usb.models import SerialDevice, USBDevice
from homeassistant.components.usb.utils import scan_serial_ports, usb_device_from_path
from homeassistant.components.usb.utils import (
async_scan_serial_ports,
usb_device_from_path,
)
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
@@ -1293,8 +1296,10 @@ async def test_register_port_event_callback_failure(
assert "Failure 2" in caplog.text
def test_scan_serial_ports_with_unique_symlinks() -> None:
"""Test scan_serial_ports returns devices with unique /dev/serial/by-id paths."""
async def test_async_scan_serial_ports_with_unique_symlinks(
hass: HomeAssistant,
) -> None:
"""Test async_scan_serial_ports returns devices with unique /dev/serial/by-id paths."""
entry1 = MagicMock(spec_set=os.DirEntry)
entry1.is_symlink.return_value = True
entry1.path = "/dev/serial/by-id/usb-device1"
@@ -1335,7 +1340,7 @@ def test_scan_serial_ports_with_unique_symlinks() -> None:
return_value=[mock_port1, mock_port2],
),
):
devices = scan_serial_ports()
devices = await async_scan_serial_ports(hass)
assert len(devices) == 2
assert devices[0].device == "/dev/serial/by-id/usb-device1"
@@ -1344,8 +1349,10 @@ def test_scan_serial_ports_with_unique_symlinks() -> None:
assert devices[1].vid == "ABCD"
def test_scan_serial_ports_without_unique_symlinks() -> None:
"""Test scan_serial_ports returns devices with original paths when no symlinks exist."""
async def test_async_scan_serial_ports_without_unique_symlinks(
hass: HomeAssistant,
) -> None:
"""Test async_scan_serial_ports returns devices with original paths when no symlinks exist."""
mock_port = MagicMock()
mock_port.device = "/dev/ttyUSB0"
mock_port.vid = 0x1234
@@ -1362,15 +1369,15 @@ def test_scan_serial_ports_without_unique_symlinks() -> None:
return_value=[mock_port],
),
):
devices = scan_serial_ports()
devices = await async_scan_serial_ports(hass)
assert len(devices) == 1
assert devices[0].device == "/dev/ttyUSB0"
assert devices[0].vid == "1234"
def test_scan_serial_ports_no_vid_pid() -> None:
"""Test scan_serial_ports returns devices without VID:PID."""
async def test_async_scan_serial_ports_no_vid_pid(hass: HomeAssistant) -> None:
"""Test async_scan_serial_ports returns devices without VID:PID."""
mock_port = MagicMock()
mock_port.device = "/dev/ttyAMA1"
mock_port.vid = None
@@ -1387,7 +1394,7 @@ def test_scan_serial_ports_no_vid_pid() -> None:
return_value=[mock_port],
),
):
devices = scan_serial_ports()
devices = await async_scan_serial_ports(hass)
assert len(devices) == 1
assert isinstance(devices[0], SerialDevice)

View File

@@ -2844,7 +2844,7 @@ async def test_config_flow_port_yellow_port_name(
with (
patch("homeassistant.components.zha.config_flow.yellow_hardware.async_info"),
patch(
"homeassistant.components.zha.config_flow.scan_serial_ports",
"homeassistant.components.zha.config_flow.async_scan_serial_ports",
return_value=[port],
),
):
@@ -2866,7 +2866,7 @@ async def test_config_flow_ports_no_hassio(hass: HomeAssistant) -> None:
with (
patch("homeassistant.components.zha.config_flow.is_hassio", return_value=False),
patch(
"homeassistant.components.zha.config_flow.scan_serial_ports",
"homeassistant.components.zha.config_flow.async_scan_serial_ports",
return_value=[],
),
):
@@ -2884,7 +2884,7 @@ async def test_config_flow_port_multiprotocol_port_name(hass: HomeAssistant) ->
"homeassistant.components.hassio.addon_manager.AddonManager.async_get_addon_info"
) as async_get_addon_info,
patch(
"homeassistant.components.zha.config_flow.scan_serial_ports",
"homeassistant.components.zha.config_flow.async_scan_serial_ports",
return_value=[],
),
):
@@ -2909,7 +2909,7 @@ async def test_config_flow_port_no_multiprotocol(hass: HomeAssistant) -> None:
side_effect=AddonError,
),
patch(
"homeassistant.components.zha.config_flow.scan_serial_ports",
"homeassistant.components.zha.config_flow.async_scan_serial_ports",
return_value=[],
),
):
@@ -2974,7 +2974,7 @@ async def test_list_serial_ports_ignored_devices(hass: HomeAssistant) -> None:
with (
patch("homeassistant.components.zha.config_flow.is_hassio", return_value=False),
patch(
"homeassistant.components.zha.config_flow.scan_serial_ports",
"homeassistant.components.zha.config_flow.async_scan_serial_ports",
return_value=mock_ports,
),
):