From 2693d512832b81c84ce3313749c96dadcf64809e Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sat, 1 Nov 2025 20:54:53 -0400 Subject: [PATCH 1/2] Ignore `/dev/ttyACM` when detecting Raspbee --- zigpy_deconz/zigbee/application.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/zigpy_deconz/zigbee/application.py b/zigpy_deconz/zigbee/application.py index f67339f..36db2dc 100644 --- a/zigpy_deconz/zigbee/application.py +++ b/zigpy_deconz/zigbee/application.py @@ -5,6 +5,7 @@ import asyncio import importlib.metadata import logging +import os.path import re import sys from typing import Any @@ -343,10 +344,13 @@ async def load_network_info(self, *, load_devices=False): node_info.manufacturer = "dresden elektronik" - if re.match( - r"/dev/tty(S|AMA|ACM)\d+", + resolved_device = await asyncio.get_running_loop().run_in_executor( + None, + os.path.realpath, self._config[zigpy.config.CONF_DEVICE][zigpy.config.CONF_DEVICE_PATH], - ): + ) + + if re.match(r"/dev/tty(S|AMA)\d+", resolved_device): node_info.model = "Raspbee" else: node_info.model = "Conbee" From c5243e33be6ffdff744ac16304e427fbe1f3a5e1 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sat, 1 Nov 2025 21:09:36 -0400 Subject: [PATCH 2/2] Add a test --- tests/test_utils.py | 32 +++++++++++++++++++++++++++++- zigpy_deconz/utils.py | 14 +++++++++++++ zigpy_deconz/zigbee/application.py | 13 ++++++------ 3 files changed, 51 insertions(+), 8 deletions(-) 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 36db2dc..652200f 100644 --- a/zigpy_deconz/zigbee/application.py +++ b/zigpy_deconz/zigbee/application.py @@ -5,8 +5,6 @@ import asyncio import importlib.metadata import logging -import os.path -import re import sys from typing import Any @@ -42,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__) @@ -344,16 +343,16 @@ async def load_network_info(self, *, load_devices=False): node_info.manufacturer = "dresden elektronik" - resolved_device = await asyncio.get_running_loop().run_in_executor( + is_usb = await asyncio.get_running_loop().run_in_executor( None, - os.path.realpath, + is_usb_serial_port, self._config[zigpy.config.CONF_DEVICE][zigpy.config.CONF_DEVICE_PATH], ) - if re.match(r"/dev/tty(S|AMA)\d+", resolved_device): - node_info.model = "Raspbee" - else: + if is_usb: node_info.model = "Conbee" + else: + node_info.model = "Raspbee" node_info.model += { FirmwarePlatform.Conbee: "",