PyPlumIO 0.5.36__tar.gz → 0.5.38__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.36 → pyplumio-0.5.38}/.pre-commit-config.yaml +2 -2
- {pyplumio-0.5.36 → pyplumio-0.5.38}/PKG-INFO +9 -9
- {pyplumio-0.5.36 → pyplumio-0.5.38}/PyPlumIO.egg-info/PKG-INFO +9 -9
- {pyplumio-0.5.36 → pyplumio-0.5.38}/PyPlumIO.egg-info/requires.txt +6 -6
- {pyplumio-0.5.36 → pyplumio-0.5.38}/README.md +2 -2
- {pyplumio-0.5.36 → pyplumio-0.5.38}/docs/source/index.rst +3 -2
- {pyplumio-0.5.36 → pyplumio-0.5.38}/docs/source/schedules.rst +16 -12
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/_version.py +9 -4
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/const.py +8 -4
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/devices/ecomax.py +20 -22
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/helpers/factory.py +1 -1
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/helpers/parameter.py +17 -8
- pyplumio-0.5.38/pyplumio/helpers/schedule.py +176 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/helpers/uid.py +2 -2
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/alerts.py +1 -1
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/ecomax_parameters.py +3 -1
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/outputs.py +1 -2
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/product_info.py +2 -2
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyproject.toml +9 -8
- pyplumio-0.5.38/requirements_test.txt +11 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/helpers/test_parameter.py +55 -11
- {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/helpers/test_schedule.py +50 -26
- {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/helpers/test_uid.py +2 -2
- {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/test_devices.py +7 -4
- pyplumio-0.5.36/pyplumio/helpers/schedule.py +0 -178
- pyplumio-0.5.36/requirements_test.txt +0 -11
- {pyplumio-0.5.36 → pyplumio-0.5.38}/.gitattributes +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/.github/CODE_OF_CONDUCT.md +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/.github/dependabot.yml +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/.github/workflows/ci.yml +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/.github/workflows/codeql-analysis.yml +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/.github/workflows/deploy.yml +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/.github/workflows/documentation.yml +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/.gitignore +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/.vscode/settings.json +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/LICENSE +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/MANIFEST.in +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/PyPlumIO.egg-info/SOURCES.txt +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/PyPlumIO.egg-info/dependency_links.txt +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/PyPlumIO.egg-info/top_level.txt +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/docs/Makefile +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/docs/make.bat +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/docs/source/callbacks.rst +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/docs/source/conf.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/docs/source/connecting.rst +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/docs/source/frames.rst +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/docs/source/mixers_thermostats.rst +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/docs/source/protocol.rst +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/docs/source/reading.rst +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/docs/source/writing.rst +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/images/ecomax.png +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/images/rs485.png +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/__init__.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/__main__.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/connection.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/devices/__init__.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/devices/ecoster.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/devices/mixer.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/devices/thermostat.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/exceptions.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/filters.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/frames/__init__.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/frames/messages.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/frames/requests.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/frames/responses.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/helpers/__init__.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/helpers/data_types.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/helpers/event_manager.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/helpers/task_manager.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/helpers/timeout.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/protocol.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/py.typed +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/stream.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/__init__.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/boiler_load.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/boiler_power.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/fan_power.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/frame_versions.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/fuel_consumption.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/fuel_level.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/lambda_sensor.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/mixer_parameters.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/mixer_sensors.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/modules.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/network_info.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/output_flags.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/pending_alerts.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/program_version.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/regulator_data.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/regulator_data_schema.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/schedules.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/statuses.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/temperatures.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/thermostat_parameters.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/thermostat_sensors.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/utils.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/requirements.txt +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/requirements_docs.txt +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/setup.cfg +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/__init__.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/conftest.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/frames/test_init.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/frames/test_messages.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/frames/test_requests.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/frames/test_responses.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/helpers/__init__.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/helpers/test_data_types.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/helpers/test_event_manager.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/helpers/test_factory.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/helpers/test_task_manager.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/helpers/test_timeout.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/ruff.toml +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/test_connection.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/test_filters.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/test_init.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/test_main.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/test_protocol.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/test_stream.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/test_utils.py +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/testdata/messages/regulator_data.json +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/testdata/messages/sensor_data.json +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/testdata/requests/alerts.json +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/testdata/requests/ecomax_control.json +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/testdata/requests/ecomax_parameters.json +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/testdata/requests/mixer_parameters.json +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/testdata/requests/set_ecomax_parameter.json +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/testdata/requests/set_mixer_parameter.json +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/testdata/requests/set_schedule.json +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/testdata/requests/set_thermostat_parameter.json +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/testdata/requests/thermostat_parameters.json +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/testdata/responses/alerts.json +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/testdata/responses/device_available.json +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/testdata/responses/ecomax_parameters.json +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/testdata/responses/mixer_parameters.json +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/testdata/responses/password.json +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/testdata/responses/program_version.json +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/testdata/responses/regulator_data_schema.json +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/testdata/responses/schedules.json +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/testdata/responses/thermostat_parameters.json +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/testdata/responses/uid.json +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/testdata/unknown/unknown_ecomax_parameter.json +0 -0
- {pyplumio-0.5.36 → pyplumio-0.5.38}/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.38
|
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,15 +27,15 @@ 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.4.
|
31
|
-
Requires-Dist: coverage==7.6.
|
32
|
-
Requires-Dist: mypy==1.
|
30
|
+
Requires-Dist: codespell==2.4.1; extra == "test"
|
31
|
+
Requires-Dist: coverage==7.6.12; extra == "test"
|
32
|
+
Requires-Dist: mypy==1.15.0; 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.
|
35
|
+
Requires-Dist: pytest-asyncio==0.25.3; extra == "test"
|
36
|
+
Requires-Dist: ruff==0.9.6; extra == "test"
|
37
37
|
Requires-Dist: tox==4.24.1; extra == "test"
|
38
|
-
Requires-Dist: types-pyserial==3.5.0.
|
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"
|
@@ -49,8 +49,8 @@ Requires-Dist: tomli==2.2.1; extra == "dev"
|
|
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.38
|
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,15 +27,15 @@ 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.4.
|
31
|
-
Requires-Dist: coverage==7.6.
|
32
|
-
Requires-Dist: mypy==1.
|
30
|
+
Requires-Dist: codespell==2.4.1; extra == "test"
|
31
|
+
Requires-Dist: coverage==7.6.12; extra == "test"
|
32
|
+
Requires-Dist: mypy==1.15.0; 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.
|
35
|
+
Requires-Dist: pytest-asyncio==0.25.3; extra == "test"
|
36
|
+
Requires-Dist: ruff==0.9.6; extra == "test"
|
37
37
|
Requires-Dist: tox==4.24.1; extra == "test"
|
38
|
-
Requires-Dist: types-pyserial==3.5.0.
|
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"
|
@@ -49,8 +49,8 @@ Requires-Dist: tomli==2.2.1; extra == "dev"
|
|
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
|
|
@@ -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.4.
|
17
|
-
coverage==7.6.
|
18
|
-
mypy==1.
|
16
|
+
codespell==2.4.1
|
17
|
+
coverage==7.6.12
|
18
|
+
mypy==1.15.0
|
19
19
|
pyserial-asyncio-fast==0.14
|
20
20
|
pytest==8.3.4
|
21
|
-
pytest-asyncio==0.25.
|
22
|
-
ruff==0.9.
|
21
|
+
pytest-asyncio==0.25.3
|
22
|
+
ruff==0.9.6
|
23
23
|
tox==4.24.1
|
24
|
-
types-pyserial==3.5.0.
|
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
|
@@ -42,9 +42,11 @@ temperature by 10 degrees Celsius.
|
|
42
42
|
Setting Schedule
|
43
43
|
----------------
|
44
44
|
|
45
|
-
To set the schedule, you can
|
46
|
-
``
|
47
|
-
|
45
|
+
To set the schedule, you can either directly set the state via key or
|
46
|
+
by using ``set_state(state)``, ``set_on()`` or ``set_off()``.
|
47
|
+
|
48
|
+
After updating the state you must call ``commit()`` method to save
|
49
|
+
changes on the device.
|
48
50
|
|
49
51
|
This example sets nighttime mode for Monday from 00:00 to 07:00 and
|
50
52
|
switches back to daytime mode from 07:00 to 00:00.
|
@@ -57,14 +59,15 @@ switches back to daytime mode from 07:00 to 00:00.
|
|
57
59
|
heating_schedule.monday.set_on(start="07:00", end="00:00")
|
58
60
|
await heating_schedule.commit()
|
59
61
|
|
60
|
-
For clarity sake, you might want to use ``
|
61
|
-
``
|
62
|
+
For clarity sake, you might want to use ``STATE_OFF`` and
|
63
|
+
``STATE_ON`` constants from ``pyplumio.helpers.schedule`` module.
|
62
64
|
|
63
65
|
.. code-block:: python
|
64
66
|
|
65
|
-
from pyplumio.helpers.schedule import
|
67
|
+
from pyplumio.helpers.schedule import STATE_OFF
|
66
68
|
|
67
|
-
heating_schedule.monday
|
69
|
+
heating_schedule.monday["18:00"] = STATE_OFF
|
70
|
+
heating_schedule.monday.set_state(STATE_OFF, "00:00", "07:00")
|
68
71
|
|
69
72
|
You may also omit one of the boundaries.
|
70
73
|
The other boundary is then set to the end or start of the day.
|
@@ -107,7 +110,7 @@ Schedule Examples
|
|
107
110
|
.. code-block:: python
|
108
111
|
|
109
112
|
import pyplumio
|
110
|
-
from pyplumio.helpers.schedule import
|
113
|
+
from pyplumio.helpers.schedule import STATE_ON, STATE_OFF
|
111
114
|
|
112
115
|
|
113
116
|
async def main():
|
@@ -124,12 +127,13 @@ Schedule Examples
|
|
124
127
|
await ecomax.set("schedule_heating_parameter", 10)
|
125
128
|
|
126
129
|
for weekday in heating_schedule:
|
127
|
-
weekday.set_state(
|
128
|
-
weekday.set_state(
|
129
|
-
weekday.set_state(
|
130
|
+
weekday.set_state(STATE_ON, "00:00", "00:30")
|
131
|
+
weekday.set_state(STATE_OFF, "00:30", "09:00")
|
132
|
+
weekday.set_state(STATE_ON, "09:00", "00:00")
|
133
|
+
weekday["19:00"] = STATE_OFF
|
130
134
|
|
131
135
|
# There will be no nighttime mode on sunday.
|
132
|
-
heating_schedule.sunday.set_state(
|
136
|
+
heating_schedule.sunday.set_state(STATE_ON)
|
133
137
|
|
134
138
|
await heating_schedule.commit()
|
135
139
|
|
@@ -1,8 +1,13 @@
|
|
1
|
-
# file generated by
|
1
|
+
# file generated by setuptools-scm
|
2
2
|
# don't change, don't track in version control
|
3
|
+
|
4
|
+
__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
|
5
|
+
|
3
6
|
TYPE_CHECKING = False
|
4
7
|
if TYPE_CHECKING:
|
5
|
-
from typing import Tuple
|
8
|
+
from typing import Tuple
|
9
|
+
from typing import Union
|
10
|
+
|
6
11
|
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
7
12
|
else:
|
8
13
|
VERSION_TUPLE = object
|
@@ -12,5 +17,5 @@ __version__: str
|
|
12
17
|
__version_tuple__: VERSION_TUPLE
|
13
18
|
version_tuple: VERSION_TUPLE
|
14
19
|
|
15
|
-
__version__ = version = '0.5.
|
16
|
-
__version_tuple__ = version_tuple = (0, 5,
|
20
|
+
__version__ = version = '0.5.38'
|
21
|
+
__version_tuple__ = version_tuple = (0, 5, 38)
|
@@ -3,11 +3,9 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
5
|
from enum import Enum, IntEnum, unique
|
6
|
-
from typing import Any, Final
|
6
|
+
from typing import Any, Final, Literal
|
7
7
|
|
8
|
-
|
9
|
-
STATE_ON: Final = "on"
|
10
|
-
STATE_OFF: Final = "off"
|
8
|
+
from typing_extensions import TypeAlias
|
11
9
|
|
12
10
|
# General attributes.
|
13
11
|
ATTR_CONNECTED: Final = "connected"
|
@@ -221,3 +219,9 @@ class UnitOfMeasurement(Enum):
|
|
221
219
|
|
222
220
|
|
223
221
|
PERCENTAGE: Final = "%"
|
222
|
+
|
223
|
+
STATE_ON: Final = "on"
|
224
|
+
STATE_OFF: Final = "off"
|
225
|
+
|
226
|
+
|
227
|
+
State: TypeAlias = Literal["on", "off"]
|
@@ -21,7 +21,7 @@ from pyplumio.devices.mixer import Mixer
|
|
21
21
|
from pyplumio.devices.thermostat import Thermostat
|
22
22
|
from pyplumio.filters import on_change
|
23
23
|
from pyplumio.frames import DataFrameDescription, Frame, Request
|
24
|
-
from pyplumio.helpers.parameter import ParameterValues
|
24
|
+
from pyplumio.helpers.parameter import STATE_OFF, STATE_ON, ParameterValues, State
|
25
25
|
from pyplumio.helpers.schedule import Schedule, ScheduleDay
|
26
26
|
from pyplumio.structures.alerts import ATTR_TOTAL_ALERTS
|
27
27
|
from pyplumio.structures.ecomax_parameters import (
|
@@ -99,8 +99,6 @@ SETUP_FRAME_TYPES: tuple[DataFrameDescription, ...] = (
|
|
99
99
|
|
100
100
|
_LOGGER = logging.getLogger(__name__)
|
101
101
|
|
102
|
-
ecomax_control_error = "ecoMAX control is not available. Please try again later."
|
103
|
-
|
104
102
|
|
105
103
|
class EcoMAX(PhysicalDevice):
|
106
104
|
"""Represents an ecoMAX controller."""
|
@@ -282,13 +280,13 @@ class EcoMAX(PhysicalDevice):
|
|
282
280
|
SCHEDULES[index]: Schedule(
|
283
281
|
name=SCHEDULES[index],
|
284
282
|
device=self,
|
285
|
-
monday=ScheduleDay(schedule[1]),
|
286
|
-
tuesday=ScheduleDay(schedule[2]),
|
287
|
-
wednesday=ScheduleDay(schedule[3]),
|
288
|
-
thursday=ScheduleDay(schedule[4]),
|
289
|
-
friday=ScheduleDay(schedule[5]),
|
290
|
-
saturday=ScheduleDay(schedule[6]),
|
291
|
-
sunday=ScheduleDay(schedule[0]),
|
283
|
+
monday=ScheduleDay.from_iterable(schedule[1]),
|
284
|
+
tuesday=ScheduleDay.from_iterable(schedule[2]),
|
285
|
+
wednesday=ScheduleDay.from_iterable(schedule[3]),
|
286
|
+
thursday=ScheduleDay.from_iterable(schedule[4]),
|
287
|
+
friday=ScheduleDay.from_iterable(schedule[5]),
|
288
|
+
saturday=ScheduleDay.from_iterable(schedule[6]),
|
289
|
+
sunday=ScheduleDay.from_iterable(schedule[0]),
|
292
290
|
)
|
293
291
|
for index, schedule in schedules
|
294
292
|
}
|
@@ -398,23 +396,23 @@ class EcoMAX(PhysicalDevice):
|
|
398
396
|
|
399
397
|
return False
|
400
398
|
|
401
|
-
async def
|
402
|
-
"""
|
399
|
+
async def _set_ecomax_state(self, state: State) -> bool:
|
400
|
+
"""Try to set the ecoMAX control state."""
|
403
401
|
try:
|
404
|
-
|
405
|
-
return await
|
402
|
+
switch: EcomaxSwitch = self.data[ATTR_ECOMAX_CONTROL]
|
403
|
+
return await switch.set(state)
|
406
404
|
except KeyError:
|
407
|
-
_LOGGER.error(
|
408
|
-
|
405
|
+
_LOGGER.error("ecoMAX control is not available. Please try again later.")
|
406
|
+
|
407
|
+
return False
|
408
|
+
|
409
|
+
async def turn_on(self) -> bool:
|
410
|
+
"""Turn on the ecoMAX controller."""
|
411
|
+
return await self._set_ecomax_state(STATE_ON)
|
409
412
|
|
410
413
|
async def turn_off(self) -> bool:
|
411
414
|
"""Turn off the ecoMAX controller."""
|
412
|
-
|
413
|
-
ecomax_control: EcomaxSwitch = self.data[ATTR_ECOMAX_CONTROL]
|
414
|
-
return await ecomax_control.turn_off()
|
415
|
-
except KeyError:
|
416
|
-
_LOGGER.error(ecomax_control_error)
|
417
|
-
return False
|
415
|
+
return await self._set_ecomax_state(STATE_OFF)
|
418
416
|
|
419
417
|
def turn_on_nowait(self) -> None:
|
420
418
|
"""Turn on the ecoMAX controller without waiting."""
|
@@ -19,7 +19,7 @@ async def _import_module(name: str) -> ModuleType:
|
|
19
19
|
return await loop.run_in_executor(None, importlib.import_module, f"pyplumio.{name}")
|
20
20
|
|
21
21
|
|
22
|
-
async def create_instance(class_path: str, cls: type[T], **kwargs: Any) -> T:
|
22
|
+
async def create_instance(class_path: str, /, cls: type[T], **kwargs: Any) -> T:
|
23
23
|
"""Return a class instance from the class path."""
|
24
24
|
module_name, class_name = class_path.rsplit(".", 1)
|
25
25
|
try:
|
@@ -11,7 +11,7 @@ from typing import TYPE_CHECKING, Any, Literal, TypeVar, Union, get_args
|
|
11
11
|
from dataslots import dataslots
|
12
12
|
from typing_extensions import TypeAlias
|
13
13
|
|
14
|
-
from pyplumio.const import BYTE_UNDEFINED, STATE_OFF, STATE_ON, UnitOfMeasurement
|
14
|
+
from pyplumio.const import BYTE_UNDEFINED, STATE_OFF, STATE_ON, State, UnitOfMeasurement
|
15
15
|
from pyplumio.frames import Request
|
16
16
|
|
17
17
|
if TYPE_CHECKING:
|
@@ -19,8 +19,8 @@ if TYPE_CHECKING:
|
|
19
19
|
|
20
20
|
_LOGGER = logging.getLogger(__name__)
|
21
21
|
|
22
|
+
|
22
23
|
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,7 +42,7 @@ 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
|
|
@@ -75,6 +75,7 @@ class ParameterDescription:
|
|
75
75
|
"""Represents a parameter description."""
|
76
76
|
|
77
77
|
name: str
|
78
|
+
optimistic: bool = False
|
78
79
|
|
79
80
|
|
80
81
|
class Parameter(ABC):
|
@@ -107,7 +108,7 @@ class Parameter(ABC):
|
|
107
108
|
"""Return a serializable string representation."""
|
108
109
|
return (
|
109
110
|
f"{self.__class__.__name__}("
|
110
|
-
f"device={self.device
|
111
|
+
f"device={self.device}, "
|
111
112
|
f"description={self.description}, "
|
112
113
|
f"values={self.values}, "
|
113
114
|
f"index={self._index})"
|
@@ -204,13 +205,21 @@ class Parameter(ABC):
|
|
204
205
|
self, value: int, retries: int = 5, timeout: float = 5.0
|
205
206
|
) -> bool:
|
206
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
|
+
)
|
207
211
|
if value == self.values.value:
|
208
212
|
# Value is unchanged
|
209
213
|
return True
|
210
214
|
|
211
|
-
self._pending_update = True
|
212
215
|
self._values.value = value
|
213
|
-
|
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
|
+
|
222
|
+
self._pending_update = True
|
214
223
|
while self.pending_update:
|
215
224
|
if retries <= 0:
|
216
225
|
_LOGGER.warning(
|
@@ -220,7 +229,7 @@ class Parameter(ABC):
|
|
220
229
|
)
|
221
230
|
return False
|
222
231
|
|
223
|
-
await self.device.queue.put(
|
232
|
+
await self.device.queue.put(request)
|
224
233
|
await asyncio.sleep(timeout)
|
225
234
|
retries -= 1
|
226
235
|
|
@@ -0,0 +1,176 @@
|
|
1
|
+
"""Contains a schedule helper classes."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from collections.abc import Iterable, Iterator, MutableMapping
|
6
|
+
from dataclasses import dataclass
|
7
|
+
import datetime as dt
|
8
|
+
from functools import lru_cache
|
9
|
+
from typing import Annotated, Final, get_args
|
10
|
+
|
11
|
+
from pyplumio.const import STATE_OFF, STATE_ON, FrameType, State
|
12
|
+
from pyplumio.devices import PhysicalDevice
|
13
|
+
from pyplumio.frames import Request
|
14
|
+
from pyplumio.structures.schedules import collect_schedule_data
|
15
|
+
|
16
|
+
TIME_FORMAT: Final = "%H:%M"
|
17
|
+
|
18
|
+
MIDNIGHT: Final = "00:00"
|
19
|
+
MIDNIGHT_DT = dt.datetime.strptime(MIDNIGHT, TIME_FORMAT)
|
20
|
+
|
21
|
+
STEP = dt.timedelta(minutes=30)
|
22
|
+
|
23
|
+
Time = Annotated[str, "Time string in %H:%M format"]
|
24
|
+
|
25
|
+
|
26
|
+
def _get_time(
|
27
|
+
index: int, start: dt.datetime = MIDNIGHT_DT, step: dt.timedelta = STEP
|
28
|
+
) -> Time:
|
29
|
+
"""Return time for a specific index."""
|
30
|
+
time_dt = start + (step * index)
|
31
|
+
return time_dt.strftime(TIME_FORMAT)
|
32
|
+
|
33
|
+
|
34
|
+
@lru_cache(maxsize=10)
|
35
|
+
def _get_time_range(start: Time, end: Time, step: dt.timedelta = STEP) -> list[Time]:
|
36
|
+
"""Get a time range.
|
37
|
+
|
38
|
+
Start and end boundaries should be specified in %H:%M format.
|
39
|
+
Both are inclusive.
|
40
|
+
"""
|
41
|
+
start_dt = dt.datetime.strptime(start, TIME_FORMAT)
|
42
|
+
end_dt = dt.datetime.strptime(end, TIME_FORMAT)
|
43
|
+
|
44
|
+
if end_dt == MIDNIGHT_DT:
|
45
|
+
# Upper boundary of the interval is midnight.
|
46
|
+
end_dt += dt.timedelta(hours=24) - step
|
47
|
+
|
48
|
+
if end_dt <= start_dt:
|
49
|
+
raise ValueError(
|
50
|
+
f"Invalid time range: start time ({start}) must be earlier "
|
51
|
+
f"than end time ({end})."
|
52
|
+
)
|
53
|
+
|
54
|
+
seconds = (end_dt - start_dt).total_seconds()
|
55
|
+
steps = seconds // step.total_seconds() + 1
|
56
|
+
|
57
|
+
return [_get_time(index, start=start_dt, step=step) for index in range(int(steps))]
|
58
|
+
|
59
|
+
|
60
|
+
class ScheduleDay(MutableMapping):
|
61
|
+
"""Represents a single day of schedule."""
|
62
|
+
|
63
|
+
__slots__ = ("_schedule",)
|
64
|
+
|
65
|
+
_schedule: dict[Time, bool]
|
66
|
+
|
67
|
+
def __init__(self, schedule: dict[Time, bool]) -> None:
|
68
|
+
"""Initialize a new schedule day."""
|
69
|
+
self._schedule = schedule
|
70
|
+
|
71
|
+
def __repr__(self) -> str:
|
72
|
+
"""Return serializable representation of the class."""
|
73
|
+
return f"ScheduleDay({self._schedule})"
|
74
|
+
|
75
|
+
def __len__(self) -> int:
|
76
|
+
"""Return a schedule length."""
|
77
|
+
return self._schedule.__len__()
|
78
|
+
|
79
|
+
def __iter__(self) -> Iterator[Time]:
|
80
|
+
"""Return an iterator."""
|
81
|
+
return self._schedule.__iter__()
|
82
|
+
|
83
|
+
def __getitem__(self, time: Time) -> State:
|
84
|
+
"""Return a schedule item."""
|
85
|
+
state = self._schedule.__getitem__(time)
|
86
|
+
return STATE_ON if state else STATE_OFF
|
87
|
+
|
88
|
+
def __delitem__(self, time: Time) -> None:
|
89
|
+
"""Delete a schedule item."""
|
90
|
+
self._schedule.__delitem__(time)
|
91
|
+
|
92
|
+
def __setitem__(self, time: Time, state: State | bool) -> None:
|
93
|
+
"""Set a schedule item."""
|
94
|
+
if state in get_args(State):
|
95
|
+
state = True if state == STATE_ON else False
|
96
|
+
if isinstance(state, bool):
|
97
|
+
self._schedule.__setitem__(time, state)
|
98
|
+
else:
|
99
|
+
raise TypeError(
|
100
|
+
f"Expected boolean value or one of: {', '.join(get_args(State))}."
|
101
|
+
)
|
102
|
+
|
103
|
+
def set_state(
|
104
|
+
self, state: State | bool, start: Time = MIDNIGHT, end: Time = MIDNIGHT
|
105
|
+
) -> None:
|
106
|
+
"""Set a schedule interval state."""
|
107
|
+
for time in _get_time_range(start, end):
|
108
|
+
self.__setitem__(time, state)
|
109
|
+
|
110
|
+
def set_on(self, start: Time = MIDNIGHT, end: Time = MIDNIGHT) -> None:
|
111
|
+
"""Set a schedule interval state to 'on'."""
|
112
|
+
self.set_state(STATE_ON, start, end)
|
113
|
+
|
114
|
+
def set_off(self, start: Time = MIDNIGHT, end: Time = MIDNIGHT) -> None:
|
115
|
+
"""Set a schedule interval state to 'off'."""
|
116
|
+
self.set_state(STATE_OFF, start, end)
|
117
|
+
|
118
|
+
@property
|
119
|
+
def schedule(self) -> dict[Time, bool]:
|
120
|
+
"""Return the schedule."""
|
121
|
+
return self._schedule
|
122
|
+
|
123
|
+
@classmethod
|
124
|
+
def from_iterable(cls: type[ScheduleDay], intervals: Iterable[bool]) -> ScheduleDay:
|
125
|
+
"""Make schedule day from iterable."""
|
126
|
+
return cls({_get_time(index): state for index, state in enumerate(intervals)})
|
127
|
+
|
128
|
+
|
129
|
+
@dataclass
|
130
|
+
class Schedule(Iterable):
|
131
|
+
"""Represents a weekly schedule."""
|
132
|
+
|
133
|
+
__slots__ = (
|
134
|
+
"name",
|
135
|
+
"device",
|
136
|
+
"sunday",
|
137
|
+
"monday",
|
138
|
+
"tuesday",
|
139
|
+
"wednesday",
|
140
|
+
"thursday",
|
141
|
+
"friday",
|
142
|
+
"saturday",
|
143
|
+
)
|
144
|
+
|
145
|
+
name: str
|
146
|
+
device: PhysicalDevice
|
147
|
+
|
148
|
+
sunday: ScheduleDay
|
149
|
+
monday: ScheduleDay
|
150
|
+
tuesday: ScheduleDay
|
151
|
+
wednesday: ScheduleDay
|
152
|
+
thursday: ScheduleDay
|
153
|
+
friday: ScheduleDay
|
154
|
+
saturday: ScheduleDay
|
155
|
+
|
156
|
+
def __iter__(self) -> Iterator[ScheduleDay]:
|
157
|
+
"""Return list of days."""
|
158
|
+
return (
|
159
|
+
self.sunday,
|
160
|
+
self.monday,
|
161
|
+
self.tuesday,
|
162
|
+
self.wednesday,
|
163
|
+
self.thursday,
|
164
|
+
self.friday,
|
165
|
+
self.saturday,
|
166
|
+
).__iter__()
|
167
|
+
|
168
|
+
async def commit(self) -> None:
|
169
|
+
"""Commit a weekly schedule to the device."""
|
170
|
+
await self.device.queue.put(
|
171
|
+
await Request.create(
|
172
|
+
FrameType.REQUEST_SET_SCHEDULE,
|
173
|
+
recipient=self.device.address,
|
174
|
+
data=collect_schedule_data(self.name, self.device),
|
175
|
+
)
|
176
|
+
)
|
@@ -10,8 +10,8 @@ POLYNOMIAL: Final = 0xA001
|
|
10
10
|
BASE5_KEY: Final = "0123456789ABCDEFGHIJKLMNZPQRSTUV"
|
11
11
|
|
12
12
|
|
13
|
-
def
|
14
|
-
"""
|
13
|
+
def unpack_uid(buffer: bytes) -> str:
|
14
|
+
"""Unpack UID from bytes."""
|
15
15
|
return _base5(buffer + _crc16(buffer))
|
16
16
|
|
17
17
|
|
@@ -825,7 +825,9 @@ ECOMAX_PARAMETERS: dict[ProductType, tuple[EcomaxParameterDescription, ...]] = {
|
|
825
825
|
),
|
826
826
|
}
|
827
827
|
|
828
|
-
ECOMAX_CONTROL_PARAMETER = EcomaxSwitchDescription(
|
828
|
+
ECOMAX_CONTROL_PARAMETER = EcomaxSwitchDescription(
|
829
|
+
name=ATTR_ECOMAX_CONTROL, optimistic=True
|
830
|
+
)
|
829
831
|
THERMOSTAT_PROFILE_PARAMETER = EcomaxNumberDescription(name=ATTR_THERMOSTAT_PROFILE)
|
830
832
|
|
831
833
|
|
@@ -2,7 +2,6 @@
|
|
2
2
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
|
-
import math
|
6
5
|
from typing import Any, Final
|
7
6
|
|
8
7
|
from pyplumio.helpers.data_types import UnsignedInt
|
@@ -60,7 +59,7 @@ class OutputsStructure(StructureDecoder):
|
|
60
59
|
ensure_dict(
|
61
60
|
data,
|
62
61
|
{
|
63
|
-
output: bool(outputs.value &
|
62
|
+
output: bool(outputs.value & 2**index)
|
64
63
|
for index, output in enumerate(OUTPUTS)
|
65
64
|
},
|
66
65
|
),
|