PyPlumIO 0.5.35__tar.gz → 0.5.37__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.35 → pyplumio-0.5.37}/.pre-commit-config.yaml +2 -2
- {pyplumio-0.5.35 → pyplumio-0.5.37}/PKG-INFO +9 -9
- {pyplumio-0.5.35 → pyplumio-0.5.37}/PyPlumIO.egg-info/PKG-INFO +9 -9
- {pyplumio-0.5.35 → pyplumio-0.5.37}/PyPlumIO.egg-info/requires.txt +6 -6
- {pyplumio-0.5.35 → pyplumio-0.5.37}/README.md +2 -2
- {pyplumio-0.5.35 → pyplumio-0.5.37}/docs/source/index.rst +3 -2
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/__init__.py +1 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/_version.py +2 -2
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/connection.py +1 -1
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/devices/__init__.py +7 -7
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/devices/ecomax.py +32 -24
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/devices/mixer.py +4 -7
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/exceptions.py +11 -4
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/helpers/parameter.py +51 -47
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/helpers/schedule.py +2 -3
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/helpers/uid.py +5 -5
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/structures/ecomax_parameters.py +3 -1
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyproject.toml +9 -8
- pyplumio-0.5.37/requirements_test.txt +11 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/tests/helpers/test_parameter.py +55 -11
- {pyplumio-0.5.35 → pyplumio-0.5.37}/tests/test_devices.py +7 -1
- pyplumio-0.5.35/requirements_test.txt +0 -11
- {pyplumio-0.5.35 → pyplumio-0.5.37}/.gitattributes +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/.github/CODE_OF_CONDUCT.md +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/.github/dependabot.yml +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/.github/workflows/ci.yml +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/.github/workflows/codeql-analysis.yml +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/.github/workflows/deploy.yml +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/.github/workflows/documentation.yml +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/.gitignore +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/.vscode/settings.json +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/LICENSE +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/MANIFEST.in +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/PyPlumIO.egg-info/SOURCES.txt +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/PyPlumIO.egg-info/dependency_links.txt +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/PyPlumIO.egg-info/top_level.txt +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/docs/Makefile +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/docs/make.bat +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/docs/source/callbacks.rst +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/docs/source/conf.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/docs/source/connecting.rst +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/docs/source/frames.rst +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/docs/source/mixers_thermostats.rst +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/docs/source/protocol.rst +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/docs/source/reading.rst +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/docs/source/schedules.rst +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/docs/source/writing.rst +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/images/ecomax.png +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/images/rs485.png +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/__main__.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/const.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/devices/ecoster.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/devices/thermostat.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/filters.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/frames/__init__.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/frames/messages.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/frames/requests.py +65 -65
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/frames/responses.py +62 -62
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/helpers/__init__.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/helpers/data_types.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/helpers/event_manager.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/helpers/factory.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/helpers/task_manager.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/helpers/timeout.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/protocol.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/py.typed +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/stream.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/structures/__init__.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/structures/alerts.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/structures/boiler_load.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/structures/boiler_power.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/structures/fan_power.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/structures/frame_versions.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/structures/fuel_consumption.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/structures/fuel_level.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/structures/lambda_sensor.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/structures/mixer_parameters.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/structures/mixer_sensors.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/structures/modules.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/structures/network_info.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/structures/output_flags.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/structures/outputs.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/structures/pending_alerts.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/structures/product_info.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/structures/program_version.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/structures/regulator_data.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/structures/regulator_data_schema.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/structures/schedules.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/structures/statuses.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/structures/temperatures.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/structures/thermostat_parameters.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/structures/thermostat_sensors.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/pyplumio/utils.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/requirements.txt +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/requirements_docs.txt +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/setup.cfg +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/tests/__init__.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/tests/conftest.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/tests/frames/test_init.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/tests/frames/test_messages.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/tests/frames/test_requests.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/tests/frames/test_responses.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/tests/helpers/__init__.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/tests/helpers/test_data_types.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/tests/helpers/test_event_manager.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/tests/helpers/test_factory.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/tests/helpers/test_schedule.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/tests/helpers/test_task_manager.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/tests/helpers/test_timeout.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/tests/helpers/test_uid.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/tests/ruff.toml +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/tests/test_connection.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/tests/test_filters.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/tests/test_init.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/tests/test_main.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/tests/test_protocol.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/tests/test_stream.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/tests/test_utils.py +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/tests/testdata/messages/regulator_data.json +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/tests/testdata/messages/sensor_data.json +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/tests/testdata/requests/alerts.json +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/tests/testdata/requests/ecomax_control.json +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/tests/testdata/requests/ecomax_parameters.json +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/tests/testdata/requests/mixer_parameters.json +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/tests/testdata/requests/set_ecomax_parameter.json +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/tests/testdata/requests/set_mixer_parameter.json +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/tests/testdata/requests/set_schedule.json +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/tests/testdata/requests/set_thermostat_parameter.json +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/tests/testdata/requests/thermostat_parameters.json +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/tests/testdata/responses/alerts.json +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/tests/testdata/responses/device_available.json +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/tests/testdata/responses/ecomax_parameters.json +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/tests/testdata/responses/mixer_parameters.json +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/tests/testdata/responses/password.json +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/tests/testdata/responses/program_version.json +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/tests/testdata/responses/regulator_data_schema.json +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/tests/testdata/responses/schedules.json +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/tests/testdata/responses/thermostat_parameters.json +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/tests/testdata/responses/uid.json +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/tests/testdata/unknown/unknown_ecomax_parameter.json +0 -0
- {pyplumio-0.5.35 → pyplumio-0.5.37}/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.9.
|
5
|
+
rev: v0.9.4
|
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.4.
|
11
|
+
rev: v2.4.1
|
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.2
|
2
2
|
Name: PyPlumIO
|
3
|
-
Version: 0.5.
|
3
|
+
Version: 0.5.37
|
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
|
@@ -27,30 +27,30 @@ Requires-Dist: dataslots==1.2.0
|
|
27
27
|
Requires-Dist: pyserial-asyncio==0.6
|
28
28
|
Requires-Dist: typing-extensions==4.12.2
|
29
29
|
Provides-Extra: test
|
30
|
-
Requires-Dist: codespell==2.
|
30
|
+
Requires-Dist: codespell==2.4.1; extra == "test"
|
31
31
|
Requires-Dist: coverage==7.6.10; extra == "test"
|
32
32
|
Requires-Dist: mypy==1.14.1; extra == "test"
|
33
33
|
Requires-Dist: pyserial-asyncio-fast==0.14; extra == "test"
|
34
34
|
Requires-Dist: pytest==8.3.4; extra == "test"
|
35
|
-
Requires-Dist: pytest-asyncio==0.25.
|
36
|
-
Requires-Dist: ruff==0.9.
|
37
|
-
Requires-Dist: tox==4.
|
38
|
-
Requires-Dist: types-pyserial==3.5.0.
|
35
|
+
Requires-Dist: pytest-asyncio==0.25.3; extra == "test"
|
36
|
+
Requires-Dist: ruff==0.9.4; extra == "test"
|
37
|
+
Requires-Dist: tox==4.24.1; extra == "test"
|
38
|
+
Requires-Dist: types-pyserial==3.5.0.20250130; extra == "test"
|
39
39
|
Provides-Extra: docs
|
40
40
|
Requires-Dist: sphinx==8.1.3; extra == "docs"
|
41
41
|
Requires-Dist: sphinx_rtd_theme==3.0.2; extra == "docs"
|
42
42
|
Requires-Dist: readthedocs-sphinx-search==0.3.2; extra == "docs"
|
43
43
|
Provides-Extra: dev
|
44
44
|
Requires-Dist: pyplumio[docs,test]; extra == "dev"
|
45
|
-
Requires-Dist: pre-commit==4.0
|
45
|
+
Requires-Dist: pre-commit==4.1.0; extra == "dev"
|
46
46
|
Requires-Dist: tomli==2.2.1; extra == "dev"
|
47
47
|
|
48
48
|
# PyPlumIO is a native ecoNET library for Plum ecoMAX controllers.
|
49
49
|
[](https://badge.fury.io/py/PyPlumIO)
|
50
50
|
[](https://pypi.python.org/pypi/pyplumio/)
|
51
51
|
[](https://github.com/denpamusic/PyPlumIO/actions/workflows/ci.yml)
|
52
|
-
[](https://codeclimate.com/github/denpamusic/PyPlumIO/maintainability)
|
53
|
+
[](https://codeclimate.com/github/denpamusic/PyPlumIO/test_coverage)
|
54
54
|
[](https://guidelines.denpa.pro/stability#release-candidate)
|
55
55
|
[](https://github.com/astral-sh/ruff)
|
56
56
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: PyPlumIO
|
3
|
-
Version: 0.5.
|
3
|
+
Version: 0.5.37
|
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
|
@@ -27,30 +27,30 @@ Requires-Dist: dataslots==1.2.0
|
|
27
27
|
Requires-Dist: pyserial-asyncio==0.6
|
28
28
|
Requires-Dist: typing-extensions==4.12.2
|
29
29
|
Provides-Extra: test
|
30
|
-
Requires-Dist: codespell==2.
|
30
|
+
Requires-Dist: codespell==2.4.1; extra == "test"
|
31
31
|
Requires-Dist: coverage==7.6.10; extra == "test"
|
32
32
|
Requires-Dist: mypy==1.14.1; extra == "test"
|
33
33
|
Requires-Dist: pyserial-asyncio-fast==0.14; extra == "test"
|
34
34
|
Requires-Dist: pytest==8.3.4; extra == "test"
|
35
|
-
Requires-Dist: pytest-asyncio==0.25.
|
36
|
-
Requires-Dist: ruff==0.9.
|
37
|
-
Requires-Dist: tox==4.
|
38
|
-
Requires-Dist: types-pyserial==3.5.0.
|
35
|
+
Requires-Dist: pytest-asyncio==0.25.3; extra == "test"
|
36
|
+
Requires-Dist: ruff==0.9.4; extra == "test"
|
37
|
+
Requires-Dist: tox==4.24.1; extra == "test"
|
38
|
+
Requires-Dist: types-pyserial==3.5.0.20250130; extra == "test"
|
39
39
|
Provides-Extra: docs
|
40
40
|
Requires-Dist: sphinx==8.1.3; extra == "docs"
|
41
41
|
Requires-Dist: sphinx_rtd_theme==3.0.2; extra == "docs"
|
42
42
|
Requires-Dist: readthedocs-sphinx-search==0.3.2; extra == "docs"
|
43
43
|
Provides-Extra: dev
|
44
44
|
Requires-Dist: pyplumio[docs,test]; extra == "dev"
|
45
|
-
Requires-Dist: pre-commit==4.0
|
45
|
+
Requires-Dist: pre-commit==4.1.0; extra == "dev"
|
46
46
|
Requires-Dist: tomli==2.2.1; extra == "dev"
|
47
47
|
|
48
48
|
# PyPlumIO is a native ecoNET library for Plum ecoMAX controllers.
|
49
49
|
[](https://badge.fury.io/py/PyPlumIO)
|
50
50
|
[](https://pypi.python.org/pypi/pyplumio/)
|
51
51
|
[](https://github.com/denpamusic/PyPlumIO/actions/workflows/ci.yml)
|
52
|
-
[](https://codeclimate.com/github/denpamusic/PyPlumIO/maintainability)
|
53
|
+
[](https://codeclimate.com/github/denpamusic/PyPlumIO/test_coverage)
|
54
54
|
[](https://guidelines.denpa.pro/stability#release-candidate)
|
55
55
|
[](https://github.com/astral-sh/ruff)
|
56
56
|
|
@@ -4,7 +4,7 @@ typing-extensions==4.12.2
|
|
4
4
|
|
5
5
|
[dev]
|
6
6
|
pyplumio[docs,test]
|
7
|
-
pre-commit==4.0
|
7
|
+
pre-commit==4.1.0
|
8
8
|
tomli==2.2.1
|
9
9
|
|
10
10
|
[docs]
|
@@ -13,12 +13,12 @@ sphinx_rtd_theme==3.0.2
|
|
13
13
|
readthedocs-sphinx-search==0.3.2
|
14
14
|
|
15
15
|
[test]
|
16
|
-
codespell==2.
|
16
|
+
codespell==2.4.1
|
17
17
|
coverage==7.6.10
|
18
18
|
mypy==1.14.1
|
19
19
|
pyserial-asyncio-fast==0.14
|
20
20
|
pytest==8.3.4
|
21
|
-
pytest-asyncio==0.25.
|
22
|
-
ruff==0.9.
|
23
|
-
tox==4.
|
24
|
-
types-pyserial==3.5.0.
|
21
|
+
pytest-asyncio==0.25.3
|
22
|
+
ruff==0.9.4
|
23
|
+
tox==4.24.1
|
24
|
+
types-pyserial==3.5.0.20250130
|
@@ -2,8 +2,8 @@
|
|
2
2
|
[](https://badge.fury.io/py/PyPlumIO)
|
3
3
|
[](https://pypi.python.org/pypi/pyplumio/)
|
4
4
|
[](https://github.com/denpamusic/PyPlumIO/actions/workflows/ci.yml)
|
5
|
-
[](https://codeclimate.com/github/denpamusic/PyPlumIO/maintainability)
|
6
|
+
[](https://codeclimate.com/github/denpamusic/PyPlumIO/test_coverage)
|
7
7
|
[](https://guidelines.denpa.pro/stability#release-candidate)
|
8
8
|
[](https://github.com/astral-sh/ruff)
|
9
9
|
|
@@ -6,8 +6,9 @@
|
|
6
6
|
Welcome to PyPlumIO's documentation!
|
7
7
|
====================================
|
8
8
|
|
9
|
-
|
10
|
-
|
9
|
+
The `PyPlumIO <https://github.com/denpamusic/PyPlumIO/>`_ projects aims to
|
10
|
+
provide complete and easy to use solution for communicating with
|
11
|
+
climate devices manufactured by `Plum Sp. z o.o. <https://www.plum.pl/>`_
|
11
12
|
|
12
13
|
Currently it supports reading and writing parameters of ecoMAX controllers by
|
13
14
|
Plum Sp. z o.o., getting service password and sending network information to
|
@@ -86,7 +86,7 @@ class Connection(ABC, TaskManager):
|
|
86
86
|
RECONNECT_TIMEOUT,
|
87
87
|
)
|
88
88
|
await asyncio.sleep(RECONNECT_TIMEOUT)
|
89
|
-
self.create_task(self._reconnect())
|
89
|
+
self.create_task(self._reconnect(), name="reconnect_task")
|
90
90
|
|
91
91
|
async def connect(self) -> None:
|
92
92
|
"""Open the connection.
|
@@ -13,7 +13,7 @@ from pyplumio.exceptions import RequestError, UnknownDeviceError
|
|
13
13
|
from pyplumio.frames import DataFrameDescription, Frame, Request, is_known_frame_type
|
14
14
|
from pyplumio.helpers.event_manager import EventManager
|
15
15
|
from pyplumio.helpers.factory import create_instance
|
16
|
-
from pyplumio.helpers.parameter import Parameter,
|
16
|
+
from pyplumio.helpers.parameter import NumericType, Parameter, State
|
17
17
|
from pyplumio.structures.frame_versions import ATTR_FRAME_VERSIONS
|
18
18
|
from pyplumio.structures.network_info import NetworkInfo
|
19
19
|
from pyplumio.utils import to_camelcase
|
@@ -57,7 +57,7 @@ class Device(ABC, EventManager):
|
|
57
57
|
async def set(
|
58
58
|
self,
|
59
59
|
name: str,
|
60
|
-
value:
|
60
|
+
value: NumericType | State | bool,
|
61
61
|
retries: int = 5,
|
62
62
|
timeout: float | None = None,
|
63
63
|
) -> bool:
|
@@ -89,7 +89,7 @@ class Device(ABC, EventManager):
|
|
89
89
|
def set_nowait(
|
90
90
|
self,
|
91
91
|
name: str,
|
92
|
-
value:
|
92
|
+
value: NumericType | State | bool,
|
93
93
|
retries: int = 5,
|
94
94
|
timeout: float | None = None,
|
95
95
|
) -> None:
|
@@ -180,7 +180,7 @@ class PhysicalDevice(Device, ABC):
|
|
180
180
|
)
|
181
181
|
|
182
182
|
errors = [
|
183
|
-
result.
|
183
|
+
result.frame_type for result in results if isinstance(result, RequestError)
|
184
184
|
]
|
185
185
|
|
186
186
|
await asyncio.gather(
|
@@ -204,9 +204,9 @@ class PhysicalDevice(Device, ABC):
|
|
204
204
|
retries -= 1
|
205
205
|
|
206
206
|
raise RequestError(
|
207
|
-
f"Failed to request
|
208
|
-
f"
|
209
|
-
frame_type,
|
207
|
+
f"Failed to request '{name}' with frame type '{frame_type}' after "
|
208
|
+
f"{retries} retries.",
|
209
|
+
frame_type=frame_type,
|
210
210
|
)
|
211
211
|
|
212
212
|
@classmethod
|
@@ -59,7 +59,8 @@ ATTR_MIXERS: Final = "mixers"
|
|
59
59
|
ATTR_THERMOSTATS: Final = "thermostats"
|
60
60
|
ATTR_FUEL_BURNED: Final = "fuel_burned"
|
61
61
|
|
62
|
-
|
62
|
+
NANOSECONDS_IN_SECOND: Final = 1_000_000_000
|
63
|
+
MAX_TIME_SINCE_LAST_FUEL_UPDATE_NS: Final = 300 * NANOSECONDS_IN_SECOND
|
63
64
|
|
64
65
|
SETUP_FRAME_TYPES: tuple[DataFrameDescription, ...] = (
|
65
66
|
DataFrameDescription(
|
@@ -105,16 +106,15 @@ class EcoMAX(PhysicalDevice):
|
|
105
106
|
"""Represents an ecoMAX controller."""
|
106
107
|
|
107
108
|
address = DeviceType.ECOMAX
|
108
|
-
|
109
|
-
_fuel_burned_timestamp_ns: int
|
110
109
|
_setup_frames = SETUP_FRAME_TYPES
|
111
110
|
|
111
|
+
_fuel_burned_time_ns: int
|
112
|
+
|
112
113
|
def __init__(self, queue: asyncio.Queue[Frame], network: NetworkInfo) -> None:
|
113
114
|
"""Initialize a new ecoMAX controller."""
|
114
115
|
super().__init__(queue, network)
|
115
|
-
self._fuel_burned_timestamp_ns = time.perf_counter_ns()
|
116
116
|
self.subscribe(ATTR_ECOMAX_PARAMETERS, self._handle_ecomax_parameters)
|
117
|
-
self.subscribe(ATTR_FUEL_CONSUMPTION, self.
|
117
|
+
self.subscribe(ATTR_FUEL_CONSUMPTION, self._add_burned_fuel_meter)
|
118
118
|
self.subscribe(ATTR_MIXER_PARAMETERS, self._handle_mixer_parameters)
|
119
119
|
self.subscribe(ATTR_MIXER_SENSORS, self._handle_mixer_sensors)
|
120
120
|
self.subscribe(ATTR_SCHEDULES, self._add_schedules)
|
@@ -124,6 +124,7 @@ class EcoMAX(PhysicalDevice):
|
|
124
124
|
self.subscribe(ATTR_THERMOSTAT_PARAMETERS, self._handle_thermostat_parameters)
|
125
125
|
self.subscribe(ATTR_THERMOSTAT_PROFILE, self._add_thermostat_profile_parameter)
|
126
126
|
self.subscribe(ATTR_THERMOSTAT_SENSORS, self._handle_thermostat_sensors)
|
127
|
+
self._fuel_burned_time_ns = time.perf_counter_ns()
|
127
128
|
|
128
129
|
async def async_setup(self) -> bool:
|
129
130
|
"""Set up an ecoMAX controller."""
|
@@ -183,13 +184,10 @@ class EcoMAX(PhysicalDevice):
|
|
183
184
|
description = ECOMAX_PARAMETERS[product.type][index]
|
184
185
|
except IndexError:
|
185
186
|
_LOGGER.warning(
|
186
|
-
(
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
"Please visit the issue tracker and open a feature "
|
191
|
-
"request to support %s"
|
192
|
-
),
|
187
|
+
"Encountered unknown ecoMAX parameter (%i): %s. "
|
188
|
+
"Your device isn't fully compatible with this software "
|
189
|
+
"and may not work properly. Please visit the issue tracker "
|
190
|
+
"and open a feature request to support %s",
|
193
191
|
index,
|
194
192
|
values,
|
195
193
|
product.model,
|
@@ -211,19 +209,29 @@ class EcoMAX(PhysicalDevice):
|
|
211
209
|
await asyncio.gather(*_ecomax_parameter_events())
|
212
210
|
return True
|
213
211
|
|
214
|
-
async def
|
215
|
-
"""Calculate
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
212
|
+
async def _add_burned_fuel_meter(self, fuel_consumption: float) -> None:
|
213
|
+
"""Calculate and dispatch the amount of fuel burned.
|
214
|
+
|
215
|
+
This method calculates the fuel burned based on the time
|
216
|
+
elapsed since the last sensor message, which contains fuel
|
217
|
+
consumption data. If the elapsed time is within the acceptable
|
218
|
+
range, it dispatches the fuel burned data. Otherwise, it logs a
|
219
|
+
warning and skips the outdated data.
|
220
|
+
"""
|
221
|
+
time_ns = time.perf_counter_ns()
|
222
|
+
nanoseconds_passed = time_ns - self._fuel_burned_time_ns
|
223
|
+
self._fuel_burned_time_ns = time_ns
|
224
|
+
if nanoseconds_passed < MAX_TIME_SINCE_LAST_FUEL_UPDATE_NS:
|
225
|
+
return self.dispatch_nowait(
|
226
|
+
ATTR_FUEL_BURNED,
|
227
|
+
fuel_consumption * nanoseconds_passed / (3600 * NANOSECONDS_IN_SECOND),
|
223
228
|
)
|
224
|
-
|
225
|
-
|
226
|
-
|
229
|
+
|
230
|
+
_LOGGER.warning(
|
231
|
+
"Skipping outdated fuel consumption data: %f (was %i seconds old)",
|
232
|
+
fuel_consumption,
|
233
|
+
nanoseconds_passed / NANOSECONDS_IN_SECOND,
|
234
|
+
)
|
227
235
|
|
228
236
|
async def _handle_mixer_parameters(
|
229
237
|
self,
|
@@ -64,13 +64,10 @@ class Mixer(VirtualDevice):
|
|
64
64
|
description = MIXER_PARAMETERS[product.type][index]
|
65
65
|
except IndexError:
|
66
66
|
_LOGGER.warning(
|
67
|
-
(
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
"Please visit the issue tracker and open a feature "
|
72
|
-
"request to support %s"
|
73
|
-
),
|
67
|
+
"Encountered unknown mixer parameter (%i): %s. "
|
68
|
+
"Your device isn't fully compatible with this software "
|
69
|
+
"and may not work properly. Please visit the issue tracker "
|
70
|
+
"and open a feature request to support %s",
|
74
71
|
index,
|
75
72
|
values,
|
76
73
|
product.model,
|
@@ -2,6 +2,8 @@
|
|
2
2
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
|
+
from pyplumio.const import FrameType
|
6
|
+
|
5
7
|
|
6
8
|
class PyPlumIOError(Exception):
|
7
9
|
"""Base PyPlumIO error class."""
|
@@ -11,13 +13,18 @@ class ConnectionFailedError(PyPlumIOError):
|
|
11
13
|
"""Raised on connection failure."""
|
12
14
|
|
13
15
|
|
14
|
-
class ProtocolError(PyPlumIOError):
|
15
|
-
"""Base class for protocol-related errors."""
|
16
|
-
|
17
|
-
|
18
16
|
class RequestError(PyPlumIOError):
|
19
17
|
"""Raised on request error."""
|
20
18
|
|
19
|
+
def __init__(self, message: str, frame_type: FrameType) -> None:
|
20
|
+
"""Initialize a new RequestError."""
|
21
|
+
super().__init__(message)
|
22
|
+
self.frame_type = frame_type
|
23
|
+
|
24
|
+
|
25
|
+
class ProtocolError(PyPlumIOError):
|
26
|
+
"""Base class for protocol-related errors."""
|
27
|
+
|
21
28
|
|
22
29
|
class ReadError(ProtocolError):
|
23
30
|
"""Raised on read error."""
|
@@ -6,7 +6,7 @@ from abc import ABC, abstractmethod
|
|
6
6
|
import asyncio
|
7
7
|
from dataclasses import dataclass
|
8
8
|
import logging
|
9
|
-
from typing import TYPE_CHECKING, Any, Literal, TypeVar, Union
|
9
|
+
from typing import TYPE_CHECKING, Any, Literal, TypeVar, Union, get_args
|
10
10
|
|
11
11
|
from dataslots import dataslots
|
12
12
|
from typing_extensions import TypeAlias
|
@@ -19,8 +19,8 @@ if TYPE_CHECKING:
|
|
19
19
|
|
20
20
|
_LOGGER = logging.getLogger(__name__)
|
21
21
|
|
22
|
-
|
23
|
-
|
22
|
+
NumericType: TypeAlias = Union[int, float]
|
23
|
+
State: TypeAlias = Literal["on", "off"]
|
24
24
|
ParameterT = TypeVar("ParameterT", bound="Parameter")
|
25
25
|
|
26
26
|
|
@@ -28,7 +28,7 @@ def unpack_parameter(
|
|
28
28
|
data: bytearray, offset: int = 0, size: int = 1
|
29
29
|
) -> ParameterValues | None:
|
30
30
|
"""Unpack a device parameter."""
|
31
|
-
if not
|
31
|
+
if not is_valid_parameter(data[offset : offset + size * 3]):
|
32
32
|
return None
|
33
33
|
|
34
34
|
value = data[offset : offset + size]
|
@@ -42,14 +42,17 @@ def unpack_parameter(
|
|
42
42
|
)
|
43
43
|
|
44
44
|
|
45
|
-
def
|
45
|
+
def is_valid_parameter(data: bytearray) -> bool:
|
46
46
|
"""Check if parameter contains any bytes besides 0xFF."""
|
47
47
|
return any(x for x in data if x != BYTE_UNDEFINED)
|
48
48
|
|
49
49
|
|
50
|
-
def
|
51
|
-
"""
|
52
|
-
|
50
|
+
def parameter_value_to_int(value: NumericType | State | bool) -> int:
|
51
|
+
"""Convert a parameter value to an integer.
|
52
|
+
|
53
|
+
If the value is STATE_OFF or STATE_ON, it returns 0 or 1 respectively.
|
54
|
+
"""
|
55
|
+
if value in get_args(State):
|
53
56
|
return 1 if value == STATE_ON else 0
|
54
57
|
|
55
58
|
return int(value)
|
@@ -72,24 +75,17 @@ class ParameterDescription:
|
|
72
75
|
"""Represents a parameter description."""
|
73
76
|
|
74
77
|
name: str
|
78
|
+
optimistic: bool = False
|
75
79
|
|
76
80
|
|
77
81
|
class Parameter(ABC):
|
78
82
|
"""Represents a base parameter."""
|
79
83
|
|
80
|
-
__slots__ = (
|
81
|
-
"device",
|
82
|
-
"description",
|
83
|
-
"_pending_update",
|
84
|
-
"_previous_value",
|
85
|
-
"_index",
|
86
|
-
"_values",
|
87
|
-
)
|
84
|
+
__slots__ = ("device", "description", "_pending_update", "_index", "_values")
|
88
85
|
|
89
86
|
device: Device
|
90
87
|
description: ParameterDescription
|
91
88
|
_pending_update: bool
|
92
|
-
_previous_value: int
|
93
89
|
_index: int
|
94
90
|
_values: ParameterValues
|
95
91
|
|
@@ -103,8 +99,8 @@ class Parameter(ABC):
|
|
103
99
|
"""Initialize a new parameter."""
|
104
100
|
self.device = device
|
105
101
|
self.description = description
|
102
|
+
self._index = index
|
106
103
|
self._pending_update = False
|
107
|
-
self._previous_value = 0
|
108
104
|
self._index = index
|
109
105
|
self._values = values if values else ParameterValues(0, 0, 0)
|
110
106
|
|
@@ -127,9 +123,9 @@ class Parameter(ABC):
|
|
127
123
|
handler = getattr(self.values, method_to_call)
|
128
124
|
return handler(other)
|
129
125
|
|
130
|
-
if isinstance(other, (int, float, bool)) or other in (
|
126
|
+
if isinstance(other, (int, float, bool)) or other in get_args(State):
|
131
127
|
handler = getattr(self.values.value, method_to_call)
|
132
|
-
return handler(
|
128
|
+
return handler(parameter_value_to_int(other))
|
133
129
|
else:
|
134
130
|
return NotImplemented
|
135
131
|
|
@@ -184,46 +180,56 @@ class Parameter(ABC):
|
|
184
180
|
)
|
185
181
|
return type(self)(self.device, self.description, values)
|
186
182
|
|
187
|
-
def validate(self, value:
|
183
|
+
def validate(self, value: NumericType | State | bool) -> int:
|
188
184
|
"""Validate a parameter value."""
|
189
|
-
|
190
|
-
if
|
185
|
+
int_value = parameter_value_to_int(value)
|
186
|
+
if int_value < self.values.min_value or int_value > self.values.max_value:
|
191
187
|
raise ValueError(
|
192
188
|
f"Invalid value: {value}. Must be between "
|
193
189
|
f"{self.min_value} and {self.max_value}."
|
194
190
|
)
|
195
191
|
|
196
|
-
return
|
192
|
+
return int_value
|
197
193
|
|
198
194
|
async def set(self, value: Any, retries: int = 5, timeout: float = 5.0) -> bool:
|
199
195
|
"""Set a parameter value."""
|
200
|
-
return await self.
|
196
|
+
return await self._attempt_update(self.validate(value), retries, timeout)
|
201
197
|
|
202
198
|
def set_nowait(self, value: Any, retries: int = 5, timeout: float = 5.0) -> None:
|
203
199
|
"""Set a parameter value without waiting."""
|
204
|
-
self.device.create_task(
|
200
|
+
self.device.create_task(
|
201
|
+
self._attempt_update(self.validate(value), retries, timeout)
|
202
|
+
)
|
205
203
|
|
206
|
-
async def
|
207
|
-
self, value:
|
204
|
+
async def _attempt_update(
|
205
|
+
self, value: int, retries: int = 5, timeout: float = 5.0
|
208
206
|
) -> bool:
|
209
|
-
"""
|
207
|
+
"""Attempt to update a parameter value on the remote device."""
|
208
|
+
_LOGGER.debug(
|
209
|
+
"Attempting to update '%s' parameter to %d", self.description.name, value
|
210
|
+
)
|
210
211
|
if value == self.values.value:
|
211
212
|
# Value is unchanged
|
212
213
|
return True
|
213
214
|
|
214
|
-
self._previous_value = self._values.value
|
215
215
|
self._values.value = value
|
216
|
+
request = await self.create_request()
|
217
|
+
if self.description.optimistic or not (initial_retries := retries):
|
218
|
+
# No retries
|
219
|
+
await self.device.queue.put(request)
|
220
|
+
return True
|
221
|
+
|
216
222
|
self._pending_update = True
|
217
223
|
while self.pending_update:
|
218
224
|
if retries <= 0:
|
219
225
|
_LOGGER.warning(
|
220
|
-
"Unable to confirm
|
226
|
+
"Unable to confirm update of '%s' parameter after %d retries",
|
221
227
|
self.description.name,
|
222
|
-
|
228
|
+
initial_retries,
|
223
229
|
)
|
224
230
|
return False
|
225
231
|
|
226
|
-
await self.device.queue.put(
|
232
|
+
await self.device.queue.put(request)
|
227
233
|
await asyncio.sleep(timeout)
|
228
234
|
retries -= 1
|
229
235
|
|
@@ -231,9 +237,7 @@ class Parameter(ABC):
|
|
231
237
|
|
232
238
|
def update(self, values: ParameterValues) -> None:
|
233
239
|
"""Update the parameter values."""
|
234
|
-
|
235
|
-
self._pending_update = False
|
236
|
-
|
240
|
+
self._pending_update = False
|
237
241
|
self._values = values
|
238
242
|
|
239
243
|
@property
|
@@ -267,17 +271,17 @@ class Parameter(ABC):
|
|
267
271
|
|
268
272
|
@property
|
269
273
|
@abstractmethod
|
270
|
-
def value(self) ->
|
274
|
+
def value(self) -> NumericType | State | bool:
|
271
275
|
"""Return the value."""
|
272
276
|
|
273
277
|
@property
|
274
278
|
@abstractmethod
|
275
|
-
def min_value(self) ->
|
279
|
+
def min_value(self) -> NumericType | State | bool:
|
276
280
|
"""Return the minimum allowed value."""
|
277
281
|
|
278
282
|
@property
|
279
283
|
@abstractmethod
|
280
|
-
def max_value(self) ->
|
284
|
+
def max_value(self) -> NumericType | State | bool:
|
281
285
|
"""Return the maximum allowed value."""
|
282
286
|
|
283
287
|
@abstractmethod
|
@@ -301,13 +305,13 @@ class Number(Parameter):
|
|
301
305
|
description: NumberDescription
|
302
306
|
|
303
307
|
async def set(
|
304
|
-
self, value:
|
308
|
+
self, value: NumericType, retries: int = 5, timeout: float = 5.0
|
305
309
|
) -> bool:
|
306
310
|
"""Set a parameter value."""
|
307
311
|
return await super().set(value, retries, timeout)
|
308
312
|
|
309
313
|
def set_nowait(
|
310
|
-
self, value:
|
314
|
+
self, value: NumericType, retries: int = 5, timeout: float = 5.0
|
311
315
|
) -> None:
|
312
316
|
"""Set a parameter value without waiting."""
|
313
317
|
super().set_nowait(value, retries, timeout)
|
@@ -317,17 +321,17 @@ class Number(Parameter):
|
|
317
321
|
return Request()
|
318
322
|
|
319
323
|
@property
|
320
|
-
def value(self) ->
|
324
|
+
def value(self) -> NumericType:
|
321
325
|
"""Return the value."""
|
322
326
|
return self.values.value
|
323
327
|
|
324
328
|
@property
|
325
|
-
def min_value(self) ->
|
329
|
+
def min_value(self) -> NumericType:
|
326
330
|
"""Return the minimum allowed value."""
|
327
331
|
return self.values.min_value
|
328
332
|
|
329
333
|
@property
|
330
|
-
def max_value(self) ->
|
334
|
+
def max_value(self) -> NumericType:
|
331
335
|
"""Return the maximum allowed value."""
|
332
336
|
return self.values.max_value
|
333
337
|
|
@@ -351,13 +355,13 @@ class Switch(Parameter):
|
|
351
355
|
description: SwitchDescription
|
352
356
|
|
353
357
|
async def set(
|
354
|
-
self, value:
|
358
|
+
self, value: State | bool, retries: int = 5, timeout: float = 5.0
|
355
359
|
) -> bool:
|
356
360
|
"""Set a parameter value."""
|
357
361
|
return await super().set(value, retries, timeout)
|
358
362
|
|
359
363
|
def set_nowait(
|
360
|
-
self, value:
|
364
|
+
self, value: State | bool, retries: int = 5, timeout: float = 5.0
|
361
365
|
) -> None:
|
362
366
|
"""Set a switch value without waiting."""
|
363
367
|
super().set_nowait(value, retries, timeout)
|
@@ -393,7 +397,7 @@ class Switch(Parameter):
|
|
393
397
|
return Request()
|
394
398
|
|
395
399
|
@property
|
396
|
-
def value(self) ->
|
400
|
+
def value(self) -> State:
|
397
401
|
"""Return the value."""
|
398
402
|
return STATE_ON if self.values.value == 1 else STATE_OFF
|
399
403
|
|