PyPlumIO 0.5.51__tar.gz → 0.5.52__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.51 → pyplumio-0.5.52}/PKG-INFO +8 -8
- {pyplumio-0.5.51 → pyplumio-0.5.52}/PyPlumIO.egg-info/PKG-INFO +8 -8
- {pyplumio-0.5.51 → pyplumio-0.5.52}/PyPlumIO.egg-info/requires.txt +7 -7
- {pyplumio-0.5.51 → pyplumio-0.5.52}/docs/source/index.rst +1 -1
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/_version.py +2 -2
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/frames/__init__.py +8 -4
- pyplumio-0.5.52/pyplumio/stream.py +243 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/alerts.py +1 -1
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/schedules.py +1 -1
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyproject.toml +7 -7
- {pyplumio-0.5.51 → pyplumio-0.5.52}/requirements.txt +1 -1
- {pyplumio-0.5.51 → pyplumio-0.5.52}/requirements_test.txt +6 -6
- pyplumio-0.5.52/tests/test_stream.py +467 -0
- pyplumio-0.5.51/pyplumio/stream.py +0 -158
- pyplumio-0.5.51/tests/test_stream.py +0 -267
- {pyplumio-0.5.51 → pyplumio-0.5.52}/.gitattributes +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/.github/CODE_OF_CONDUCT.md +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/.github/dependabot.yml +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/.github/workflows/ci.yml +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/.github/workflows/codeql.yml +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/.github/workflows/deploy.yml +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/.github/workflows/documentation.yml +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/.gitignore +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/.pre-commit-config.yaml +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/.qlty/qlty.toml +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/.vscode/settings.json +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/LICENSE +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/MANIFEST.in +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/PyPlumIO.egg-info/SOURCES.txt +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/PyPlumIO.egg-info/dependency_links.txt +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/PyPlumIO.egg-info/top_level.txt +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/README.md +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/docs/Makefile +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/docs/make.bat +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/docs/source/callbacks.rst +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/docs/source/conf.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/docs/source/connecting.rst +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/docs/source/frames.rst +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/docs/source/mixers_thermostats.rst +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/docs/source/protocol.rst +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/docs/source/reading.rst +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/docs/source/schedules.rst +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/docs/source/writing.rst +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/images/ecomax.png +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/images/rs485.png +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/__init__.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/__main__.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/connection.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/const.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/data_types.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/devices/__init__.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/devices/ecomax.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/devices/ecoster.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/devices/mixer.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/devices/thermostat.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/exceptions.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/filters.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/frames/messages.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/frames/requests.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/frames/responses.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/helpers/__init__.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/helpers/async_cache.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/helpers/event_manager.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/helpers/factory.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/helpers/task_manager.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/helpers/timeout.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/parameters/__init__.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/parameters/custom/__init__.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/parameters/custom/ecomax_860d3_hb.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/parameters/ecomax.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/parameters/mixer.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/parameters/thermostat.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/protocol.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/py.typed +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/__init__.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/boiler_load.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/boiler_power.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/ecomax_parameters.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/fan_power.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/frame_versions.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/fuel_consumption.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/fuel_level.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/lambda_sensor.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/mixer_parameters.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/mixer_sensors.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/modules.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/network_info.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/output_flags.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/outputs.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/pending_alerts.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/product_info.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/program_version.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/regulator_data.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/regulator_data_schema.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/statuses.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/temperatures.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/thermostat_parameters.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/thermostat_sensors.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/utils.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/requirements_docs.txt +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/setup.cfg +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/__init__.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/conftest.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/devices/__init__.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/devices/test_ecomax.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/devices/test_ecoster.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/devices/test_init.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/devices/test_mixer.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/devices/test_thermostat.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/frames/test_init.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/frames/test_messages.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/frames/test_requests.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/frames/test_responses.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/helpers/__init__.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/helpers/test_async_cache.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/helpers/test_event_manager.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/helpers/test_factory.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/helpers/test_task_manager.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/helpers/test_timeout.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/helpers/test_uid.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/parameters/__init__.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/parameters/custom/__init__.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/parameters/custom/test_ecomax_860d3_hb.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/parameters/custom/test_init.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/parameters/test_ecomax.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/parameters/test_init.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/parameters/test_mixers.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/parameters/test_thermostats.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/ruff.toml +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/structures/__init__.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/structures/test_alerts.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/structures/test_boiler_load.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/structures/test_boiler_power.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/structures/test_ecomax_parameters.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/structures/test_fan_power.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/structures/test_frame_versions.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/structures/test_fuel_consumption.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/structures/test_fuel_level.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/structures/test_lambda_sensor.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/structures/test_mixer_parameters.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/structures/test_product_info.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/structures/test_schedules.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/test_connection.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/test_data_types.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/test_filters.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/test_init.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/test_main.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/test_protocol.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/test_utils.py +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/messages/regulator_data.json +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/messages/sensor_data.json +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/parameters/ecomax_860d3_hb.json +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/requests/alerts.json +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/requests/ecomax_control.json +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/requests/ecomax_parameters.json +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/requests/mixer_parameters.json +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/requests/set_ecomax_parameter.json +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/requests/set_mixer_parameter.json +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/requests/set_schedule.json +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/requests/set_thermostat_parameter.json +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/requests/thermostat_parameters.json +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/responses/alerts.json +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/responses/device_available.json +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/responses/ecomax_parameters.json +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/responses/mixer_parameters.json +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/responses/password.json +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/responses/program_version.json +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/responses/regulator_data_schema.json +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/responses/schedules.json +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/responses/thermostat_parameters.json +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/responses/uid.json +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/unknown/unknown_ecomax_parameter.json +0 -0
- {pyplumio-0.5.51 → pyplumio-0.5.52}/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.5.
|
3
|
+
Version: 0.5.52
|
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
|
@@ -25,17 +25,17 @@ Description-Content-Type: text/markdown
|
|
25
25
|
License-File: LICENSE
|
26
26
|
Requires-Dist: dataslots==1.2.0
|
27
27
|
Requires-Dist: pyserial-asyncio==0.6
|
28
|
-
Requires-Dist: typing-extensions==4.
|
28
|
+
Requires-Dist: typing-extensions==4.14.0
|
29
29
|
Provides-Extra: test
|
30
30
|
Requires-Dist: codespell==2.4.1; extra == "test"
|
31
|
-
Requires-Dist: coverage==7.
|
32
|
-
Requires-Dist: freezegun==1.5.
|
33
|
-
Requires-Dist: mypy==1.
|
31
|
+
Requires-Dist: coverage==7.9.1; extra == "test"
|
32
|
+
Requires-Dist: freezegun==1.5.2; extra == "test"
|
33
|
+
Requires-Dist: mypy==1.16.1; extra == "test"
|
34
34
|
Requires-Dist: numpy<3.0.0,>=2.0.0; extra == "test"
|
35
35
|
Requires-Dist: pyserial-asyncio-fast==0.16; extra == "test"
|
36
|
-
Requires-Dist: pytest==8.
|
37
|
-
Requires-Dist: pytest-asyncio==0.
|
38
|
-
Requires-Dist: ruff==0.11.
|
36
|
+
Requires-Dist: pytest==8.4.1; extra == "test"
|
37
|
+
Requires-Dist: pytest-asyncio==1.0.0; extra == "test"
|
38
|
+
Requires-Dist: ruff==0.11.13; extra == "test"
|
39
39
|
Requires-Dist: tox==4.26.0; extra == "test"
|
40
40
|
Requires-Dist: types-pyserial==3.5.0.20250326; extra == "test"
|
41
41
|
Provides-Extra: docs
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: PyPlumIO
|
3
|
-
Version: 0.5.
|
3
|
+
Version: 0.5.52
|
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
|
@@ -25,17 +25,17 @@ Description-Content-Type: text/markdown
|
|
25
25
|
License-File: LICENSE
|
26
26
|
Requires-Dist: dataslots==1.2.0
|
27
27
|
Requires-Dist: pyserial-asyncio==0.6
|
28
|
-
Requires-Dist: typing-extensions==4.
|
28
|
+
Requires-Dist: typing-extensions==4.14.0
|
29
29
|
Provides-Extra: test
|
30
30
|
Requires-Dist: codespell==2.4.1; extra == "test"
|
31
|
-
Requires-Dist: coverage==7.
|
32
|
-
Requires-Dist: freezegun==1.5.
|
33
|
-
Requires-Dist: mypy==1.
|
31
|
+
Requires-Dist: coverage==7.9.1; extra == "test"
|
32
|
+
Requires-Dist: freezegun==1.5.2; extra == "test"
|
33
|
+
Requires-Dist: mypy==1.16.1; extra == "test"
|
34
34
|
Requires-Dist: numpy<3.0.0,>=2.0.0; extra == "test"
|
35
35
|
Requires-Dist: pyserial-asyncio-fast==0.16; extra == "test"
|
36
|
-
Requires-Dist: pytest==8.
|
37
|
-
Requires-Dist: pytest-asyncio==0.
|
38
|
-
Requires-Dist: ruff==0.11.
|
36
|
+
Requires-Dist: pytest==8.4.1; extra == "test"
|
37
|
+
Requires-Dist: pytest-asyncio==1.0.0; extra == "test"
|
38
|
+
Requires-Dist: ruff==0.11.13; extra == "test"
|
39
39
|
Requires-Dist: tox==4.26.0; extra == "test"
|
40
40
|
Requires-Dist: types-pyserial==3.5.0.20250326; extra == "test"
|
41
41
|
Provides-Extra: docs
|
@@ -1,6 +1,6 @@
|
|
1
1
|
dataslots==1.2.0
|
2
2
|
pyserial-asyncio==0.6
|
3
|
-
typing-extensions==4.
|
3
|
+
typing-extensions==4.14.0
|
4
4
|
|
5
5
|
[dev]
|
6
6
|
pyplumio[docs,test]
|
@@ -14,13 +14,13 @@ readthedocs-sphinx-search==0.3.2
|
|
14
14
|
|
15
15
|
[test]
|
16
16
|
codespell==2.4.1
|
17
|
-
coverage==7.
|
18
|
-
freezegun==1.5.
|
19
|
-
mypy==1.
|
17
|
+
coverage==7.9.1
|
18
|
+
freezegun==1.5.2
|
19
|
+
mypy==1.16.1
|
20
20
|
numpy<3.0.0,>=2.0.0
|
21
21
|
pyserial-asyncio-fast==0.16
|
22
|
-
pytest==8.
|
23
|
-
pytest-asyncio==0.
|
24
|
-
ruff==0.11.
|
22
|
+
pytest==8.4.1
|
23
|
+
pytest-asyncio==1.0.0
|
24
|
+
ruff==0.11.13
|
25
25
|
tox==4.26.0
|
26
26
|
types-pyserial==3.5.0.20250326
|
@@ -6,7 +6,7 @@
|
|
6
6
|
Welcome to PyPlumIO's documentation!
|
7
7
|
====================================
|
8
8
|
|
9
|
-
The `PyPlumIO <https://github.com/denpamusic/PyPlumIO/>`_
|
9
|
+
The `PyPlumIO <https://github.com/denpamusic/PyPlumIO/>`_ project aims to
|
10
10
|
provide complete and easy to use solution for communicating with
|
11
11
|
climate devices manufactured by `Plum Sp. z o.o. <https://www.plum.pl/>`_
|
12
12
|
|
@@ -15,10 +15,14 @@ from pyplumio.utils import ensure_dict, to_camelcase
|
|
15
15
|
|
16
16
|
FRAME_START: Final = 0x68
|
17
17
|
FRAME_END: Final = 0x16
|
18
|
-
|
18
|
+
|
19
|
+
|
20
|
+
HEADER_INDEX: Final = 0
|
19
21
|
FRAME_TYPE_SIZE: Final = 1
|
20
|
-
|
22
|
+
BCC_SIZE: Final = 1
|
23
|
+
BCC_INDEX: Final = -2
|
21
24
|
DELIMITER_SIZE: Final = 1
|
25
|
+
|
22
26
|
ECONET_TYPE: Final = 48
|
23
27
|
ECONET_VERSION: Final = 5
|
24
28
|
|
@@ -199,7 +203,7 @@ class Frame(ABC):
|
|
199
203
|
struct_header.size
|
200
204
|
+ FRAME_TYPE_SIZE
|
201
205
|
+ len(self.message)
|
202
|
-
+
|
206
|
+
+ BCC_SIZE
|
203
207
|
+ DELIMITER_SIZE
|
204
208
|
)
|
205
209
|
|
@@ -209,7 +213,7 @@ class Frame(ABC):
|
|
209
213
|
buffer = bytearray(struct_header.size)
|
210
214
|
struct_header.pack_into(
|
211
215
|
buffer,
|
212
|
-
|
216
|
+
HEADER_INDEX,
|
213
217
|
FRAME_START,
|
214
218
|
self.length,
|
215
219
|
int(self.recipient),
|
@@ -0,0 +1,243 @@
|
|
1
|
+
"""Contains a frame reader and writer classes."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import asyncio
|
6
|
+
from asyncio import IncompleteReadError, StreamReader, StreamWriter
|
7
|
+
import logging
|
8
|
+
from typing import Final, NamedTuple, SupportsIndex
|
9
|
+
|
10
|
+
from pyplumio.const import DeviceType
|
11
|
+
from pyplumio.devices import is_known_device_type
|
12
|
+
from pyplumio.exceptions import ChecksumError, ReadError, UnknownDeviceError
|
13
|
+
from pyplumio.frames import (
|
14
|
+
BCC_INDEX,
|
15
|
+
DELIMITER_SIZE,
|
16
|
+
FRAME_START,
|
17
|
+
FRAME_TYPE_SIZE,
|
18
|
+
HEADER_SIZE,
|
19
|
+
Frame,
|
20
|
+
bcc,
|
21
|
+
struct_header,
|
22
|
+
)
|
23
|
+
from pyplumio.helpers.timeout import timeout
|
24
|
+
|
25
|
+
READER_TIMEOUT: Final = 10
|
26
|
+
WRITER_TIMEOUT: Final = 10
|
27
|
+
|
28
|
+
MIN_FRAME_LENGTH: Final = 10
|
29
|
+
MAX_FRAME_LENGTH: Final = 1000
|
30
|
+
|
31
|
+
DEFAULT_BUFFER_SIZE: Final = 5000
|
32
|
+
|
33
|
+
_LOGGER = logging.getLogger(__name__)
|
34
|
+
|
35
|
+
|
36
|
+
class FrameWriter:
|
37
|
+
"""Represents a frame writer."""
|
38
|
+
|
39
|
+
__slots__ = ("_writer",)
|
40
|
+
|
41
|
+
_writer: StreamWriter
|
42
|
+
|
43
|
+
def __init__(self, writer: StreamWriter) -> None:
|
44
|
+
"""Initialize a new frame writer."""
|
45
|
+
self._writer = writer
|
46
|
+
|
47
|
+
@timeout(WRITER_TIMEOUT)
|
48
|
+
async def write(self, frame: Frame) -> None:
|
49
|
+
"""Send the frame and wait until send buffer is empty."""
|
50
|
+
self._writer.write(frame.bytes)
|
51
|
+
await self._writer.drain()
|
52
|
+
_LOGGER.debug("Sent frame: %s, bytes: %s", frame, frame.bytes)
|
53
|
+
|
54
|
+
async def close(self) -> None:
|
55
|
+
"""Close the frame writer."""
|
56
|
+
try:
|
57
|
+
self._writer.close()
|
58
|
+
await self.wait_closed()
|
59
|
+
except (OSError, asyncio.TimeoutError):
|
60
|
+
_LOGGER.exception(
|
61
|
+
"Failed to close the frame writer due to an unexpected error"
|
62
|
+
)
|
63
|
+
|
64
|
+
@timeout(WRITER_TIMEOUT)
|
65
|
+
async def wait_closed(self) -> None:
|
66
|
+
"""Wait until the frame writer is closed."""
|
67
|
+
await self._writer.wait_closed()
|
68
|
+
|
69
|
+
|
70
|
+
class BufferManager:
|
71
|
+
"""Represents a buffered reader for reading frames."""
|
72
|
+
|
73
|
+
__slots__ = ("_buffer", "_reader")
|
74
|
+
|
75
|
+
_buffer: bytearray
|
76
|
+
_reader: StreamReader
|
77
|
+
|
78
|
+
def __init__(self, reader: StreamReader) -> None:
|
79
|
+
"""Initialize a new buffered reader."""
|
80
|
+
self._buffer = bytearray()
|
81
|
+
self._reader = reader
|
82
|
+
|
83
|
+
async def ensure_buffer(self, size: int) -> None:
|
84
|
+
"""Ensure the internal buffer size."""
|
85
|
+
bytes_to_read = size - len(self._buffer)
|
86
|
+
if bytes_to_read <= 0:
|
87
|
+
return None
|
88
|
+
|
89
|
+
try:
|
90
|
+
data = await self._reader.readexactly(bytes_to_read)
|
91
|
+
self._buffer.extend(data)
|
92
|
+
self.trim_to(size)
|
93
|
+
except IncompleteReadError as e:
|
94
|
+
raise ReadError(
|
95
|
+
f"Incomplete read. Tried to read {bytes_to_read} additional bytes "
|
96
|
+
f"to reach a total of {size}, but only {len(e.partial)} bytes were "
|
97
|
+
"available from stream."
|
98
|
+
) from e
|
99
|
+
except asyncio.CancelledError:
|
100
|
+
_LOGGER.debug("Read operation cancelled while ensuring buffer")
|
101
|
+
raise
|
102
|
+
except Exception as e:
|
103
|
+
raise OSError(
|
104
|
+
f"Serial connection broken while trying to ensure {size} bytes: {e}"
|
105
|
+
) from e
|
106
|
+
|
107
|
+
async def consume(self, size: int) -> None:
|
108
|
+
"""Consume the specified number of bytes from the buffer."""
|
109
|
+
await self.ensure_buffer(size)
|
110
|
+
self._buffer = self._buffer[size:]
|
111
|
+
|
112
|
+
async def peek(self, size: int) -> bytearray:
|
113
|
+
"""Read the specified number of bytes without consuming them."""
|
114
|
+
await self.ensure_buffer(size)
|
115
|
+
return self._buffer[:size]
|
116
|
+
|
117
|
+
async def read(self, size: int) -> bytearray:
|
118
|
+
"""Read the bytes from buffer or stream and consume them."""
|
119
|
+
try:
|
120
|
+
return await self.peek(size)
|
121
|
+
finally:
|
122
|
+
await self.consume(size)
|
123
|
+
|
124
|
+
def seek_to(self, delimiter: SupportsIndex) -> bool:
|
125
|
+
"""Trim the buffer to the first occurrence of the delimiter.
|
126
|
+
|
127
|
+
Returns True if the delimiter was found and trimmed, False otherwise.
|
128
|
+
"""
|
129
|
+
if not self._buffer or (index := self._buffer.find(delimiter)) == -1:
|
130
|
+
return False
|
131
|
+
|
132
|
+
self._buffer = self._buffer[index:]
|
133
|
+
return True
|
134
|
+
|
135
|
+
def trim_to(self, size: int) -> None:
|
136
|
+
"""Trim buffer to size."""
|
137
|
+
if len(self._buffer) > size:
|
138
|
+
self._buffer = self._buffer[-size:]
|
139
|
+
|
140
|
+
async def fill(self) -> None:
|
141
|
+
"""Fill the buffer with data from the stream."""
|
142
|
+
try:
|
143
|
+
chunk = await self._reader.read(MAX_FRAME_LENGTH)
|
144
|
+
except asyncio.CancelledError:
|
145
|
+
_LOGGER.debug("Read operation cancelled while filling read buffer.")
|
146
|
+
raise
|
147
|
+
except Exception as e:
|
148
|
+
raise OSError(
|
149
|
+
f"Serial connection broken while filling read buffer: {e}"
|
150
|
+
) from e
|
151
|
+
|
152
|
+
if not chunk:
|
153
|
+
_LOGGER.debug("Stream ended while filling read buffer.")
|
154
|
+
raise OSError(
|
155
|
+
"Serial connection broken: stream ended while filling read buffer"
|
156
|
+
)
|
157
|
+
|
158
|
+
self._buffer.extend(chunk)
|
159
|
+
self.trim_to(DEFAULT_BUFFER_SIZE)
|
160
|
+
|
161
|
+
@property
|
162
|
+
def buffer(self) -> bytearray:
|
163
|
+
"""Return the internal buffer."""
|
164
|
+
return self._buffer
|
165
|
+
|
166
|
+
|
167
|
+
class Header(NamedTuple):
|
168
|
+
"""Represents a frame header."""
|
169
|
+
|
170
|
+
frame_length: int
|
171
|
+
recipient: int
|
172
|
+
sender: int
|
173
|
+
econet_type: int
|
174
|
+
econet_version: int
|
175
|
+
|
176
|
+
|
177
|
+
class FrameReader:
|
178
|
+
"""Represents a frame reader."""
|
179
|
+
|
180
|
+
__slots__ = ("_buffer",)
|
181
|
+
|
182
|
+
_buffer: BufferManager
|
183
|
+
|
184
|
+
def __init__(self, reader: StreamReader) -> None:
|
185
|
+
"""Initialize a new frame reader."""
|
186
|
+
self._buffer = BufferManager(reader)
|
187
|
+
|
188
|
+
async def _read_header(self) -> Header:
|
189
|
+
"""Locate and read a frame header."""
|
190
|
+
while True:
|
191
|
+
if self._buffer.seek_to(FRAME_START):
|
192
|
+
header_bytes = await self._buffer.peek(HEADER_SIZE)
|
193
|
+
return Header(*struct_header.unpack_from(header_bytes)[DELIMITER_SIZE:])
|
194
|
+
|
195
|
+
await self._buffer.fill()
|
196
|
+
|
197
|
+
@timeout(READER_TIMEOUT)
|
198
|
+
async def read(self) -> Frame | None:
|
199
|
+
"""Read the frame and return corresponding handler object."""
|
200
|
+
header = await self._read_header()
|
201
|
+
frame_length, recipient, sender, econet_type, econet_version = header
|
202
|
+
|
203
|
+
if frame_length > MAX_FRAME_LENGTH or frame_length < MIN_FRAME_LENGTH:
|
204
|
+
await self._buffer.consume(HEADER_SIZE)
|
205
|
+
raise ReadError(
|
206
|
+
f"Unexpected frame length ({frame_length}), expected between "
|
207
|
+
f"{MIN_FRAME_LENGTH} and {MAX_FRAME_LENGTH}"
|
208
|
+
)
|
209
|
+
|
210
|
+
frame_bytes = await self._buffer.peek(frame_length)
|
211
|
+
checksum = bcc(frame_bytes[:BCC_INDEX])
|
212
|
+
if checksum != frame_bytes[BCC_INDEX]:
|
213
|
+
await self._buffer.consume(HEADER_SIZE)
|
214
|
+
raise ChecksumError(
|
215
|
+
f"Incorrect frame checksum: calculated {checksum}, "
|
216
|
+
f"expected {frame_bytes[BCC_INDEX]}. Frame data: {frame_bytes.hex()}"
|
217
|
+
)
|
218
|
+
|
219
|
+
await self._buffer.consume(frame_length)
|
220
|
+
if recipient not in (DeviceType.ECONET, DeviceType.ALL):
|
221
|
+
_LOGGER.debug(
|
222
|
+
"Skipping frame intended for different recipient (%s)", recipient
|
223
|
+
)
|
224
|
+
return None
|
225
|
+
|
226
|
+
if not is_known_device_type(sender):
|
227
|
+
raise UnknownDeviceError(f"Unknown sender type ({sender})")
|
228
|
+
|
229
|
+
payload_bytes = frame_bytes[HEADER_SIZE:BCC_INDEX]
|
230
|
+
frame = await Frame.create(
|
231
|
+
frame_type=payload_bytes[0],
|
232
|
+
recipient=DeviceType(recipient),
|
233
|
+
sender=DeviceType(sender),
|
234
|
+
econet_type=econet_type,
|
235
|
+
econet_version=econet_version,
|
236
|
+
message=payload_bytes[FRAME_TYPE_SIZE:],
|
237
|
+
)
|
238
|
+
_LOGGER.debug("Received frame: %s, bytes: %s", frame, frame_bytes.hex())
|
239
|
+
|
240
|
+
return frame
|
241
|
+
|
242
|
+
|
243
|
+
__all__ = ["FrameReader", "FrameWriter"]
|
@@ -26,7 +26,7 @@ classifiers = [
|
|
26
26
|
dependencies = [
|
27
27
|
"dataslots==1.2.0",
|
28
28
|
"pyserial-asyncio==0.6",
|
29
|
-
"typing-extensions==4.
|
29
|
+
"typing-extensions==4.14.0"
|
30
30
|
]
|
31
31
|
dynamic = ["version"]
|
32
32
|
|
@@ -38,14 +38,14 @@ dynamic = ["version"]
|
|
38
38
|
[project.optional-dependencies]
|
39
39
|
test = [
|
40
40
|
"codespell==2.4.1",
|
41
|
-
"coverage==7.
|
42
|
-
"freezegun==1.5.
|
43
|
-
"mypy==1.
|
41
|
+
"coverage==7.9.1",
|
42
|
+
"freezegun==1.5.2",
|
43
|
+
"mypy==1.16.1",
|
44
44
|
"numpy<3.0.0,>=2.0.0",
|
45
45
|
"pyserial-asyncio-fast==0.16",
|
46
|
-
"pytest==8.
|
47
|
-
"pytest-asyncio==0.
|
48
|
-
"ruff==0.11.
|
46
|
+
"pytest==8.4.1",
|
47
|
+
"pytest-asyncio==1.0.0",
|
48
|
+
"ruff==0.11.13",
|
49
49
|
"tox==4.26.0",
|
50
50
|
"types-pyserial==3.5.0.20250326"
|
51
51
|
]
|
@@ -1,13 +1,13 @@
|
|
1
1
|
codespell==2.4.1
|
2
|
-
coverage==7.
|
3
|
-
freezegun==1.5.
|
4
|
-
mypy==1.
|
2
|
+
coverage==7.9.1
|
3
|
+
freezegun==1.5.2
|
4
|
+
mypy==1.16.1
|
5
5
|
numpy<3.0.0,>=2.0.0
|
6
6
|
pre-commit==4.2.0
|
7
7
|
pyserial-asyncio-fast==0.16
|
8
|
-
pytest-asyncio==0.
|
9
|
-
pytest==8.
|
10
|
-
ruff==0.11.
|
8
|
+
pytest-asyncio==1.0.0
|
9
|
+
pytest==8.4.1
|
10
|
+
ruff==0.11.13
|
11
11
|
tomli==2.2.1
|
12
12
|
tox==4.26.0
|
13
13
|
types-pyserial==3.5.0.20250326
|