PyPlumIO 0.5.28__tar.gz → 0.5.30__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.28 → pyplumio-0.5.30}/.pre-commit-config.yaml +2 -2
- {pyplumio-0.5.28 → pyplumio-0.5.30}/PKG-INFO +10 -10
- {pyplumio-0.5.28 → pyplumio-0.5.30}/PyPlumIO.egg-info/PKG-INFO +10 -10
- {pyplumio-0.5.28 → pyplumio-0.5.30}/PyPlumIO.egg-info/requires.txt +9 -9
- {pyplumio-0.5.28 → pyplumio-0.5.30}/docs/source/connecting.rst +9 -5
- {pyplumio-0.5.28 → pyplumio-0.5.30}/docs/source/mixers_thermostats.rst +2 -2
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/_version.py +2 -2
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/devices/__init__.py +42 -5
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/devices/ecomax.py +2 -31
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/devices/mixer.py +1 -1
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/devices/thermostat.py +1 -1
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/filters.py +10 -10
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/frames/__init__.py +13 -6
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/helpers/data_types.py +7 -7
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/helpers/parameter.py +38 -2
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/helpers/schedule.py +1 -1
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/stream.py +33 -31
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/ecomax_parameters.py +37 -26
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/mixer_parameters.py +20 -10
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/regulator_data.py +1 -1
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/schedules.py +13 -2
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/thermostat_parameters.py +24 -6
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyproject.toml +10 -9
- pyplumio-0.5.30/requirements_docs.txt +3 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/requirements_test.txt +7 -7
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/frames/test_messages.py +3 -3
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/frames/test_responses.py +3 -3
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/helpers/test_parameter.py +18 -4
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/test_devices.py +49 -8
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/test_stream.py +48 -58
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/testdata/responses/ecomax_parameters.json +2 -2
- pyplumio-0.5.28/requirements_docs.txt +0 -3
- {pyplumio-0.5.28 → pyplumio-0.5.30}/.gitattributes +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/.github/CODE_OF_CONDUCT.md +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/.github/dependabot.yml +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/.github/workflows/ci.yml +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/.github/workflows/codeql-analysis.yml +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/.github/workflows/deploy.yml +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/.github/workflows/documentation.yml +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/.gitignore +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/.vscode/settings.json +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/LICENSE +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/MANIFEST.in +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/PyPlumIO.egg-info/SOURCES.txt +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/PyPlumIO.egg-info/dependency_links.txt +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/PyPlumIO.egg-info/top_level.txt +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/README.md +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/docs/Makefile +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/docs/make.bat +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/docs/source/callbacks.rst +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/docs/source/conf.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/docs/source/frames.rst +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/docs/source/index.rst +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/docs/source/protocol.rst +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/docs/source/reading.rst +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/docs/source/schedules.rst +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/docs/source/writing.rst +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/images/ecomax.png +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/images/rs485.png +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/__init__.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/__main__.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/connection.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/const.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/devices/ecoster.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/exceptions.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/frames/messages.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/frames/requests.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/frames/responses.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/helpers/__init__.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/helpers/event_manager.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/helpers/factory.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/helpers/task_manager.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/helpers/timeout.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/helpers/uid.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/protocol.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/py.typed +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/__init__.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/alerts.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/boiler_load.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/boiler_power.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/fan_power.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/frame_versions.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/fuel_consumption.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/fuel_level.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/lambda_sensor.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/mixer_sensors.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/modules.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/network_info.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/output_flags.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/outputs.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/pending_alerts.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/product_info.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/program_version.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/regulator_data_schema.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/statuses.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/temperatures.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/thermostat_sensors.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/utils.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/requirements.txt +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/setup.cfg +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/__init__.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/conftest.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/frames/test_init.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/frames/test_requests.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/helpers/__init__.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/helpers/test_data_types.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/helpers/test_event_manager.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/helpers/test_factory.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/helpers/test_schedule.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/helpers/test_task_manager.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/helpers/test_timeout.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/helpers/test_uid.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/ruff.toml +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/test_connection.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/test_filters.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/test_init.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/test_main.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/test_protocol.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/test_utils.py +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/testdata/messages/regulator_data.json +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/testdata/messages/sensor_data.json +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/testdata/requests/alerts.json +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/testdata/requests/ecomax_control.json +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/testdata/requests/ecomax_parameters.json +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/testdata/requests/mixer_parameters.json +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/testdata/requests/set_ecomax_parameter.json +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/testdata/requests/set_mixer_parameter.json +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/testdata/requests/set_schedule.json +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/testdata/requests/set_thermostat_parameter.json +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/testdata/requests/thermostat_parameters.json +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/testdata/responses/alerts.json +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/testdata/responses/device_available.json +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/testdata/responses/mixer_parameters.json +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/testdata/responses/password.json +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/testdata/responses/program_version.json +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/testdata/responses/regulator_data_schema.json +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/testdata/responses/schedules.json +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/testdata/responses/thermostat_parameters.json +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/testdata/responses/uid.json +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/testdata/unknown/unknown_ecomax_parameter.json +0 -0
- {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/testdata/unknown/unknown_mixer_parameter.json +0 -0
@@ -2,7 +2,7 @@
|
|
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.
|
5
|
+
rev: v0.8.1
|
6
6
|
hooks:
|
7
7
|
- id: ruff
|
8
8
|
args:
|
@@ -12,6 +12,6 @@ repos:
|
|
12
12
|
hooks:
|
13
13
|
- id: codespell
|
14
14
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
15
|
-
rev: v1.
|
15
|
+
rev: v1.13.0
|
16
16
|
hooks:
|
17
17
|
- id: mypy
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: PyPlumIO
|
3
|
-
Version: 0.5.
|
3
|
+
Version: 0.5.30
|
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: pyserial-asyncio==0.6
|
|
27
27
|
Requires-Dist: typing-extensions==4.12.2
|
28
28
|
Provides-Extra: test
|
29
29
|
Requires-Dist: codespell==2.3.0; extra == "test"
|
30
|
-
Requires-Dist: coverage==7.6.
|
31
|
-
Requires-Dist: mypy==1.
|
30
|
+
Requires-Dist: coverage==7.6.8; extra == "test"
|
31
|
+
Requires-Dist: mypy==1.13.0; extra == "test"
|
32
32
|
Requires-Dist: pyserial-asyncio-fast==0.14; extra == "test"
|
33
|
-
Requires-Dist: pytest==8.3.
|
33
|
+
Requires-Dist: pytest==8.3.4; extra == "test"
|
34
34
|
Requires-Dist: pytest-asyncio==0.24.0; extra == "test"
|
35
|
-
Requires-Dist: ruff==0.
|
36
|
-
Requires-Dist: tox==4.
|
35
|
+
Requires-Dist: ruff==0.8.1; extra == "test"
|
36
|
+
Requires-Dist: tox==4.23.2; extra == "test"
|
37
37
|
Requires-Dist: types-pyserial==3.5.0.20240826; extra == "test"
|
38
38
|
Provides-Extra: docs
|
39
|
-
Requires-Dist: sphinx==
|
40
|
-
Requires-Dist: sphinx_rtd_theme==
|
39
|
+
Requires-Dist: sphinx==8.1.3; extra == "docs"
|
40
|
+
Requires-Dist: sphinx_rtd_theme==3.0.2; extra == "docs"
|
41
41
|
Requires-Dist: readthedocs-sphinx-search==0.3.2; extra == "docs"
|
42
42
|
Provides-Extra: dev
|
43
43
|
Requires-Dist: pyplumio[docs,test]; extra == "dev"
|
44
|
-
Requires-Dist: pre-commit==
|
45
|
-
Requires-Dist: tomli==2.
|
44
|
+
Requires-Dist: pre-commit==4.0.1; extra == "dev"
|
45
|
+
Requires-Dist: tomli==2.2.1; extra == "dev"
|
46
46
|
|
47
47
|
# PyPlumIO is a native ecoNET library for Plum ecoMAX controllers.
|
48
48
|
[](https://badge.fury.io/py/PyPlumIO)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: PyPlumIO
|
3
|
-
Version: 0.5.
|
3
|
+
Version: 0.5.30
|
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: pyserial-asyncio==0.6
|
|
27
27
|
Requires-Dist: typing-extensions==4.12.2
|
28
28
|
Provides-Extra: test
|
29
29
|
Requires-Dist: codespell==2.3.0; extra == "test"
|
30
|
-
Requires-Dist: coverage==7.6.
|
31
|
-
Requires-Dist: mypy==1.
|
30
|
+
Requires-Dist: coverage==7.6.8; extra == "test"
|
31
|
+
Requires-Dist: mypy==1.13.0; extra == "test"
|
32
32
|
Requires-Dist: pyserial-asyncio-fast==0.14; extra == "test"
|
33
|
-
Requires-Dist: pytest==8.3.
|
33
|
+
Requires-Dist: pytest==8.3.4; extra == "test"
|
34
34
|
Requires-Dist: pytest-asyncio==0.24.0; extra == "test"
|
35
|
-
Requires-Dist: ruff==0.
|
36
|
-
Requires-Dist: tox==4.
|
35
|
+
Requires-Dist: ruff==0.8.1; extra == "test"
|
36
|
+
Requires-Dist: tox==4.23.2; extra == "test"
|
37
37
|
Requires-Dist: types-pyserial==3.5.0.20240826; extra == "test"
|
38
38
|
Provides-Extra: docs
|
39
|
-
Requires-Dist: sphinx==
|
40
|
-
Requires-Dist: sphinx_rtd_theme==
|
39
|
+
Requires-Dist: sphinx==8.1.3; extra == "docs"
|
40
|
+
Requires-Dist: sphinx_rtd_theme==3.0.2; extra == "docs"
|
41
41
|
Requires-Dist: readthedocs-sphinx-search==0.3.2; extra == "docs"
|
42
42
|
Provides-Extra: dev
|
43
43
|
Requires-Dist: pyplumio[docs,test]; extra == "dev"
|
44
|
-
Requires-Dist: pre-commit==
|
45
|
-
Requires-Dist: tomli==2.
|
44
|
+
Requires-Dist: pre-commit==4.0.1; extra == "dev"
|
45
|
+
Requires-Dist: tomli==2.2.1; extra == "dev"
|
46
46
|
|
47
47
|
# PyPlumIO is a native ecoNET library for Plum ecoMAX controllers.
|
48
48
|
[](https://badge.fury.io/py/PyPlumIO)
|
@@ -4,21 +4,21 @@ typing-extensions==4.12.2
|
|
4
4
|
|
5
5
|
[dev]
|
6
6
|
pyplumio[docs,test]
|
7
|
-
pre-commit==
|
8
|
-
tomli==2.
|
7
|
+
pre-commit==4.0.1
|
8
|
+
tomli==2.2.1
|
9
9
|
|
10
10
|
[docs]
|
11
|
-
sphinx==
|
12
|
-
sphinx_rtd_theme==
|
11
|
+
sphinx==8.1.3
|
12
|
+
sphinx_rtd_theme==3.0.2
|
13
13
|
readthedocs-sphinx-search==0.3.2
|
14
14
|
|
15
15
|
[test]
|
16
16
|
codespell==2.3.0
|
17
|
-
coverage==7.6.
|
18
|
-
mypy==1.
|
17
|
+
coverage==7.6.8
|
18
|
+
mypy==1.13.0
|
19
19
|
pyserial-asyncio-fast==0.14
|
20
|
-
pytest==8.3.
|
20
|
+
pytest==8.3.4
|
21
21
|
pytest-asyncio==0.24.0
|
22
|
-
ruff==0.
|
23
|
-
tox==4.
|
22
|
+
ruff==0.8.1
|
23
|
+
tox==4.23.2
|
24
24
|
types-pyserial==3.5.0.20240826
|
@@ -76,11 +76,15 @@ working with device classes and queues.
|
|
76
76
|
)
|
77
77
|
|
78
78
|
while connection.connected:
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
79
|
+
try:
|
80
|
+
if isinstance(
|
81
|
+
(frame := await connection.reader.read()), responses.AlertsResponse
|
82
|
+
):
|
83
|
+
print(frame.data)
|
84
|
+
break
|
85
|
+
except pyplumio.ProtocolError:
|
86
|
+
# Skip protocol errors and read the next frame.
|
87
|
+
pass
|
84
88
|
|
85
89
|
|
86
90
|
asyncio.run(main())
|
@@ -49,8 +49,8 @@ get it's current_temp property and set it's target temperature to
|
|
49
49
|
# Set mixer target temperature to 50 degrees Celsius.
|
50
50
|
await mixer.set("mixer_target_temp", 50)
|
51
51
|
|
52
|
-
|
53
|
-
|
52
|
+
Thermostat Examples
|
53
|
+
-------------------
|
54
54
|
|
55
55
|
In the following example, we'll get single thermostat by it's index,
|
56
56
|
get current room temperature and set daytime target temperature to 20
|
@@ -5,17 +5,21 @@ from __future__ import annotations
|
|
5
5
|
from abc import ABC
|
6
6
|
import asyncio
|
7
7
|
from functools import cache
|
8
|
+
import logging
|
8
9
|
from typing import Any, ClassVar
|
9
10
|
|
10
11
|
from pyplumio.const import ATTR_FRAME_ERRORS, ATTR_LOADED, DeviceType, FrameType
|
11
12
|
from pyplumio.exceptions import UnknownDeviceError
|
12
|
-
from pyplumio.frames import DataFrameDescription, Frame, Request
|
13
|
+
from pyplumio.frames import DataFrameDescription, Frame, Request, is_known_frame_type
|
13
14
|
from pyplumio.helpers.event_manager import EventManager
|
14
15
|
from pyplumio.helpers.factory import create_instance
|
15
16
|
from pyplumio.helpers.parameter import Parameter, ParameterValue
|
17
|
+
from pyplumio.structures.frame_versions import ATTR_FRAME_VERSIONS
|
16
18
|
from pyplumio.structures.network_info import NetworkInfo
|
17
19
|
from pyplumio.utils import to_camelcase
|
18
20
|
|
21
|
+
_LOGGER = logging.getLogger(__name__)
|
22
|
+
|
19
23
|
|
20
24
|
@cache
|
21
25
|
def is_known_device_type(device_type: int) -> bool:
|
@@ -45,7 +49,7 @@ class Device(ABC, EventManager):
|
|
45
49
|
|
46
50
|
queue: asyncio.Queue[Frame]
|
47
51
|
|
48
|
-
def __init__(self, queue: asyncio.Queue[Frame]):
|
52
|
+
def __init__(self, queue: asyncio.Queue[Frame]) -> None:
|
49
53
|
"""Initialize a new device."""
|
50
54
|
super().__init__()
|
51
55
|
self.queue = queue
|
@@ -125,15 +129,48 @@ class PhysicalDevice(Device, ABC):
|
|
125
129
|
address: ClassVar[int]
|
126
130
|
_network: NetworkInfo
|
127
131
|
_setup_frames: tuple[DataFrameDescription, ...]
|
132
|
+
_frame_versions: dict[int, int]
|
128
133
|
|
129
|
-
def __init__(self, queue: asyncio.Queue[Frame], network: NetworkInfo):
|
134
|
+
def __init__(self, queue: asyncio.Queue[Frame], network: NetworkInfo) -> None:
|
130
135
|
"""Initialize a new physical device."""
|
131
136
|
super().__init__(queue)
|
132
137
|
self._network = network
|
138
|
+
self._frame_versions = {}
|
139
|
+
|
140
|
+
async def update_frame_versions(versions: dict[int, int]) -> None:
|
141
|
+
"""Check frame versions and update outdated frames."""
|
142
|
+
for frame_type, version in versions.items():
|
143
|
+
if (
|
144
|
+
is_known_frame_type(frame_type)
|
145
|
+
and self.supports_frame_type(frame_type)
|
146
|
+
and not self.has_frame_version(frame_type, version)
|
147
|
+
):
|
148
|
+
_LOGGER.debug(
|
149
|
+
"Updating frame %s to version %i", repr(frame_type), version
|
150
|
+
)
|
151
|
+
request = await Request.create(frame_type, recipient=self.address)
|
152
|
+
self.queue.put_nowait(request)
|
153
|
+
self._frame_versions[frame_type] = version
|
154
|
+
|
155
|
+
self.subscribe(ATTR_FRAME_VERSIONS, update_frame_versions)
|
156
|
+
|
157
|
+
def has_frame_version(self, frame_type: int, version: int | None = None) -> bool:
|
158
|
+
"""Return True if frame data is up to date, False otherwise."""
|
159
|
+
if frame_type not in self._frame_versions:
|
160
|
+
return False
|
161
|
+
|
162
|
+
if version is None or self._frame_versions[frame_type] == version:
|
163
|
+
return True
|
164
|
+
|
165
|
+
return False
|
166
|
+
|
167
|
+
def supports_frame_type(self, frame_type: int) -> bool:
|
168
|
+
"""Check if frame type is supported by the device."""
|
169
|
+
return frame_type not in self.data.get(ATTR_FRAME_ERRORS, [])
|
133
170
|
|
134
171
|
def handle_frame(self, frame: Frame) -> None:
|
135
172
|
"""Handle frame received from the device."""
|
136
|
-
frame.
|
173
|
+
frame.assign_to(self)
|
137
174
|
if frame.data is not None:
|
138
175
|
for name, value in frame.data.items():
|
139
176
|
self.dispatch_nowait(name, value)
|
@@ -188,7 +225,7 @@ class VirtualDevice(Device, ABC):
|
|
188
225
|
|
189
226
|
def __init__(
|
190
227
|
self, queue: asyncio.Queue[Frame], parent: PhysicalDevice, index: int = 0
|
191
|
-
):
|
228
|
+
) -> None:
|
192
229
|
"""Initialize a new sub-device."""
|
193
230
|
super().__init__(queue)
|
194
231
|
self.parent = parent
|
@@ -9,7 +9,6 @@ import time
|
|
9
9
|
from typing import Any, Final
|
10
10
|
|
11
11
|
from pyplumio.const import (
|
12
|
-
ATTR_FRAME_ERRORS,
|
13
12
|
ATTR_PASSWORD,
|
14
13
|
ATTR_SENSORS,
|
15
14
|
ATTR_STATE,
|
@@ -21,7 +20,7 @@ from pyplumio.devices import PhysicalDevice
|
|
21
20
|
from pyplumio.devices.mixer import Mixer
|
22
21
|
from pyplumio.devices.thermostat import Thermostat
|
23
22
|
from pyplumio.filters import on_change
|
24
|
-
from pyplumio.frames import DataFrameDescription, Frame, Request
|
23
|
+
from pyplumio.frames import DataFrameDescription, Frame, Request
|
25
24
|
from pyplumio.helpers.parameter import ParameterValues
|
26
25
|
from pyplumio.helpers.schedule import Schedule, ScheduleDay
|
27
26
|
from pyplumio.structures.alerts import ATTR_TOTAL_ALERTS
|
@@ -35,7 +34,6 @@ from pyplumio.structures.ecomax_parameters import (
|
|
35
34
|
EcomaxSwitch,
|
36
35
|
EcomaxSwitchDescription,
|
37
36
|
)
|
38
|
-
from pyplumio.structures.frame_versions import ATTR_FRAME_VERSIONS
|
39
37
|
from pyplumio.structures.fuel_consumption import ATTR_FUEL_CONSUMPTION
|
40
38
|
from pyplumio.structures.mixer_parameters import ATTR_MIXER_PARAMETERS
|
41
39
|
from pyplumio.structures.mixer_sensors import ATTR_MIXER_SENSORS
|
@@ -106,17 +104,14 @@ class EcoMAX(PhysicalDevice):
|
|
106
104
|
|
107
105
|
address = DeviceType.ECOMAX
|
108
106
|
|
109
|
-
_frame_versions: dict[int, int]
|
110
107
|
_fuel_burned_timestamp_ns: int
|
111
108
|
_setup_frames = SETUP_FRAME_TYPES
|
112
109
|
|
113
|
-
def __init__(self, queue: asyncio.Queue[Frame], network: NetworkInfo):
|
110
|
+
def __init__(self, queue: asyncio.Queue[Frame], network: NetworkInfo) -> None:
|
114
111
|
"""Initialize a new ecoMAX controller."""
|
115
112
|
super().__init__(queue, network)
|
116
|
-
self._frame_versions = {}
|
117
113
|
self._fuel_burned_timestamp_ns = time.perf_counter_ns()
|
118
114
|
self.subscribe(ATTR_ECOMAX_PARAMETERS, self._handle_ecomax_parameters)
|
119
|
-
self.subscribe(ATTR_FRAME_VERSIONS, self._update_frame_versions)
|
120
115
|
self.subscribe(ATTR_FUEL_CONSUMPTION, self._add_burned_fuel_counter)
|
121
116
|
self.subscribe(ATTR_MIXER_PARAMETERS, self._handle_mixer_parameters)
|
122
117
|
self.subscribe(ATTR_MIXER_SENSORS, self._handle_mixer_sensors)
|
@@ -142,17 +137,6 @@ class EcoMAX(PhysicalDevice):
|
|
142
137
|
|
143
138
|
super().handle_frame(frame)
|
144
139
|
|
145
|
-
def _has_frame_version(self, frame_type: FrameType | int, version: int) -> bool:
|
146
|
-
"""Check if ecoMAX controller has this version of the frame."""
|
147
|
-
return (
|
148
|
-
frame_type in self._frame_versions
|
149
|
-
and self._frame_versions[frame_type] == version
|
150
|
-
)
|
151
|
-
|
152
|
-
def _frame_is_supported(self, frame_type: FrameType | int) -> bool:
|
153
|
-
"""Check if frame is supported by the device."""
|
154
|
-
return frame_type not in self.data.get(ATTR_FRAME_ERRORS, [])
|
155
|
-
|
156
140
|
def _mixers(self, indexes: Iterable[int]) -> Generator[Mixer, None, None]:
|
157
141
|
"""Iterate through the mixer indexes.
|
158
142
|
|
@@ -224,19 +208,6 @@ class EcoMAX(PhysicalDevice):
|
|
224
208
|
await asyncio.gather(*_ecomax_parameter_events())
|
225
209
|
return True
|
226
210
|
|
227
|
-
async def _update_frame_versions(self, versions: dict[int, int]) -> None:
|
228
|
-
"""Check frame versions and update outdated frames."""
|
229
|
-
for frame_type, version in versions.items():
|
230
|
-
if (
|
231
|
-
is_known_frame_type(frame_type)
|
232
|
-
and self._frame_is_supported(frame_type)
|
233
|
-
and not self._has_frame_version(frame_type, version)
|
234
|
-
):
|
235
|
-
# We don't have this frame or it's version has changed.
|
236
|
-
request = await Request.create(frame_type, recipient=self.address)
|
237
|
-
self.queue.put_nowait(request)
|
238
|
-
self._frame_versions[frame_type] = version
|
239
|
-
|
240
211
|
async def _add_burned_fuel_counter(self, fuel_consumption: float) -> None:
|
241
212
|
"""Calculate fuel burned since last sensor's data message."""
|
242
213
|
current_timestamp_ns = time.perf_counter_ns()
|
@@ -30,7 +30,7 @@ class Mixer(VirtualDevice):
|
|
30
30
|
|
31
31
|
def __init__(
|
32
32
|
self, queue: asyncio.Queue[Frame], parent: PhysicalDevice, index: int = 0
|
33
|
-
):
|
33
|
+
) -> None:
|
34
34
|
"""Initialize a new mixer."""
|
35
35
|
super().__init__(queue, parent, index)
|
36
36
|
self.subscribe(ATTR_MIXER_SENSORS, self._handle_mixer_sensors)
|
@@ -26,7 +26,7 @@ class Thermostat(VirtualDevice):
|
|
26
26
|
|
27
27
|
def __init__(
|
28
28
|
self, queue: asyncio.Queue[Frame], parent: PhysicalDevice, index: int = 0
|
29
|
-
):
|
29
|
+
) -> None:
|
30
30
|
"""Initialize a new thermostat."""
|
31
31
|
super().__init__(queue, parent, index)
|
32
32
|
self.subscribe(ATTR_THERMOSTAT_SENSORS, self._handle_thermostat_sensors)
|
@@ -66,13 +66,12 @@ def _significantly_changed(
|
|
66
66
|
def _significantly_changed(old: Comparable, new: Comparable) -> bool:
|
67
67
|
"""Check if value is significantly changed."""
|
68
68
|
if isinstance(old, Parameter) and isinstance(new, Parameter):
|
69
|
-
|
70
|
-
elif isinstance(old, SupportsFloat) and isinstance(new, SupportsFloat):
|
71
|
-
result = not math.isclose(old, new, abs_tol=TOLERANCE)
|
72
|
-
else:
|
73
|
-
result = old != new
|
69
|
+
return new.pending_update or old.values.__ne__(new.values)
|
74
70
|
|
75
|
-
|
71
|
+
if isinstance(old, SupportsFloat) and isinstance(new, SupportsFloat):
|
72
|
+
return not math.isclose(old, new, abs_tol=TOLERANCE)
|
73
|
+
|
74
|
+
return old.__ne__(new)
|
76
75
|
|
77
76
|
|
78
77
|
@overload
|
@@ -91,10 +90,11 @@ def _diffence_between(
|
|
91
90
|
"""Return a difference between values."""
|
92
91
|
if isinstance(old, list) and isinstance(new, list):
|
93
92
|
return [x for x in new if x not in old]
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
93
|
+
|
94
|
+
if isinstance(old, SupportsSubtraction) and isinstance(new, SupportsSubtraction):
|
95
|
+
return new.__sub__(old)
|
96
|
+
|
97
|
+
return None
|
98
98
|
|
99
99
|
|
100
100
|
class Filter(ABC):
|
@@ -24,6 +24,7 @@ ECONET_VERSION: Final = 5
|
|
24
24
|
|
25
25
|
# Frame header structure.
|
26
26
|
struct_header = struct.Struct("<BH4B")
|
27
|
+
HEADER_SIZE = struct_header.size
|
27
28
|
|
28
29
|
if TYPE_CHECKING:
|
29
30
|
from pyplumio.devices import PhysicalDevice
|
@@ -73,22 +74,20 @@ class Frame(ABC):
|
|
73
74
|
|
74
75
|
__slots__ = (
|
75
76
|
"recipient",
|
76
|
-
"recipient_device",
|
77
77
|
"sender",
|
78
|
-
"sender_device",
|
79
78
|
"econet_type",
|
80
79
|
"econet_version",
|
80
|
+
"_handler",
|
81
81
|
"_message",
|
82
82
|
"_data",
|
83
83
|
)
|
84
84
|
|
85
85
|
recipient: DeviceType
|
86
|
-
recipient_device: PhysicalDevice | None
|
87
86
|
sender: DeviceType
|
88
|
-
sender_device: PhysicalDevice | None
|
89
87
|
econet_type: int
|
90
88
|
econet_version: int
|
91
89
|
frame_type: ClassVar[FrameType]
|
90
|
+
_handler: PhysicalDevice | None
|
92
91
|
_message: bytearray | None
|
93
92
|
_data: dict[str, Any] | None
|
94
93
|
|
@@ -104,11 +103,10 @@ class Frame(ABC):
|
|
104
103
|
) -> None:
|
105
104
|
"""Process a frame data and message."""
|
106
105
|
self.recipient = recipient
|
107
|
-
self.recipient_device = None
|
108
106
|
self.sender = sender
|
109
|
-
self.sender_device = None
|
110
107
|
self.econet_type = econet_type
|
111
108
|
self.econet_version = econet_version
|
109
|
+
self._handler = None
|
112
110
|
self._data = data if not kwargs else ensure_dict(data, kwargs)
|
113
111
|
self._message = message
|
114
112
|
|
@@ -153,6 +151,15 @@ class Frame(ABC):
|
|
153
151
|
"""Return a frame message represented as hex string."""
|
154
152
|
return self.bytes.hex(*args, **kwargs)
|
155
153
|
|
154
|
+
def assign_to(self, device: PhysicalDevice) -> None:
|
155
|
+
"""Assign device to the frame."""
|
156
|
+
self._handler = device
|
157
|
+
|
158
|
+
@property
|
159
|
+
def handler(self) -> PhysicalDevice | None:
|
160
|
+
"""Return the device associated to the frame."""
|
161
|
+
return self._handler
|
162
|
+
|
156
163
|
@property
|
157
164
|
def data(self) -> dict[str, Any]:
|
158
165
|
"""Return the frame data."""
|
@@ -19,7 +19,7 @@ class DataType(ABC, Generic[T]):
|
|
19
19
|
_value: T
|
20
20
|
_size: int
|
21
21
|
|
22
|
-
def __init__(self, value: T | None = None):
|
22
|
+
def __init__(self, value: T | None = None) -> None:
|
23
23
|
"""Initialize a new data type."""
|
24
24
|
if value is not None:
|
25
25
|
self._value = value
|
@@ -112,7 +112,7 @@ class BitArray(DataType[int]):
|
|
112
112
|
|
113
113
|
_index: int
|
114
114
|
|
115
|
-
def __init__(self, value: bool | None = None, index: int = 0):
|
115
|
+
def __init__(self, value: bool | None = None, index: int = 0) -> None:
|
116
116
|
"""Initialize a new bit array."""
|
117
117
|
super().__init__(value)
|
118
118
|
self._index = index
|
@@ -199,7 +199,7 @@ class String(DataType[str]):
|
|
199
199
|
|
200
200
|
__slots__ = ()
|
201
201
|
|
202
|
-
def __init__(self, value: str = ""):
|
202
|
+
def __init__(self, value: str = "") -> None:
|
203
203
|
"""Initialize a new null-terminated string data type."""
|
204
204
|
super().__init__(value)
|
205
205
|
self._size = len(self.value) + 1
|
@@ -219,7 +219,7 @@ class VarBytes(DataType[bytes]):
|
|
219
219
|
|
220
220
|
__slots__ = ()
|
221
221
|
|
222
|
-
def __init__(self, value: bytes = b""):
|
222
|
+
def __init__(self, value: bytes = b"") -> None:
|
223
223
|
"""Initialize a new variable-length bytes data type."""
|
224
224
|
super().__init__(value)
|
225
225
|
self._size = len(value) + 1
|
@@ -239,7 +239,7 @@ class VarString(DataType[str]):
|
|
239
239
|
|
240
240
|
__slots__ = ()
|
241
241
|
|
242
|
-
def __init__(self, value: str = ""):
|
242
|
+
def __init__(self, value: str = "") -> None:
|
243
243
|
"""Initialize a new variable length bytes data type."""
|
244
244
|
super().__init__(value)
|
245
245
|
self._size = len(value) + 1
|
@@ -326,7 +326,7 @@ class UnsignedInt(BuiltInDataType[int]):
|
|
326
326
|
_struct = struct.Struct("<I")
|
327
327
|
|
328
328
|
|
329
|
-
class Float(BuiltInDataType[
|
329
|
+
class Float(BuiltInDataType[float]):
|
330
330
|
"""Represents a float."""
|
331
331
|
|
332
332
|
__slots__ = ()
|
@@ -334,7 +334,7 @@ class Float(BuiltInDataType[int]):
|
|
334
334
|
_struct = struct.Struct("<f")
|
335
335
|
|
336
336
|
|
337
|
-
class Double(BuiltInDataType[
|
337
|
+
class Double(BuiltInDataType[float]):
|
338
338
|
"""Represents a double."""
|
339
339
|
|
340
340
|
__slots__ = ()
|
@@ -77,11 +77,19 @@ class ParameterDescription:
|
|
77
77
|
class Parameter(ABC):
|
78
78
|
"""Represents a base parameter."""
|
79
79
|
|
80
|
-
__slots__ = (
|
80
|
+
__slots__ = (
|
81
|
+
"device",
|
82
|
+
"description",
|
83
|
+
"_pending_update",
|
84
|
+
"_previous_value",
|
85
|
+
"_index",
|
86
|
+
"_values",
|
87
|
+
)
|
81
88
|
|
82
89
|
device: Device
|
83
90
|
description: ParameterDescription
|
84
91
|
_pending_update: bool
|
92
|
+
_previous_value: int
|
85
93
|
_index: int
|
86
94
|
_values: ParameterValues
|
87
95
|
|
@@ -96,6 +104,7 @@ class Parameter(ABC):
|
|
96
104
|
self.device = device
|
97
105
|
self.description = description
|
98
106
|
self._pending_update = False
|
107
|
+
self._previous_value = 0
|
99
108
|
self._index = index
|
100
109
|
self._values = values if values else ParameterValues(0, 0, 0)
|
101
110
|
|
@@ -185,6 +194,7 @@ class Parameter(ABC):
|
|
185
194
|
f"Value must be between '{self.min_value}' and '{self.max_value}'"
|
186
195
|
)
|
187
196
|
|
197
|
+
self._previous_value = self._values.value
|
188
198
|
self._values.value = value
|
189
199
|
self._pending_update = True
|
190
200
|
while self.pending_update:
|
@@ -196,6 +206,9 @@ class Parameter(ABC):
|
|
196
206
|
return False
|
197
207
|
|
198
208
|
await self.device.queue.put(await self.create_request())
|
209
|
+
if not self.is_tracking_changes:
|
210
|
+
await self.force_refresh()
|
211
|
+
|
199
212
|
await asyncio.sleep(timeout)
|
200
213
|
retries -= 1
|
201
214
|
|
@@ -203,8 +216,19 @@ class Parameter(ABC):
|
|
203
216
|
|
204
217
|
def update(self, values: ParameterValues) -> None:
|
205
218
|
"""Update the parameter values."""
|
219
|
+
if self.pending_update and self._previous_value != values.value:
|
220
|
+
self._pending_update = False
|
221
|
+
|
206
222
|
self._values = values
|
207
|
-
|
223
|
+
|
224
|
+
async def force_refresh(self) -> None:
|
225
|
+
"""Refresh the parameter from remote."""
|
226
|
+
await self.device.queue.put(await self.create_refresh_request())
|
227
|
+
|
228
|
+
@property
|
229
|
+
def is_tracking_changes(self) -> bool:
|
230
|
+
"""Return True if remote's tracking changes, False otherwise."""
|
231
|
+
return False
|
208
232
|
|
209
233
|
@property
|
210
234
|
def pending_update(self) -> bool:
|
@@ -254,6 +278,10 @@ class Parameter(ABC):
|
|
254
278
|
async def create_request(self) -> Request:
|
255
279
|
"""Create a request to change the parameter."""
|
256
280
|
|
281
|
+
@abstractmethod
|
282
|
+
async def create_refresh_request(self) -> Request:
|
283
|
+
"""Create a request to refresh the parameter."""
|
284
|
+
|
257
285
|
|
258
286
|
@dataslots
|
259
287
|
@dataclass
|
@@ -286,6 +314,10 @@ class Number(Parameter):
|
|
286
314
|
"""Create a request to change the number."""
|
287
315
|
return Request()
|
288
316
|
|
317
|
+
async def create_refresh_request(self) -> Request:
|
318
|
+
"""Create a request to refresh the number."""
|
319
|
+
return Request()
|
320
|
+
|
289
321
|
@property
|
290
322
|
def value(self) -> int | float:
|
291
323
|
"""Return the value."""
|
@@ -362,6 +394,10 @@ class Switch(Parameter):
|
|
362
394
|
"""Create a request to change the switch."""
|
363
395
|
return Request()
|
364
396
|
|
397
|
+
async def create_refresh_request(self) -> Request:
|
398
|
+
"""Create a request to refresh the switch."""
|
399
|
+
return Request()
|
400
|
+
|
365
401
|
@property
|
366
402
|
def value(self) -> Literal["off", "on"]:
|
367
403
|
"""Return the value."""
|