From 12f82689052482ac9b64c0cb5caab4675b451271 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 11:31:49 -0500 Subject: [PATCH 1/4] Migrate serial backend from pyserial to serialx (#1) * Initial plan * feat: migrate serial backend to serialx Co-authored-by: balloob <1444314+balloob@users.noreply.github.com> * docs: clarify serialx port initialization Co-authored-by: balloob <1444314+balloob@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: balloob <1444314+balloob@users.noreply.github.com> --- pymonoprice/__init__.py | 38 +++++++++++++++++++++----------------- pyproject.toml | 6 +----- requirements.txt | 3 +-- tests/test_monoprice.py | 4 +--- 4 files changed, 24 insertions(+), 27 deletions(-) diff --git a/pymonoprice/__init__.py b/pymonoprice/__init__.py index 5cfea53..b127cdf 100644 --- a/pymonoprice/__init__.py +++ b/pymonoprice/__init__.py @@ -3,10 +3,9 @@ import asyncio import logging import re -import serial +import serialx from dataclasses import dataclass from functools import wraps -from serial_asyncio_fast import create_serial_connection, SerialTransport from threading import RLock from typing import TYPE_CHECKING @@ -27,6 +26,10 @@ TIMEOUT = 2 # Number of seconds before serial operation timeout +class SerialTimeoutException(Exception): + """Serial read timeout.""" + + def synchronized( func: Callable[Concatenate[Monoprice, _P], _T] ) -> Callable[Concatenate[Monoprice, _P], _T]: @@ -123,23 +126,24 @@ def __init__(self, port_url: str, lock: RLock) -> None: Monoprice amplifier interface """ self._lock = lock - self._port = serial.serial_for_url(port_url, do_not_open=True) - self._port.baudrate = 9600 - self._port.stopbits = serial.STOPBITS_ONE - self._port.bytesize = serial.EIGHTBITS - self._port.parity = serial.PARITY_NONE - self._port.timeout = TIMEOUT - self._port.write_timeout = TIMEOUT + self._port = serialx.Serial( + port_url, + baudrate=9600, + stopbits=serialx.StopBits.ONE, + byte_size=8, + parity=serialx.Parity.NONE, + buffer_character_count=0, + buffer_burst_timeout=TIMEOUT, + ) + # serialx requires explicit open/configure outside of a context manager. self._port.open() + self._port.configure_port() def _send_request(self, request: bytes) -> None: """ :param request: request that is sent to the monoprice """ _LOGGER.debug('Sending "%s"', request) - # clear - self._port.reset_output_buffer() - self._port.reset_input_buffer() # send self._port.write(request) self._port.flush() @@ -157,7 +161,7 @@ def _process_request(self, request: bytes, num_eols_to_read: int = 1) -> str: while True: c = self._port.read(1) if not c: - raise serial.SerialTimeoutException( + raise SerialTimeoutException( "Connection timed out! Last received bytes {}".format( [hex(a) for a in result] ) @@ -394,12 +398,12 @@ def __init__(self) -> None: super().__init__() self._lock = asyncio.Lock() self._tasks: set[asyncio.Task[None]] = set() - self._transport: SerialTransport | None = None + self._transport: serialx.SerialTransport | None = None self._connected = asyncio.Event() self.q: asyncio.Queue[bytes] = asyncio.Queue() - def connection_made(self, transport: asyncio.BaseTransport) -> None: - self._transport = transport # type: ignore[assignment] + def connection_made(self, transport: serialx.SerialTransport) -> None: + self._transport = transport self._connected.set() _LOGGER.debug("port opened %s", self._transport) @@ -520,7 +524,7 @@ async def get_async_monoprice(port_url: str) -> MonopriceAsync: lock = asyncio.Lock() loop = asyncio.get_running_loop() - _, protocol = await create_serial_connection( + _, protocol = await serialx.create_serial_connection( loop, MonopriceProtocol, port_url, baudrate=9600 ) return MonopriceAsync(protocol, lock) # type: ignore[arg-type] diff --git a/pyproject.toml b/pyproject.toml index f67d83a..8883cb6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,8 +11,7 @@ license = { text = "MIT" } authors = [{ name = "On Freund", email = "onfreund@gmail.com" }] requires-python = ">=3.12" dependencies = [ - "pyserial>=3.4", - "pyserial-asyncio-fast>=0.16", + "serialx>=0.7.0", ] classifiers = [ "Development Status :: 4 - Beta", @@ -51,6 +50,3 @@ warn_unreachable = true warn_unused_configs = true warn_unused_ignores = true -[[tool.mypy.overrides]] -module = ["serial", "serial_asyncio_fast"] -ignore_missing_imports = true diff --git a/requirements.txt b/requirements.txt index 5874bac..839810c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1 @@ -pyserial>=3.4 -pyserial-asyncio-fast>=0.16 \ No newline at end of file +serialx>=0.7.0 diff --git a/tests/test_monoprice.py b/tests/test_monoprice.py index 9eb9646..8231c24 100644 --- a/tests/test_monoprice.py +++ b/tests/test_monoprice.py @@ -1,7 +1,5 @@ import unittest -import serial - import pymonoprice from pymonoprice import (get_monoprice, get_async_monoprice, ZoneStatus) from tests import create_dummy_port @@ -324,7 +322,7 @@ def test_restore_zone(self): self.assertEqual(0, len(self.responses)) def test_timeout(self): - with self.assertRaises(serial.SerialTimeoutException): + with self.assertRaises(pymonoprice.SerialTimeoutException): self.monoprice.set_source(3, 3) From f5307d5858a5e81a0f7de8ff02f57f2cd8713957 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 22 Apr 2026 17:15:35 -0400 Subject: [PATCH 2/4] Newer serialx --- pymonoprice/__init__.py | 15 ++++++--------- requirements.txt | 2 +- tests/test_monoprice.py | 3 ++- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/pymonoprice/__init__.py b/pymonoprice/__init__.py index b127cdf..af25da2 100644 --- a/pymonoprice/__init__.py +++ b/pymonoprice/__init__.py @@ -26,10 +26,6 @@ TIMEOUT = 2 # Number of seconds before serial operation timeout -class SerialTimeoutException(Exception): - """Serial read timeout.""" - - def synchronized( func: Callable[Concatenate[Monoprice, _P], _T] ) -> Callable[Concatenate[Monoprice, _P], _T]: @@ -132,18 +128,19 @@ def __init__(self, port_url: str, lock: RLock) -> None: stopbits=serialx.StopBits.ONE, byte_size=8, parity=serialx.Parity.NONE, - buffer_character_count=0, - buffer_burst_timeout=TIMEOUT, + read_timeout=TIMEOUT, + write_timeout=TIMEOUT, ) - # serialx requires explicit open/configure outside of a context manager. self._port.open() - self._port.configure_port() def _send_request(self, request: bytes) -> None: """ :param request: request that is sent to the monoprice """ _LOGGER.debug('Sending "%s"', request) + # clear + self._port.reset_output_buffer() + self._port.reset_input_buffer() # send self._port.write(request) self._port.flush() @@ -161,7 +158,7 @@ def _process_request(self, request: bytes, num_eols_to_read: int = 1) -> str: while True: c = self._port.read(1) if not c: - raise SerialTimeoutException( + raise serialx.SerialTimeoutException( "Connection timed out! Last received bytes {}".format( [hex(a) for a in result] ) diff --git a/requirements.txt b/requirements.txt index 839810c..5ddc9a3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -serialx>=0.7.0 +serialx>=1.4.1 diff --git a/tests/test_monoprice.py b/tests/test_monoprice.py index 8231c24..4cdb804 100644 --- a/tests/test_monoprice.py +++ b/tests/test_monoprice.py @@ -1,6 +1,7 @@ import unittest import pymonoprice +import serialx from pymonoprice import (get_monoprice, get_async_monoprice, ZoneStatus) from tests import create_dummy_port import asyncio @@ -322,7 +323,7 @@ def test_restore_zone(self): self.assertEqual(0, len(self.responses)) def test_timeout(self): - with self.assertRaises(pymonoprice.SerialTimeoutException): + with self.assertRaises(serialx.SerialTimeoutException): self.monoprice.set_source(3, 3) From 08df3b31d648b16064e766b52815b52842143892 Mon Sep 17 00:00:00 2001 From: On Freund Date: Sat, 27 Jun 2026 09:45:50 +0300 Subject: [PATCH 3/4] Update pymonoprice/__init__.py Co-authored-by: puddly <32534428+puddly@users.noreply.github.com> --- pymonoprice/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymonoprice/__init__.py b/pymonoprice/__init__.py index af25da2..7dfc231 100644 --- a/pymonoprice/__init__.py +++ b/pymonoprice/__init__.py @@ -122,7 +122,7 @@ def __init__(self, port_url: str, lock: RLock) -> None: Monoprice amplifier interface """ self._lock = lock - self._port = serialx.Serial( + self._port = serialx.serial_for_url( port_url, baudrate=9600, stopbits=serialx.StopBits.ONE, From 7e2780191fca58e68e55aa24ec87d93578f7c3f6 Mon Sep 17 00:00:00 2001 From: On Freund Date: Sat, 27 Jun 2026 10:28:06 +0300 Subject: [PATCH 4/4] Fix mypy error in connection_made override Use asyncio.BaseTransport to satisfy the LSP constraint from the base protocol. Co-Authored-By: Claude Sonnet 4.6 --- pymonoprice/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pymonoprice/__init__.py b/pymonoprice/__init__.py index 7dfc231..bb8415f 100644 --- a/pymonoprice/__init__.py +++ b/pymonoprice/__init__.py @@ -399,7 +399,8 @@ def __init__(self) -> None: self._connected = asyncio.Event() self.q: asyncio.Queue[bytes] = asyncio.Queue() - def connection_made(self, transport: serialx.SerialTransport) -> None: + def connection_made(self, transport: asyncio.BaseTransport) -> None: + assert isinstance(transport, serialx.SerialTransport) self._transport = transport self._connected.set() _LOGGER.debug("port opened %s", self._transport)