PyPlumIO 0.6.3__tar.gz → 0.6.4__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.6.3 → pyplumio-0.6.4}/.pre-commit-config.yaml +1 -1
- {pyplumio-0.6.3 → pyplumio-0.6.4}/PKG-INFO +3 -3
- {pyplumio-0.6.3 → pyplumio-0.6.4}/PyPlumIO.egg-info/PKG-INFO +3 -3
- {pyplumio-0.6.3 → pyplumio-0.6.4}/PyPlumIO.egg-info/requires.txt +2 -2
- {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/_version.py +3 -3
- {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/connection.py +7 -7
- {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/devices/__init__.py +20 -14
- {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/devices/ecomax.py +16 -12
- {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/parameters/__init__.py +2 -2
- {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/protocol.py +68 -99
- {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/stream.py +6 -5
- {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/structures/network_info.py +23 -21
- {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/structures/schedules.py +1 -1
- {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/utils.py +6 -4
- {pyplumio-0.6.3 → pyplumio-0.6.4}/pyproject.toml +2 -2
- {pyplumio-0.6.3 → pyplumio-0.6.4}/requirements_test.txt +2 -2
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/conftest.py +1 -1
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/devices/test_ecomax.py +6 -6
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/devices/test_ecoster.py +1 -1
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/devices/test_init.py +3 -1
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/parameters/test_init.py +9 -9
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/parameters/test_mixers.py +3 -1
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/parameters/test_thermostats.py +3 -1
- pyplumio-0.6.4/tests/test_protocol.py +257 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/test_stream.py +2 -2
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/responses/device_available.json +4 -4
- pyplumio-0.6.3/tests/test_protocol.py +0 -469
- {pyplumio-0.6.3 → pyplumio-0.6.4}/.gitattributes +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/.github/CODE_OF_CONDUCT.md +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/.github/dependabot.yml +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/.github/workflows/ci.yml +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/.github/workflows/codeql.yml +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/.github/workflows/deploy.yml +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/.github/workflows/documentation.yml +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/.gitignore +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/.qlty/qlty.toml +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/.vscode/settings.json +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/LICENSE +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/MANIFEST.in +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/PyPlumIO.egg-info/SOURCES.txt +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/PyPlumIO.egg-info/dependency_links.txt +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/PyPlumIO.egg-info/top_level.txt +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/README.md +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/docs/Makefile +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/docs/make.bat +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/docs/source/callbacks.rst +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/docs/source/conf.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/docs/source/connecting.rst +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/docs/source/frames.rst +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/docs/source/index.rst +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/docs/source/mixers_thermostats.rst +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/docs/source/protocol.rst +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/docs/source/reading.rst +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/docs/source/schedules.rst +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/docs/source/writing.rst +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/images/ecomax.png +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/images/rs485.png +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/__init__.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/__main__.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/const.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/data_types.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/devices/ecoster.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/devices/mixer.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/devices/thermostat.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/exceptions.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/filters.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/frames/__init__.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/frames/messages.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/frames/requests.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/frames/responses.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/helpers/__init__.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/helpers/async_cache.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/helpers/event_manager.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/helpers/factory.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/helpers/task_manager.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/parameters/custom/__init__.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/parameters/custom/ecomax_860d3_hb.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/parameters/ecomax.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/parameters/mixer.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/parameters/thermostat.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/py.typed +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/structures/__init__.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/structures/alerts.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/structures/ecomax_parameters.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/structures/frame_versions.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/structures/mixer_parameters.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/structures/product_info.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/structures/program_version.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/structures/regulator_data.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/structures/regulator_data_schema.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/structures/sensor_data.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/structures/thermostat_parameters.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/requirements.txt +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/requirements_docs.txt +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/setup.cfg +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/__init__.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/devices/__init__.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/devices/test_mixer.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/devices/test_thermostat.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/frames/test_init.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/frames/test_messages.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/frames/test_requests.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/frames/test_responses.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/helpers/__init__.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/helpers/test_async_cache.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/helpers/test_event_manager.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/helpers/test_factory.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/helpers/test_task_manager.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/helpers/test_uid.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/parameters/__init__.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/parameters/custom/__init__.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/parameters/custom/test_ecomax_860d3_hb.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/parameters/custom/test_init.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/parameters/test_ecomax.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/ruff.toml +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/structures/__init__.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/structures/test_alerts.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/structures/test_ecomax_parameters.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/structures/test_frame_versions.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/structures/test_mixer_parameters.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/structures/test_product_info.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/structures/test_schedules.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/test_connection.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/test_data_types.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/test_filters.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/test_init.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/test_main.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/test_utils.py +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/messages/regulator_data.json +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/messages/sensor_data.json +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/parameters/ecomax_860d3_hb.json +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/requests/alerts.json +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/requests/ecomax_control.json +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/requests/ecomax_parameters.json +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/requests/mixer_parameters.json +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/requests/set_ecomax_parameter.json +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/requests/set_mixer_parameter.json +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/requests/set_schedule.json +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/requests/set_thermostat_parameter.json +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/requests/thermostat_parameters.json +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/responses/alerts.json +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/responses/ecomax_parameters.json +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/responses/mixer_parameters.json +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/responses/password.json +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/responses/program_version.json +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/responses/regulator_data_schema.json +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/responses/schedules.json +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/responses/thermostat_parameters.json +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/responses/uid.json +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/unknown/unknown_ecomax_parameter.json +0 -0
- {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/unknown/unknown_mixer_parameter.json +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyPlumIO
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.4
|
|
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
|
|
@@ -32,8 +32,8 @@ Requires-Dist: numpy<3.0.0,>=2.0.0; extra == "test"
|
|
|
32
32
|
Requires-Dist: pyserial-asyncio-fast==0.16; extra == "test"
|
|
33
33
|
Requires-Dist: pytest==8.4.2; extra == "test"
|
|
34
34
|
Requires-Dist: pytest-asyncio==1.2.0; extra == "test"
|
|
35
|
-
Requires-Dist: ruff==0.14.
|
|
36
|
-
Requires-Dist: tox==4.
|
|
35
|
+
Requires-Dist: ruff==0.14.3; extra == "test"
|
|
36
|
+
Requires-Dist: tox==4.32.0; extra == "test"
|
|
37
37
|
Requires-Dist: types-pyserial==3.5.0.20251001; extra == "test"
|
|
38
38
|
Provides-Extra: docs
|
|
39
39
|
Requires-Dist: sphinx==8.1.3; extra == "docs"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyPlumIO
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.4
|
|
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
|
|
@@ -32,8 +32,8 @@ Requires-Dist: numpy<3.0.0,>=2.0.0; extra == "test"
|
|
|
32
32
|
Requires-Dist: pyserial-asyncio-fast==0.16; extra == "test"
|
|
33
33
|
Requires-Dist: pytest==8.4.2; extra == "test"
|
|
34
34
|
Requires-Dist: pytest-asyncio==1.2.0; extra == "test"
|
|
35
|
-
Requires-Dist: ruff==0.14.
|
|
36
|
-
Requires-Dist: tox==4.
|
|
35
|
+
Requires-Dist: ruff==0.14.3; extra == "test"
|
|
36
|
+
Requires-Dist: tox==4.32.0; extra == "test"
|
|
37
37
|
Requires-Dist: types-pyserial==3.5.0.20251001; extra == "test"
|
|
38
38
|
Provides-Extra: docs
|
|
39
39
|
Requires-Dist: sphinx==8.1.3; extra == "docs"
|
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.6.
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 6,
|
|
31
|
+
__version__ = version = '0.6.4'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 6, 4)
|
|
33
33
|
|
|
34
|
-
__commit_id__ = commit_id = '
|
|
34
|
+
__commit_id__ = commit_id = 'gc17c7ed5e'
|
|
@@ -19,8 +19,8 @@ from pyplumio.utils import timeout
|
|
|
19
19
|
|
|
20
20
|
_LOGGER = logging.getLogger(__name__)
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
TRY_CONNECT_FOR_SECONDS: Final = 5
|
|
23
|
+
RECONNECT_AFTER_SECONDS: Final = 20
|
|
24
24
|
|
|
25
25
|
try:
|
|
26
26
|
import serial_asyncio_fast as pyserial_asyncio
|
|
@@ -88,9 +88,9 @@ class Connection(ABC, TaskManager):
|
|
|
88
88
|
except ConnectionFailedError:
|
|
89
89
|
_LOGGER.error(
|
|
90
90
|
"Can't connect to the device, retrying in %.1f seconds",
|
|
91
|
-
|
|
91
|
+
RECONNECT_AFTER_SECONDS,
|
|
92
92
|
)
|
|
93
|
-
await asyncio.sleep(
|
|
93
|
+
await asyncio.sleep(RECONNECT_AFTER_SECONDS)
|
|
94
94
|
self.create_task(self._reconnect(), name="reconnect_task")
|
|
95
95
|
|
|
96
96
|
async def connect(self) -> None:
|
|
@@ -126,7 +126,7 @@ class Connection(ABC, TaskManager):
|
|
|
126
126
|
"""Return connection options."""
|
|
127
127
|
return self._options
|
|
128
128
|
|
|
129
|
-
@timeout(
|
|
129
|
+
@timeout(TRY_CONNECT_FOR_SECONDS)
|
|
130
130
|
@abstractmethod
|
|
131
131
|
async def _open_connection(
|
|
132
132
|
self,
|
|
@@ -162,7 +162,7 @@ class TcpConnection(Connection):
|
|
|
162
162
|
f"TcpConnection(host={self.host}, port={self.port}, options={self.options})"
|
|
163
163
|
)
|
|
164
164
|
|
|
165
|
-
@timeout(
|
|
165
|
+
@timeout(TRY_CONNECT_FOR_SECONDS)
|
|
166
166
|
async def _open_connection(
|
|
167
167
|
self,
|
|
168
168
|
) -> tuple[asyncio.StreamReader, asyncio.StreamWriter]:
|
|
@@ -203,7 +203,7 @@ class SerialConnection(Connection):
|
|
|
203
203
|
f"options={self.options})"
|
|
204
204
|
)
|
|
205
205
|
|
|
206
|
-
@timeout(
|
|
206
|
+
@timeout(TRY_CONNECT_FOR_SECONDS)
|
|
207
207
|
async def _open_connection(
|
|
208
208
|
self,
|
|
209
209
|
) -> tuple[asyncio.StreamReader, asyncio.StreamWriter]:
|
|
@@ -52,14 +52,18 @@ def get_device_handler(device_type: int) -> str:
|
|
|
52
52
|
class Device(ABC, EventManager):
|
|
53
53
|
"""Represents a device."""
|
|
54
54
|
|
|
55
|
-
__slots__ = ("
|
|
55
|
+
__slots__ = ("_write_queue",)
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
_write_queue: asyncio.Queue[Frame]
|
|
58
58
|
|
|
59
|
-
def __init__(self,
|
|
59
|
+
def __init__(self, write_queue: asyncio.Queue[Frame]) -> None:
|
|
60
60
|
"""Initialize a new device."""
|
|
61
61
|
super().__init__()
|
|
62
|
-
self.
|
|
62
|
+
self._write_queue = write_queue
|
|
63
|
+
|
|
64
|
+
def queue_send(self, frame: Frame) -> None:
|
|
65
|
+
"""Send frame to the write queue."""
|
|
66
|
+
self._write_queue.put_nowait(frame)
|
|
63
67
|
|
|
64
68
|
async def set(
|
|
65
69
|
self,
|
|
@@ -130,17 +134,19 @@ class PhysicalDevice(Device, ABC):
|
|
|
130
134
|
virtual devices associated with them via parent property.
|
|
131
135
|
"""
|
|
132
136
|
|
|
133
|
-
__slots__ = ("address", "
|
|
137
|
+
__slots__ = ("address", "_network_info", "_frame_versions")
|
|
134
138
|
|
|
135
139
|
address: ClassVar[int]
|
|
136
140
|
|
|
137
|
-
|
|
141
|
+
_network_info: NetworkInfo
|
|
138
142
|
_frame_versions: dict[int, int]
|
|
139
143
|
|
|
140
|
-
def __init__(
|
|
144
|
+
def __init__(
|
|
145
|
+
self, write_queue: asyncio.Queue[Frame], network_info: NetworkInfo
|
|
146
|
+
) -> None:
|
|
141
147
|
"""Initialize a new physical device."""
|
|
142
|
-
super().__init__(
|
|
143
|
-
self.
|
|
148
|
+
super().__init__(write_queue)
|
|
149
|
+
self._network_info = network_info
|
|
144
150
|
self._frame_versions = {}
|
|
145
151
|
|
|
146
152
|
@event_listener(filter=on_change)
|
|
@@ -167,7 +173,7 @@ class PhysicalDevice(Device, ABC):
|
|
|
167
173
|
"""Request frame version from the device."""
|
|
168
174
|
_LOGGER.debug("Updating frame %s to version %i", repr(frame_type), version)
|
|
169
175
|
request = await Request.create(frame_type, recipient=self.address)
|
|
170
|
-
self.
|
|
176
|
+
self.queue_send(request)
|
|
171
177
|
|
|
172
178
|
def has_frame_version(self, frame_type: FrameType | int, version: int) -> bool:
|
|
173
179
|
"""Return True if frame data is up to date, False otherwise."""
|
|
@@ -199,7 +205,7 @@ class PhysicalDevice(Device, ABC):
|
|
|
199
205
|
initial_retries = retries
|
|
200
206
|
while retries > 0:
|
|
201
207
|
try:
|
|
202
|
-
self.
|
|
208
|
+
self.queue_send(request)
|
|
203
209
|
return await self.get(name, timeout=timeout)
|
|
204
210
|
except asyncio.TimeoutError:
|
|
205
211
|
retries -= 1
|
|
@@ -211,7 +217,7 @@ class PhysicalDevice(Device, ABC):
|
|
|
211
217
|
)
|
|
212
218
|
|
|
213
219
|
@classmethod
|
|
214
|
-
async def create(cls, device_type:
|
|
220
|
+
async def create(cls, device_type: DeviceType, **kwargs: Any) -> PhysicalDevice:
|
|
215
221
|
"""Create a physical device handler object."""
|
|
216
222
|
return await create_instance(get_device_handler(device_type), cls=cls, **kwargs)
|
|
217
223
|
|
|
@@ -225,10 +231,10 @@ class VirtualDevice(Device, ABC):
|
|
|
225
231
|
index: int
|
|
226
232
|
|
|
227
233
|
def __init__(
|
|
228
|
-
self,
|
|
234
|
+
self, write_queue: asyncio.Queue[Frame], parent: PhysicalDevice, index: int = 0
|
|
229
235
|
) -> None:
|
|
230
236
|
"""Initialize a new sub-device."""
|
|
231
|
-
super().__init__(
|
|
237
|
+
super().__init__(write_queue)
|
|
232
238
|
self.parent = parent
|
|
233
239
|
self.index = index
|
|
234
240
|
|
|
@@ -42,7 +42,7 @@ from pyplumio.structures.ecomax_parameters import (
|
|
|
42
42
|
ATTR_ECOMAX_PARAMETERS,
|
|
43
43
|
)
|
|
44
44
|
from pyplumio.structures.mixer_parameters import ATTR_MIXER_PARAMETERS
|
|
45
|
-
from pyplumio.structures.network_info import
|
|
45
|
+
from pyplumio.structures.network_info import ATTR_NETWORK_INFO, NetworkInfo
|
|
46
46
|
from pyplumio.structures.product_info import ATTR_PRODUCT, ProductInfo
|
|
47
47
|
from pyplumio.structures.regulator_data_schema import ATTR_REGDATA_SCHEMA
|
|
48
48
|
from pyplumio.structures.schedules import (
|
|
@@ -121,7 +121,7 @@ REQUIRED_KEYS: tuple[DataKey, ...] = (
|
|
|
121
121
|
|
|
122
122
|
REQUIRED_TYPES = [frame_type for _, frame_type in REQUIRED_KEYS]
|
|
123
123
|
|
|
124
|
-
|
|
124
|
+
WAIT_FOR_SETUP_SECONDS: Final = 60.0
|
|
125
125
|
|
|
126
126
|
|
|
127
127
|
@device_handler(DeviceType.ECOMAX)
|
|
@@ -132,34 +132,38 @@ class EcoMAX(PhysicalDevice):
|
|
|
132
132
|
|
|
133
133
|
_fuel_meter: FuelMeter
|
|
134
134
|
|
|
135
|
-
def __init__(
|
|
135
|
+
def __init__(
|
|
136
|
+
self, write_queue: asyncio.Queue[Frame], network_info: NetworkInfo
|
|
137
|
+
) -> None:
|
|
136
138
|
"""Initialize a new ecoMAX controller."""
|
|
137
|
-
super().__init__(
|
|
139
|
+
super().__init__(write_queue, network_info)
|
|
138
140
|
self._fuel_meter = FuelMeter()
|
|
139
141
|
|
|
140
142
|
def handle_frame(self, frame: Frame) -> None:
|
|
141
143
|
"""Handle frame received from the ecoMAX device."""
|
|
142
144
|
if isinstance(frame, Request) and (
|
|
143
|
-
response := frame.response(data={
|
|
145
|
+
response := frame.response(data={ATTR_NETWORK_INFO: self._network_info})
|
|
144
146
|
):
|
|
145
|
-
self.
|
|
147
|
+
self.queue_send(response)
|
|
146
148
|
|
|
147
149
|
super().handle_frame(frame)
|
|
148
150
|
|
|
149
151
|
def _mixers(self, indexes: Iterable[int]) -> Generator[Mixer]:
|
|
150
|
-
"""Iterate through
|
|
152
|
+
"""Iterate through indexes and yield a Mixer instance.
|
|
151
153
|
|
|
152
154
|
For each index, return or create an instance of the mixer class.
|
|
153
155
|
Once done, dispatch the 'mixers' event without waiting.
|
|
154
156
|
"""
|
|
155
157
|
mixers: dict[int, Mixer] = self._data.setdefault(ATTR_MIXERS, {})
|
|
156
158
|
for index in indexes:
|
|
157
|
-
yield mixers.setdefault(
|
|
159
|
+
yield mixers.setdefault(
|
|
160
|
+
index, Mixer(self._write_queue, parent=self, index=index)
|
|
161
|
+
)
|
|
158
162
|
|
|
159
163
|
return self.dispatch_nowait(ATTR_MIXERS, mixers)
|
|
160
164
|
|
|
161
165
|
def _thermostats(self, indexes: Iterable[int]) -> Generator[Thermostat]:
|
|
162
|
-
"""Iterate through
|
|
166
|
+
"""Iterate through indexes and yield a Thermostat instance.
|
|
163
167
|
|
|
164
168
|
For each index, return or create an instance of the thermostat
|
|
165
169
|
class. Once done, dispatch the 'thermostats' event without
|
|
@@ -168,7 +172,7 @@ class EcoMAX(PhysicalDevice):
|
|
|
168
172
|
thermostats: dict[int, Thermostat] = self._data.setdefault(ATTR_THERMOSTATS, {})
|
|
169
173
|
for index in indexes:
|
|
170
174
|
yield thermostats.setdefault(
|
|
171
|
-
index, Thermostat(self.
|
|
175
|
+
index, Thermostat(self._write_queue, parent=self, index=index)
|
|
172
176
|
)
|
|
173
177
|
|
|
174
178
|
return self.dispatch_nowait(ATTR_THERMOSTATS, thermostats)
|
|
@@ -220,11 +224,11 @@ class EcoMAX(PhysicalDevice):
|
|
|
220
224
|
_LOGGER.debug("Setting up device entry")
|
|
221
225
|
|
|
222
226
|
try:
|
|
223
|
-
await self.wait_for(ATTR_SENSORS, timeout=
|
|
227
|
+
await self.wait_for(ATTR_SENSORS, timeout=WAIT_FOR_SETUP_SECONDS)
|
|
224
228
|
except asyncio.TimeoutError:
|
|
225
229
|
_LOGGER.error(
|
|
226
230
|
"Could not setup device entry; no response from device for %u seconds",
|
|
227
|
-
|
|
231
|
+
WAIT_FOR_SETUP_SECONDS,
|
|
228
232
|
)
|
|
229
233
|
return False
|
|
230
234
|
|
|
@@ -204,7 +204,7 @@ class Parameter(ABC):
|
|
|
204
204
|
self._values = replace(self._values, value=value)
|
|
205
205
|
request = await self.create_request()
|
|
206
206
|
if self.description.optimistic:
|
|
207
|
-
|
|
207
|
+
self.device.queue_send(request)
|
|
208
208
|
return True
|
|
209
209
|
|
|
210
210
|
self.update_done.clear()
|
|
@@ -233,7 +233,7 @@ class Parameter(ABC):
|
|
|
233
233
|
|
|
234
234
|
async def _send_update_request(self, request: Request, timeout: float) -> bool:
|
|
235
235
|
"""Send update request to the remote and confirm the result."""
|
|
236
|
-
|
|
236
|
+
self.device.queue_send(request)
|
|
237
237
|
with suppress(asyncio.TimeoutError):
|
|
238
238
|
# Wait for the update to be done
|
|
239
239
|
await asyncio.wait_for(self.update_done.wait(), timeout=timeout)
|
|
@@ -5,6 +5,7 @@ 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 contextlib import suppress
|
|
8
9
|
from dataclasses import dataclass, field
|
|
9
10
|
from datetime import datetime
|
|
10
11
|
import logging
|
|
@@ -46,7 +47,7 @@ class Protocol(ABC):
|
|
|
46
47
|
self.writer = None
|
|
47
48
|
self._on_connection_lost = set()
|
|
48
49
|
|
|
49
|
-
async def
|
|
50
|
+
async def _close_writer(self) -> None:
|
|
50
51
|
"""Ensure that writer is closed."""
|
|
51
52
|
if self.writer:
|
|
52
53
|
await self.writer.close()
|
|
@@ -91,26 +92,14 @@ class DummyProtocol(Protocol):
|
|
|
91
92
|
"""Close writer and call connection lost callbacks."""
|
|
92
93
|
if self.connected.is_set():
|
|
93
94
|
self.connected.clear()
|
|
94
|
-
await self.
|
|
95
|
+
await self._close_writer()
|
|
95
96
|
await asyncio.gather(*(callback() for callback in self.on_connection_lost))
|
|
96
97
|
|
|
97
98
|
async def shutdown(self) -> None:
|
|
98
99
|
"""Shutdown the protocol."""
|
|
99
100
|
if self.connected.is_set():
|
|
100
101
|
self.connected.clear()
|
|
101
|
-
await self.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
@dataclass(slots=True)
|
|
105
|
-
class Queues:
|
|
106
|
-
"""Represents asyncio queues."""
|
|
107
|
-
|
|
108
|
-
read: asyncio.Queue[Frame] = field(default_factory=asyncio.Queue)
|
|
109
|
-
write: asyncio.Queue[Frame] = field(default_factory=asyncio.Queue)
|
|
110
|
-
|
|
111
|
-
async def join(self) -> None:
|
|
112
|
-
"""Wait for queues to finish."""
|
|
113
|
-
await asyncio.gather(self.read.join(), self.write.join())
|
|
102
|
+
await self._close_writer()
|
|
114
103
|
|
|
115
104
|
|
|
116
105
|
NEVER: Final = "never"
|
|
@@ -194,7 +183,7 @@ class DeviceStatistics:
|
|
|
194
183
|
"""Return a hash of the statistics based on unique address."""
|
|
195
184
|
return self.address
|
|
196
185
|
|
|
197
|
-
async def update_last_seen(self, _: Any) -> None:
|
|
186
|
+
async def update_last_seen(self, _: Any | None = None) -> None:
|
|
198
187
|
"""Update last seen property."""
|
|
199
188
|
self.last_seen = datetime.now()
|
|
200
189
|
|
|
@@ -202,41 +191,34 @@ class DeviceStatistics:
|
|
|
202
191
|
class AsyncProtocol(Protocol, EventManager[PhysicalDevice]):
|
|
203
192
|
"""Represents an async protocol.
|
|
204
193
|
|
|
205
|
-
This protocol implements
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
The frame producer tries to read frames from the write queue.
|
|
209
|
-
If any is available, it sends them to the device via frame writer.
|
|
194
|
+
This protocol implements frame handling using a single task and
|
|
195
|
+
write queue.
|
|
210
196
|
|
|
211
|
-
|
|
212
|
-
|
|
197
|
+
The frame handler processes frames in both directions:
|
|
198
|
+
- Sends frames from the write queue to the device via frame writer
|
|
199
|
+
- Reads incoming frames via frame reader and processes them
|
|
213
200
|
|
|
214
|
-
|
|
215
|
-
|
|
201
|
+
Each received frame is passed to appropriate device handler for
|
|
202
|
+
further processing.
|
|
216
203
|
"""
|
|
217
204
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
_queues: Queues
|
|
221
|
-
_entry_lock: asyncio.Lock
|
|
205
|
+
_network_info: NetworkInfo
|
|
206
|
+
_write_queue: asyncio.Queue[Frame]
|
|
222
207
|
_statistics: Statistics
|
|
223
208
|
|
|
224
209
|
def __init__(
|
|
225
210
|
self,
|
|
226
211
|
ethernet_parameters: EthernetParameters | None = None,
|
|
227
212
|
wireless_parameters: WirelessParameters | None = None,
|
|
228
|
-
consumers_count: int = 3,
|
|
229
213
|
) -> None:
|
|
230
214
|
"""Initialize a new async protocol."""
|
|
231
215
|
super().__init__()
|
|
232
|
-
self.
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
wlan=wireless_parameters or WirelessParameters(status=False),
|
|
216
|
+
self._network_info = NetworkInfo(
|
|
217
|
+
ethernet=ethernet_parameters or EthernetParameters(status=False),
|
|
218
|
+
wireless=wireless_parameters or WirelessParameters(status=False),
|
|
236
219
|
)
|
|
237
|
-
self.
|
|
220
|
+
self._write_queue = asyncio.Queue()
|
|
238
221
|
self._statistics = Statistics()
|
|
239
|
-
self._queues = Queues()
|
|
240
222
|
|
|
241
223
|
def connection_established(
|
|
242
224
|
self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
|
|
@@ -244,17 +226,11 @@ class AsyncProtocol(Protocol, EventManager[PhysicalDevice]):
|
|
|
244
226
|
"""Start frame producer and consumers."""
|
|
245
227
|
self.reader = FrameReader(reader)
|
|
246
228
|
self.writer = FrameWriter(writer)
|
|
247
|
-
self.
|
|
229
|
+
self._write_queue.put_nowait(StartMasterRequest(recipient=DeviceType.ECOMAX))
|
|
248
230
|
self.create_task(
|
|
249
|
-
self.
|
|
250
|
-
name="
|
|
231
|
+
self.frame_handler(reader=self.reader, writer=self.writer),
|
|
232
|
+
name="frame_handler_task",
|
|
251
233
|
)
|
|
252
|
-
for consumer_id in range(self.consumers_count):
|
|
253
|
-
self.create_task(
|
|
254
|
-
self.frame_consumer(self._queues.read),
|
|
255
|
-
name=f"frame_consumer_task ({consumer_id})",
|
|
256
|
-
)
|
|
257
|
-
|
|
258
234
|
for device in self.data.values():
|
|
259
235
|
device.dispatch_nowait(ATTR_CONNECTED, True)
|
|
260
236
|
|
|
@@ -262,57 +238,57 @@ class AsyncProtocol(Protocol, EventManager[PhysicalDevice]):
|
|
|
262
238
|
self.statistics.reset_transfer_statistics()
|
|
263
239
|
self.statistics.connected_since = datetime.now()
|
|
264
240
|
|
|
265
|
-
async def
|
|
266
|
-
"""
|
|
241
|
+
async def _mark_disconnected(self) -> None:
|
|
242
|
+
"""Mark as protocol as disconnected."""
|
|
267
243
|
self.connected.clear()
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
244
|
+
for device in self.data.values():
|
|
245
|
+
device.dispatch_nowait(ATTR_CONNECTED, False)
|
|
246
|
+
|
|
247
|
+
def _clear_write_queue(self) -> None:
|
|
248
|
+
"""Clear the write queue."""
|
|
249
|
+
with suppress(asyncio.QueueEmpty):
|
|
250
|
+
while not self._write_queue.empty():
|
|
251
|
+
self._write_queue.get_nowait()
|
|
252
|
+
self._write_queue.task_done()
|
|
272
253
|
|
|
273
254
|
async def connection_lost(self) -> None:
|
|
274
255
|
"""Close the connection and call connection lost callbacks."""
|
|
275
256
|
if self.connected.is_set():
|
|
276
|
-
await self.
|
|
257
|
+
await self._mark_disconnected()
|
|
277
258
|
await asyncio.gather(*(callback() for callback in self.on_connection_lost))
|
|
259
|
+
await self._close_writer()
|
|
278
260
|
|
|
279
261
|
async def shutdown(self) -> None:
|
|
280
262
|
"""Shutdown the protocol and close the connection."""
|
|
281
|
-
await self._queues.join()
|
|
282
|
-
self.cancel_tasks()
|
|
283
|
-
await self.wait_until_done()
|
|
284
263
|
if self.connected.is_set():
|
|
285
|
-
await self.
|
|
264
|
+
await self._mark_disconnected()
|
|
286
265
|
await asyncio.gather(*(device.shutdown() for device in self.data.values()))
|
|
266
|
+
await self._close_writer()
|
|
287
267
|
|
|
288
|
-
|
|
289
|
-
self
|
|
290
|
-
|
|
291
|
-
"""Send frame from the queue to the remote device."""
|
|
292
|
-
frame = await queue.get()
|
|
293
|
-
await writer.write(frame)
|
|
294
|
-
queue.task_done()
|
|
295
|
-
self.statistics.update_sent(frame)
|
|
296
|
-
|
|
297
|
-
async def _read_into_queue(
|
|
298
|
-
self, reader: FrameReader, queue: asyncio.Queue[Frame]
|
|
299
|
-
) -> None:
|
|
300
|
-
"""Read frame from the remote device into the queue."""
|
|
301
|
-
if frame := await reader.read():
|
|
302
|
-
queue.put_nowait(frame)
|
|
303
|
-
self.statistics.update_received(frame)
|
|
268
|
+
self._clear_write_queue()
|
|
269
|
+
self.cancel_tasks()
|
|
270
|
+
await self.wait_until_done()
|
|
304
271
|
|
|
305
|
-
async def
|
|
306
|
-
self, queues: Queues, reader: FrameReader, writer: FrameWriter
|
|
307
|
-
) -> None:
|
|
272
|
+
async def frame_handler(self, reader: FrameReader, writer: FrameWriter) -> None:
|
|
308
273
|
"""Handle frame reads and writes."""
|
|
309
274
|
await self.connected.wait()
|
|
310
275
|
while self.connected.is_set():
|
|
311
276
|
try:
|
|
312
|
-
|
|
313
|
-
|
|
277
|
+
frame: Frame | None
|
|
278
|
+
|
|
279
|
+
# Handle pending writes.
|
|
280
|
+
if not self._write_queue.empty():
|
|
281
|
+
frame = await self._write_queue.get()
|
|
282
|
+
await writer.write(frame)
|
|
283
|
+
self._write_queue.task_done()
|
|
284
|
+
self.statistics.update_sent(frame)
|
|
285
|
+
|
|
286
|
+
# Read and process frame.
|
|
287
|
+
if frame := await reader.read():
|
|
288
|
+
self.statistics.update_received(frame)
|
|
289
|
+
device = await self._get_device_entry(frame.sender)
|
|
290
|
+
device.handle_frame(frame)
|
|
314
291
|
|
|
315
|
-
await self._read_into_queue(reader, queues.read)
|
|
316
292
|
except ProtocolError as e:
|
|
317
293
|
self.statistics.failed_frames += 1
|
|
318
294
|
_LOGGER.debug("Can't process received frame: %s", e)
|
|
@@ -323,36 +299,29 @@ class AsyncProtocol(Protocol, EventManager[PhysicalDevice]):
|
|
|
323
299
|
except Exception:
|
|
324
300
|
_LOGGER.exception("Unexpected exception")
|
|
325
301
|
|
|
326
|
-
async def frame_consumer(self, queue: asyncio.Queue[Frame]) -> None:
|
|
327
|
-
"""Handle frame processing."""
|
|
328
|
-
await self.connected.wait()
|
|
329
|
-
while self.connected.is_set():
|
|
330
|
-
frame = await queue.get()
|
|
331
|
-
device = await self.get_device_entry(frame.sender)
|
|
332
|
-
device.handle_frame(frame)
|
|
333
|
-
queue.task_done()
|
|
334
|
-
|
|
335
302
|
@acache
|
|
336
|
-
async def
|
|
303
|
+
async def _get_device_entry(self, device_type: DeviceType) -> PhysicalDevice:
|
|
337
304
|
"""Return the device entry."""
|
|
338
305
|
name = device_type.name.lower()
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
self.statistics.update_devices(device)
|
|
348
|
-
|
|
349
|
-
return self.data[name]
|
|
306
|
+
device = await PhysicalDevice.create(
|
|
307
|
+
device_type, write_queue=self._write_queue, network_info=self._network_info
|
|
308
|
+
)
|
|
309
|
+
device.dispatch_nowait(ATTR_CONNECTED, True)
|
|
310
|
+
device.dispatch_nowait(ATTR_SETUP, True)
|
|
311
|
+
await self.dispatch(name, device)
|
|
312
|
+
self.statistics.update_devices(device)
|
|
313
|
+
return device
|
|
350
314
|
|
|
351
315
|
@property
|
|
352
316
|
def statistics(self) -> Statistics:
|
|
353
317
|
"""Return the statistics."""
|
|
354
318
|
return self._statistics
|
|
355
319
|
|
|
320
|
+
@property
|
|
321
|
+
def network_info(self) -> NetworkInfo:
|
|
322
|
+
"""Return the network info."""
|
|
323
|
+
return self._network_info
|
|
324
|
+
|
|
356
325
|
|
|
357
326
|
__all__ = [
|
|
358
327
|
"Protocol",
|
|
@@ -22,8 +22,9 @@ from pyplumio.frames import (
|
|
|
22
22
|
)
|
|
23
23
|
from pyplumio.utils import timeout
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
WAIT_FOR_READ_SECONDS: Final = 10
|
|
26
|
+
WAIT_FOR_WRITE_SECONDS: Final = 10
|
|
27
|
+
FORCE_CLOSE_AFTER_SECONDS: Final = 10
|
|
27
28
|
|
|
28
29
|
MIN_FRAME_LENGTH: Final = 10
|
|
29
30
|
MAX_FRAME_LENGTH: Final = 1000
|
|
@@ -44,7 +45,7 @@ class FrameWriter:
|
|
|
44
45
|
"""Initialize a new frame writer."""
|
|
45
46
|
self._writer = writer
|
|
46
47
|
|
|
47
|
-
@timeout(
|
|
48
|
+
@timeout(WAIT_FOR_WRITE_SECONDS)
|
|
48
49
|
async def write(self, frame: Frame) -> None:
|
|
49
50
|
"""Send the frame and wait until send buffer is empty."""
|
|
50
51
|
self._writer.write(frame.bytes)
|
|
@@ -61,7 +62,7 @@ class FrameWriter:
|
|
|
61
62
|
"Failed to close the frame writer due to an unexpected error"
|
|
62
63
|
)
|
|
63
64
|
|
|
64
|
-
@timeout(
|
|
65
|
+
@timeout(FORCE_CLOSE_AFTER_SECONDS)
|
|
65
66
|
async def wait_closed(self) -> None:
|
|
66
67
|
"""Wait until the frame writer is closed."""
|
|
67
68
|
await self._writer.wait_closed()
|
|
@@ -188,7 +189,7 @@ class FrameReader:
|
|
|
188
189
|
|
|
189
190
|
await self._reader.read_into_buffer(MAX_FRAME_LENGTH)
|
|
190
191
|
|
|
191
|
-
@timeout(
|
|
192
|
+
@timeout(WAIT_FOR_READ_SECONDS)
|
|
192
193
|
async def read(self) -> Frame | None:
|
|
193
194
|
"""Read the frame and return corresponding handler object."""
|
|
194
195
|
header = await self._read_header()
|