From 5071c6ecbd0de98fd2bff616bca0304a476f1b1f Mon Sep 17 00:00:00 2001 From: Roy Date: Sun, 7 Jun 2026 21:41:44 +0200 Subject: [PATCH 1/8] fix(win32): handle missing asyncio protocol methods in _MethodProxy On Windows, when a serial port closes or disconnects, asyncio's proactor event loop calls eof_received() on the transport's protocol. _MethodProxy looks this up via __getattr__ from a fixed _mapping dict, but eof_received is not in that mapping, so a KeyError is raised: Fatal error: protocol.eof_received() call failed. ... File ".../serialx/platforms/serial_win32.py", line 434, in __getattr__ return self._mapping[name] KeyError: 'eof_received' This surfaces as a noisy Fatal error log on every HA shutdown/restart cycle when a Zigbee or other serial device is connected on Windows. --- serialx/platforms/serial_win32.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/serialx/platforms/serial_win32.py b/serialx/platforms/serial_win32.py index 0aaf567..220982d 100644 --- a/serialx/platforms/serial_win32.py +++ b/serialx/platforms/serial_win32.py @@ -451,9 +451,16 @@ def __init__(self, name: str, mapping: dict[str, Any]): self._name = name self._mapping = mapping - def __getattr__(self, name: str) -> Any: + def __getattr__(self, name: str): """Forward attribute access to the mapping.""" - return self._mapping[name] + try: + return self._mapping[name] + except KeyError: + # asyncio calls optional protocol methods (e.g. eof_received, + # pause_writing, resume_writing) that are not part of the + # serialx transport contract. Return a no-op so the proactor + # event loop does not log a fatal error on Windows shutdown. + return lambda *args, **kwargs: None class Win32SerialTransport(BaseSerialTransport): From 449bbb4af03a6ad37f383441cc7ae4213e9c9547 Mon Sep 17 00:00:00 2001 From: Roy Date: Sun, 7 Jun 2026 23:01:53 +0200 Subject: [PATCH 2/8] Modify __getattr__ to raise AttributeError on missing attrs Change the __getattr__ method to raise an AttributeError instead of returning a no-op lambda for missing attributes. --- serialx/platforms/serial_win32.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/serialx/platforms/serial_win32.py b/serialx/platforms/serial_win32.py index 220982d..6f1b3b8 100644 --- a/serialx/platforms/serial_win32.py +++ b/serialx/platforms/serial_win32.py @@ -451,16 +451,14 @@ def __init__(self, name: str, mapping: dict[str, Any]): self._name = name self._mapping = mapping - def __getattr__(self, name: str): + def __getattr__(self, name: str) -> Any: """Forward attribute access to the mapping.""" try: return self._mapping[name] except KeyError: - # asyncio calls optional protocol methods (e.g. eof_received, - # pause_writing, resume_writing) that are not part of the - # serialx transport contract. Return a no-op so the proactor - # event loop does not log a fatal error on Windows shutdown. - return lambda *args, **kwargs: None + raise AttributeError( + f"{self._name!r} has no attribute {name!r}" + ) from None class Win32SerialTransport(BaseSerialTransport): From 3dc6815fc7949564c5b00731c5867c0825807eb5 Mon Sep 17 00:00:00 2001 From: Roy Date: Mon, 8 Jun 2026 00:22:59 +0200 Subject: [PATCH 3/8] Implement eof_received method in serial_win32.py Add eof_received method to handle EOF in asyncio proactor --- serialx/platforms/serial_win32.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/serialx/platforms/serial_win32.py b/serialx/platforms/serial_win32.py index 6f1b3b8..46b1109 100644 --- a/serialx/platforms/serial_win32.py +++ b/serialx/platforms/serial_win32.py @@ -451,6 +451,13 @@ def __init__(self, name: str, mapping: dict[str, Any]): self._name = name self._mapping = mapping + def eof_received(self): + # PATCH: asyncio proactor calls this on pipe EOF; _MethodProxy doesn't + # implement it, causing a fatal AttributeError. Return False to signal + # the transport should close normally. + # See: asyncio/proactor_events.py _eof_received() + return False + def __getattr__(self, name: str) -> Any: """Forward attribute access to the mapping.""" try: From 078ba911c3ba80f11f1fc65b6c5e84b1fa6c5ed9 Mon Sep 17 00:00:00 2001 From: Roy Date: Mon, 8 Jun 2026 00:29:14 +0200 Subject: [PATCH 4/8] Annotate eof_received method with return type Add return type annotation for eof_received method. --- serialx/platforms/serial_win32.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/serialx/platforms/serial_win32.py b/serialx/platforms/serial_win32.py index 46b1109..1f82f02 100644 --- a/serialx/platforms/serial_win32.py +++ b/serialx/platforms/serial_win32.py @@ -451,7 +451,8 @@ def __init__(self, name: str, mapping: dict[str, Any]): self._name = name self._mapping = mapping - def eof_received(self): + def eof_received(self) -> bool: + """Handle EOF by signalling the transport to close.""" # PATCH: asyncio proactor calls this on pipe EOF; _MethodProxy doesn't # implement it, causing a fatal AttributeError. Return False to signal # the transport should close normally. From 1252be74b2a95cf1546cb08158de07961c54c094 Mon Sep 17 00:00:00 2001 From: Roy Date: Mon, 8 Jun 2026 00:33:28 +0200 Subject: [PATCH 5/8] Add pre-commit configuration for code quality checks --- .github/workflows/pre-commit-config.yaml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .github/workflows/pre-commit-config.yaml diff --git a/.github/workflows/pre-commit-config.yaml b/.github/workflows/pre-commit-config.yaml new file mode 100644 index 0000000..b8fdb51 --- /dev/null +++ b/.github/workflows/pre-commit-config.yaml @@ -0,0 +1,13 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.9.0 + hooks: + - id: ruff + - id: ruff-format + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-toml From 2761c1d51cfcaaafc25e3d56f58860d97febc872 Mon Sep 17 00:00:00 2001 From: Roy Date: Mon, 8 Jun 2026 00:42:05 +0200 Subject: [PATCH 6/8] Simplify AttributeError raising in serial_win32.py --- serialx/platforms/serial_win32.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/serialx/platforms/serial_win32.py b/serialx/platforms/serial_win32.py index 1f82f02..c81366d 100644 --- a/serialx/platforms/serial_win32.py +++ b/serialx/platforms/serial_win32.py @@ -464,9 +464,7 @@ def __getattr__(self, name: str) -> Any: try: return self._mapping[name] except KeyError: - raise AttributeError( - f"{self._name!r} has no attribute {name!r}" - ) from None + raise AttributeError(f"{self._name!r} has no attribute {name!r}") from None class Win32SerialTransport(BaseSerialTransport): From b47f3c9b44dd87f9b815c96232d4ec7b306df8ac Mon Sep 17 00:00:00 2001 From: Roy Date: Mon, 8 Jun 2026 00:45:23 +0200 Subject: [PATCH 7/8] Delete .github/workflows/pre-commit-config.yaml --- .github/workflows/pre-commit-config.yaml | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 .github/workflows/pre-commit-config.yaml diff --git a/.github/workflows/pre-commit-config.yaml b/.github/workflows/pre-commit-config.yaml deleted file mode 100644 index b8fdb51..0000000 --- a/.github/workflows/pre-commit-config.yaml +++ /dev/null @@ -1,13 +0,0 @@ -repos: - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.0 - hooks: - - id: ruff - - id: ruff-format - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 - hooks: - - id: trailing-whitespace - - id: end-of-file-fixer - - id: check-yaml - - id: check-toml From 071bd15fc0e43bf8cdbc0b5bc7b1ec3275dc8729 Mon Sep 17 00:00:00 2001 From: Roy Date: Mon, 8 Jun 2026 01:49:57 +0200 Subject: [PATCH 8/8] Update serial_win32.py --- serialx/platforms/serial_win32.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/serialx/platforms/serial_win32.py b/serialx/platforms/serial_win32.py index c81366d..662faf6 100644 --- a/serialx/platforms/serial_win32.py +++ b/serialx/platforms/serial_win32.py @@ -453,10 +453,6 @@ def __init__(self, name: str, mapping: dict[str, Any]): def eof_received(self) -> bool: """Handle EOF by signalling the transport to close.""" - # PATCH: asyncio proactor calls this on pipe EOF; _MethodProxy doesn't - # implement it, causing a fatal AttributeError. Return False to signal - # the transport should close normally. - # See: asyncio/proactor_events.py _eof_received() return False def __getattr__(self, name: str) -> Any: