PyPlumIO 0.5.47__tar.gz → 0.5.48__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.
Files changed (163) hide show
  1. {pyplumio-0.5.47 → pyplumio-0.5.48}/PKG-INFO +2 -2
  2. {pyplumio-0.5.47 → pyplumio-0.5.48}/PyPlumIO.egg-info/PKG-INFO +2 -2
  3. {pyplumio-0.5.47 → pyplumio-0.5.48}/PyPlumIO.egg-info/SOURCES.txt +10 -1
  4. {pyplumio-0.5.47 → pyplumio-0.5.48}/PyPlumIO.egg-info/requires.txt +1 -1
  5. {pyplumio-0.5.47 → pyplumio-0.5.48}/docs/source/callbacks.rst +14 -0
  6. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/_version.py +2 -2
  7. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/devices/ecomax.py +2 -2
  8. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/devices/mixer.py +2 -2
  9. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/devices/thermostat.py +2 -2
  10. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/filters.py +62 -7
  11. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/helpers/async_cache.py +3 -2
  12. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/helpers/factory.py +9 -13
  13. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/parameters/__init__.py +3 -60
  14. pyplumio-0.5.48/pyplumio/parameters/custom/__init__.py +111 -0
  15. pyplumio-0.5.48/pyplumio/parameters/custom/ecomax_860d3_hb.py +38 -0
  16. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/parameters/ecomax.py +7 -54
  17. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyproject.toml +1 -1
  18. {pyplumio-0.5.47 → pyplumio-0.5.48}/requirements_test.txt +1 -1
  19. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/conftest.py +2 -2
  20. pyplumio-0.5.48/tests/devices/__init__.py +1 -0
  21. pyplumio-0.5.48/tests/devices/test_ecomax.py +553 -0
  22. pyplumio-0.5.48/tests/devices/test_ecoster.py +12 -0
  23. pyplumio-0.5.48/tests/devices/test_init.py +188 -0
  24. pyplumio-0.5.48/tests/devices/test_mixer.py +118 -0
  25. pyplumio-0.5.48/tests/devices/test_thermostat.py +134 -0
  26. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/frames/test_init.py +1 -1
  27. pyplumio-0.5.48/tests/parameters/custom/__init__.py +1 -0
  28. pyplumio-0.5.48/tests/parameters/custom/test_init.py +89 -0
  29. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/parameters/test_ecomax.py +3 -3
  30. pyplumio-0.5.48/tests/parameters/test_init.py +552 -0
  31. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/test_connection.py +5 -6
  32. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/test_filters.py +20 -4
  33. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/test_protocol.py +8 -10
  34. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/test_stream.py +32 -18
  35. pyplumio-0.5.47/tests/parameters/test_init.py +0 -413
  36. pyplumio-0.5.47/tests/test_devices.py +0 -836
  37. {pyplumio-0.5.47 → pyplumio-0.5.48}/.gitattributes +0 -0
  38. {pyplumio-0.5.47 → pyplumio-0.5.48}/.github/CODE_OF_CONDUCT.md +0 -0
  39. {pyplumio-0.5.47 → pyplumio-0.5.48}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  40. {pyplumio-0.5.47 → pyplumio-0.5.48}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  41. {pyplumio-0.5.47 → pyplumio-0.5.48}/.github/dependabot.yml +0 -0
  42. {pyplumio-0.5.47 → pyplumio-0.5.48}/.github/workflows/ci.yml +0 -0
  43. {pyplumio-0.5.47 → pyplumio-0.5.48}/.github/workflows/codeql.yml +0 -0
  44. {pyplumio-0.5.47 → pyplumio-0.5.48}/.github/workflows/deploy.yml +0 -0
  45. {pyplumio-0.5.47 → pyplumio-0.5.48}/.github/workflows/documentation.yml +0 -0
  46. {pyplumio-0.5.47 → pyplumio-0.5.48}/.gitignore +0 -0
  47. {pyplumio-0.5.47 → pyplumio-0.5.48}/.pre-commit-config.yaml +0 -0
  48. {pyplumio-0.5.47 → pyplumio-0.5.48}/.qlty/qlty.toml +0 -0
  49. {pyplumio-0.5.47 → pyplumio-0.5.48}/.vscode/settings.json +0 -0
  50. {pyplumio-0.5.47 → pyplumio-0.5.48}/LICENSE +0 -0
  51. {pyplumio-0.5.47 → pyplumio-0.5.48}/MANIFEST.in +0 -0
  52. {pyplumio-0.5.47 → pyplumio-0.5.48}/PyPlumIO.egg-info/dependency_links.txt +0 -0
  53. {pyplumio-0.5.47 → pyplumio-0.5.48}/PyPlumIO.egg-info/top_level.txt +0 -0
  54. {pyplumio-0.5.47 → pyplumio-0.5.48}/README.md +0 -0
  55. {pyplumio-0.5.47 → pyplumio-0.5.48}/docs/Makefile +0 -0
  56. {pyplumio-0.5.47 → pyplumio-0.5.48}/docs/make.bat +0 -0
  57. {pyplumio-0.5.47 → pyplumio-0.5.48}/docs/source/conf.py +0 -0
  58. {pyplumio-0.5.47 → pyplumio-0.5.48}/docs/source/connecting.rst +0 -0
  59. {pyplumio-0.5.47 → pyplumio-0.5.48}/docs/source/frames.rst +0 -0
  60. {pyplumio-0.5.47 → pyplumio-0.5.48}/docs/source/index.rst +0 -0
  61. {pyplumio-0.5.47 → pyplumio-0.5.48}/docs/source/mixers_thermostats.rst +0 -0
  62. {pyplumio-0.5.47 → pyplumio-0.5.48}/docs/source/protocol.rst +0 -0
  63. {pyplumio-0.5.47 → pyplumio-0.5.48}/docs/source/reading.rst +0 -0
  64. {pyplumio-0.5.47 → pyplumio-0.5.48}/docs/source/schedules.rst +0 -0
  65. {pyplumio-0.5.47 → pyplumio-0.5.48}/docs/source/writing.rst +0 -0
  66. {pyplumio-0.5.47 → pyplumio-0.5.48}/images/ecomax.png +0 -0
  67. {pyplumio-0.5.47 → pyplumio-0.5.48}/images/rs485.png +0 -0
  68. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/__init__.py +0 -0
  69. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/__main__.py +0 -0
  70. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/connection.py +0 -0
  71. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/const.py +0 -0
  72. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/data_types.py +0 -0
  73. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/devices/__init__.py +0 -0
  74. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/devices/ecoster.py +0 -0
  75. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/exceptions.py +0 -0
  76. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/frames/__init__.py +0 -0
  77. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/frames/messages.py +0 -0
  78. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/frames/requests.py +0 -0
  79. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/frames/responses.py +0 -0
  80. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/helpers/__init__.py +0 -0
  81. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/helpers/event_manager.py +0 -0
  82. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/helpers/schedule.py +0 -0
  83. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/helpers/task_manager.py +0 -0
  84. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/helpers/timeout.py +0 -0
  85. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/helpers/uid.py +0 -0
  86. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/parameters/mixer.py +0 -0
  87. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/parameters/thermostat.py +0 -0
  88. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/protocol.py +0 -0
  89. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/py.typed +0 -0
  90. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/stream.py +0 -0
  91. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/structures/__init__.py +0 -0
  92. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/structures/alerts.py +0 -0
  93. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/structures/boiler_load.py +0 -0
  94. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/structures/boiler_power.py +0 -0
  95. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/structures/ecomax_parameters.py +0 -0
  96. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/structures/fan_power.py +0 -0
  97. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/structures/frame_versions.py +0 -0
  98. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/structures/fuel_consumption.py +0 -0
  99. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/structures/fuel_level.py +0 -0
  100. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/structures/lambda_sensor.py +0 -0
  101. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/structures/mixer_parameters.py +0 -0
  102. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/structures/mixer_sensors.py +0 -0
  103. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/structures/modules.py +0 -0
  104. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/structures/network_info.py +0 -0
  105. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/structures/output_flags.py +0 -0
  106. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/structures/outputs.py +0 -0
  107. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/structures/pending_alerts.py +0 -0
  108. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/structures/product_info.py +0 -0
  109. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/structures/program_version.py +0 -0
  110. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/structures/regulator_data.py +0 -0
  111. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/structures/regulator_data_schema.py +0 -0
  112. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/structures/schedules.py +0 -0
  113. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/structures/statuses.py +0 -0
  114. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/structures/temperatures.py +0 -0
  115. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/structures/thermostat_parameters.py +0 -0
  116. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/structures/thermostat_sensors.py +0 -0
  117. {pyplumio-0.5.47 → pyplumio-0.5.48}/pyplumio/utils.py +0 -0
  118. {pyplumio-0.5.47 → pyplumio-0.5.48}/requirements.txt +0 -0
  119. {pyplumio-0.5.47 → pyplumio-0.5.48}/requirements_docs.txt +0 -0
  120. {pyplumio-0.5.47 → pyplumio-0.5.48}/setup.cfg +0 -0
  121. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/__init__.py +0 -0
  122. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/frames/test_messages.py +0 -0
  123. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/frames/test_requests.py +0 -0
  124. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/frames/test_responses.py +0 -0
  125. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/helpers/__init__.py +0 -0
  126. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/helpers/test_async_cache.py +0 -0
  127. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/helpers/test_event_manager.py +0 -0
  128. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/helpers/test_factory.py +0 -0
  129. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/helpers/test_schedule.py +0 -0
  130. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/helpers/test_task_manager.py +0 -0
  131. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/helpers/test_timeout.py +0 -0
  132. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/helpers/test_uid.py +0 -0
  133. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/parameters/__init__.py +0 -0
  134. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/parameters/test_mixers.py +0 -0
  135. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/parameters/test_thermostats.py +0 -0
  136. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/ruff.toml +0 -0
  137. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/test_data_types.py +0 -0
  138. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/test_init.py +0 -0
  139. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/test_main.py +0 -0
  140. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/test_utils.py +0 -0
  141. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/testdata/messages/regulator_data.json +0 -0
  142. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/testdata/messages/sensor_data.json +0 -0
  143. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/testdata/requests/alerts.json +0 -0
  144. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/testdata/requests/ecomax_control.json +0 -0
  145. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/testdata/requests/ecomax_parameters.json +0 -0
  146. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/testdata/requests/mixer_parameters.json +0 -0
  147. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/testdata/requests/set_ecomax_parameter.json +0 -0
  148. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/testdata/requests/set_mixer_parameter.json +0 -0
  149. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/testdata/requests/set_schedule.json +0 -0
  150. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/testdata/requests/set_thermostat_parameter.json +0 -0
  151. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/testdata/requests/thermostat_parameters.json +0 -0
  152. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/testdata/responses/alerts.json +0 -0
  153. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/testdata/responses/device_available.json +0 -0
  154. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/testdata/responses/ecomax_parameters.json +0 -0
  155. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/testdata/responses/mixer_parameters.json +0 -0
  156. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/testdata/responses/password.json +0 -0
  157. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/testdata/responses/program_version.json +0 -0
  158. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/testdata/responses/regulator_data_schema.json +0 -0
  159. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/testdata/responses/schedules.json +0 -0
  160. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/testdata/responses/thermostat_parameters.json +0 -0
  161. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/testdata/responses/uid.json +0 -0
  162. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/testdata/unknown/unknown_ecomax_parameter.json +0 -0
  163. {pyplumio-0.5.47 → pyplumio-0.5.48}/tests/testdata/unknown/unknown_mixer_parameter.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPlumIO
3
- Version: 0.5.47
3
+ Version: 0.5.48
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.8; extra == "test"
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.47
3
+ Version: 0.5.48
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.8; extra == "test"
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,6 +132,8 @@ 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_init.py
128
137
  tests/testdata/messages/regulator_data.json
129
138
  tests/testdata/messages/sensor_data.json
130
139
  tests/testdata/requests/alerts.json
@@ -21,6 +21,6 @@ numpy<3.0.0,>=2.0.0
21
21
  pyserial-asyncio-fast==0.16
22
22
  pytest==8.3.5
23
23
  pytest-asyncio==0.26.0
24
- ruff==0.11.8
24
+ ruff==0.11.9
25
25
  tox==4.25.0
26
26
  types-pyserial==3.5.0.20250326
@@ -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
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.5.47'
21
- __version_tuple__ = version_tuple = (0, 5, 47)
20
+ __version__ = version = '0.5.48'
21
+ __version_tuple__ = version_tuple = (0, 5, 48)
@@ -260,10 +260,10 @@ class EcoMAX(PhysicalDevice):
260
260
  """Update ecoMAX parameters and dispatch the events."""
261
261
  _LOGGER.info("Received device parameters")
262
262
  product_info: ProductInfo = await self.get(ATTR_PRODUCT)
263
+ parameter_types = await get_ecomax_parameter_types(product_info)
263
264
 
264
- def _ecomax_parameter_events() -> Generator[Coroutine, Any, None]:
265
+ def _ecomax_parameter_events() -> Generator[Coroutine]:
265
266
  """Get dispatch calls for ecoMAX parameter events."""
266
- parameter_types = get_ecomax_parameter_types(product_info)
267
267
  for index, values in parameters:
268
268
  try:
269
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, Any, None]:
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, Any, None]:
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(old: SupportsFloat, new: SupportsFloat) -> bool: ...
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(old: SupportsComparison, new: SupportsComparison) -> bool: ...
79
+ def is_close(
80
+ old: SupportsComparison, new: SupportsComparison, tolerance: None = None
81
+ ) -> bool: ...
77
82
 
78
83
 
79
- def is_close(old: Comparable, new: Comparable) -> bool:
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=TOLERANCE)
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: Callable[..., Awaitable[Any]]) -> Any:
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()
@@ -25,19 +25,15 @@ async def import_module(name: str) -> ModuleType:
25
25
  async def create_instance(class_path: str, /, cls: type[T], **kwargs: Any) -> T:
26
26
  """Return a class instance from the class path."""
27
27
  module_name, class_name = class_path.rsplit(".", 1)
