diff --git a/homeassistant/components/usb/__init__.py b/homeassistant/components/usb/__init__.py index 46950ba5b91..d4201d7f284 100644 --- a/homeassistant/components/usb/__init__.py +++ b/homeassistant/components/usb/__init__.py @@ -362,10 +362,33 @@ class USBDiscovery: 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.""" diff --git a/tests/components/usb/test_init.py b/tests/components/usb/test_init.py index effc63bf8aa..bbd802afc95 100644 --- a/tests/components/usb/test_init.py +++ b/tests/components/usb/test_init.py @@ -1054,3 +1054,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"