From 9712d4e9ab150db651673796bc055ad3f158e640 Mon Sep 17 00:00:00 2001 From: Sam Stenvall Date: Wed, 27 Nov 2024 14:40:12 +0200 Subject: [PATCH 1/4] Don't assume we've disconnected, actually disconnect Should hopefully fix #5 --- custom_components/extron/extron.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/custom_components/extron/extron.py b/custom_components/extron/extron.py index 957e2f8..856e1ee 100644 --- a/custom_components/extron/extron.py +++ b/custom_components/extron/extron.py @@ -60,6 +60,10 @@ async def disconnect(self): self._writer.close() await self._writer.wait_closed() + async def reconnect(self): + await self.disconnect() + await self.connect() + def is_connected(self) -> bool: return self._connected @@ -85,7 +89,7 @@ async def run_command(self, command: str): logger.warning("Connection seems to be broken, will attempt to reconnect") finally: if not self._connected: - await self.connect() + await self.reconnect() async def query_model_name(self): return await self.run_command("1I") From e07042591d7c9b919b21a8794caf9f1065315ebb Mon Sep 17 00:00:00 2001 From: Sam Stenvall Date: Wed, 27 Nov 2024 14:47:51 +0200 Subject: [PATCH 2/4] Enable the flake8-return ruff rules, fix reported issues --- custom_components/extron/extron.py | 6 ++++-- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/custom_components/extron/extron.py b/custom_components/extron/extron.py index 856e1ee..7a5d73c 100644 --- a/custom_components/extron/extron.py +++ b/custom_components/extron/extron.py @@ -38,6 +38,8 @@ async def _read_until(self, phrase: str) -> str | None: if b.endswith(phrase.encode()): return b.decode() + return None + async def attempt_login(self): async with self._semaphore: await self._read_until("Password:") @@ -80,8 +82,8 @@ async def run_command(self, command: str): if response is None: raise RuntimeError("Command failed") - else: - return response.strip() + + return response.strip() except TimeoutError: raise RuntimeError("Command timed out") except (ConnectionResetError, BrokenPipeError): diff --git a/pyproject.toml b/pyproject.toml index fcaa10a..a945c80 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,7 +51,7 @@ exclude = [ ignore = [] per-file-ignores = {} # https://docs.astral.sh/ruff/rules/ -select = ["E4", "E7", "E9", "F", "W", "N", "UP", "I"] +select = ["E4", "E7", "E9", "F", "W", "N", "UP", "I", "RET"] # Allow unused variables when underscore-prefixed. dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" From 6b5e2dd455c8486db9dddf7b778c4776f02c3f9f Mon Sep 17 00:00:00 2001 From: Sam Stenvall Date: Wed, 27 Nov 2024 14:48:38 +0200 Subject: [PATCH 3/4] Fix accidentally returning None from run_command() Should fix the situation described in https://github.com/NitorCreations/ha-extron/issues/5#issuecomment-2503729588 --- custom_components/extron/extron.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/custom_components/extron/extron.py b/custom_components/extron/extron.py index 7a5d73c..656ab16 100644 --- a/custom_components/extron/extron.py +++ b/custom_components/extron/extron.py @@ -76,7 +76,7 @@ async def _run_command_internal(self, command: str): return await self._read_until("\r\n") - async def run_command(self, command: str): + async def run_command(self, command: str) -> str: try: response = await asyncio.wait_for(self._run_command_internal(command), timeout=3) @@ -88,9 +88,10 @@ async def run_command(self, command: str): raise RuntimeError("Command timed out") except (ConnectionResetError, BrokenPipeError): self._connected = False - logger.warning("Connection seems to be broken, will attempt to reconnect") + raise RuntimeError("Connection was reset") finally: if not self._connected: + logger.warning("Connection seems to be broken, will attempt to reconnect") await self.reconnect() async def query_model_name(self): From 8fe8cb5980bf4067a7f03ef47ebce3259130ca4a Mon Sep 17 00:00:00 2001 From: Sam Stenvall Date: Wed, 27 Nov 2024 15:03:55 +0200 Subject: [PATCH 4/4] Raise ResponseError if we encounter an explicit error code Fixes #6 --- custom_components/extron/extron.py | 13 +++++++++++++ tests/test_extron.py | 15 +++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 tests/test_extron.py diff --git a/custom_components/extron/extron.py b/custom_components/extron/extron.py index 656ab16..33be466 100644 --- a/custom_components/extron/extron.py +++ b/custom_components/extron/extron.py @@ -1,11 +1,13 @@ import asyncio import logging +import re from asyncio import StreamReader, StreamWriter from asyncio.exceptions import TimeoutError from enum import Enum logger = logging.getLogger(__name__) +error_regexp = re.compile("E[0-9]{2}") class DeviceType(Enum): @@ -18,6 +20,14 @@ class AuthenticationError(Exception): pass +class ResponseError(Exception): + pass + + +def is_error_response(response: str) -> bool: + return error_regexp.match(response) is not None + + class ExtronDevice: def __init__(self, host: str, port: int, password: str) -> None: self._host = host @@ -83,6 +93,9 @@ async def run_command(self, command: str) -> str: if response is None: raise RuntimeError("Command failed") + if is_error_response(response): + raise ResponseError(f"Command failed with error code {response}") + return response.strip() except TimeoutError: raise RuntimeError("Command timed out") diff --git a/tests/test_extron.py b/tests/test_extron.py new file mode 100644 index 0000000..5b62e25 --- /dev/null +++ b/tests/test_extron.py @@ -0,0 +1,15 @@ +import unittest + +from custom_components.extron.extron import is_error_response + + +class ExtronTestCase(unittest.TestCase): + def test_is_error_response(self): + self.assertTrue(is_error_response("E01")) + self.assertTrue(is_error_response("E74\n")) + self.assertTrue(is_error_response("E23\r\n ")) + self.assertFalse(is_error_response("0")) + + +if __name__ == "__main__": + unittest.main()