PyPlumIO 0.5.44__tar.gz → 0.5.49__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.44 → pyplumio-0.5.49}/PKG-INFO +2 -2
- {pyplumio-0.5.44 → pyplumio-0.5.49}/PyPlumIO.egg-info/PKG-INFO +2 -2
- {pyplumio-0.5.44 → pyplumio-0.5.49}/PyPlumIO.egg-info/SOURCES.txt +12 -1
- {pyplumio-0.5.44 → pyplumio-0.5.49}/PyPlumIO.egg-info/requires.txt +1 -1
- {pyplumio-0.5.44 → pyplumio-0.5.49}/docs/source/callbacks.rst +14 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/_version.py +2 -2
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/devices/__init__.py +8 -4
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/devices/ecomax.py +5 -3
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/devices/mixer.py +2 -2
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/devices/thermostat.py +2 -2
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/filters.py +62 -7
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/helpers/async_cache.py +3 -2
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/helpers/factory.py +12 -13
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/helpers/schedule.py +4 -3
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/helpers/timeout.py +1 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/parameters/__init__.py +3 -60
- pyplumio-0.5.49/pyplumio/parameters/custom/__init__.py +111 -0
- pyplumio-0.5.49/pyplumio/parameters/custom/ecomax_860d3_hb.py +80 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/parameters/ecomax.py +7 -54
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyproject.toml +1 -1
- {pyplumio-0.5.44 → pyplumio-0.5.49}/requirements_test.txt +1 -1
- pyplumio-0.5.49/tests/__init__.py +1 -0
- pyplumio-0.5.49/tests/conftest.py +188 -0
- pyplumio-0.5.49/tests/devices/__init__.py +1 -0
- pyplumio-0.5.49/tests/devices/test_ecomax.py +554 -0
- pyplumio-0.5.49/tests/devices/test_ecoster.py +12 -0
- pyplumio-0.5.49/tests/devices/test_init.py +188 -0
- pyplumio-0.5.49/tests/devices/test_mixer.py +118 -0
- pyplumio-0.5.49/tests/devices/test_thermostat.py +134 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/tests/frames/test_init.py +18 -8
- {pyplumio-0.5.44 → pyplumio-0.5.49}/tests/frames/test_messages.py +10 -16
- {pyplumio-0.5.44 → pyplumio-0.5.49}/tests/frames/test_requests.py +1 -25
- {pyplumio-0.5.44 → pyplumio-0.5.49}/tests/frames/test_responses.py +1 -19
- {pyplumio-0.5.44 → pyplumio-0.5.49}/tests/helpers/test_event_manager.py +2 -1
- pyplumio-0.5.49/tests/helpers/test_factory.py +41 -0
- pyplumio-0.5.49/tests/helpers/test_schedule.py +190 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/tests/helpers/test_task_manager.py +7 -7
- {pyplumio-0.5.44 → pyplumio-0.5.49}/tests/helpers/test_timeout.py +8 -10
- pyplumio-0.5.49/tests/helpers/test_uid.py +17 -0
- pyplumio-0.5.49/tests/parameters/custom/__init__.py +1 -0
- pyplumio-0.5.49/tests/parameters/custom/test_ecomax_860d3_hb.py +87 -0
- pyplumio-0.5.49/tests/parameters/custom/test_init.py +89 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/tests/parameters/test_ecomax.py +3 -3
- pyplumio-0.5.49/tests/parameters/test_init.py +550 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/tests/parameters/test_thermostats.py +1 -1
- pyplumio-0.5.49/tests/test_connection.py +223 -0
- pyplumio-0.5.49/tests/test_data_types.py +87 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/tests/test_filters.py +43 -26
- {pyplumio-0.5.44 → pyplumio-0.5.49}/tests/test_main.py +3 -1
- {pyplumio-0.5.44 → pyplumio-0.5.49}/tests/test_protocol.py +12 -14
- pyplumio-0.5.49/tests/test_stream.py +267 -0
- pyplumio-0.5.49/tests/test_utils.py +58 -0
- pyplumio-0.5.49/tests/testdata/parameters/ecomax_860d3_hb.json +20 -0
- pyplumio-0.5.49/tests/testdata/unknown/unknown_ecomax_parameter.json +20 -0
- pyplumio-0.5.49/tests/testdata/unknown/unknown_mixer_parameter.json +11 -0
- pyplumio-0.5.44/tests/__init__.py +0 -56
- pyplumio-0.5.44/tests/conftest.py +0 -41
- pyplumio-0.5.44/tests/helpers/test_factory.py +0 -35
- pyplumio-0.5.44/tests/helpers/test_schedule.py +0 -150
- pyplumio-0.5.44/tests/helpers/test_uid.py +0 -17
- pyplumio-0.5.44/tests/parameters/test_init.py +0 -405
- pyplumio-0.5.44/tests/test_connection.py +0 -250
- pyplumio-0.5.44/tests/test_data_types.py +0 -299
- pyplumio-0.5.44/tests/test_devices.py +0 -742
- pyplumio-0.5.44/tests/test_stream.py +0 -186
- pyplumio-0.5.44/tests/test_utils.py +0 -34
- pyplumio-0.5.44/tests/testdata/unknown/unknown_ecomax_parameter.json +0 -18
- pyplumio-0.5.44/tests/testdata/unknown/unknown_mixer_parameter.json +0 -9
- {pyplumio-0.5.44 → pyplumio-0.5.49}/.gitattributes +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/.github/CODE_OF_CONDUCT.md +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/.github/dependabot.yml +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/.github/workflows/ci.yml +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/.github/workflows/codeql.yml +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/.github/workflows/deploy.yml +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/.github/workflows/documentation.yml +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/.gitignore +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/.pre-commit-config.yaml +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/.qlty/qlty.toml +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/.vscode/settings.json +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/LICENSE +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/MANIFEST.in +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/PyPlumIO.egg-info/dependency_links.txt +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/PyPlumIO.egg-info/top_level.txt +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/README.md +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/docs/Makefile +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/docs/make.bat +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/docs/source/conf.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/docs/source/connecting.rst +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/docs/source/frames.rst +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/docs/source/index.rst +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/docs/source/mixers_thermostats.rst +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/docs/source/protocol.rst +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/docs/source/reading.rst +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/docs/source/schedules.rst +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/docs/source/writing.rst +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/images/ecomax.png +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/images/rs485.png +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/__init__.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/__main__.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/connection.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/const.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/data_types.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/devices/ecoster.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/exceptions.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/frames/__init__.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/frames/messages.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/frames/requests.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/frames/responses.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/helpers/__init__.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/helpers/event_manager.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/helpers/task_manager.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/helpers/uid.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/parameters/mixer.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/parameters/thermostat.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/protocol.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/py.typed +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/stream.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/structures/__init__.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/structures/alerts.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/structures/boiler_load.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/structures/boiler_power.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/structures/ecomax_parameters.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/structures/fan_power.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/structures/frame_versions.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/structures/fuel_consumption.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/structures/fuel_level.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/structures/lambda_sensor.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/structures/mixer_parameters.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/structures/mixer_sensors.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/structures/modules.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/structures/network_info.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/structures/output_flags.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/structures/outputs.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/structures/pending_alerts.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/structures/product_info.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/structures/program_version.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/structures/regulator_data.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/structures/regulator_data_schema.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/structures/schedules.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/structures/statuses.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/structures/temperatures.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/structures/thermostat_parameters.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/structures/thermostat_sensors.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/pyplumio/utils.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/requirements.txt +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/requirements_docs.txt +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/setup.cfg +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/tests/helpers/__init__.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/tests/helpers/test_async_cache.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/tests/parameters/__init__.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/tests/parameters/test_mixers.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/tests/ruff.toml +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/tests/test_init.py +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/tests/testdata/messages/regulator_data.json +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/tests/testdata/messages/sensor_data.json +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/tests/testdata/requests/alerts.json +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/tests/testdata/requests/ecomax_control.json +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/tests/testdata/requests/ecomax_parameters.json +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/tests/testdata/requests/mixer_parameters.json +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/tests/testdata/requests/set_ecomax_parameter.json +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/tests/testdata/requests/set_mixer_parameter.json +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/tests/testdata/requests/set_schedule.json +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/tests/testdata/requests/set_thermostat_parameter.json +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/tests/testdata/requests/thermostat_parameters.json +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/tests/testdata/responses/alerts.json +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/tests/testdata/responses/device_available.json +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/tests/testdata/responses/ecomax_parameters.json +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/tests/testdata/responses/mixer_parameters.json +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/tests/testdata/responses/password.json +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/tests/testdata/responses/program_version.json +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/tests/testdata/responses/regulator_data_schema.json +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/tests/testdata/responses/schedules.json +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/tests/testdata/responses/thermostat_parameters.json +0 -0
- {pyplumio-0.5.44 → pyplumio-0.5.49}/tests/testdata/responses/uid.json +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: PyPlumIO
|
3
|
-
Version: 0.5.
|
3
|
+
Version: 0.5.49
|
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
|
@@ -35,7 +35,7 @@ Requires-Dist: numpy<3.0.0,>=2.0.0; extra == "test"
|
|
35
35
|
Requires-Dist: pyserial-asyncio-fast==0.16; extra == "test"
|
36
36
|
Requires-Dist: pytest==8.3.5; extra == "test"
|
37
37
|
Requires-Dist: pytest-asyncio==0.26.0; extra == "test"
|
38
|
-
Requires-Dist: ruff==0.11.
|
38
|
+
Requires-Dist: ruff==0.11.9; extra == "test"
|
39
39
|
Requires-Dist: tox==4.25.0; extra == "test"
|
40
40
|
Requires-Dist: types-pyserial==3.5.0.20250326; extra == "test"
|
41
41
|
Provides-Extra: docs
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: PyPlumIO
|
3
|
-
Version: 0.5.
|
3
|
+
Version: 0.5.49
|
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
|
@@ -35,7 +35,7 @@ Requires-Dist: numpy<3.0.0,>=2.0.0; extra == "test"
|
|
35
35
|
Requires-Dist: pyserial-asyncio-fast==0.16; extra == "test"
|
36
36
|
Requires-Dist: pytest==8.3.5; extra == "test"
|
37
37
|
Requires-Dist: pytest-asyncio==0.26.0; extra == "test"
|
38
|
-
Requires-Dist: ruff==0.11.
|
38
|
+
Requires-Dist: ruff==0.11.9; extra == "test"
|
39
39
|
Requires-Dist: tox==4.25.0; extra == "test"
|
40
40
|
Requires-Dist: types-pyserial==3.5.0.20250326; extra == "test"
|
41
41
|
Provides-Extra: docs
|
@@ -70,6 +70,8 @@ pyplumio/parameters/__init__.py
|
|
70
70
|
pyplumio/parameters/ecomax.py
|
71
71
|
pyplumio/parameters/mixer.py
|
72
72
|
pyplumio/parameters/thermostat.py
|
73
|
+
pyplumio/parameters/custom/__init__.py
|
74
|
+
pyplumio/parameters/custom/ecomax_860d3_hb.py
|
73
75
|
pyplumio/structures/__init__.py
|
74
76
|
pyplumio/structures/alerts.py
|
75
77
|
pyplumio/structures/boiler_load.py
|
@@ -101,13 +103,18 @@ tests/conftest.py
|
|
101
103
|
tests/ruff.toml
|
102
104
|
tests/test_connection.py
|
103
105
|
tests/test_data_types.py
|
104
|
-
tests/test_devices.py
|
105
106
|
tests/test_filters.py
|
106
107
|
tests/test_init.py
|
107
108
|
tests/test_main.py
|
108
109
|
tests/test_protocol.py
|
109
110
|
tests/test_stream.py
|
110
111
|
tests/test_utils.py
|
112
|
+
tests/devices/__init__.py
|
113
|
+
tests/devices/test_ecomax.py
|
114
|
+
tests/devices/test_ecoster.py
|
115
|
+
tests/devices/test_init.py
|
116
|
+
tests/devices/test_mixer.py
|
117
|
+
tests/devices/test_thermostat.py
|
111
118
|
tests/frames/test_init.py
|
112
119
|
tests/frames/test_messages.py
|
113
120
|
tests/frames/test_requests.py
|
@@ -125,8 +132,12 @@ tests/parameters/test_ecomax.py
|
|
125
132
|
tests/parameters/test_init.py
|
126
133
|
tests/parameters/test_mixers.py
|
127
134
|
tests/parameters/test_thermostats.py
|
135
|
+
tests/parameters/custom/__init__.py
|
136
|
+
tests/parameters/custom/test_ecomax_860d3_hb.py
|
137
|
+
tests/parameters/custom/test_init.py
|
128
138
|
tests/testdata/messages/regulator_data.json
|
129
139
|
tests/testdata/messages/sensor_data.json
|
140
|
+
tests/testdata/parameters/ecomax_860d3_hb.json
|
130
141
|
tests/testdata/requests/alerts.json
|
131
142
|
tests/testdata/requests/ecomax_control.json
|
132
143
|
tests/testdata/requests/ecomax_parameters.json
|
@@ -79,6 +79,20 @@ value is changed.
|
|
79
79
|
# last call.
|
80
80
|
ecomax.subscribe("heating_temp", filters.on_change(my_callback))
|
81
81
|
|
82
|
+
.. autofunction:: pyplumio.filters.deadband
|
83
|
+
|
84
|
+
This filter await the callback on signifacant changes only.
|
85
|
+
Significance is defined by ``tolerance`` argument (i. e. tolerance=0.1
|
86
|
+
will only await callback when value is changed by more that 0.1).
|
87
|
+
|
88
|
+
.. code-block:: python
|
89
|
+
|
90
|
+
from pyplumio import filters
|
91
|
+
|
92
|
+
# Await the callback once heating_temp value is changed by more
|
93
|
+
# than 0.1 since last call.
|
94
|
+
ecomax.subscribe("heating_temp", filters.deadband(my_callback, tolerance=0.1))
|
95
|
+
|
82
96
|
.. autofunction:: pyplumio.filters.debounce
|
83
97
|
|
84
98
|
This filter will only await the callback once value is settled across
|
@@ -39,7 +39,11 @@ def get_device_handler(device_type: int) -> str:
|
|
39
39
|
|
40
40
|
type_name = to_camelcase(
|
41
41
|
DeviceType(device_type).name,
|
42
|
-
overrides={
|
42
|
+
overrides={
|
43
|
+
"ecomax": "EcoMAX",
|
44
|
+
"ecoster": "EcoSTER",
|
45
|
+
"econet": "EcoNET",
|
46
|
+
},
|
43
47
|
)
|
44
48
|
return f"devices.{type_name.lower()}.{type_name}"
|
45
49
|
|
@@ -68,7 +72,7 @@ class Device(ABC, EventManager):
|
|
68
72
|
:param name: Name of the parameter
|
69
73
|
:type name: str
|
70
74
|
:param value: New value for the parameter
|
71
|
-
:type value: int | float | bool | Literal["
|
75
|
+
:type value: int | float | bool | Literal["on", "off"]
|
72
76
|
:param retries: Try setting parameter for this amount of
|
73
77
|
times, defaults to 5
|
74
78
|
:type retries: int, optional
|
@@ -100,7 +104,7 @@ class Device(ABC, EventManager):
|
|
100
104
|
:param name: Name of the parameter
|
101
105
|
:type name: str
|
102
106
|
:param value: New value for the parameter
|
103
|
-
:type value: int | float | bool | Literal["
|
107
|
+
:type value: int | float | bool | Literal["on", "off"]
|
104
108
|
:param retries: Try setting parameter for this amount of
|
105
109
|
times, defaults to 5
|
106
110
|
:type retries: int, optional
|
@@ -141,7 +145,7 @@ class PhysicalDevice(Device, ABC):
|
|
141
145
|
@event_listener(filter=on_change)
|
142
146
|
async def on_event_frame_versions(self, versions: dict[int, int]) -> None:
|
143
147
|
"""Check frame versions and update outdated frames."""
|
144
|
-
_LOGGER.info("Received version table")
|
148
|
+
_LOGGER.info("Received frame version table")
|
145
149
|
for frame_type, version in versions.items():
|
146
150
|
if (
|
147
151
|
is_known_frame_type(frame_type)
|
@@ -12,6 +12,7 @@ from pyplumio.const import (
|
|
12
12
|
ATTR_FRAME_ERRORS,
|
13
13
|
ATTR_PASSWORD,
|
14
14
|
ATTR_SENSORS,
|
15
|
+
ATTR_SETUP,
|
15
16
|
STATE_OFF,
|
16
17
|
STATE_ON,
|
17
18
|
DeviceState,
|
@@ -192,7 +193,8 @@ class EcoMAX(PhysicalDevice):
|
|
192
193
|
self, frame_type: FrameType | int, version: int
|
193
194
|
) -> None:
|
194
195
|
"""Request frame version from the device."""
|
195
|
-
|
196
|
+
setup_done = self.get_nowait(ATTR_SETUP, False)
|
197
|
+
if setup_done or frame_type not in REQUIRED_TYPES:
|
196
198
|
await super()._request_frame_version(frame_type, version)
|
197
199
|
|
198
200
|
async def _set_ecomax_state(self, state: State) -> bool:
|
@@ -258,10 +260,10 @@ class EcoMAX(PhysicalDevice):
|
|
258
260
|
"""Update ecoMAX parameters and dispatch the events."""
|
259
261
|
_LOGGER.info("Received device parameters")
|
260
262
|
product_info: ProductInfo = await self.get(ATTR_PRODUCT)
|
263
|
+
parameter_types = await get_ecomax_parameter_types(product_info)
|
261
264
|
|
262
|
-
def _ecomax_parameter_events() -> Generator[Coroutine
|
265
|
+
def _ecomax_parameter_events() -> Generator[Coroutine]:
|
263
266
|
"""Get dispatch calls for ecoMAX parameter events."""
|
264
|
-
parameter_types = get_ecomax_parameter_types(product_info)
|
265
267
|
for index, values in parameters:
|
266
268
|
try:
|
267
269
|
description = parameter_types[index]
|
@@ -42,10 +42,10 @@ class Mixer(VirtualDevice):
|
|
42
42
|
"""Update mixer parameters and dispatch the events."""
|
43
43
|
_LOGGER.info("Received mixer %i parameters", self.index)
|
44
44
|
product_info: ProductInfo = await self.parent.get(ATTR_PRODUCT)
|
45
|
+
parameter_types = get_mixer_parameter_types(product_info)
|
45
46
|
|
46
|
-
def _mixer_parameter_events() -> Generator[Coroutine
|
47
|
+
def _mixer_parameter_events() -> Generator[Coroutine]:
|
47
48
|
"""Get dispatch calls for mixer parameter events."""
|
48
|
-
parameter_types = get_mixer_parameter_types(product_info)
|
49
49
|
for index, values in parameters:
|
50
50
|
try:
|
51
51
|
description = parameter_types[index]
|
@@ -40,10 +40,10 @@ class Thermostat(VirtualDevice):
|
|
40
40
|
) -> bool:
|
41
41
|
"""Update thermostat parameters and dispatch the events."""
|
42
42
|
_LOGGER.info("Received thermostat %i parameters", self.index)
|
43
|
+
parameter_types = get_thermostat_parameter_types()
|
43
44
|
|
44
|
-
def _thermostat_parameter_events() -> Generator[Coroutine
|
45
|
+
def _thermostat_parameter_events() -> Generator[Coroutine]:
|
45
46
|
"""Get dispatch calls for thermostat parameter events."""
|
46
|
-
parameter_types = get_thermostat_parameter_types()
|
47
47
|
for index, values in parameters:
|
48
48
|
description = parameter_types[index]
|
49
49
|
handler = (
|
@@ -36,7 +36,6 @@ with suppress(ImportError):
|
|
36
36
|
|
37
37
|
|
38
38
|
UNDEFINED: Final = "undefined"
|
39
|
-
TOLERANCE: Final = 0.1
|
40
39
|
|
41
40
|
|
42
41
|
@runtime_checkable
|
@@ -63,26 +62,34 @@ class SupportsComparison(Protocol):
|
|
63
62
|
|
64
63
|
Comparable = TypeVar("Comparable", Parameter, SupportsFloat, SupportsComparison)
|
65
64
|
|
65
|
+
DEFAULT_TOLERANCE: Final = 1e-6
|
66
|
+
|
66
67
|
|
67
68
|
@overload
|
68
|
-
def is_close(old: Parameter, new: Parameter) -> bool: ...
|
69
|
+
def is_close(old: Parameter, new: Parameter, tolerance: None = None) -> bool: ...
|
69
70
|
|
70
71
|
|
71
72
|
@overload
|
72
|
-
def is_close(
|
73
|
+
def is_close(
|
74
|
+
old: SupportsFloat, new: SupportsFloat, tolerance: float = DEFAULT_TOLERANCE
|
75
|
+
) -> bool: ...
|
73
76
|
|
74
77
|
|
75
78
|
@overload
|
76
|
-
def is_close(
|
79
|
+
def is_close(
|
80
|
+
old: SupportsComparison, new: SupportsComparison, tolerance: None = None
|
81
|
+
) -> bool: ...
|
77
82
|
|
78
83
|
|
79
|
-
def is_close(
|
84
|
+
def is_close(
|
85
|
+
old: Comparable, new: Comparable, tolerance: float | None = DEFAULT_TOLERANCE
|
86
|
+
) -> bool:
|
80
87
|
"""Check if value is significantly changed."""
|
81
88
|
if isinstance(old, Parameter) and isinstance(new, Parameter):
|
82
89
|
return new.pending_update or old.values.__ne__(new.values)
|
83
90
|
|
84
|
-
if isinstance(old, SupportsFloat) and isinstance(new, SupportsFloat):
|
85
|
-
return not math.isclose(old, new, abs_tol=
|
91
|
+
if tolerance and isinstance(old, SupportsFloat) and isinstance(new, SupportsFloat):
|
92
|
+
return not math.isclose(old, new, abs_tol=tolerance)
|
86
93
|
|
87
94
|
return old.__ne__(new)
|
88
95
|
|
@@ -293,6 +300,53 @@ def custom(callback: Callback, filter_fn: _FilterT) -> _Custom:
|
|
293
300
|
return _Custom(callback, filter_fn)
|
294
301
|
|
295
302
|
|
303
|
+
class _Deadband(Filter):
|
304
|
+
"""Represents a deadband filter.
|
305
|
+
|
306
|
+
Calls a callback only when value is significantly changed from the
|
307
|
+
previous callback call.
|
308
|
+
"""
|
309
|
+
|
310
|
+
__slots__ = ("_tolerance",)
|
311
|
+
|
312
|
+
_tolerance: float
|
313
|
+
|
314
|
+
def __init__(self, callback: Callback, tolerance: float) -> None:
|
315
|
+
"""Initialize a new value changed filter."""
|
316
|
+
self._tolerance = tolerance
|
317
|
+
super().__init__(callback)
|
318
|
+
|
319
|
+
async def __call__(self, new_value: Any) -> Any:
|
320
|
+
"""Set a new value for the callback."""
|
321
|
+
if not isinstance(new_value, (float, int, Decimal)):
|
322
|
+
raise TypeError(
|
323
|
+
"Deadband filter can only be used with numeric values, got "
|
324
|
+
f"{type(new_value).__name__}: {new_value}"
|
325
|
+
)
|
326
|
+
|
327
|
+
if self._value == UNDEFINED or is_close(
|
328
|
+
self._value, new_value, tolerance=self._tolerance
|
329
|
+
):
|
330
|
+
self._value = new_value
|
331
|
+
return await self._callback(new_value)
|
332
|
+
|
333
|
+
|
334
|
+
def deadband(callback: Callback, tolerance: float) -> _Deadband:
|
335
|
+
"""Create a new deadband filter.
|
336
|
+
|
337
|
+
A callback function will only be called when the value is significantly changed
|
338
|
+
from the previous callback call.
|
339
|
+
|
340
|
+
:param callback: A callback function to be awaited on significant value change
|
341
|
+
:type callback: Callback
|
342
|
+
:param tolerance: The minimum difference required to trigger the callback
|
343
|
+
:type tolerance: float
|
344
|
+
:return: An instance of callable filter
|
345
|
+
:rtype: _Deadband
|
346
|
+
"""
|
347
|
+
return _Deadband(callback, tolerance)
|
348
|
+
|
349
|
+
|
296
350
|
class _Debounce(Filter):
|
297
351
|
"""Represents a debounce filter.
|
298
352
|
|
@@ -468,6 +522,7 @@ __all__ = [
|
|
468
522
|
"aggregate",
|
469
523
|
"clamp",
|
470
524
|
"custom",
|
525
|
+
"deadband",
|
471
526
|
"debounce",
|
472
527
|
"delta",
|
473
528
|
"on_change",
|
@@ -4,10 +4,11 @@ from collections.abc import Awaitable
|
|
4
4
|
from functools import wraps
|
5
5
|
from typing import Any, Callable, TypeVar, cast
|
6
6
|
|
7
|
-
from typing_extensions import ParamSpec
|
7
|
+
from typing_extensions import ParamSpec, TypeAlias
|
8
8
|
|
9
9
|
T = TypeVar("T")
|
10
10
|
P = ParamSpec("P")
|
11
|
+
_CallableT: TypeAlias = Callable[..., Awaitable[Any]]
|
11
12
|
|
12
13
|
|
13
14
|
class AsyncCache:
|
@@ -21,7 +22,7 @@ class AsyncCache:
|
|
21
22
|
"""Initialize the cache."""
|
22
23
|
self.cache = {}
|
23
24
|
|
24
|
-
async def get(self, key: str, coro:
|
25
|
+
async def get(self, key: str, coro: _CallableT) -> Any:
|
25
26
|
"""Get a value from the cache or compute and store it."""
|
26
27
|
if key not in self.cache:
|
27
28
|
self.cache[key] = await coro()
|
@@ -8,11 +8,14 @@ import logging
|
|
8
8
|
from types import ModuleType
|
9
9
|
from typing import Any, TypeVar
|
10
10
|
|
11
|
+
from pyplumio.helpers.async_cache import acache
|
12
|
+
|
11
13
|
_LOGGER = logging.getLogger(__name__)
|
12
14
|
|
13
15
|
T = TypeVar("T")
|
14
16
|
|
15
17
|
|
18
|
+
@acache
|
16
19
|
async def import_module(name: str) -> ModuleType:
|
17
20
|
"""Import module by name."""
|
18
21
|
loop = asyncio.get_running_loop()
|
@@ -22,19 +25,15 @@ async def import_module(name: str) -> ModuleType:
|
|
22
25
|
async def create_instance(class_path: str, /, cls: type[T], **kwargs: Any) -> T:
|
23
26
|
"""Return a class instance from the class path."""
|
24
27
|
module_name, class_name = class_path.rsplit(".", 1)
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
return instance
|
35
|
-
except Exception:
|
36
|
-
_LOGGER.exception("Failed to create instance for class path '%s'", class_path)
|
37
|
-
raise
|
28
|
+
module = await import_module(module_name)
|
29
|
+
instance = getattr(module, class_name)(**kwargs)
|
30
|
+
if not isinstance(instance, cls):
|
31
|
+
raise TypeError(
|
32
|
+
f"Expected instance of '{cls.__name__}', but got "
|
33
|
+
f"'{type(instance).__name__}' from '{class_name}'"
|
34
|
+
)
|
35
|
+
|
36
|
+
return instance
|
38
37
|
|
39
38
|
|
40
39
|
__all__ = ["create_instance"]
|
@@ -15,13 +15,14 @@ from pyplumio.structures.schedules import collect_schedule_data
|
|
15
15
|
|
16
16
|
TIME_FORMAT: Final = "%H:%M"
|
17
17
|
|
18
|
-
|
18
|
+
|
19
|
+
Time = Annotated[str, "Time string in %H:%M format"]
|
20
|
+
|
21
|
+
MIDNIGHT: Final = Time("00:00")
|
19
22
|
MIDNIGHT_DT = dt.datetime.strptime(MIDNIGHT, TIME_FORMAT)
|
20
23
|
|
21
24
|
STEP = dt.timedelta(minutes=30)
|
22
25
|
|
23
|
-
Time = Annotated[str, "Time string in %H:%M format"]
|
24
|
-
|
25
26
|
|
26
27
|
def get_time(
|
27
28
|
index: int, start: dt.datetime = MIDNIGHT_DT, step: dt.timedelta = STEP
|
@@ -4,7 +4,6 @@ from __future__ import annotations
|
|
4
4
|
|
5
5
|
from abc import ABC, abstractmethod
|
6
6
|
import asyncio
|
7
|
-
from collections.abc import Sequence
|
8
7
|
from dataclasses import dataclass
|
9
8
|
import logging
|
10
9
|
from typing import TYPE_CHECKING, Any, Literal, TypeVar, Union, get_args
|
@@ -12,16 +11,8 @@ from typing import TYPE_CHECKING, Any, Literal, TypeVar, Union, get_args
|
|
12
11
|
from dataslots import dataslots
|
13
12
|
from typing_extensions import TypeAlias
|
14
13
|
|
15
|
-
from pyplumio.const import
|
16
|
-
BYTE_UNDEFINED,
|
17
|
-
STATE_OFF,
|
18
|
-
STATE_ON,
|
19
|
-
ProductModel,
|
20
|
-
State,
|
21
|
-
UnitOfMeasurement,
|
22
|
-
)
|
14
|
+
from pyplumio.const import BYTE_UNDEFINED, STATE_OFF, STATE_ON, State, UnitOfMeasurement
|
23
15
|
from pyplumio.frames import Request
|
24
|
-
from pyplumio.structures.product_info import ProductInfo
|
25
16
|
from pyplumio.utils import is_divisible
|
26
17
|
|
27
18
|
if TYPE_CHECKING:
|
@@ -120,8 +111,8 @@ class Parameter(ABC):
|
|
120
111
|
other = other.values
|
121
112
|
|
122
113
|
if isinstance(other, ParameterValues):
|
123
|
-
handler = getattr(self.values, method_to_call)
|
124
|
-
return handler(other)
|
114
|
+
handler = getattr(self.values.value, method_to_call)
|
115
|
+
return handler(other.value)
|
125
116
|
|
126
117
|
if isinstance(other, (int, float, bool)) or other in get_args(State):
|
127
118
|
handler = getattr(self.values.value, method_to_call)
|
@@ -486,53 +477,6 @@ class Switch(Parameter):
|
|
486
477
|
return STATE_ON
|
487
478
|
|
488
479
|
|
489
|
-
@dataclass
|
490
|
-
class ParameterOverride:
|
491
|
-
"""Represents a parameter override."""
|
492
|
-
|
493
|
-
__slot__ = ("original", "replacement", "product_model", "product_id")
|
494
|
-
|
495
|
-
original: str
|
496
|
-
replacement: ParameterDescription
|
497
|
-
product_model: ProductModel
|
498
|
-
product_id: int
|
499
|
-
|
500
|
-
|
501
|
-
_DescriptorT = TypeVar("_DescriptorT", bound=ParameterDescription)
|
502
|
-
|
503
|
-
|
504
|
-
def patch_parameter_types(
|
505
|
-
product_info: ProductInfo,
|
506
|
-
parameter_types: list[_DescriptorT],
|
507
|
-
parameter_overrides: Sequence[ParameterOverride],
|
508
|
-
) -> list[_DescriptorT]:
|
509
|
-
"""Patch the parameter types based on the provided overrides.
|
510
|
-
|
511
|
-
Note:
|
512
|
-
The `# type: ignore[assignment]` comment is used to suppress a
|
513
|
-
type-checking error caused by mypy bug. For more details, see:
|
514
|
-
https://github.com/python/mypy/issues/13596
|
515
|
-
|
516
|
-
"""
|
517
|
-
replacements = {
|
518
|
-
override.original: override.replacement
|
519
|
-
for override in parameter_overrides
|
520
|
-
if override.product_model.value == product_info.model
|
521
|
-
and override.product_id == product_info.id
|
522
|
-
}
|
523
|
-
for index, description in enumerate(parameter_types):
|
524
|
-
if description.name in replacements:
|
525
|
-
_LOGGER.info(
|
526
|
-
"Replacing parameter description for '%s' with '%s' (%s)",
|
527
|
-
description.name,
|
528
|
-
replacements[description.name],
|
529
|
-
product_info.model,
|
530
|
-
)
|
531
|
-
parameter_types[index] = replacements[description.name] # type: ignore[assignment]
|
532
|
-
|
533
|
-
return parameter_types
|
534
|
-
|
535
|
-
|
536
480
|
__all__ = [
|
537
481
|
"Number",
|
538
482
|
"NumberDescription",
|
@@ -542,7 +486,6 @@ __all__ = [
|
|
542
486
|
"Parameter",
|
543
487
|
"ParameterDescription",
|
544
488
|
"ParameterValues",
|
545
|
-
"patch_parameter_types",
|
546
489
|
"State",
|
547
490
|
"Switch",
|
548
491
|
"SwitchDescription",
|
@@ -0,0 +1,111 @@
|
|
1
|
+
"""Custom parameters for products."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from collections.abc import Sequence
|
6
|
+
from dataclasses import dataclass
|
7
|
+
import logging
|
8
|
+
from typing import ClassVar, TypeVar, cast
|
9
|
+
|
10
|
+
from pyplumio.helpers.factory import create_instance
|
11
|
+
from pyplumio.parameters import ParameterDescription
|
12
|
+
from pyplumio.structures.product_info import ProductInfo
|
13
|
+
from pyplumio.utils import to_camelcase
|
14
|
+
|
15
|
+
_LOGGER = logging.getLogger(__name__)
|
16
|
+
|
17
|
+
|
18
|
+
@dataclass
|
19
|
+
class Signature:
|
20
|
+
"""Represents a product signature."""
|
21
|
+
|
22
|
+
__slots__ = ("id", "model")
|
23
|
+
|
24
|
+
id: int
|
25
|
+
model: str
|
26
|
+
|
27
|
+
|
28
|
+
@dataclass
|
29
|
+
class CustomParameter:
|
30
|
+
"""Represents a custom parameter."""
|
31
|
+
|
32
|
+
__slot__ = ("original", "replacement")
|
33
|
+
|
34
|
+
original: str
|
35
|
+
replacement: ParameterDescription
|
36
|
+
|
37
|
+
|
38
|
+
class CustomParameters:
|
39
|
+
"""Represents a custom parameters."""
|
40
|
+
|
41
|
+
__slots__ = ("signature", "replacements")
|
42
|
+
|
43
|
+
signature: ClassVar[Signature]
|
44
|
+
replacements: ClassVar[Sequence[CustomParameter]]
|
45
|
+
|
46
|
+
def validate(self, product_info: ProductInfo) -> bool:
|
47
|
+
"""Validate the product info."""
|
48
|
+
return (
|
49
|
+
self.signature.id == product_info.id
|
50
|
+
and self.signature.model == product_info.model
|
51
|
+
)
|
52
|
+
|
53
|
+
|
54
|
+
async def _load_custom_parameters(
|
55
|
+
product_info: ProductInfo,
|
56
|
+
) -> dict[str, ParameterDescription] | None:
|
57
|
+
"""Load custom parameters."""
|
58
|
+
module_name = product_info.model.replace("-", "_").replace(" ", "_").lower()
|
59
|
+
module_path = f"parameters.custom.{module_name}"
|
60
|
+
class_name = to_camelcase(module_name).upper().replace("ECOMAX", "EcoMAX")
|
61
|
+
class_path = f"{module_path}.{class_name}"
|
62
|
+
try:
|
63
|
+
_LOGGER.debug(
|
64
|
+
"Trying to load custom parameters for %s from %s",
|
65
|
+
product_info.model,
|
66
|
+
class_path,
|
67
|
+
)
|
68
|
+
custom_parameters = await create_instance(class_path, cls=CustomParameters)
|
69
|
+
if not custom_parameters.validate(product_info):
|
70
|
+
raise ValueError
|
71
|
+
except (ImportError, TypeError, ValueError):
|
72
|
+
_LOGGER.debug("No custom parameters found for %s", product_info.model)
|
73
|
+
return None
|
74
|
+
|
75
|
+
return {
|
76
|
+
custom_parameter.original: custom_parameter.replacement
|
77
|
+
for custom_parameter in custom_parameters.replacements
|
78
|
+
}
|
79
|
+
|
80
|
+
|
81
|
+
_DescriptionT = TypeVar("_DescriptionT", bound=ParameterDescription)
|
82
|
+
|
83
|
+
|
84
|
+
async def inject_custom_parameters(
|
85
|
+
product_info: ProductInfo, parameter_types: list[_DescriptionT]
|
86
|
+
) -> list[_DescriptionT]:
|
87
|
+
"""Patch the parameter types based on the provided overrides."""
|
88
|
+
if custom_parameters := await _load_custom_parameters(product_info):
|
89
|
+
_LOGGER.debug("Custom parameters found for %s", product_info.model)
|
90
|
+
return cast(
|
91
|
+
list[_DescriptionT],
|
92
|
+
[
|
93
|
+
replacement
|
94
|
+
if original.name in custom_parameters
|
95
|
+
and (replacement := custom_parameters[original.name])
|
96
|
+
and (base_class := original.__class__.__bases__[0])
|
97
|
+
and isinstance(replacement, base_class)
|
98
|
+
else original
|
99
|
+
for original in parameter_types
|
100
|
+
],
|
101
|
+
)
|
102
|
+
|
103
|
+
return parameter_types
|
104
|
+
|
105
|
+
|
106
|
+
__all__ = (
|
107
|
+
"inject_custom_parameters",
|
108
|
+
"CustomParameters",
|
109
|
+
"CustomParameter",
|
110
|
+
"Signature",
|
111
|
+
)
|