28
- try:
29
- module = await import_module(module_name)
30
- instance = getattr(module, class_name)(**kwargs)
31
- if not isinstance(instance, cls):
32
- raise TypeError(
33
- f"Expected instance of '{cls.__name__}', but got "
34
- f"'{type(instance).__name__}' from '{class_name}'"
35
- )
36
-
37
- return instance
38
- except Exception:
39
- _LOGGER.exception("Failed to create instance for class path '%s'", class_path)
40
- 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
41
37
 
42
38
 
43
39
  __all__ = ["create_instance"]
@@ -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
+ )
@@ -0,0 +1,38 @@
1
+ """Contains patch for ecoMAX 860D3-HB."""
2
+
3
+ from pyplumio.const import UnitOfMeasurement
4
+ from pyplumio.parameters.custom import CustomParameter, CustomParameters, Signature
5
+ from pyplumio.parameters.ecomax import EcomaxNumberDescription
6
+
7
+
8
+ class EcoMAX860D3HB(CustomParameters):
9
+ """Replacements for ecoMAX 860D3-HB."""
10
+
11
+ __slots__ = ()
12
+
13
+ signature = Signature(model="ecoMAX 860D3-HB", id=48)
14
+
15
+ replacements = (
16
+ CustomParameter(
17
+ original="summer_mode_disable_temp",
18
+ replacement=EcomaxNumberDescription(name="__unknown_parameter_1"),
19
+ ),
20
+ CustomParameter(
21
+ original="water_heater_target_temp",
22
+ replacement=EcomaxNumberDescription(name="summer_mode"),
23
+ ),
24
+ CustomParameter(
25
+ original="min_water_heater_target_temp",
26
+ replacement=EcomaxNumberDescription(
27
+ name="summer_mode_enable_temp",
28
+ unit_of_measurement=UnitOfMeasurement.CELSIUS,
29
+ ),
30
+ ),
31
+ CustomParameter(
32
+ original="max_water_heater_target_temp",
33
+ replacement=EcomaxNumberDescription(
34
+ name="summer_mode_disable_temp",
35
+ unit_of_measurement=UnitOfMeasurement.CELSIUS,
36
+ ),
37
+ ),
38
+ )
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from dataclasses import dataclass
6
- from functools import cache, partial
6
+ from functools import partial
7
7
  from typing import TYPE_CHECKING
8
8
 
9
9
  from dataslots import dataslots
@@ -15,21 +15,20 @@ from pyplumio.const import (
15
15
  ATTR_VALUE,
16
16
  PERCENTAGE,
17
17
  FrameType,
18
- ProductModel,
19
18
  ProductType,
20
19
  UnitOfMeasurement,
21
20
  )
