PyPlumIO 0.5.19__tar.gz → 0.5.20__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyplumio-0.5.19 → pyplumio-0.5.20}/.pre-commit-config.yaml +2 -2
- {pyplumio-0.5.19 → pyplumio-0.5.20}/PKG-INFO +2 -2
- {pyplumio-0.5.19 → pyplumio-0.5.20}/PyPlumIO.egg-info/PKG-INFO +2 -2
- {pyplumio-0.5.19 → pyplumio-0.5.20}/PyPlumIO.egg-info/requires.txt +1 -1
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/_version.py +2 -2
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/connection.py +1 -4
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/devices/__init__.py +6 -3
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/helpers/event_manager.py +0 -5
- pyplumio-0.5.20/pyplumio/helpers/timeout.py +33 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/protocol.py +31 -39
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/stream.py +27 -28
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyproject.toml +1 -1
- {pyplumio-0.5.19 → pyplumio-0.5.20}/requirements_test.txt +1 -1
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/helpers/test_timeout.py +10 -12
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/test_protocol.py +22 -21
- pyplumio-0.5.19/pyplumio/helpers/timeout.py +0 -47
- {pyplumio-0.5.19 → pyplumio-0.5.20}/.gitattributes +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/.github/CODE_OF_CONDUCT.md +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/.github/dependabot.yml +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/.github/workflows/ci.yml +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/.github/workflows/codeql-analysis.yml +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/.github/workflows/deploy.yml +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/.github/workflows/documentation.yml +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/.gitignore +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/.vscode/settings.json +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/LICENSE +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/MANIFEST.in +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/PyPlumIO.egg-info/SOURCES.txt +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/PyPlumIO.egg-info/dependency_links.txt +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/PyPlumIO.egg-info/top_level.txt +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/README.md +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/docs/Makefile +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/docs/make.bat +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/docs/source/callbacks.rst +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/docs/source/conf.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/docs/source/connecting.rst +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/docs/source/index.rst +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/docs/source/mixers_thermostats.rst +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/docs/source/protocol.rst +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/docs/source/reading.rst +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/docs/source/schedules.rst +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/docs/source/writing.rst +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/images/ecomax.png +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/images/rs485.png +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/__init__.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/__main__.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/const.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/devices/ecomax.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/devices/ecoster.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/devices/mixer.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/devices/thermostat.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/exceptions.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/filters.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/frames/__init__.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/frames/messages.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/frames/requests.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/frames/responses.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/helpers/__init__.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/helpers/data_types.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/helpers/factory.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/helpers/parameter.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/helpers/schedule.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/helpers/task_manager.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/helpers/typing.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/helpers/uid.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/py.typed +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/structures/__init__.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/structures/alerts.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/structures/boiler_load.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/structures/boiler_power.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/structures/ecomax_parameters.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/structures/fan_power.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/structures/frame_versions.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/structures/fuel_consumption.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/structures/fuel_level.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/structures/lambda_sensor.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/structures/mixer_parameters.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/structures/mixer_sensors.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/structures/modules.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/structures/network_info.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/structures/output_flags.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/structures/outputs.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/structures/pending_alerts.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/structures/product_info.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/structures/program_version.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/structures/regulator_data.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/structures/regulator_data_schema.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/structures/schedules.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/structures/statuses.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/structures/temperatures.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/structures/thermostat_parameters.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/structures/thermostat_sensors.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/pyplumio/utils.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/requirements.txt +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/requirements_docs.txt +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/setup.cfg +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/__init__.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/conftest.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/frames/test_init.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/frames/test_messages.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/frames/test_requests.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/frames/test_responses.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/helpers/__init__.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/helpers/test_data_types.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/helpers/test_event_manager.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/helpers/test_factory.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/helpers/test_parameter.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/helpers/test_schedule.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/helpers/test_task_manager.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/helpers/test_uid.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/ruff.toml +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/test_connection.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/test_devices.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/test_filters.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/test_init.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/test_main.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/test_stream.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/test_utils.py +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/testdata/messages/regulator_data.json +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/testdata/messages/sensor_data.json +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/testdata/requests/alerts.json +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/testdata/requests/ecomax_control.json +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/testdata/requests/ecomax_parameters.json +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/testdata/requests/mixer_parameters.json +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/testdata/requests/set_ecomax_parameter.json +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/testdata/requests/set_mixer_parameter.json +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/testdata/requests/set_schedule.json +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/testdata/requests/set_thermostat_parameter.json +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/testdata/requests/thermostat_parameters.json +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/testdata/responses/alerts.json +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/testdata/responses/device_available.json +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/testdata/responses/ecomax_parameters.json +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/testdata/responses/mixer_parameters.json +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/testdata/responses/password.json +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/testdata/responses/program_version.json +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/testdata/responses/regulator_data_schema.json +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/testdata/responses/schedules.json +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/testdata/responses/thermostat_parameters.json +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/testdata/responses/uid.json +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/testdata/unknown/unknown_ecomax_parameter.json +0 -0
- {pyplumio-0.5.19 → pyplumio-0.5.20}/tests/testdata/unknown/unknown_mixer_parameter.json +0 -0
@@ -2,13 +2,13 @@
|
|
2
2
|
# See https://pre-commit.com/hooks.html for more hooks
|
3
3
|
repos:
|
4
4
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
5
|
-
rev: v0.4.
|
5
|
+
rev: v0.4.7
|
6
6
|
hooks:
|
7
7
|
- id: ruff
|
8
8
|
args:
|
9
9
|
- --fix
|
10
10
|
- repo: https://github.com/codespell-project/codespell
|
11
|
-
rev: v2.
|
11
|
+
rev: v2.3.0
|
12
12
|
hooks:
|
13
13
|
- id: codespell
|
14
14
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: PyPlumIO
|
3
|
-
Version: 0.5.
|
3
|
+
Version: 0.5.20
|
4
4
|
Summary: PyPlumIO is a native ecoNET library for Plum ecoMAX controllers.
|
5
5
|
Author-email: Denis Paavilainen <denpa@denpa.pro>
|
6
6
|
License: MIT License
|
@@ -30,7 +30,7 @@ Requires-Dist: mypy==1.10.0; extra == "test"
|
|
30
30
|
Requires-Dist: pyserial-asyncio-fast==0.11; extra == "test"
|
31
31
|
Requires-Dist: pytest==8.2.1; extra == "test"
|
32
32
|
Requires-Dist: pytest-asyncio==0.23.7; extra == "test"
|
33
|
-
Requires-Dist: ruff==0.4.
|
33
|
+
Requires-Dist: ruff==0.4.7; extra == "test"
|
34
34
|
Requires-Dist: tox==4.15.0; extra == "test"
|
35
35
|
Requires-Dist: types-pyserial==3.5.0.20240527; extra == "test"
|
36
36
|
Provides-Extra: docs
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: PyPlumIO
|
3
|
-
Version: 0.5.
|
3
|
+
Version: 0.5.20
|
4
4
|
Summary: PyPlumIO is a native ecoNET library for Plum ecoMAX controllers.
|
5
5
|
Author-email: Denis Paavilainen <denpa@denpa.pro>
|
6
6
|
License: MIT License
|
@@ -30,7 +30,7 @@ Requires-Dist: mypy==1.10.0; extra == "test"
|
|
30
30
|
Requires-Dist: pyserial-asyncio-fast==0.11; extra == "test"
|
31
31
|
Requires-Dist: pytest==8.2.1; extra == "test"
|
32
32
|
Requires-Dist: pytest-asyncio==0.23.7; extra == "test"
|
33
|
-
Requires-Dist: ruff==0.4.
|
33
|
+
Requires-Dist: ruff==0.4.7; extra == "test"
|
34
34
|
Requires-Dist: tox==4.15.0; extra == "test"
|
35
35
|
Requires-Dist: types-pyserial==3.5.0.20240527; extra == "test"
|
36
36
|
Provides-Extra: docs
|
@@ -72,10 +72,7 @@ class Connection(ABC, TaskManager):
|
|
72
72
|
async def _connect(self) -> None:
|
73
73
|
"""Establish connection and initialize the protocol object."""
|
74
74
|
try:
|
75
|
-
reader, writer =
|
76
|
-
tuple[asyncio.StreamReader, asyncio.StreamWriter],
|
77
|
-
await self._open_connection(),
|
78
|
-
)
|
75
|
+
reader, writer = await self._open_connection()
|
79
76
|
self.protocol.connection_established(reader, writer)
|
80
77
|
except (OSError, SerialException, asyncio.TimeoutError) as err:
|
81
78
|
raise ConnectionFailedError from err
|
@@ -111,6 +111,11 @@ class Device(ABC, EventManager):
|
|
111
111
|
"""
|
112
112
|
self.create_task(self.set(name, value, timeout, retries))
|
113
113
|
|
114
|
+
async def shutdown(self) -> None:
|
115
|
+
"""Cancel device tasks."""
|
116
|
+
self.cancel_tasks()
|
117
|
+
await self.wait_until_done()
|
118
|
+
|
114
119
|
|
115
120
|
class AddressableDevice(Device, ABC):
|
116
121
|
"""Represents an addressable device."""
|
@@ -139,9 +144,7 @@ class AddressableDevice(Device, ABC):
|
|
139
144
|
"""Set up addressable device."""
|
140
145
|
results = await asyncio.gather(
|
141
146
|
*{
|
142
|
-
self.
|
143
|
-
self.request(description.provides, description.frame_type)
|
144
|
-
)
|
147
|
+
self.request(description.provides, description.frame_type)
|
145
148
|
for description in self._setup_frames
|
146
149
|
},
|
147
150
|
return_exceptions=True,
|
@@ -165,11 +165,6 @@ class EventManager(TaskManager):
|
|
165
165
|
if not event.is_set():
|
166
166
|
event.set()
|
167
167
|
|
168
|
-
async def shutdown(self) -> None:
|
169
|
-
"""Cancel scheduled tasks."""
|
170
|
-
self.cancel_tasks()
|
171
|
-
await self.wait_until_done()
|
172
|
-
|
173
168
|
@property
|
174
169
|
def events(self) -> dict[str, asyncio.Event]:
|
175
170
|
"""Return the events."""
|
@@ -0,0 +1,33 @@
|
|
1
|
+
"""Contains a timeout decorator."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import asyncio
|
6
|
+
from collections.abc import Awaitable, Callable, Coroutine
|
7
|
+
from functools import wraps
|
8
|
+
import logging
|
9
|
+
from typing import Any, TypeVar
|
10
|
+
|
11
|
+
from typing_extensions import ParamSpec
|
12
|
+
|
13
|
+
T = TypeVar("T")
|
14
|
+
P = ParamSpec("P")
|
15
|
+
|
16
|
+
_LOGGER = logging.getLogger(__name__)
|
17
|
+
|
18
|
+
|
19
|
+
def timeout(
|
20
|
+
seconds: int,
|
21
|
+
) -> Callable[[Callable[P, Awaitable[T]]], Callable[P, Coroutine[Any, Any, T]]]:
|
22
|
+
"""Decorate a timeout for the awaitable."""
|
23
|
+
|
24
|
+
def decorator(
|
25
|
+
func: Callable[P, Awaitable[T]],
|
26
|
+
) -> Callable[P, Coroutine[Any, Any, T]]:
|
27
|
+
@wraps(func)
|
28
|
+
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
29
|
+
return await asyncio.wait_for(func(*args, **kwargs), timeout=seconds)
|
30
|
+
|
31
|
+
return wrapper
|
32
|
+
|
33
|
+
return decorator
|
@@ -5,17 +5,12 @@ from __future__ import annotations
|
|
5
5
|
from abc import ABC, abstractmethod
|
6
6
|
import asyncio
|
7
7
|
from collections.abc import Awaitable, Callable
|
8
|
+
from dataclasses import dataclass
|
8
9
|
import logging
|
9
|
-
from typing import NamedTuple, cast
|
10
10
|
|
11
11
|
from pyplumio.const import ATTR_CONNECTED, DeviceType
|
12
12
|
from pyplumio.devices import AddressableDevice
|
13
|
-
from pyplumio.exceptions import
|
14
|
-
FrameDataError,
|
15
|
-
FrameError,
|
16
|
-
ReadError,
|
17
|
-
UnknownDeviceError,
|
18
|
-
)
|
13
|
+
from pyplumio.exceptions import FrameError, ReadError, UnknownDeviceError
|
19
14
|
from pyplumio.frames import Frame
|
20
15
|
from pyplumio.frames.requests import StartMasterRequest
|
21
16
|
from pyplumio.helpers.event_manager import EventManager
|
@@ -101,12 +96,20 @@ class DummyProtocol(Protocol):
|
|
101
96
|
await self.close_writer()
|
102
97
|
|
103
98
|
|
104
|
-
|
99
|
+
@dataclass
|
100
|
+
class Queues:
|
105
101
|
"""Represents asyncio queues."""
|
106
102
|
|
103
|
+
__slots__ = ("read", "write")
|
104
|
+
|
107
105
|
read: asyncio.Queue
|
108
106
|
write: asyncio.Queue
|
109
107
|
|
108
|
+
async def join(self) -> None:
|
109
|
+
"""Wait for queues to finish."""
|
110
|
+
for queue in (self.read, self.write):
|
111
|
+
await queue.join()
|
112
|
+
|
110
113
|
|
111
114
|
class AsyncProtocol(Protocol, EventManager):
|
112
115
|
"""Represents an async protocol.
|
@@ -117,11 +120,11 @@ class AsyncProtocol(Protocol, EventManager):
|
|
117
120
|
The frame producer tries to read frames from the write queue.
|
118
121
|
If any is available, it sends them to the device via frame writer.
|
119
122
|
|
120
|
-
It then reads stream via frame reader
|
121
|
-
|
123
|
+
It then reads stream via frame reader and puts received frame
|
124
|
+
into the read queue.
|
122
125
|
|
123
|
-
Frame consumers read frames from the read queue
|
124
|
-
|
126
|
+
Frame consumers read frames from the read queue, create device
|
127
|
+
entry, if needed, and send frame to the entry for the processing.
|
125
128
|
"""
|
126
129
|
|
127
130
|
consumers_count: int
|
@@ -139,18 +142,10 @@ class AsyncProtocol(Protocol, EventManager):
|
|
139
142
|
super().__init__()
|
140
143
|
self.consumers_count = consumers_count
|
141
144
|
self._network = NetworkInfo(
|
142
|
-
eth=(
|
143
|
-
|
144
|
-
if ethernet_parameters is None
|
145
|
-
else ethernet_parameters
|
146
|
-
),
|
147
|
-
wlan=(
|
148
|
-
WirelessParameters(status=False)
|
149
|
-
if wireless_parameters is None
|
150
|
-
else wireless_parameters
|
151
|
-
),
|
145
|
+
eth=ethernet_parameters or EthernetParameters(status=False),
|
146
|
+
wlan=wireless_parameters or WirelessParameters(status=False),
|
152
147
|
)
|
153
|
-
self._queues = Queues(asyncio.Queue(), asyncio.Queue())
|
148
|
+
self._queues = Queues(read=asyncio.Queue(), write=asyncio.Queue())
|
154
149
|
|
155
150
|
def connection_established(
|
156
151
|
self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
|
@@ -159,7 +154,9 @@ class AsyncProtocol(Protocol, EventManager):
|
|
159
154
|
self.reader = FrameReader(reader)
|
160
155
|
self.writer = FrameWriter(writer)
|
161
156
|
self._queues.write.put_nowait(StartMasterRequest(recipient=DeviceType.ECOMAX))
|
162
|
-
self.create_task(
|
157
|
+
self.create_task(
|
158
|
+
self.frame_producer(self._queues, reader=self.reader, writer=self.writer)
|
159
|
+
)
|
163
160
|
for _ in range(self.consumers_count):
|
164
161
|
self.create_task(self.frame_consumer(self._queues.read))
|
165
162
|
|
@@ -184,8 +181,9 @@ class AsyncProtocol(Protocol, EventManager):
|
|
184
181
|
|
185
182
|
async def shutdown(self) -> None:
|
186
183
|
"""Shutdown protocol tasks."""
|
187
|
-
await
|
188
|
-
|
184
|
+
await self._queues.join()
|
185
|
+
self.cancel_tasks()
|
186
|
+
await self.wait_until_done()
|
189
187
|
for device in self.data.values():
|
190
188
|
await device.shutdown()
|
191
189
|
|
@@ -193,33 +191,27 @@ class AsyncProtocol(Protocol, EventManager):
|
|
193
191
|
self.connected.clear()
|
194
192
|
await self.close_writer()
|
195
193
|
|
196
|
-
async def frame_producer(
|
194
|
+
async def frame_producer(
|
195
|
+
self, queues: Queues, reader: FrameReader, writer: FrameWriter
|
196
|
+
) -> None:
|
197
197
|
"""Handle frame reads and writes."""
|
198
198
|
await self.connected.wait()
|
199
|
-
reader = cast(FrameReader, self.reader)
|
200
|
-
writer = cast(FrameWriter, self.writer)
|
201
199
|
while self.connected.is_set():
|
202
200
|
try:
|
203
|
-
if queues.write.
|
201
|
+
if not queues.write.empty():
|
204
202
|
await writer.write(await queues.write.get())
|
205
203
|
queues.write.task_done()
|
206
204
|
|
207
205
|
if (response := await reader.read()) is not None:
|
208
206
|
queues.read.put_nowait(response)
|
209
207
|
|
210
|
-
except
|
211
|
-
_LOGGER.warning("Incorrect payload: %s", e)
|
212
|
-
except ReadError as e:
|
213
|
-
_LOGGER.debug("Read error: %s", e)
|
214
|
-
except UnknownDeviceError as e:
|
215
|
-
_LOGGER.debug("Unknown device: %s", e)
|
216
|
-
except FrameError as e:
|
208
|
+
except (ReadError, UnknownDeviceError, FrameError) as e:
|
217
209
|
_LOGGER.debug("Can't process received frame: %s", e)
|
218
210
|
except (OSError, asyncio.TimeoutError):
|
219
211
|
self.create_task(self.connection_lost())
|
220
212
|
break
|
221
|
-
except Exception
|
222
|
-
_LOGGER.exception(
|
213
|
+
except Exception:
|
214
|
+
_LOGGER.exception("Unexpected exception")
|
223
215
|
|
224
216
|
async def frame_consumer(self, queue: asyncio.Queue) -> None:
|
225
217
|
"""Handle frame processing."""
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
5
5
|
import asyncio
|
6
6
|
from asyncio import IncompleteReadError, StreamReader, StreamWriter
|
7
7
|
import logging
|
8
|
-
from typing import Final
|
8
|
+
from typing import Final, NamedTuple
|
9
9
|
|
10
10
|
from pyplumio.const import DeviceType
|
11
11
|
from pyplumio.devices import is_known_device_type
|
@@ -54,6 +54,18 @@ class FrameWriter:
|
|
54
54
|
await self._writer.wait_closed()
|
55
55
|
|
56
56
|
|
57
|
+
class Header(NamedTuple):
|
58
|
+
"""Represents a frame header."""
|
59
|
+
|
60
|
+
bytes: bytes
|
61
|
+
frame_start: int
|
62
|
+
frame_length: int
|
63
|
+
recipient: int
|
64
|
+
sender: int
|
65
|
+
econet_type: int
|
66
|
+
econet_version: int
|
67
|
+
|
68
|
+
|
57
69
|
class FrameReader:
|
58
70
|
"""Represents a frame reader."""
|
59
71
|
|
@@ -65,11 +77,11 @@ class FrameReader:
|
|
65
77
|
"""Initialize a new frame reader."""
|
66
78
|
self._reader = reader
|
67
79
|
|
68
|
-
async def _read_header(self) ->
|
80
|
+
async def _read_header(self) -> Header:
|
69
81
|
"""Locate and read a frame header.
|
70
82
|
|
71
83
|
Raise pyplumio.ReadError if header size is too small and
|
72
|
-
OSError
|
84
|
+
OSError if serial connection is broken.
|
73
85
|
"""
|
74
86
|
while buffer := await self._reader.read(1):
|
75
87
|
if FRAME_START not in buffer:
|
@@ -79,23 +91,7 @@ class FrameReader:
|
|
79
91
|
if len(buffer) < struct_header.size:
|
80
92
|
raise ReadError(f"Header can't be less than {struct_header.size} bytes")
|
81
93
|
|
82
|
-
|
83
|
-
_,
|
84
|
-
length,
|
85
|
-
recipient,
|
86
|
-
sender,
|
87
|
-
econet_type,
|
88
|
-
econet_version,
|
89
|
-
] = struct_header.unpack_from(buffer)
|
90
|
-
|
91
|
-
return (
|
92
|
-
buffer,
|
93
|
-
length,
|
94
|
-
recipient,
|
95
|
-
sender,
|
96
|
-
econet_type,
|
97
|
-
econet_version,
|
98
|
-
)
|
94
|
+
return Header(buffer, *struct_header.unpack_from(buffer))
|
99
95
|
|
100
96
|
raise OSError("Serial connection broken")
|
101
97
|
|
@@ -108,8 +104,9 @@ class FrameReader:
|
|
108
104
|
checksum.
|
109
105
|
"""
|
110
106
|
(
|
111
|
-
|
112
|
-
|
107
|
+
header_bytes,
|
108
|
+
_,
|
109
|
+
frame_length,
|
113
110
|
recipient,
|
114
111
|
sender,
|
115
112
|
econet_type,
|
@@ -122,19 +119,21 @@ class FrameReader:
|
|
122
119
|
if not is_known_device_type(sender):
|
123
120
|
raise UnknownDeviceError(f"Unknown sender type ({sender})")
|
124
121
|
|
125
|
-
if
|
126
|
-
raise ReadError(f"Unexpected frame length ({
|
122
|
+
if frame_length > MAX_FRAME_LENGTH or frame_length < MIN_FRAME_LENGTH:
|
123
|
+
raise ReadError(f"Unexpected frame length ({frame_length})")
|
127
124
|
|
128
125
|
try:
|
129
|
-
payload = await self._reader.readexactly(
|
126
|
+
payload = await self._reader.readexactly(frame_length - struct_header.size)
|
130
127
|
except IncompleteReadError as e:
|
131
128
|
raise ReadError(
|
132
129
|
"Got an incomplete frame while trying to read "
|
133
|
-
+ f"'{
|
130
|
+
+ f"'{frame_length - struct_header.size}' bytes"
|
134
131
|
) from e
|
135
132
|
|
136
|
-
if
|
137
|
-
raise ChecksumError(
|
133
|
+
if (checksum := bcc(header_bytes + payload[:-2])) and checksum != payload[-2]:
|
134
|
+
raise ChecksumError(
|
135
|
+
f"Incorrect frame checksum ({checksum} != {payload[-2]})"
|
136
|
+
)
|
138
137
|
|
139
138
|
frame = await Frame.create(
|
140
139
|
frame_type=payload[0],
|
@@ -1,7 +1,6 @@
|
|
1
1
|
"""Contains tests for the timeout decorator class."""
|
2
2
|
|
3
3
|
import asyncio
|
4
|
-
import logging
|
5
4
|
from unittest.mock import AsyncMock, Mock, patch
|
6
5
|
|
7
6
|
import pytest
|
@@ -9,28 +8,27 @@ import pytest
|
|
9
8
|
from pyplumio.helpers.timeout import timeout
|
10
9
|
|
11
10
|
|
12
|
-
@patch(
|
13
|
-
|
11
|
+
@patch(
|
12
|
+
"asyncio.wait_for",
|
13
|
+
new_callable=AsyncMock,
|
14
|
+
side_effect=("test", asyncio.TimeoutError),
|
15
|
+
)
|
16
|
+
async def test_timeout(mock_wait_for) -> None:
|
14
17
|
"""Test a timeout decorator."""
|
15
18
|
# Mock function to pass to the decorator.
|
16
19
|
mock_func = Mock()
|
17
20
|
mock_func.return_value = "test"
|
18
|
-
mock_func.__name__ = "func_name"
|
19
21
|
|
20
22
|
# Call the decorator.
|
21
|
-
decorator = timeout(10
|
23
|
+
decorator = timeout(10)
|
22
24
|
wrapper = decorator(mock_func)
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
assert result is None
|
27
|
-
assert "Function 'func_name' timed out" in caplog.text
|
28
|
-
assert "func_name" in caplog.text
|
25
|
+
result = await wrapper("test_arg", kwarg="test_kwarg")
|
26
|
+
assert result == "test"
|
29
27
|
mock_wait_for.assert_awaited_once_with("test", timeout=10)
|
30
28
|
mock_func.assert_called_once_with("test_arg", kwarg="test_kwarg")
|
31
29
|
|
32
30
|
# Check with raise_exception set to true.
|
33
|
-
decorator = timeout(10
|
31
|
+
decorator = timeout(10)
|
34
32
|
wrapper = decorator(mock_func)
|
35
33
|
with pytest.raises(asyncio.TimeoutError):
|
36
34
|
await wrapper("test_arg", kwarg="test_kwarg")
|
@@ -205,8 +205,8 @@ async def test_async_protocol_shutdown(
|
|
205
205
|
bypass_asyncio_events,
|
206
206
|
) -> None:
|
207
207
|
"""Test shutting down connection with an async protocol."""
|
208
|
-
mock_read_queue =
|
209
|
-
mock_write_queue =
|
208
|
+
mock_read_queue = AsyncMock()
|
209
|
+
mock_write_queue = AsyncMock()
|
210
210
|
|
211
211
|
mock_writer = AsyncMock()
|
212
212
|
mock_writer.close = AsyncMock()
|
@@ -233,12 +233,7 @@ async def test_async_protocol_shutdown(
|
|
233
233
|
|
234
234
|
mock_shutdown.assert_awaited_once()
|
235
235
|
mock_cancel_tasks.assert_called_once()
|
236
|
-
|
237
|
-
calls = [
|
238
|
-
call(mock_read_queue.join(), mock_write_queue.join()),
|
239
|
-
call(*async_protocol.tasks, return_exceptions=True),
|
240
|
-
]
|
241
|
-
mock_gather.assert_has_awaits(calls)
|
236
|
+
mock_gather.assert_awaited_once_with(*async_protocol.tasks, return_exceptions=True)
|
242
237
|
mock_writer.close.assert_awaited_once()
|
243
238
|
assert async_protocol.writer is None
|
244
239
|
|
@@ -251,8 +246,8 @@ async def test_async_protocol_frame_producer(
|
|
251
246
|
response = Response(sender=DeviceType.ECOMAX)
|
252
247
|
|
253
248
|
# Create mock frame reader and writer.
|
254
|
-
|
255
|
-
|
249
|
+
mock_reader = AsyncMock(spec=FrameReader)
|
250
|
+
mock_reader.read = AsyncMock(
|
256
251
|
side_effect=(
|
257
252
|
response,
|
258
253
|
FrameError("test frame error"),
|
@@ -264,12 +259,14 @@ async def test_async_protocol_frame_producer(
|
|
264
259
|
)
|
265
260
|
)
|
266
261
|
|
267
|
-
|
262
|
+
mock_writer = AsyncMock(spec=FrameWriter)
|
268
263
|
|
269
264
|
# Create mock queues.
|
270
265
|
mock_read_queue = AsyncMock(spec=asyncio.Queue)
|
271
266
|
mock_write_queue = AsyncMock(spec=asyncio.Queue)
|
272
|
-
mock_write_queue.
|
267
|
+
mock_write_queue.empty = Mock(
|
268
|
+
side_effect=(False, True, True, True, True, True, True)
|
269
|
+
)
|
273
270
|
mock_write_queue.get = AsyncMock(return_value="test_request")
|
274
271
|
|
275
272
|
with (
|
@@ -279,7 +276,11 @@ async def test_async_protocol_frame_producer(
|
|
279
276
|
) as mock_connection_lost,
|
280
277
|
caplog.at_level(logging.DEBUG),
|
281
278
|
):
|
282
|
-
await async_protocol.frame_producer(
|
279
|
+
await async_protocol.frame_producer(
|
280
|
+
Queues(mock_read_queue, mock_write_queue),
|
281
|
+
reader=mock_reader,
|
282
|
+
writer=mock_writer,
|
283
|
+
)
|
283
284
|
|
284
285
|
assert caplog.record_tuples == [
|
285
286
|
(
|
@@ -290,32 +291,32 @@ async def test_async_protocol_frame_producer(
|
|
290
291
|
(
|
291
292
|
"pyplumio.protocol",
|
292
293
|
logging.DEBUG,
|
293
|
-
"
|
294
|
+
"Can't process received frame: test unknown device error",
|
294
295
|
),
|
295
296
|
(
|
296
297
|
"pyplumio.protocol",
|
297
298
|
logging.DEBUG,
|
298
|
-
"
|
299
|
+
"Can't process received frame: test read error",
|
299
300
|
),
|
300
301
|
(
|
301
302
|
"pyplumio.protocol",
|
302
|
-
logging.
|
303
|
-
"
|
303
|
+
logging.DEBUG,
|
304
|
+
"Can't process received frame: test frame data error",
|
304
305
|
),
|
305
306
|
(
|
306
307
|
"pyplumio.protocol",
|
307
308
|
logging.ERROR,
|
308
|
-
"
|
309
|
+
"Unexpected exception",
|
309
310
|
),
|
310
311
|
]
|
311
312
|
|
312
|
-
|
313
|
+
mock_writer.write.assert_awaited_once_with("test_request")
|
313
314
|
mock_write_queue.task_done.assert_called_once()
|
314
315
|
mock_read_queue.put_nowait.assert_called_once_with(response)
|
315
316
|
mock_connection_lost.assert_called_once()
|
316
317
|
assert mock_write_queue.get.await_count == 1
|
317
|
-
assert mock_write_queue.
|
318
|
-
assert
|
318
|
+
assert mock_write_queue.empty.call_count == 7
|
319
|
+
assert mock_reader.read.await_count == 7
|
319
320
|
|
320
321
|
|
321
322
|
@patch("pyplumio.frames.requests.CheckDeviceRequest.response")
|
@@ -1,47 +0,0 @@
|
|
1
|
-
"""Contains a timeout decorator."""
|
2
|
-
|
3
|
-
from __future__ import annotations
|
4
|
-
|
5
|
-
import asyncio
|
6
|
-
from collections.abc import Awaitable, Callable, Coroutine
|
7
|
-
from functools import wraps
|
8
|
-
import logging
|
9
|
-
from typing import Any, TypeVar
|
10
|
-
|
11
|
-
from typing_extensions import ParamSpec
|
12
|
-
|
13
|
-
T = TypeVar("T")
|
14
|
-
P = ParamSpec("P")
|
15
|
-
|
16
|
-
_LOGGER = logging.getLogger(__name__)
|
17
|
-
|
18
|
-
|
19
|
-
def timeout(
|
20
|
-
seconds: int, raise_exception: bool = True
|
21
|
-
) -> Callable[[Callable[P, Awaitable[T]]], Callable[P, Coroutine[Any, Any, T | None]]]:
|
22
|
-
"""Decorate a timeout for the awaitable.
|
23
|
-
|
24
|
-
Return None on exception if raise_exception parameter is set to false.
|
25
|
-
"""
|
26
|
-
|
27
|
-
def decorator(
|
28
|
-
func: Callable[P, Awaitable[T]],
|
29
|
-
) -> Callable[P, Coroutine[Any, Any, T | None]]:
|
30
|
-
@wraps(func)
|
31
|
-
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T | None:
|
32
|
-
try:
|
33
|
-
return await asyncio.wait_for(func(*args, **kwargs), timeout=seconds)
|
34
|
-
except asyncio.TimeoutError:
|
35
|
-
if raise_exception:
|
36
|
-
raise
|
37
|
-
|
38
|
-
_LOGGER.warning(
|
39
|
-
"Function '%s' timed out after %i seconds",
|
40
|
-
func.__name__,
|
41
|
-
seconds,
|
42
|
-
)
|
43
|
-
return None
|
44
|
-
|
45
|
-
return wrapper
|
46
|
-
|
47
|
-
return decorator
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|