Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prioritize the correct CP2102N serial port on macOS #116461

Draft
wants to merge 1 commit into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
31 changes: 27 additions & 4 deletions homeassistant/components/usb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,10 +362,33 @@ async def _async_process_discovered_usb_device(self, device: USBDevice) -> None:

async def _async_process_ports(self, ports: list[ListPortInfo]) -> None:
"""Process each discovered port."""
for port in ports:
if port.vid is None and port.pid is None:
continue
await self._async_process_discovered_usb_device(usb_device_from_port(port))
usb_devices = [
usb_device_from_port(port)
for port in ports
if port.vid is not None or port.pid is not None
]

# CP2102N chips create *two* serial ports on macOS: `/dev/cu.usbserial-` and
# `/dev/cu.SLAB_USBtoUART*`. The former does not work and we should ignore them.
if sys.platform == "darwin":
silabs_serials = {
dev.serial_number
for dev in usb_devices
if dev.device.startswith("/dev/cu.SLAB_USBtoUART")
}

usb_devices = [
dev
for dev in usb_devices
if dev.serial_number not in silabs_serials
or (
dev.serial_number in silabs_serials
and dev.device.startswith("/dev/cu.SLAB_USBtoUART")
)
]

for usb_device in usb_devices:
await self._async_process_discovered_usb_device(usb_device)

async def _async_scan_serial(self) -> None:
"""Scan serial ports."""
Expand Down
106 changes: 106 additions & 0 deletions tests/components/usb/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -1052,3 +1052,109 @@ async def test_resolve_serial_by_id(
assert len(mock_config_flow.mock_calls) == 1
assert mock_config_flow.mock_calls[0][1][0] == "test1"
assert mock_config_flow.mock_calls[0][2]["data"].device == "/dev/serial/by-id/bla"


@pytest.mark.parametrize(
"ports",
[
[
MagicMock(
device="/dev/cu.usbserial-2120",
vid=0x3039,
pid=0x3039,
serial_number=conbee_device.serial_number,
manufacturer=conbee_device.manufacturer,
description=conbee_device.description,
),
MagicMock(
device="/dev/cu.usbserial-1120",
vid=0x3039,
pid=0x3039,
serial_number=slae_sh_device.serial_number,
manufacturer=slae_sh_device.manufacturer,
description=slae_sh_device.description,
),
MagicMock(
device="/dev/cu.SLAB_USBtoUART",
vid=0x3039,
pid=0x3039,
serial_number=conbee_device.serial_number,
manufacturer=conbee_device.manufacturer,
description=conbee_device.description,
),
MagicMock(
device="/dev/cu.SLAB_USBtoUART2",
vid=0x3039,
pid=0x3039,
serial_number=slae_sh_device.serial_number,
manufacturer=slae_sh_device.manufacturer,
description=slae_sh_device.description,
),
],
[
MagicMock(
device="/dev/cu.SLAB_USBtoUART2",
vid=0x3039,
pid=0x3039,
serial_number=slae_sh_device.serial_number,
manufacturer=slae_sh_device.manufacturer,
description=slae_sh_device.description,
),
MagicMock(
device="/dev/cu.SLAB_USBtoUART",
vid=0x3039,
pid=0x3039,
serial_number=conbee_device.serial_number,
manufacturer=conbee_device.manufacturer,
description=conbee_device.description,
),
MagicMock(
device="/dev/cu.usbserial-1120",
vid=0x3039,
pid=0x3039,
serial_number=slae_sh_device.serial_number,
manufacturer=slae_sh_device.manufacturer,
description=slae_sh_device.description,
),
MagicMock(
device="/dev/cu.usbserial-2120",
vid=0x3039,
pid=0x3039,
serial_number=conbee_device.serial_number,
manufacturer=conbee_device.manufacturer,
description=conbee_device.description,
),
],
],
)
async def test_cp2102n_ordering_on_macos(
ports: list[MagicMock], hass: HomeAssistant, hass_ws_client: WebSocketGenerator
) -> None:
"""Test CP2102N ordering on macOS."""

new_usb = [
{"domain": "test1", "vid": "3039", "pid": "3039", "description": "*2652*"}
]

with (
patch("sys.platform", "darwin"),
patch("pyudev.Context", side_effect=ImportError),
patch("homeassistant.components.usb.async_get_usb", return_value=new_usb),
patch("homeassistant.components.usb.comports", return_value=ports),
patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
):
assert await async_setup_component(hass, "usb", {"usb": {}})
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
ws_client = await hass_ws_client(hass)
await ws_client.send_json({"id": 1, "type": "usb/scan"})
response = await ws_client.receive_json()
assert response["success"]
await hass.async_block_till_done()

assert len(mock_config_flow.mock_calls) == 1
assert mock_config_flow.mock_calls[0][1][0] == "test1"

# We always use `cu.SLAB_USBtoUART`
assert mock_config_flow.mock_calls[0][2]["data"].device == "/dev/cu.SLAB_USBtoUART2"