diff --git a/tests/test_utils.py b/tests/test_utils.py index d3841d8..e91d545 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -2,7 +2,9 @@ import asyncio import logging -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, patch + +import pytest from zigpy_deconz import utils @@ -22,3 +24,31 @@ async def test_restart_forever(caplog): assert caplog.text.count("failed, restarting...") >= 2 assert caplog.text.count("RuntimeError") == 2 assert len(mock.mock_calls) >= 4 + + +@pytest.mark.parametrize( + ("device_path", "realpath_result", "expected_is_usb"), + [ + # Platform serial ports (Raspbee) + ("/dev/conbee", "/dev/ttyS0", False), + ("/dev/raspbee", "/dev/ttyAMA0", False), + ("/dev/ttyS0", "/dev/ttyS0", False), + ("/dev/ttyAMA0", "/dev/ttyAMA0", False), + ("/dev/ttyS1", "/dev/ttyS1", False), + ("/dev/ttyAMA1", "/dev/ttyAMA1", False), + # USB serial ports (Conbee) + ("/dev/conbee", "/dev/ttyUSB0", True), + ("/dev/ttyUSB0", "/dev/ttyUSB0", True), + ("/dev/ttyACM0", "/dev/ttyACM0", True), + ("/dev/ttyUSB1", "/dev/ttyUSB1", True), + ("/dev/ttyACM1", "/dev/ttyACM1", True), + # Symlink to USB serial (Conbee) + ("/dev/serial/by-id/usb-conbee", "/dev/ttyUSB0", True), + ], +) +def test_is_usb_serial_port(device_path, realpath_result, expected_is_usb): + """Test is_usb_serial_port with various device paths and realpath results.""" + with patch("zigpy_deconz.utils.os.path.realpath", return_value=realpath_result): + result = utils.is_usb_serial_port(device_path) + + assert result == expected_is_usb diff --git a/zigpy_deconz/utils.py b/zigpy_deconz/utils.py index fa3ab2e..9aa7330 100644 --- a/zigpy_deconz/utils.py +++ b/zigpy_deconz/utils.py @@ -5,6 +5,8 @@ import asyncio import functools import logging +import os.path +import re LOGGER = logging.getLogger(__name__) @@ -23,3 +25,15 @@ async def replacement(*args, **kwargs): await asyncio.sleep(restart_delay) return replacement + + +def is_usb_serial_port(device_path: str) -> bool: + """Check if a device path is a USB serial port.""" + resolved_device = os.path.realpath(device_path) + + # Platform serial ports (Raspbee) + if re.match(r"/dev/tty(S|AMA)\d+", resolved_device): + return False + + # Everything else is assumed to be USB serial + return True diff --git a/zigpy_deconz/zigbee/application.py b/zigpy_deconz/zigbee/application.py index f67339f..652200f 100644 --- a/zigpy_deconz/zigbee/application.py +++ b/zigpy_deconz/zigbee/application.py @@ -5,7 +5,6 @@ import asyncio import importlib.metadata import logging -import re import sys from typing import Any @@ -41,6 +40,7 @@ ) from zigpy_deconz.config import CONFIG_SCHEMA import zigpy_deconz.exception +from zigpy_deconz.utils import is_usb_serial_port LIB_VERSION = importlib.metadata.version("zigpy-deconz") LOGGER = logging.getLogger(__name__) @@ -343,13 +343,16 @@ async def load_network_info(self, *, load_devices=False): node_info.manufacturer = "dresden elektronik" - if re.match( - r"/dev/tty(S|AMA|ACM)\d+", + is_usb = await asyncio.get_running_loop().run_in_executor( + None, + is_usb_serial_port, self._config[zigpy.config.CONF_DEVICE][zigpy.config.CONF_DEVICE_PATH], - ): - node_info.model = "Raspbee" - else: + ) + + if is_usb: node_info.model = "Conbee" + else: + node_info.model = "Raspbee" node_info.model += { FirmwarePlatform.Conbee: "",