From b0511519a1fb8cc85fa3e8d61064e5abaa68a896 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 8 Apr 2026 14:29:27 -0400 Subject: [PATCH] Expose async serial port scanning helper in USB integration (#167706) --- .../homeassistant_sky_connect/__init__.py | 4 +-- homeassistant/components/usb/__init__.py | 5 ++-- homeassistant/components/usb/utils.py | 8 ++++++ homeassistant/components/zha/config_flow.py | 8 ++++-- .../homeassistant_sky_connect/test_init.py | 2 +- tests/components/usb/__init__.py | 2 +- tests/components/usb/test_init.py | 27 ++++++++++++------- tests/components/zha/test_config_flow.py | 10 +++---- 8 files changed, 43 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/homeassistant_sky_connect/__init__.py b/homeassistant/components/homeassistant_sky_connect/__init__.py index a386a49894ad..317f8eacf72d 100644 --- a/homeassistant/components/homeassistant_sky_connect/__init__.py +++ b/homeassistant/components/homeassistant_sky_connect/__init__.py @@ -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] diff --git a/homeassistant/components/usb/__init__.py b/homeassistant/components/usb/__init__.py index 0ffba6bdb920..f21f24228f2d 100644 --- a/homeassistant/components/usb/__init__.py +++ b/homeassistant/components/usb/__init__.py @@ -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) ] diff --git a/homeassistant/components/usb/utils.py b/homeassistant/components/usb/utils.py index d9481ff8ba95..783b603aeae0 100644 --- a/homeassistant/components/usb/utils.py +++ b/homeassistant/components/usb/utils.py @@ -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.""" diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index 2342023540b4..d5a183a34a52 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -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: diff --git a/tests/components/homeassistant_sky_connect/test_init.py b/tests/components/homeassistant_sky_connect/test_init.py index 37039a968fb3..90c0cbbcc280 100644 --- a/tests/components/homeassistant_sky_connect/test_init.py +++ b/tests/components/homeassistant_sky_connect/test_init.py @@ -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", diff --git a/tests/components/usb/__init__.py b/tests/components/usb/__init__.py index 6db0cea1ffee..b849acdf0a98 100644 --- a/tests/components/usb/__init__.py +++ b/tests/components/usb/__init__.py @@ -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: diff --git a/tests/components/usb/test_init.py b/tests/components/usb/test_init.py index 6945c80ce07e..dfa0232f46d6 100644 --- a/tests/components/usb/test_init.py +++ b/tests/components/usb/test_init.py @@ -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) diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 3fce8b9ebf3d..ed35f04213d3 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -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, ), ):