From 0685ed7d73cf2a690a49877afe259a92f2c27158 Mon Sep 17 00:00:00 2001 From: mvn23 Date: Sun, 15 Sep 2024 21:40:20 +0200 Subject: [PATCH 1/3] Move poll task generation into a function in poll_task.py Introduce OpenThermPollTaskName class --- pyotgw/poll_task.py | 37 +++++++++++++++++++++++++++++++++++++ pyotgw/pyotgw.py | 38 +++++++------------------------------- tests/test_poll_task.py | 16 ++++++++-------- tests/test_pyotgw.py | 10 +++++----- 4 files changed, 57 insertions(+), 44 deletions(-) diff --git a/pyotgw/poll_task.py b/pyotgw/poll_task.py index 4bcf7f4..42deeee 100644 --- a/pyotgw/poll_task.py +++ b/pyotgw/poll_task.py @@ -4,9 +4,12 @@ import asyncio from collections.abc import Callable +from enum import StrEnum import logging from typing import TYPE_CHECKING +from . import vars as v + if TYPE_CHECKING: from .pyotgw import OpenThermGateway from .types import OpenThermDataSource, OpenThermReport @@ -14,6 +17,40 @@ _LOGGER = logging.getLogger(__name__) +class OpenThermPollTaskName(StrEnum): + """Poll task names.""" + + GPIO_STATE = "gpio_state" + + +def get_all_poll_tasks(gateway: OpenThermGateway): + """Get all poll tasks for a gateway.""" + return { + OpenThermPollTaskName.GPIO_STATE: OpenThermPollTask( + OpenThermPollTaskName.GPIO_STATE, + gateway, + OpenThermReport.GPIO_STATES, + { + OpenThermDataSource.GATEWAY: { + v.OTGW_GPIO_A_STATE: 0, + v.OTGW_GPIO_B_STATE: 0, + }, + }, + ( + lambda: 0 + in ( + gateway.status.status[OpenThermDataSource.GATEWAY].get( + v.OTGW_GPIO_A + ), + gateway.status.status[OpenThermDataSource.GATEWAY].get( + v.OTGW_GPIO_B + ), + ) + ), + ) + } + + class OpenThermPollTask: """ Describes a task that polls the gateway for certain reports. diff --git a/pyotgw/pyotgw.py b/pyotgw/pyotgw.py index 66760bb..0fd1d99 100644 --- a/pyotgw/pyotgw.py +++ b/pyotgw/pyotgw.py @@ -6,11 +6,11 @@ import logging from collections.abc import Awaitable, Callable from datetime import datetime -from typing import TYPE_CHECKING, Final, Literal +from typing import TYPE_CHECKING, Literal from . import vars as v from .connection import ConnectionManager -from .poll_task import OpenThermPollTask +from .poll_task import get_all_poll_tasks, OpenThermPollTaskName from .reports import convert_report_response_to_status_update from .status import StatusManager from .types import ( @@ -26,39 +26,13 @@ _LOGGER = logging.getLogger(__name__) -GPIO_POLL_TASK_NAME: Final = "gpio" - - class OpenThermGateway: # pylint: disable=too-many-public-methods """Main OpenThermGateway object abstraction""" def __init__(self) -> None: """Create an OpenThermGateway object.""" self._transport = None - self._poll_tasks = { - GPIO_POLL_TASK_NAME: OpenThermPollTask( - GPIO_POLL_TASK_NAME, - self, - OpenThermReport.GPIO_STATES, - { - OpenThermDataSource.GATEWAY: { - v.OTGW_GPIO_A_STATE: 0, - v.OTGW_GPIO_B_STATE: 0, - }, - }, - ( - lambda: 0 - in ( - self.status.status[OpenThermDataSource.GATEWAY].get( - v.OTGW_GPIO_A - ), - self.status.status[OpenThermDataSource.GATEWAY].get( - v.OTGW_GPIO_B - ), - ) - ), - ) - } + self._poll_tasks = get_all_poll_tasks(self) self._protocol = None self._skip_init = False self.status = StatusManager() @@ -519,7 +493,9 @@ async def set_gpio_mode( var = getattr(v, f"OTGW_GPIO_{gpio_id}") status_otgw[var] = ret self.status.submit_partial_update(OpenThermDataSource.GATEWAY, status_otgw) - await self._poll_tasks[GPIO_POLL_TASK_NAME].start_or_stop_as_needed() + await self._poll_tasks[ + OpenThermPollTaskName.GPIO_STATE + ].start_or_stop_as_needed() return ret async def set_setback_temp( @@ -865,7 +841,7 @@ async def _wait_for_cmd( self._protocol.command_processor.issue_cmd(cmd, value), timeout, ) - except asyncio.TimeoutError: + except TimeoutError: _LOGGER.error("Timed out waiting for command: %s, value: %s.", cmd, value) return except (RuntimeError, SyntaxError, ValueError) as exc: diff --git a/tests/test_poll_task.py b/tests/test_poll_task.py index d6c2b90..785c5f6 100644 --- a/tests/test_poll_task.py +++ b/tests/test_poll_task.py @@ -3,8 +3,8 @@ import pytest from unittest.mock import call, AsyncMock -from pyotgw.poll_task import OpenThermPollTask -from pyotgw.pyotgw import OpenThermGateway, GPIO_POLL_TASK_NAME +from pyotgw.poll_task import OpenThermPollTask, OpenThermPollTaskName +from pyotgw.pyotgw import OpenThermGateway from pyotgw.types import OpenThermDataSource, OpenThermReport import pyotgw.vars as v @@ -12,7 +12,7 @@ TASK_TEST_PARAMETERS = ("task_name",) TASK_TEST_VALUES = [ - (GPIO_POLL_TASK_NAME,), + (OpenThermPollTaskName.GPIO_STATE,), ] @@ -31,7 +31,7 @@ async def test_init(pygw: OpenThermGateway, task_name: str) -> None: @pytest.mark.asyncio async def test_gpio_start_stop(pygw: OpenThermGateway) -> None: """Test task.start() and task.stop()""" - task = pygw._poll_tasks[GPIO_POLL_TASK_NAME] + task = pygw._poll_tasks[OpenThermPollTaskName.GPIO_STATE] assert not task.is_running pygw.status.submit_partial_update(OpenThermDataSource.GATEWAY, {v.OTGW_GPIO_A: 0}) task.start() @@ -43,7 +43,7 @@ async def test_gpio_start_stop(pygw: OpenThermGateway) -> None: @pytest.mark.asyncio async def test_gpio_start_or_stop_as_needed(pygw: OpenThermGateway) -> None: """Test task.start_or_stop_as_needed()""" - task = pygw._poll_tasks[GPIO_POLL_TASK_NAME] + task = pygw._poll_tasks[OpenThermPollTaskName.GPIO_STATE] assert task.is_running is False await task.start_or_stop_as_needed() assert task.is_running is False @@ -59,7 +59,7 @@ async def test_gpio_start_or_stop_as_needed(pygw: OpenThermGateway) -> None: def test_gpio_should_run(pygw: OpenThermGateway) -> None: """Test task.should_run()""" - task = pygw._poll_tasks[GPIO_POLL_TASK_NAME] + task = pygw._poll_tasks[OpenThermPollTaskName.GPIO_STATE] assert task.should_run is False pygw.status.submit_partial_update(OpenThermDataSource.GATEWAY, {v.OTGW_GPIO_A: 0}) assert task.should_run is True @@ -68,7 +68,7 @@ def test_gpio_should_run(pygw: OpenThermGateway) -> None: @pytest.mark.asyncio async def test_gpio_is_running(pygw: OpenThermGateway) -> None: """Test task.should_run()""" - task = pygw._poll_tasks[GPIO_POLL_TASK_NAME] + task = pygw._poll_tasks[OpenThermPollTaskName.GPIO_STATE] assert task.is_running is False pygw.status.submit_partial_update(OpenThermDataSource.GATEWAY, {v.OTGW_GPIO_A: 0}) task.start() @@ -79,7 +79,7 @@ async def test_gpio_is_running(pygw: OpenThermGateway) -> None: async def test_gpio_polling_routing(pygw: OpenThermGateway) -> None: """Test task._polling_routing()""" pygw.get_report = AsyncMock() - task = pygw._poll_tasks[GPIO_POLL_TASK_NAME] + task = pygw._poll_tasks[OpenThermPollTaskName.GPIO_STATE] task._interval = 0.01 pygw.status.submit_partial_update(OpenThermDataSource.GATEWAY, {v.OTGW_GPIO_A: 0}) task.start() diff --git a/tests/test_pyotgw.py b/tests/test_pyotgw.py index 41fd0f1..915c455 100644 --- a/tests/test_pyotgw.py +++ b/tests/test_pyotgw.py @@ -9,7 +9,7 @@ import serial import pyotgw.vars as v -from pyotgw.pyotgw import GPIO_POLL_TASK_NAME +from pyotgw.poll_task import OpenThermPollTaskName from pyotgw.types import ( OpenThermCommand, OpenThermDataSource, @@ -28,11 +28,11 @@ async def test_cleanup(pygw): pygw.status.submit_partial_update(OpenThermDataSource.GATEWAY, {v.OTGW_GPIO_A: 0}) pygw.loop = asyncio.get_running_loop() - pygw._poll_tasks[GPIO_POLL_TASK_NAME].start() + pygw._poll_tasks[OpenThermPollTaskName.GPIO_STATE].start() - assert pygw._poll_tasks[GPIO_POLL_TASK_NAME].is_running + assert pygw._poll_tasks[OpenThermPollTaskName.GPIO_STATE].is_running await pygw.cleanup() - assert not pygw._poll_tasks[GPIO_POLL_TASK_NAME].is_running + assert not pygw._poll_tasks[OpenThermPollTaskName.GPIO_STATE].is_running @pytest.mark.asyncio @@ -56,7 +56,7 @@ async def test_connect_success_and_reconnect_with_gpio(caplog, pygw, pygw_proto) await pygw.connect("loop://") init_and_wait.assert_called_once() - assert pygw._poll_tasks[GPIO_POLL_TASK_NAME].is_running + assert pygw._poll_tasks[OpenThermPollTaskName.GPIO_STATE].is_running await pygw.connection.watchdog.stop() await pygw.connection.watchdog._callback() From 5a02ca7432fe035e5c335b31b6564ac92c5a44d1 Mon Sep 17 00:00:00 2001 From: mvn23 Date: Mon, 16 Sep 2024 11:58:39 +0200 Subject: [PATCH 2/3] * Configure ruff linter * Apply linter fixes --- pyotgw/commandprocessor.py | 2 +- pyotgw/connection.py | 4 ++-- pyotgw/messageprocessor.py | 3 +-- pyotgw/protocol.py | 2 +- pyotgw/pyotgw.py | 11 +++++++---- pyotgw/status.py | 2 +- pyproject.toml | 10 ++++++++++ tests/data.py | 2 +- tests/test_messages.py | 2 +- tests/test_poll_task.py | 3 ++- tests/test_pyotgw.py | 8 ++++---- tests/test_reports.py | 6 ++++-- tests/test_status.py | 2 +- 13 files changed, 36 insertions(+), 21 deletions(-) diff --git a/pyotgw/commandprocessor.py b/pyotgw/commandprocessor.py index 96cc890..13ae133 100644 --- a/pyotgw/commandprocessor.py +++ b/pyotgw/commandprocessor.py @@ -3,9 +3,9 @@ from __future__ import annotations import asyncio +from asyncio.queues import QueueFull import logging import re -from asyncio.queues import QueueFull from typing import TYPE_CHECKING from . import vars as v diff --git a/pyotgw/connection.py b/pyotgw/connection.py index 4358314..01b753d 100644 --- a/pyotgw/connection.py +++ b/pyotgw/connection.py @@ -7,10 +7,10 @@ from __future__ import annotations import asyncio -import logging from collections.abc import Awaitable, Callable from dataclasses import dataclass from functools import partial +import logging from typing import TYPE_CHECKING, Literal import serial @@ -181,7 +181,7 @@ async def _attempt_connect(self) -> tuple[asyncio.Transport, asyncio.Protocol]: ) self._error = err - except asyncio.TimeoutError as err: + except TimeoutError as err: if not isinstance(err, type(self._error)): _LOGGER.error( "The serial device on %s is not responding. " diff --git a/pyotgw/messageprocessor.py b/pyotgw/messageprocessor.py index ef511c3..8f0afca 100644 --- a/pyotgw/messageprocessor.py +++ b/pyotgw/messageprocessor.py @@ -7,8 +7,7 @@ import re from typing import TYPE_CHECKING -from . import messages as m -from . import vars as v +from . import messages as m, vars as v from .types import ( OpenThermCommand, OpenThermDataSource, diff --git a/pyotgw/protocol.py b/pyotgw/protocol.py index 653b97d..d38f820 100644 --- a/pyotgw/protocol.py +++ b/pyotgw/protocol.py @@ -3,9 +3,9 @@ from __future__ import annotations import asyncio +from collections.abc import Awaitable, Callable import logging import re -from collections.abc import Awaitable, Callable from typing import TYPE_CHECKING from .commandprocessor import CommandProcessor diff --git a/pyotgw/pyotgw.py b/pyotgw/pyotgw.py index 0fd1d99..0397261 100644 --- a/pyotgw/pyotgw.py +++ b/pyotgw/pyotgw.py @@ -3,14 +3,14 @@ from __future__ import annotations import asyncio -import logging from collections.abc import Awaitable, Callable from datetime import datetime +import logging from typing import TYPE_CHECKING, Literal from . import vars as v from .connection import ConnectionManager -from .poll_task import get_all_poll_tasks, OpenThermPollTaskName +from .poll_task import OpenThermPollTaskName, get_all_poll_tasks from .reports import convert_report_response_to_status_update from .status import StatusManager from .types import ( @@ -181,7 +181,7 @@ async def set_outside_temp( async def set_clock( self, - date: datetime = datetime.now(), + date: datetime | None = None, timeout: asyncio.Timeout = v.OTGW_DEFAULT_TIMEOUT, ) -> str | None: """ @@ -195,6 +195,8 @@ async def set_clock( This method is a coroutine """ cmd = OpenThermCommand.SET_CLOCK + if date is None: + date = datetime.now() value = f"{date.strftime('%H:%M')}/{date.isoweekday()}" return await self._wait_for_cmd(cmd, value, timeout) @@ -317,7 +319,8 @@ async def get_report( report_type: OpenThermReport, timeout: asyncio.Timeout = v.OTGW_DEFAULT_TIMEOUT, ) -> dict[OpenThermDataSource, dict] | None: - """Get the report, update status dict accordingly. Return updated status dict.""" + """Get the report, update status dict accordingly. + Return updated status dict.""" ret = await self._wait_for_cmd(OpenThermCommand.REPORT, report_type, timeout) if ( ret is None diff --git a/pyotgw/status.py b/pyotgw/status.py index eaf370e..61cb93b 100644 --- a/pyotgw/status.py +++ b/pyotgw/status.py @@ -3,9 +3,9 @@ from __future__ import annotations import asyncio -import logging from collections.abc import Awaitable, Callable from copy import deepcopy +import logging from . import vars as v from .types import OpenThermDataSource diff --git a/pyproject.toml b/pyproject.toml index 3309853..f9cba6c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,16 @@ [tool.pytest.ini_options] asyncio_default_fixture_loop_scope = "function" +[tool.ruff.lint] +select = [ + "E", # pycodestyle + "F", # Pyflakes + "UP", # pyupgrade + "B", # flake8-bugbear + "SIM", # flake8-simplify + "I", # isort +] + [tool.ruff.lint.isort] force-sort-within-sections = true known-first-party = [ diff --git a/tests/data.py b/tests/data.py index 1a91306..75cdcbd 100644 --- a/tests/data.py +++ b/tests/data.py @@ -2,8 +2,8 @@ from types import SimpleNamespace -import pyotgw.vars as v from pyotgw.types import OpenThermDataSource, OpenThermMessageID, OpenThermMessageType +import pyotgw.vars as v _report_responses_51 = { v.OTGW_REPORT_ABOUT: "A=OpenTherm Gateway 5.1", diff --git a/tests/test_messages.py b/tests/test_messages.py index ace1eab..c76f26a 100644 --- a/tests/test_messages.py +++ b/tests/test_messages.py @@ -1,7 +1,7 @@ """Tests for pyotgw/messages.py""" -import pyotgw.messages as m from pyotgw.messageprocessor import MessageProcessor +import pyotgw.messages as m def test_message_registry(): diff --git a/tests/test_poll_task.py b/tests/test_poll_task.py index 785c5f6..b39fd0f 100644 --- a/tests/test_poll_task.py +++ b/tests/test_poll_task.py @@ -1,7 +1,8 @@ """Tests for pyotgw/poll_tasks.py""" +from unittest.mock import AsyncMock, call + import pytest -from unittest.mock import call, AsyncMock from pyotgw.poll_task import OpenThermPollTask, OpenThermPollTaskName from pyotgw.pyotgw import OpenThermGateway diff --git a/tests/test_pyotgw.py b/tests/test_pyotgw.py index 915c455..9b66102 100644 --- a/tests/test_pyotgw.py +++ b/tests/test_pyotgw.py @@ -1,14 +1,13 @@ """Tests for pyotgw/pyotgw.py""" import asyncio -import logging from datetime import datetime +import logging from unittest.mock import AsyncMock, MagicMock, call, patch import pytest import serial -import pyotgw.vars as v from pyotgw.poll_task import OpenThermPollTaskName from pyotgw.types import ( OpenThermCommand, @@ -16,6 +15,7 @@ OpenThermGatewayOpMode, OpenThermReport, ) +import pyotgw.vars as v from tests.data import pygw_reports, pygw_status from tests.helpers import called_x_times, respond_to_reports @@ -402,8 +402,8 @@ def get_response_42(cmd, val): """Get response from dict or raise ValueError""" try: return pygw_reports.report_responses_42[val] - except KeyError: - raise ValueError + except KeyError as e: + raise ValueError from e with patch.object( pygw, diff --git a/tests/test_reports.py b/tests/test_reports.py index 26af697..e2b30c1 100644 --- a/tests/test_reports.py +++ b/tests/test_reports.py @@ -4,7 +4,6 @@ import pytest -import pyotgw.vars as v from pyotgw.reports import _CONVERSIONS, convert_report_response_to_status_update from pyotgw.types import ( OpenThermDataSource, @@ -20,6 +19,7 @@ OpenThermThermostatDetection, OpenThermVoltageReferenceLevel, ) +import pyotgw.vars as v REPORT_TEST_PARAMETERS = ("report", "response", "expected_dict") @@ -44,7 +44,9 @@ "D=R", { OpenThermDataSource.GATEWAY: { - v.OTGW_TEMP_SENSOR: OpenThermTemperatureSensorFunction.RETURN_WATER_TEMPERATURE + v.OTGW_TEMP_SENSOR: ( + OpenThermTemperatureSensorFunction.RETURN_WATER_TEMPERATURE + ) } }, ), diff --git a/tests/test_status.py b/tests/test_status.py index 40d1494..608670d 100644 --- a/tests/test_status.py +++ b/tests/test_status.py @@ -6,8 +6,8 @@ import pytest -import pyotgw.vars as v from pyotgw.types import OpenThermDataSource +import pyotgw.vars as v from tests.helpers import called_once From a128d2d5e7799b8d37232f516b3896aa4d3192ab Mon Sep 17 00:00:00 2001 From: mvn23 Date: Mon, 16 Sep 2024 12:04:27 +0200 Subject: [PATCH 3/3] Fix linter errors in tests --- tests/helpers.py | 17 +++++++++++------ tests/test_commandprocessor.py | 9 ++++++--- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/tests/helpers.py b/tests/helpers.py index 03853ef..e4e04b3 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -32,13 +32,23 @@ async def _wait(): def respond_to_reports( - cmds: list[OpenThermReport] = [], responses: list[str] = [] + cmds: list[OpenThermReport] | None = None, responses: list[str] | None = None ) -> Callable[[OpenThermCommand, str, float | None], str]: """ Respond to PR= commands with test values. Override response values by specifying cmds and responses in order. """ + if len(cmds) != len(responses): + raise ValueError( + "There should be an equal amount of provided cmds and responses" + ) + + if cmds is None: + cmds = [] + if responses is None: + responses = [] + default_responses = { OpenThermReport.ABOUT: "A=OpenTherm Gateway 5.8", OpenThermReport.BUILD: "B=17:52 12-03-2023", @@ -58,11 +68,6 @@ def respond_to_reports( OpenThermReport.DHW: "W=1", } - if len(cmds) != len(responses): - raise ValueError( - "There should be an equal amount of provided cmds and responses" - ) - for cmd, response in zip(cmds, responses): if cmd not in default_responses: raise ValueError(f"Command {cmd} not found in default responses.") diff --git a/tests/test_commandprocessor.py b/tests/test_commandprocessor.py index b946faa..9041dbe 100644 --- a/tests/test_commandprocessor.py +++ b/tests/test_commandprocessor.py @@ -149,7 +149,8 @@ async def test_issue_cmd(caplog, pygw_proto): ( "pyotgw.commandprocessor", logging.WARNING, - f"Command {OpenThermCommand.CONTROL_SETPOINT_2} failed with InvalidCommand, retrying...", + f"Command {OpenThermCommand.CONTROL_SETPOINT_2} failed with InvalidCommand," + " retrying...", ), ] caplog.clear() @@ -185,12 +186,14 @@ async def test_issue_cmd(caplog, pygw_proto): ( "pyotgw.commandprocessor", logging.WARNING, - f"Command {OpenThermCommand.CONTROL_HEATING_2} failed with Error 03, retrying...", + f"Command {OpenThermCommand.CONTROL_HEATING_2} failed with Error 03," + " retrying...", ), ( "pyotgw.commandprocessor", logging.WARNING, - f"Command {OpenThermCommand.CONTROL_HEATING_2} failed with {OpenThermCommand.CONTROL_HEATING_2}: BV, retrying...", + f"Command {OpenThermCommand.CONTROL_HEATING_2} failed with" + " {OpenThermCommand.CONTROL_HEATING_2}: BV, retrying...", ), ]