PyPlumIO 0.5.34__tar.gz → 0.5.36__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.34 → pyplumio-0.5.36}/PKG-INFO +6 -6
- {pyplumio-0.5.34 → pyplumio-0.5.36}/PyPlumIO.egg-info/PKG-INFO +6 -6
- {pyplumio-0.5.34 → pyplumio-0.5.36}/PyPlumIO.egg-info/requires.txt +5 -5
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/__init__.py +1 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/_version.py +2 -2
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/connection.py +1 -1
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/devices/__init__.py +9 -8
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/devices/ecomax.py +32 -24
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/devices/mixer.py +4 -7
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/exceptions.py +11 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/helpers/parameter.py +42 -47
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/helpers/schedule.py +2 -3
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/helpers/uid.py +5 -5
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyproject.toml +5 -5
- {pyplumio-0.5.34 → pyplumio-0.5.36}/requirements_test.txt +5 -5
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/helpers/test_parameter.py +13 -7
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/test_devices.py +9 -3
- {pyplumio-0.5.34 → pyplumio-0.5.36}/.gitattributes +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/.github/CODE_OF_CONDUCT.md +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/.github/dependabot.yml +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/.github/workflows/ci.yml +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/.github/workflows/codeql-analysis.yml +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/.github/workflows/deploy.yml +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/.github/workflows/documentation.yml +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/.gitignore +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/.pre-commit-config.yaml +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/.vscode/settings.json +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/LICENSE +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/MANIFEST.in +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/PyPlumIO.egg-info/SOURCES.txt +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/PyPlumIO.egg-info/dependency_links.txt +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/PyPlumIO.egg-info/top_level.txt +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/README.md +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/docs/Makefile +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/docs/make.bat +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/docs/source/callbacks.rst +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/docs/source/conf.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/docs/source/connecting.rst +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/docs/source/frames.rst +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/docs/source/index.rst +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/docs/source/mixers_thermostats.rst +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/docs/source/protocol.rst +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/docs/source/reading.rst +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/docs/source/schedules.rst +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/docs/source/writing.rst +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/images/ecomax.png +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/images/rs485.png +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/__main__.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/const.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/devices/ecoster.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/devices/thermostat.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/filters.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/frames/__init__.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/frames/messages.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/frames/requests.py +65 -65
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/frames/responses.py +62 -62
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/helpers/__init__.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/helpers/data_types.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/helpers/event_manager.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/helpers/factory.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/helpers/task_manager.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/helpers/timeout.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/protocol.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/py.typed +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/stream.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/__init__.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/alerts.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/boiler_load.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/boiler_power.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/ecomax_parameters.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/fan_power.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/frame_versions.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/fuel_consumption.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/fuel_level.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/lambda_sensor.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/mixer_parameters.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/mixer_sensors.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/modules.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/network_info.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/output_flags.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/outputs.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/pending_alerts.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/product_info.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/program_version.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/regulator_data.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/regulator_data_schema.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/schedules.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/statuses.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/temperatures.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/thermostat_parameters.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/thermostat_sensors.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/utils.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/requirements.txt +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/requirements_docs.txt +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/setup.cfg +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/__init__.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/conftest.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/frames/test_init.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/frames/test_messages.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/frames/test_requests.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/frames/test_responses.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/helpers/__init__.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/helpers/test_data_types.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/helpers/test_event_manager.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/helpers/test_factory.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/helpers/test_schedule.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/helpers/test_task_manager.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/helpers/test_timeout.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/helpers/test_uid.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/ruff.toml +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/test_connection.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/test_filters.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/test_init.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/test_main.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/test_protocol.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/test_stream.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/test_utils.py +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/testdata/messages/regulator_data.json +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/testdata/messages/sensor_data.json +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/testdata/requests/alerts.json +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/testdata/requests/ecomax_control.json +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/testdata/requests/ecomax_parameters.json +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/testdata/requests/mixer_parameters.json +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/testdata/requests/set_ecomax_parameter.json +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/testdata/requests/set_mixer_parameter.json +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/testdata/requests/set_schedule.json +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/testdata/requests/set_thermostat_parameter.json +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/testdata/requests/thermostat_parameters.json +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/testdata/responses/alerts.json +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/testdata/responses/device_available.json +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/testdata/responses/ecomax_parameters.json +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/testdata/responses/mixer_parameters.json +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/testdata/responses/password.json +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/testdata/responses/program_version.json +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/testdata/responses/regulator_data_schema.json +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/testdata/responses/schedules.json +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/testdata/responses/thermostat_parameters.json +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/testdata/responses/uid.json +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/testdata/unknown/unknown_ecomax_parameter.json +0 -0
- {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/testdata/unknown/unknown_mixer_parameter.json +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: PyPlumIO
|
3
|
-
Version: 0.5.
|
3
|
+
Version: 0.5.36
|
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,22 +27,22 @@ 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.0; 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
35
|
Requires-Dist: pytest-asyncio==0.25.2; extra == "test"
|
36
|
-
Requires-Dist: ruff==0.9.
|
37
|
-
Requires-Dist: tox==4.
|
38
|
-
Requires-Dist: types-pyserial==3.5.0.
|
36
|
+
Requires-Dist: ruff==0.9.3; extra == "test"
|
37
|
+
Requires-Dist: tox==4.24.1; extra == "test"
|
38
|
+
Requires-Dist: types-pyserial==3.5.0.20250124; 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.
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: PyPlumIO
|
3
|
-
Version: 0.5.
|
3
|
+
Version: 0.5.36
|
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,22 +27,22 @@ 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.0; 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
35
|
Requires-Dist: pytest-asyncio==0.25.2; extra == "test"
|
36
|
-
Requires-Dist: ruff==0.9.
|
37
|
-
Requires-Dist: tox==4.
|
38
|
-
Requires-Dist: types-pyserial==3.5.0.
|
36
|
+
Requires-Dist: ruff==0.9.3; extra == "test"
|
37
|
+
Requires-Dist: tox==4.24.1; extra == "test"
|
38
|
+
Requires-Dist: types-pyserial==3.5.0.20250124; 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.
|
@@ -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.0
|
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
21
|
pytest-asyncio==0.25.2
|
22
|
-
ruff==0.9.
|
23
|
-
tox==4.
|
24
|
-
types-pyserial==3.5.0.
|
22
|
+
ruff==0.9.3
|
23
|
+
tox==4.24.1
|
24
|
+
types-pyserial==3.5.0.20250124
|
@@ -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.
|
@@ -9,11 +9,11 @@ import logging
|
|
9
9
|
from typing import Any, ClassVar
|
10
10
|
|
11
11
|
from pyplumio.const import ATTR_FRAME_ERRORS, ATTR_LOADED, DeviceType, FrameType
|
12
|
-
from pyplumio.exceptions import UnknownDeviceError
|
12
|
+
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(
|
@@ -203,9 +203,10 @@ class PhysicalDevice(Device, ABC):
|
|
203
203
|
except asyncio.TimeoutError:
|
204
204
|
retries -= 1
|
205
205
|
|
206
|
-
raise
|
207
|
-
f"Failed to request
|
208
|
-
f"
|
206
|
+
raise RequestError(
|
207
|
+
f"Failed to request '{name}' with frame type '{frame_type}' after "
|
208
|
+
f"{retries} retries.",
|
209
|
+
frame_type=frame_type,
|
209
210
|
)
|
210
211
|
|
211
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,6 +13,15 @@ class ConnectionFailedError(PyPlumIOError):
|
|
11
13
|
"""Raised on connection failure."""
|
12
14
|
|
13
15
|
|
16
|
+
class RequestError(PyPlumIOError):
|
17
|
+
"""Raised on request error."""
|
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
|
+
|
14
25
|
class ProtocolError(PyPlumIOError):
|
15
26
|
"""Base class for protocol-related errors."""
|
16
27
|
|
@@ -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 validate_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 validate_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)
|
@@ -77,19 +80,11 @@ class ParameterDescription:
|
|
77
80
|
class Parameter(ABC):
|
78
81
|
"""Represents a base parameter."""
|
79
82
|
|
80
|
-
__slots__ = (
|
81
|
-
"device",
|
82
|
-
"description",
|
83
|
-
"_pending_update",
|
84
|
-
"_previous_value",
|
85
|
-
"_index",
|
86
|
-
"_values",
|
87
|
-
)
|
83
|
+
__slots__ = ("device", "description", "_pending_update", "_index", "_values")
|
88
84
|
|
89
85
|
device: Device
|
90
86
|
description: ParameterDescription
|
91
87
|
_pending_update: bool
|
92
|
-
_previous_value: int
|
93
88
|
_index: int
|
94
89
|
_values: ParameterValues
|
95
90
|
|
@@ -103,8 +98,8 @@ class Parameter(ABC):
|
|
103
98
|
"""Initialize a new parameter."""
|
104
99
|
self.device = device
|
105
100
|
self.description = description
|
101
|
+
self._index = index
|
106
102
|
self._pending_update = False
|
107
|
-
self._previous_value = 0
|
108
103
|
self._index = index
|
109
104
|
self._values = values if values else ParameterValues(0, 0, 0)
|
110
105
|
|
@@ -127,9 +122,9 @@ class Parameter(ABC):
|
|
127
122
|
handler = getattr(self.values, method_to_call)
|
128
123
|
return handler(other)
|
129
124
|
|
130
|
-
if isinstance(other, (int, float, bool)) or other in (
|
125
|
+
if isinstance(other, (int, float, bool)) or other in get_args(State):
|
131
126
|
handler = getattr(self.values.value, method_to_call)
|
132
|
-
return handler(
|
127
|
+
return handler(parameter_value_to_int(other))
|
133
128
|
else:
|
134
129
|
return NotImplemented
|
135
130
|
|
@@ -184,42 +179,44 @@ class Parameter(ABC):
|
|
184
179
|
)
|
185
180
|
return type(self)(self.device, self.description, values)
|
186
181
|
|
187
|
-
def validate(self, value:
|
182
|
+
def validate(self, value: NumericType | State | bool) -> int:
|
188
183
|
"""Validate a parameter value."""
|
189
|
-
|
190
|
-
if
|
184
|
+
int_value = parameter_value_to_int(value)
|
185
|
+
if int_value < self.values.min_value or int_value > self.values.max_value:
|
191
186
|
raise ValueError(
|
192
187
|
f"Invalid value: {value}. Must be between "
|
193
188
|
f"{self.min_value} and {self.max_value}."
|
194
189
|
)
|
195
190
|
|
196
|
-
return
|
191
|
+
return int_value
|
197
192
|
|
198
193
|
async def set(self, value: Any, retries: int = 5, timeout: float = 5.0) -> bool:
|
199
194
|
"""Set a parameter value."""
|
200
|
-
return await self.
|
195
|
+
return await self._attempt_update(self.validate(value), retries, timeout)
|
201
196
|
|
202
197
|
def set_nowait(self, value: Any, retries: int = 5, timeout: float = 5.0) -> None:
|
203
198
|
"""Set a parameter value without waiting."""
|
204
|
-
self.device.create_task(
|
199
|
+
self.device.create_task(
|
200
|
+
self._attempt_update(self.validate(value), retries, timeout)
|
201
|
+
)
|
205
202
|
|
206
|
-
async def
|
207
|
-
self, value:
|
203
|
+
async def _attempt_update(
|
204
|
+
self, value: int, retries: int = 5, timeout: float = 5.0
|
208
205
|
) -> bool:
|
209
|
-
"""
|
206
|
+
"""Attempt to update a parameter value on the remote device."""
|
210
207
|
if value == self.values.value:
|
211
208
|
# Value is unchanged
|
212
209
|
return True
|
213
210
|
|
214
|
-
self._previous_value = self._values.value
|
215
|
-
self._values.value = value
|
216
211
|
self._pending_update = True
|
212
|
+
self._values.value = value
|
213
|
+
initial_retries = retries
|
217
214
|
while self.pending_update:
|
218
215
|
if retries <= 0:
|
219
216
|
_LOGGER.warning(
|
220
|
-
"
|
217
|
+
"Unable to confirm update of '%s' parameter after %d retries",
|
221
218
|
self.description.name,
|
222
|
-
|
219
|
+
initial_retries,
|
223
220
|
)
|
224
221
|
return False
|
225
222
|
|
@@ -231,9 +228,7 @@ class Parameter(ABC):
|
|
231
228
|
|
232
229
|
def update(self, values: ParameterValues) -> None:
|
233
230
|
"""Update the parameter values."""
|
234
|
-
|
235
|
-
self._pending_update = False
|
236
|
-
|
231
|
+
self._pending_update = False
|
237
232
|
self._values = values
|
238
233
|
|
239
234
|
@property
|
@@ -267,17 +262,17 @@ class Parameter(ABC):
|
|
267
262
|
|
268
263
|
@property
|
269
264
|
@abstractmethod
|
270
|
-
def value(self) ->
|
265
|
+
def value(self) -> NumericType | State | bool:
|
271
266
|
"""Return the value."""
|
272
267
|
|
273
268
|
@property
|
274
269
|
@abstractmethod
|
275
|
-
def min_value(self) ->
|
270
|
+
def min_value(self) -> NumericType | State | bool:
|
276
271
|
"""Return the minimum allowed value."""
|
277
272
|
|
278
273
|
@property
|
279
274
|
@abstractmethod
|
280
|
-
def max_value(self) ->
|
275
|
+
def max_value(self) -> NumericType | State | bool:
|
281
276
|
"""Return the maximum allowed value."""
|
282
277
|
|
283
278
|
@abstractmethod
|
@@ -301,13 +296,13 @@ class Number(Parameter):
|
|
301
296
|
description: NumberDescription
|
302
297
|
|
303
298
|
async def set(
|
304
|
-
self, value:
|
299
|
+
self, value: NumericType, retries: int = 5, timeout: float = 5.0
|
305
300
|
) -> bool:
|
306
301
|
"""Set a parameter value."""
|
307
302
|
return await super().set(value, retries, timeout)
|
308
303
|
|
309
304
|
def set_nowait(
|
310
|
-
self, value:
|
305
|
+
self, value: NumericType, retries: int = 5, timeout: float = 5.0
|
311
306
|
) -> None:
|
312
307
|
"""Set a parameter value without waiting."""
|
313
308
|
super().set_nowait(value, retries, timeout)
|
@@ -317,17 +312,17 @@ class Number(Parameter):
|
|
317
312
|
return Request()
|
318
313
|
|
319
314
|
@property
|
320
|
-
def value(self) ->
|
315
|
+
def value(self) -> NumericType:
|
321
316
|
"""Return the value."""
|
322
317
|
return self.values.value
|
323
318
|
|
324
319
|
@property
|
325
|
-
def min_value(self) ->
|
320
|
+
def min_value(self) -> NumericType:
|
326
321
|
"""Return the minimum allowed value."""
|
327
322
|
return self.values.min_value
|
328
323
|
|
329
324
|
@property
|
330
|
-
def max_value(self) ->
|
325
|
+
def max_value(self) -> NumericType:
|
331
326
|
"""Return the maximum allowed value."""
|
332
327
|
return self.values.max_value
|
333
328
|
|
@@ -351,13 +346,13 @@ class Switch(Parameter):
|
|
351
346
|
description: SwitchDescription
|
352
347
|
|
353
348
|
async def set(
|
354
|
-
self, value:
|
349
|
+
self, value: State | bool, retries: int = 5, timeout: float = 5.0
|
355
350
|
) -> bool:
|
356
351
|
"""Set a parameter value."""
|
357
352
|
return await super().set(value, retries, timeout)
|
358
353
|
|
359
354
|
def set_nowait(
|
360
|
-
self, value:
|
355
|
+
self, value: State | bool, retries: int = 5, timeout: float = 5.0
|
361
356
|
) -> None:
|
362
357
|
"""Set a switch value without waiting."""
|
363
358
|
super().set_nowait(value, retries, timeout)
|
@@ -393,7 +388,7 @@ class Switch(Parameter):
|
|
393
388
|
return Request()
|
394
389
|
|
395
390
|
@property
|
396
|
-
def value(self) ->
|
391
|
+
def value(self) -> State:
|
397
392
|
"""Return the value."""
|
398
393
|
return STATE_ON if self.values.value == 1 else STATE_OFF
|
399
394
|
|
@@ -21,8 +21,7 @@ TIME_FORMAT: Final = "%H:%M"
|
|
21
21
|
STATE_NIGHT: Final = "night"
|
22
22
|
STATE_DAY: Final = "day"
|
23
23
|
|
24
|
-
|
25
|
-
OFF_STATES: Final = (STATE_OFF, STATE_NIGHT)
|
24
|
+
_ON_STATES: Final = {STATE_ON, STATE_DAY}
|
26
25
|
|
27
26
|
ScheduleState: TypeAlias = Literal["on", "off", "day", "night"]
|
28
27
|
Time = Annotated[str, "time in HH:MM format"]
|
@@ -113,7 +112,7 @@ class ScheduleDay(MutableMapping):
|
|
113
112
|
)
|
114
113
|
|
115
114
|
for index in _get_time_range(start, end):
|
116
|
-
self._intervals[index] = True if state in
|
115
|
+
self._intervals[index] = True if state in _ON_STATES else False
|
117
116
|
|
118
117
|
def set_on(self, start: Time = "00:00", end: Time = "00:00") -> None:
|
119
118
|
"""Set a schedule interval state to 'on'."""
|
@@ -1,4 +1,4 @@
|
|
1
|
-
"""Contains
|
1
|
+
"""Contains UID helpers."""
|
2
2
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
@@ -18,18 +18,18 @@ def decode_uid(buffer: bytes) -> str:
|
|
18
18
|
def _base5(buffer: bytes) -> str:
|
19
19
|
"""Encode bytes to a base5 encoded string."""
|
20
20
|
number = int.from_bytes(buffer, byteorder="little")
|
21
|
-
output =
|
21
|
+
output = []
|
22
22
|
while number:
|
23
|
-
output
|
23
|
+
output.append(BASE5_KEY[number & 0b00011111])
|
24
24
|
number >>= 5
|
25
25
|
|
26
|
-
return output
|
26
|
+
return "".join(reversed(output))
|
27
27
|
|
28
28
|
|
29
29
|
def _crc16(buffer: bytes) -> bytes:
|
30
30
|
"""Return a CRC 16."""
|
31
31
|
crc16 = reduce(_crc16_byte, buffer, CRC)
|
32
|
-
return crc16.to_bytes(byteorder="little"
|
32
|
+
return crc16.to_bytes(length=2, byteorder="little")
|
33
33
|
|
34
34
|
|
35
35
|
def _crc16_byte(crc: int, byte: int) -> int:
|
@@ -37,15 +37,15 @@ dynamic = ["version"]
|
|
37
37
|
|
38
38
|
[project.optional-dependencies]
|
39
39
|
test = [
|
40
|
-
"codespell==2.
|
40
|
+
"codespell==2.4.0",
|
41
41
|
"coverage==7.6.10",
|
42
42
|
"mypy==1.14.1",
|
43
43
|
"pyserial-asyncio-fast==0.14",
|
44
44
|
"pytest==8.3.4",
|
45
45
|
"pytest-asyncio==0.25.2",
|
46
|
-
"ruff==0.9.
|
47
|
-
"tox==4.
|
48
|
-
"types-pyserial==3.5.0.
|
46
|
+
"ruff==0.9.3",
|
47
|
+
"tox==4.24.1",
|
48
|
+
"types-pyserial==3.5.0.20250124"
|
49
49
|
]
|
50
50
|
docs = [
|
51
51
|
"sphinx==8.1.3",
|
@@ -54,7 +54,7 @@ docs = [
|
|
54
54
|
]
|
55
55
|
dev = [
|
56
56
|
"pyplumio[test,docs]",
|
57
|
-
"pre-commit==4.0
|
57
|
+
"pre-commit==4.1.0",
|
58
58
|
"tomli==2.2.1"
|
59
59
|
]
|
60
60
|
|
@@ -1,11 +1,11 @@
|
|
1
|
-
codespell==2.
|
1
|
+
codespell==2.4.0
|
2
2
|
coverage==7.6.10
|
3
3
|
mypy==1.14.1
|
4
|
-
pre-commit==4.0
|
4
|
+
pre-commit==4.1.0
|
5
5
|
pyserial-asyncio-fast==0.14
|
6
6
|
pytest-asyncio==0.25.2
|
7
7
|
pytest==8.3.4
|
8
|
-
ruff==0.9.
|
8
|
+
ruff==0.9.3
|
9
9
|
tomli==2.2.1
|
10
|
-
tox==4.
|
11
|
-
types-pyserial==3.5.0.
|
10
|
+
tox==4.24.1
|
11
|
+
types-pyserial==3.5.0.20250124
|