22
21
  from pyplumio.frames import Request
22
+ from pyplumio.helpers.async_cache import acache
23
23
  from pyplumio.parameters import (
24
24
  OffsetNumber,
25
25
  OffsetNumberDescription,
26
26
  Parameter,
27
27
  ParameterDescription,
28
- ParameterOverride,
29
28
  Switch,
30
29
  SwitchDescription,
31
- patch_parameter_types,
32
30
  )
31
+ from pyplumio.parameters.custom import inject_custom_parameters
33
32
  from pyplumio.structures.ecomax_parameters import ATTR_ECOMAX_CONTROL
34
33
  from pyplumio.structures.product_info import ProductInfo
35
34
  from pyplumio.structures.thermostat_parameters import ATTR_THERMOSTAT_PROFILE
@@ -798,58 +797,13 @@ ECOMAX_CONTROL_PARAMETER = EcomaxSwitchDescription(
798
797
  THERMOSTAT_PROFILE_PARAMETER = EcomaxNumberDescription(name=ATTR_THERMOSTAT_PROFILE)
799
798
 
800
799
 
801
- @dataclass
802
- class EcomaxParameterOverride(ParameterOverride):
803
- """Represents an ecoMAX parameter override."""
804
-
805
- __slots__ = ()
806
-
807
- replacement: EcomaxParameterDescription
808
-
809
-
810
- PARAMETER_OVERRIDES: tuple[EcomaxParameterOverride, ...] = (
811
- EcomaxParameterOverride(
812
- original="summer_mode_disable_temp",
813
- replacement=EcomaxNumberDescription(name="__unknown_parameter_1"),
814
- product_model=ProductModel.ECOMAX_860D3_HB,
815
- product_id=48,
816
- ),
817
- EcomaxParameterOverride(
818
- original="water_heater_target_temp",
819
- replacement=EcomaxNumberDescription(name="summer_mode"),
820
- product_model=ProductModel.ECOMAX_860D3_HB,
821
- product_id=48,
822
- ),
823
- EcomaxParameterOverride(
824
- original="min_water_heater_target_temp",
825
- replacement=EcomaxNumberDescription(
826
- name="summer_mode_enable_temp",
827
- unit_of_measurement=UnitOfMeasurement.CELSIUS,
828
- ),
829
- product_model=ProductModel.ECOMAX_860D3_HB,
830
- product_id=48,
831
- ),
832
- EcomaxParameterOverride(
833
- original="max_water_heater_target_temp",
834
- replacement=EcomaxNumberDescription(
835
- name="summer_mode_disable_temp",
836
- unit_of_measurement=UnitOfMeasurement.CELSIUS,
837
- ),
838
- product_model=ProductModel.ECOMAX_860D3_HB,
839
- product_id=48,
840
- ),
841
- )
842
-
843
-
844
- @cache
845
- def get_ecomax_parameter_types(
800
+ @acache
801
+ async def get_ecomax_parameter_types(
846
802
  product_info: ProductInfo,
847
803
  ) -> list[EcomaxParameterDescription]:
848
804
  """Return ecoMAX parameter types for specific product."""
849
- return patch_parameter_types(
850
- product_info,
851
- parameter_types=PARAMETER_TYPES[product_info.type],
852
- parameter_overrides=PARAMETER_OVERRIDES,
805
+ return await inject_custom_parameters(
806
+ product_info, parameter_types=PARAMETER_TYPES[product_info.type]
853
807
  )
854
808
 
855
809
 
@@ -862,7 +816,6 @@ __all__ = [
862
816
  "EcomaxSwitch",
863
817
  "EcomaxSwitchDescription",
864
818
  "get_ecomax_parameter_types",
865
- "PARAMETER_OVERRIDES",
866
819
  "PARAMETER_TYPES",
867
820
  "THERMOSTAT_PROFILE_PARAMETER",
868
821
  ]
@@ -45,7 +45,7 @@ test = [
45
45
  "pyserial-asyncio-fast==0.16",
46
46
  "pytest==8.3.5",
47
47
  "pytest-asyncio==0.26.0",
48
- "ruff==0.11.8",
48
+ "ruff==0.11.9",
49
49
  "tox==4.25.0",
50
50
  "types-pyserial==3.5.0.20250326"
51
51
  ]