PyPlumIO 0.5.43__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.
Files changed (176) hide show
  1. {pyplumio-0.5.43 → pyplumio-0.5.49}/PKG-INFO +4 -2
  2. {pyplumio-0.5.43 → pyplumio-0.5.49}/PyPlumIO.egg-info/PKG-INFO +4 -2
  3. {pyplumio-0.5.43 → pyplumio-0.5.49}/PyPlumIO.egg-info/SOURCES.txt +16 -3
  4. {pyplumio-0.5.43 → pyplumio-0.5.49}/PyPlumIO.egg-info/requires.txt +3 -1
  5. {pyplumio-0.5.43 → pyplumio-0.5.49}/docs/source/callbacks.rst +20 -3
  6. {pyplumio-0.5.43 → pyplumio-0.5.49}/docs/source/frames.rst +1 -1
  7. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/_version.py +2 -2
  8. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/const.py +1 -3
  9. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/devices/__init__.py +23 -31
  10. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/devices/ecomax.py +107 -61
  11. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/devices/mixer.py +8 -8
  12. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/devices/thermostat.py +11 -8
  13. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/filters.py +106 -29
  14. pyplumio-0.5.49/pyplumio/helpers/async_cache.py +49 -0
  15. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/helpers/event_manager.py +20 -2
  16. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/helpers/factory.py +12 -13
  17. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/helpers/schedule.py +4 -3
  18. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/helpers/timeout.py +7 -5
  19. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/parameters/__init__.py +4 -55
  20. pyplumio-0.5.49/pyplumio/parameters/custom/__init__.py +111 -0
  21. pyplumio-0.5.49/pyplumio/parameters/custom/ecomax_860d3_hb.py +80 -0
  22. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/parameters/ecomax.py +7 -54
  23. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/protocol.py +15 -7
  24. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/structures/alerts.py +1 -1
  25. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/structures/boiler_power.py +1 -1
  26. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/structures/fan_power.py +1 -1
  27. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/structures/frame_versions.py +1 -1
  28. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/structures/fuel_consumption.py +1 -1
  29. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/structures/lambda_sensor.py +1 -1
  30. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/structures/mixer_sensors.py +1 -1
  31. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/structures/network_info.py +1 -1
  32. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/structures/output_flags.py +1 -1
  33. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/structures/outputs.py +1 -1
  34. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/structures/product_info.py +1 -1
  35. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/structures/regulator_data.py +1 -1
  36. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/structures/regulator_data_schema.py +1 -1
  37. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/structures/temperatures.py +1 -1
  38. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/structures/thermostat_parameters.py +5 -7
  39. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/structures/thermostat_sensors.py +1 -1
  40. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyproject.toml +3 -1
  41. {pyplumio-0.5.43 → pyplumio-0.5.49}/requirements_test.txt +3 -1
  42. pyplumio-0.5.49/tests/__init__.py +1 -0
  43. pyplumio-0.5.49/tests/conftest.py +188 -0
  44. pyplumio-0.5.49/tests/devices/__init__.py +1 -0
  45. pyplumio-0.5.49/tests/devices/test_ecomax.py +554 -0
  46. pyplumio-0.5.49/tests/devices/test_ecoster.py +12 -0
  47. pyplumio-0.5.49/tests/devices/test_init.py +188 -0
  48. pyplumio-0.5.49/tests/devices/test_mixer.py +118 -0
  49. pyplumio-0.5.49/tests/devices/test_thermostat.py +134 -0
  50. {pyplumio-0.5.43 → pyplumio-0.5.49}/tests/frames/test_init.py +18 -8
  51. {pyplumio-0.5.43 → pyplumio-0.5.49}/tests/frames/test_messages.py +10 -16
  52. {pyplumio-0.5.43 → pyplumio-0.5.49}/tests/frames/test_requests.py +1 -25
  53. {pyplumio-0.5.43 → pyplumio-0.5.49}/tests/frames/test_responses.py +1 -19
  54. pyplumio-0.5.49/tests/helpers/test_async_cache.py +49 -0
  55. {pyplumio-0.5.43 → pyplumio-0.5.49}/tests/helpers/test_event_manager.py +19 -3
  56. pyplumio-0.5.49/tests/helpers/test_factory.py +41 -0
  57. pyplumio-0.5.49/tests/helpers/test_schedule.py +190 -0
  58. {pyplumio-0.5.43 → pyplumio-0.5.49}/tests/helpers/test_task_manager.py +7 -7
  59. {pyplumio-0.5.43 → pyplumio-0.5.49}/tests/helpers/test_timeout.py +8 -10
  60. pyplumio-0.5.49/tests/helpers/test_uid.py +17 -0
  61. pyplumio-0.5.49/tests/parameters/custom/__init__.py +1 -0
  62. pyplumio-0.5.49/tests/parameters/custom/test_ecomax_860d3_hb.py +87 -0
  63. pyplumio-0.5.49/tests/parameters/custom/test_init.py +89 -0
  64. {pyplumio-0.5.43 → pyplumio-0.5.49}/tests/parameters/test_ecomax.py +3 -3
  65. pyplumio-0.5.49/tests/parameters/test_init.py +550 -0
  66. {pyplumio-0.5.43 → pyplumio-0.5.49}/tests/parameters/test_thermostats.py +1 -1
  67. pyplumio-0.5.49/tests/test_connection.py +223 -0
  68. pyplumio-0.5.49/tests/test_data_types.py +87 -0
  69. {pyplumio-0.5.43 → pyplumio-0.5.49}/tests/test_filters.py +103 -34
  70. {pyplumio-0.5.43 → pyplumio-0.5.49}/tests/test_main.py +3 -1
  71. {pyplumio-0.5.43 → pyplumio-0.5.49}/tests/test_protocol.py +21 -16
  72. pyplumio-0.5.49/tests/test_stream.py +267 -0
  73. pyplumio-0.5.49/tests/test_utils.py +58 -0
  74. pyplumio-0.5.49/tests/testdata/parameters/ecomax_860d3_hb.json +20 -0
  75. {pyplumio-0.5.43 → pyplumio-0.5.49}/tests/testdata/responses/regulator_data_schema.json +44 -44
  76. pyplumio-0.5.49/tests/testdata/unknown/unknown_ecomax_parameter.json +20 -0
  77. pyplumio-0.5.49/tests/testdata/unknown/unknown_mixer_parameter.json +11 -0
  78. pyplumio-0.5.43/tests/__init__.py +0 -56
  79. pyplumio-0.5.43/tests/conftest.py +0 -33
  80. pyplumio-0.5.43/tests/helpers/test_data_types.py +0 -299
  81. pyplumio-0.5.43/tests/helpers/test_factory.py +0 -35
  82. pyplumio-0.5.43/tests/helpers/test_schedule.py +0 -150
  83. pyplumio-0.5.43/tests/helpers/test_uid.py +0 -17
  84. pyplumio-0.5.43/tests/parameters/test_init.py +0 -398
  85. pyplumio-0.5.43/tests/test_connection.py +0 -246
  86. pyplumio-0.5.43/tests/test_devices.py +0 -736
  87. pyplumio-0.5.43/tests/test_stream.py +0 -186
  88. pyplumio-0.5.43/tests/test_utils.py +0 -34
  89. pyplumio-0.5.43/tests/testdata/unknown/unknown_ecomax_parameter.json +0 -18
  90. pyplumio-0.5.43/tests/testdata/unknown/unknown_mixer_parameter.json +0 -9
  91. {pyplumio-0.5.43 → pyplumio-0.5.49}/.gitattributes +0 -0
  92. {pyplumio-0.5.43 → pyplumio-0.5.49}/.github/CODE_OF_CONDUCT.md +0 -0
  93. {pyplumio-0.5.43 → pyplumio-0.5.49}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  94. {pyplumio-0.5.43 → pyplumio-0.5.49}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  95. {pyplumio-0.5.43 → pyplumio-0.5.49}/.github/dependabot.yml +0 -0
  96. {pyplumio-0.5.43 → pyplumio-0.5.49}/.github/workflows/ci.yml +0 -0
  97. {pyplumio-0.5.43 → pyplumio-0.5.49}/.github/workflows/codeql.yml +0 -0
  98. {pyplumio-0.5.43 → pyplumio-0.5.49}/.github/workflows/deploy.yml +0 -0
  99. {pyplumio-0.5.43 → pyplumio-0.5.49}/.github/workflows/documentation.yml +0 -0
  100. {pyplumio-0.5.43 → pyplumio-0.5.49}/.gitignore +0 -0
  101. {pyplumio-0.5.43 → pyplumio-0.5.49}/.pre-commit-config.yaml +0 -0
  102. {pyplumio-0.5.43 → pyplumio-0.5.49}/.qlty/qlty.toml +0 -0
  103. {pyplumio-0.5.43 → pyplumio-0.5.49}/.vscode/settings.json +0 -0
  104. {pyplumio-0.5.43 → pyplumio-0.5.49}/LICENSE +0 -0
  105. {pyplumio-0.5.43 → pyplumio-0.5.49}/MANIFEST.in +0 -0
  106. {pyplumio-0.5.43 → pyplumio-0.5.49}/PyPlumIO.egg-info/dependency_links.txt +0 -0
  107. {pyplumio-0.5.43 → pyplumio-0.5.49}/PyPlumIO.egg-info/top_level.txt +0 -0
  108. {pyplumio-0.5.43 → pyplumio-0.5.49}/README.md +0 -0
  109. {pyplumio-0.5.43 → pyplumio-0.5.49}/docs/Makefile +0 -0
  110. {pyplumio-0.5.43 → pyplumio-0.5.49}/docs/make.bat +0 -0
  111. {pyplumio-0.5.43 → pyplumio-0.5.49}/docs/source/conf.py +0 -0
  112. {pyplumio-0.5.43 → pyplumio-0.5.49}/docs/source/connecting.rst +0 -0
  113. {pyplumio-0.5.43 → pyplumio-0.5.49}/docs/source/index.rst +0 -0
  114. {pyplumio-0.5.43 → pyplumio-0.5.49}/docs/source/mixers_thermostats.rst +0 -0
  115. {pyplumio-0.5.43 → pyplumio-0.5.49}/docs/source/protocol.rst +0 -0
  116. {pyplumio-0.5.43 → pyplumio-0.5.49}/docs/source/reading.rst +0 -0
  117. {pyplumio-0.5.43 → pyplumio-0.5.49}/docs/source/schedules.rst +0 -0
  118. {pyplumio-0.5.43 → pyplumio-0.5.49}/docs/source/writing.rst +0 -0
  119. {pyplumio-0.5.43 → pyplumio-0.5.49}/images/ecomax.png +0 -0
  120. {pyplumio-0.5.43 → pyplumio-0.5.49}/images/rs485.png +0 -0
  121. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/__init__.py +0 -0
  122. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/__main__.py +0 -0
  123. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/connection.py +0 -0
  124. {pyplumio-0.5.43/pyplumio/helpers → pyplumio-0.5.49/pyplumio}/data_types.py +0 -0
  125. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/devices/ecoster.py +0 -0
  126. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/exceptions.py +0 -0
  127. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/frames/__init__.py +0 -0
  128. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/frames/messages.py +0 -0
  129. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/frames/requests.py +0 -0
  130. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/frames/responses.py +0 -0
  131. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/helpers/__init__.py +0 -0
  132. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/helpers/task_manager.py +0 -0
  133. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/helpers/uid.py +0 -0
  134. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/parameters/mixer.py +0 -0
  135. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/parameters/thermostat.py +0 -0
  136. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/py.typed +0 -0
  137. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/stream.py +0 -0
  138. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/structures/__init__.py +0 -0
  139. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/structures/boiler_load.py +0 -0
  140. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/structures/ecomax_parameters.py +0 -0
  141. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/structures/fuel_level.py +0 -0
  142. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/structures/mixer_parameters.py +0 -0
  143. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/structures/modules.py +0 -0
  144. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/structures/pending_alerts.py +0 -0
  145. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/structures/program_version.py +0 -0
  146. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/structures/schedules.py +0 -0
  147. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/structures/statuses.py +0 -0
  148. {pyplumio-0.5.43 → pyplumio-0.5.49}/pyplumio/utils.py +0 -0
  149. {pyplumio-0.5.43 → pyplumio-0.5.49}/requirements.txt +0 -0
  150. {pyplumio-0.5.43 → pyplumio-0.5.49}/requirements_docs.txt +0 -0
  151. {pyplumio-0.5.43 → pyplumio-0.5.49}/setup.cfg +0 -0
  152. {pyplumio-0.5.43 → pyplumio-0.5.49}/tests/helpers/__init__.py +0 -0
  153. {pyplumio-0.5.43 → pyplumio-0.5.49}/tests/parameters/__init__.py +0 -0
  154. {pyplumio-0.5.43 → pyplumio-0.5.49}/tests/parameters/test_mixers.py +0 -0
  155. {pyplumio-0.5.43 → pyplumio-0.5.49}/tests/ruff.toml +0 -0
  156. {pyplumio-0.5.43 → pyplumio-0.5.49}/tests/test_init.py +0 -0
  157. {pyplumio-0.5.43 → pyplumio-0.5.49}/tests/testdata/messages/regulator_data.json +0 -0
  158. {pyplumio-0.5.43 → pyplumio-0.5.49}/tests/testdata/messages/sensor_data.json +0 -0
  159. {pyplumio-0.5.43 → pyplumio-0.5.49}/tests/testdata/requests/alerts.json +0 -0
  160. {pyplumio-0.5.43 → pyplumio-0.5.49}/tests/testdata/requests/ecomax_control.json +0 -0
  161. {pyplumio-0.5.43 → pyplumio-0.5.49}/tests/testdata/requests/ecomax_parameters.json +0 -0
  162. {pyplumio-0.5.43 → pyplumio-0.5.49}/tests/testdata/requests/mixer_parameters.json +0 -0
  163. {pyplumio-0.5.43 → pyplumio-0.5.49}/tests/testdata/requests/set_ecomax_parameter.json +0 -0
  164. {pyplumio-0.5.43 → pyplumio-0.5.49}/tests/testdata/requests/set_mixer_parameter.json +0 -0
  165. {pyplumio-0.5.43 → pyplumio-0.5.49}/tests/testdata/requests/set_schedule.json +0 -0
  166. {pyplumio-0.5.43 → pyplumio-0.5.49}/tests/testdata/requests/set_thermostat_parameter.json +0 -0
  167. {pyplumio-0.5.43 → pyplumio-0.5.49}/tests/testdata/requests/thermostat_parameters.json +0 -0
  168. {pyplumio-0.5.43 → pyplumio-0.5.49}/tests/testdata/responses/alerts.json +0 -0
  169. {pyplumio-0.5.43 → pyplumio-0.5.49}/tests/testdata/responses/device_available.json +0 -0
  170. {pyplumio-0.5.43 → pyplumio-0.5.49}/tests/testdata/responses/ecomax_parameters.json +0 -0
  171. {pyplumio-0.5.43 → pyplumio-0.5.49}/tests/testdata/responses/mixer_parameters.json +0 -0
  172. {pyplumio-0.5.43 → pyplumio-0.5.49}/tests/testdata/responses/password.json +0 -0
  173. {pyplumio-0.5.43 → pyplumio-0.5.49}/tests/testdata/responses/program_version.json +0 -0
  174. {pyplumio-0.5.43 → pyplumio-0.5.49}/tests/testdata/responses/schedules.json +0 -0
  175. {pyplumio-0.5.43 → pyplumio-0.5.49}/tests/testdata/responses/thermostat_parameters.json +0 -0
  176. {pyplumio-0.5.43 → 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.43
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
@@ -29,11 +29,13 @@ Requires-Dist: typing-extensions==4.13.2
29
29
  Provides-Extra: test
30
30
  Requires-Dist: codespell==2.4.1; extra == "test"
31
31
  Requires-Dist: coverage==7.8.0; extra == "test"
32
+ Requires-Dist: freezegun==1.5.1; extra == "test"
32
33
  Requires-Dist: mypy==1.15.0; extra == "test"
34
+ Requires-Dist: numpy<3.0.0,>=2.0.0; extra == "test"
33
35
  Requires-Dist: pyserial-asyncio-fast==0.16; extra == "test"
34
36
  Requires-Dist: pytest==8.3.5; extra == "test"
35
37
  Requires-Dist: pytest-asyncio==0.26.0; extra == "test"
36
- Requires-Dist: ruff==0.11.7; extra == "test"
38
+ Requires-Dist: ruff==0.11.9; extra == "test"
37
39
  Requires-Dist: tox==4.25.0; extra == "test"
38
40
  Requires-Dist: types-pyserial==3.5.0.20250326; extra == "test"
39
41
  Provides-Extra: docs
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPlumIO
3
- Version: 0.5.43
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
@@ -29,11 +29,13 @@ Requires-Dist: typing-extensions==4.13.2
29
29
  Provides-Extra: test
30
30
  Requires-Dist: codespell==2.4.1; extra == "test"
31
31
  Requires-Dist: coverage==7.8.0; extra == "test"
32
+ Requires-Dist: freezegun==1.5.1; extra == "test"
32
33
  Requires-Dist: mypy==1.15.0; extra == "test"
34
+ Requires-Dist: numpy<3.0.0,>=2.0.0; extra == "test"
33
35
  Requires-Dist: pyserial-asyncio-fast==0.16; extra == "test"
34
36
  Requires-Dist: pytest==8.3.5; extra == "test"
35
37
  Requires-Dist: pytest-asyncio==0.26.0; extra == "test"
36
- Requires-Dist: ruff==0.11.7; extra == "test"
38
+ Requires-Dist: ruff==0.11.9; extra == "test"
37
39
  Requires-Dist: tox==4.25.0; extra == "test"
38
40
  Requires-Dist: types-pyserial==3.5.0.20250326; extra == "test"
39
41
  Provides-Extra: docs
@@ -42,6 +42,7 @@ pyplumio/__main__.py
42
42
  pyplumio/_version.py
43
43
  pyplumio/connection.py
44
44
  pyplumio/const.py
45
+ pyplumio/data_types.py
45
46
  pyplumio/exceptions.py
46
47
  pyplumio/filters.py
47
48
  pyplumio/protocol.py
@@ -58,7 +59,7 @@ pyplumio/frames/messages.py
58
59
  pyplumio/frames/requests.py
59
60
  pyplumio/frames/responses.py
60
61
  pyplumio/helpers/__init__.py
61
- pyplumio/helpers/data_types.py
62
+ pyplumio/helpers/async_cache.py
62
63
  pyplumio/helpers/event_manager.py
63
64
  pyplumio/helpers/factory.py
64
65
  pyplumio/helpers/schedule.py
@@ -69,6 +70,8 @@ pyplumio/parameters/__init__.py
69
70
  pyplumio/parameters/ecomax.py
70
71
  pyplumio/parameters/mixer.py
71
72
  pyplumio/parameters/thermostat.py
73
+ pyplumio/parameters/custom/__init__.py
74
+ pyplumio/parameters/custom/ecomax_860d3_hb.py
72
75
  pyplumio/structures/__init__.py
73
76
  pyplumio/structures/alerts.py
74
77
  pyplumio/structures/boiler_load.py
@@ -99,19 +102,25 @@ tests/__init__.py
99
102
  tests/conftest.py
100
103
  tests/ruff.toml
101
104
  tests/test_connection.py
102
- tests/test_devices.py
105
+ tests/test_data_types.py
103
106
  tests/test_filters.py
104
107
  tests/test_init.py
105
108
  tests/test_main.py
106
109
  tests/test_protocol.py
107
110
  tests/test_stream.py
108
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
109
118
  tests/frames/test_init.py
110
119
  tests/frames/test_messages.py
111
120
  tests/frames/test_requests.py
112
121
  tests/frames/test_responses.py
113
122
  tests/helpers/__init__.py
114
- tests/helpers/test_data_types.py
123
+ tests/helpers/test_async_cache.py
115
124
  tests/helpers/test_event_manager.py
116
125
  tests/helpers/test_factory.py
117
126
  tests/helpers/test_schedule.py
@@ -123,8 +132,12 @@ tests/parameters/test_ecomax.py
123
132
  tests/parameters/test_init.py
124
133
  tests/parameters/test_mixers.py
125
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
126
138
  tests/testdata/messages/regulator_data.json
127
139
  tests/testdata/messages/sensor_data.json
140
+ tests/testdata/parameters/ecomax_860d3_hb.json
128
141
  tests/testdata/requests/alerts.json
129
142
  tests/testdata/requests/ecomax_control.json
130
143
  tests/testdata/requests/ecomax_parameters.json
@@ -15,10 +15,12 @@ readthedocs-sphinx-search==0.3.2
15
15
  [test]
16
16
  codespell==2.4.1
17
17
  coverage==7.8.0
18
+ freezegun==1.5.1
18
19
  mypy==1.15.0
20
+ numpy<3.0.0,>=2.0.0
19
21
  pyserial-asyncio-fast==0.16
20
22
  pytest==8.3.5
21
23
  pytest-asyncio==0.26.0
22
- ruff==0.11.7
24
+ ruff==0.11.9
23
25
  tox==4.25.0
24
26
  types-pyserial==3.5.0.20250326
@@ -38,15 +38,18 @@ All built-in filters are described below.
38
38
 
39
39
  .. autofunction:: pyplumio.filters.aggregate
40
40
 
41
- This filter aggregates value for specified amount of seconds and
42
- then calls the callback with the sum of values collected.
41
+ This filter aggregates values for specified amount of seconds or until
42
+ certain sample size is reached and then calls the callback with the
43
+ sum of values collected.
43
44
 
44
45
  .. code-block:: python
45
46
 
46
47
  from pyplumio import filters
47
48
 
48
49
  # Await the callback with the fuel burned during 30 seconds.
49
- ecomax.subscribe("fuel_burned", filters.aggregate(my_callback, seconds=30))
50
+ ecomax.subscribe(
51
+ "fuel_burned", filters.aggregate(my_callback, seconds=30, sample_size=100)
52
+ )
50
53
 
51
54
  .. autofunction:: pyplumio.filters.clamp
52
55
 
@@ -76,6 +79,20 @@ value is changed.
76
79
  # last call.
77
80
  ecomax.subscribe("heating_temp", filters.on_change(my_callback))
78
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
+
79
96
  .. autofunction:: pyplumio.filters.debounce
80
97
 
81
98
  This filter will only await the callback once value is settled across
@@ -436,7 +436,7 @@ Get the regulator data schema, that describes the data type of :ref:`regulatorda
436
436
  It's represented by dictionary, that's indexed by regulator data field id and
437
437
  a member of the following DataType class, that defines the regulator data field type.
438
438
 
439
- .. autoclass:: pyplumio.helpers.data_types.DataType
439
+ .. autoclass:: pyplumio.data_types.DataType
440
440
 
441
441
  Request
442
442
  ^^^^^^^
@@ -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.43'
21
- __version_tuple__ = version_tuple = (0, 5, 43)
20
+ __version__ = version = '0.5.49'
21
+ __version_tuple__ = version_tuple = (0, 5, 49)
@@ -14,11 +14,11 @@ ATTR_CURRENT_TEMP: Final = "current_temp"
14
14
  ATTR_DEVICE_INDEX: Final = "device_index"
15
15
  ATTR_FRAME_ERRORS: Final = "frame_errors"
16
16
  ATTR_INDEX: Final = "index"
17
- ATTR_LOADED: Final = "loaded"
18
17
  ATTR_OFFSET: Final = "offset"
19
18
  ATTR_PARAMETER: Final = "parameter"
20
19
  ATTR_PASSWORD: Final = "password"
21
20
  ATTR_SCHEDULE: Final = "schedule"
21
+ ATTR_SETUP: Final = "setup"
22
22
  ATTR_SENSORS: Final = "sensors"
23
23
  ATTR_START: Final = "start"
24
24
  ATTR_STATE: Final = "state"
@@ -229,6 +229,4 @@ PERCENTAGE: Final = "%"
229
229
 
230
230
  STATE_ON: Final = "on"
231
231
  STATE_OFF: Final = "off"
232
-
233
-
234
232
  State: TypeAlias = Literal["on", "off"]
@@ -8,14 +8,13 @@ from functools import cache
8
8
  import logging
9
9
  from typing import Any, ClassVar
10
10
 
11
- from pyplumio.const import ATTR_FRAME_ERRORS, ATTR_LOADED, DeviceType, FrameType, State
11
+ from pyplumio.const import ATTR_FRAME_ERRORS, DeviceType, FrameType, State
12
12
  from pyplumio.exceptions import RequestError, UnknownDeviceError
13
13
  from pyplumio.filters import on_change
14
- from pyplumio.frames import DataFrameDescription, Frame, Request, is_known_frame_type
14
+ from pyplumio.frames import Frame, Request, is_known_frame_type
15
15
  from pyplumio.helpers.event_manager import EventManager, event_listener
16
16
  from pyplumio.helpers.factory import create_instance
17
17
  from pyplumio.parameters import NumericType, Parameter
18
- from pyplumio.structures.frame_versions import ATTR_FRAME_VERSIONS
19
18
  from pyplumio.structures.network_info import NetworkInfo
20
19
  from pyplumio.utils import to_camelcase
21
20
 
@@ -40,7 +39,11 @@ def get_device_handler(device_type: int) -> str:
40
39
 
41
40
  type_name = to_camelcase(
42
41
  DeviceType(device_type).name,
43
- overrides={"ecomax": "EcoMAX", "ecoster": "EcoSTER"},
42
+ overrides={
43
+ "ecomax": "EcoMAX",
44
+ "ecoster": "EcoSTER",
45
+ "econet": "EcoNET",
46
+ },
44
47
  )
45
48
  return f"devices.{type_name.lower()}.{type_name}"
46
49
 
@@ -69,7 +72,7 @@ class Device(ABC, EventManager):
69
72
  :param name: Name of the parameter
70
73
  :type name: str
71
74
  :param value: New value for the parameter
72
- :type value: int | float | bool | Literal["off", "on"]
75
+ :type value: int | float | bool | Literal["on", "off"]
73
76
  :param retries: Try setting parameter for this amount of
74
77
  times, defaults to 5
75
78
  :type retries: int, optional
@@ -101,7 +104,7 @@ class Device(ABC, EventManager):
101
104
  :param name: Name of the parameter
102
105
  :type name: str
103
106
  :param value: New value for the parameter
104
- :type value: int | float | bool | Literal["off", "on"]
107
+ :type value: int | float | bool | Literal["on", "off"]
105
108
  :param retries: Try setting parameter for this amount of
106
109
  times, defaults to 5
107
110
  :type retries: int, optional
@@ -126,11 +129,11 @@ class PhysicalDevice(Device, ABC):
126
129
  virtual devices associated with them via parent property.
127
130
  """
128
131
 
129
- __slots__ = ("address", "_network", "_setup_frames", "_frame_versions")
132
+ __slots__ = ("address", "_network", "_frame_versions")
130
133
 
131
134
  address: ClassVar[int]
135
+
132
136
  _network: NetworkInfo
133
- _setup_frames: tuple[DataFrameDescription, ...]
134
137
  _frame_versions: dict[int, int]
135
138
 
136
139
  def __init__(self, queue: asyncio.Queue[Frame], network: NetworkInfo) -> None:
@@ -139,20 +142,27 @@ class PhysicalDevice(Device, ABC):
139
142
  self._network = network
140
143
  self._frame_versions = {}
141
144
 
142
- @event_listener(ATTR_FRAME_VERSIONS, on_change)
145
+ @event_listener(filter=on_change)
143
146
  async def on_event_frame_versions(self, versions: dict[int, int]) -> None:
144
147
  """Check frame versions and update outdated frames."""
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)
148
152
  and self.supports_frame_type(frame_type)
149
153
  and not self.has_frame_version(frame_type, version)
150
154
  ):
151
- _LOGGER.debug("Updating frame %s to version %i", frame_type, version)
152
- request = await Request.create(frame_type, recipient=self.address)
153
- self.queue.put_nowait(request)
155
+ await self._request_frame_version(frame_type, version)
154
156
  self._frame_versions[frame_type] = version
155
157
 
158
+ async def _request_frame_version(
159
+ self, frame_type: FrameType | int, version: int
160
+ ) -> None:
161
+ """Request frame version from the device."""
162
+ _LOGGER.info("Updating frame %s to version %i", repr(frame_type), version)
163
+ request = await Request.create(frame_type, recipient=self.address)
164
+ self.queue.put_nowait(request)
165
+
156
166
  def has_frame_version(self, frame_type: FrameType | int, version: int) -> bool:
157
167
  """Return True if frame data is up to date, False otherwise."""
158
168
  return (
@@ -171,25 +181,6 @@ class PhysicalDevice(Device, ABC):
171
181
  for name, value in frame.data.items():
172
182
  self.dispatch_nowait(name, value)
173
183
 
174
- async def async_setup(self) -> bool:
175
- """Set up addressable device."""
176
- results = await asyncio.gather(
177
- *(
178
- self.request(description.provides, description.frame_type)
179
- for description in self._setup_frames
180
- ),
181
- return_exceptions=True,
182
- )
183
-
184
- errors = [
185
- result.frame_type for result in results if isinstance(result, RequestError)
186
- ]
187
-
188
- await asyncio.gather(
189
- self.dispatch(ATTR_FRAME_ERRORS, errors), self.dispatch(ATTR_LOADED, True)
190
- )
191
- return True
192
-
193
184
  async def request(
194
185
  self, name: str, frame_type: FrameType, retries: int = 3, timeout: float = 3.0
195
186
  ) -> Any:
@@ -197,6 +188,7 @@ class PhysicalDevice(Device, ABC):
197
188
 
198
189
  If value is not available before timeout, retry request.
199
190
  """
191
+ _LOGGER.info("Requesting '%s' with %s", name, repr(frame_type))
200
192
  request = await Request.create(frame_type, recipient=self.address)
201
193
  while retries > 0:
202
194
  try:
@@ -3,15 +3,16 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
- from collections.abc import Coroutine, Generator, Iterable, Sequence
6
+ from collections.abc import Coroutine, Generator, Iterable
7
7
  import logging
8
8
  import time
9
9
  from typing import Any, Final
10
10
 
11
11
  from pyplumio.const import (
12
+ ATTR_FRAME_ERRORS,
12
13
  ATTR_PASSWORD,
13
14
  ATTR_SENSORS,
14
- ATTR_STATE,
15
+ ATTR_SETUP,
15
16
  STATE_OFF,
16
17
  STATE_ON,
17
18
  DeviceState,
@@ -22,6 +23,7 @@ from pyplumio.const import (
22
23
  from pyplumio.devices import PhysicalDevice
23
24
  from pyplumio.devices.mixer import Mixer
24
25
  from pyplumio.devices.thermostat import Thermostat
26
+ from pyplumio.exceptions import RequestError
25
27
  from pyplumio.filters import on_change
26
28
  from pyplumio.frames import DataFrameDescription, Frame, Request
27
29
  from pyplumio.helpers.event_manager import event_listener
@@ -40,14 +42,12 @@ from pyplumio.structures.ecomax_parameters import (
40
42
  ATTR_ECOMAX_CONTROL,
41
43
  ATTR_ECOMAX_PARAMETERS,
42
44
  )
43
- from pyplumio.structures.fuel_consumption import ATTR_FUEL_CONSUMPTION
44
45
  from pyplumio.structures.mixer_parameters import ATTR_MIXER_PARAMETERS
45
46
  from pyplumio.structures.mixer_sensors import ATTR_MIXER_SENSORS
46
47
  from pyplumio.structures.network_info import ATTR_NETWORK, NetworkInfo
47
48
  from pyplumio.structures.product_info import ATTR_PRODUCT, ProductInfo
48
49
  from pyplumio.structures.regulator_data_schema import ATTR_REGDATA_SCHEMA
49
50
  from pyplumio.structures.schedules import (
50
- ATTR_SCHEDULE_PARAMETERS,
51
51
  ATTR_SCHEDULES,
52
52
  SCHEDULE_PARAMETERS,
53
53
  SCHEDULES,
@@ -55,20 +55,53 @@ from pyplumio.structures.schedules import (
55
55
  ScheduleSwitch,
56
56
  ScheduleSwitchDescription,
57
57
  )
58
- from pyplumio.structures.thermostat_parameters import (
59
- ATTR_THERMOSTAT_PARAMETERS,
60
- ATTR_THERMOSTAT_PROFILE,
61
- )
58
+ from pyplumio.structures.thermostat_parameters import ATTR_THERMOSTAT_PARAMETERS
62
59
  from pyplumio.structures.thermostat_sensors import ATTR_THERMOSTAT_SENSORS
63
60
 
61
+ _LOGGER = logging.getLogger(__name__)
62
+
63
+
64
64
  ATTR_MIXERS: Final = "mixers"
65
65
  ATTR_THERMOSTATS: Final = "thermostats"
66
66
  ATTR_FUEL_BURNED: Final = "fuel_burned"
67
67
 
68
- NANOSECONDS_IN_SECOND: Final = 1_000_000_000
69
- MAX_TIME_SINCE_LAST_FUEL_UPDATE_NS: Final = 300 * NANOSECONDS_IN_SECOND
68
+ MAX_TIME_SINCE_LAST_FUEL_UPDATE: Final = 5 * 60
69
+
70
+
71
+ class FuelMeter:
72
+ """Represents a fuel meter.
73
+
74
+ Calculates the fuel burned based on the time
75
+ elapsed since the last sensor message, which contains fuel
76
+ consumption data. If the elapsed time is within the acceptable
77
+ range, it returns the fuel burned data. Otherwise, it logs a
78
+ warning and returns None.
79
+ """
80
+
81
+ __slots__ = ("_last_update_time",)
82
+
83
+ _last_update_time: float
70
84
 
71
- SETUP_FRAME_TYPES: tuple[DataFrameDescription, ...] = (
85
+ def __init__(self) -> None:
86
+ """Initialize a new fuel meter."""
87
+ self._last_update_time = time.monotonic()
88
+
89
+ def calculate(self, fuel_consumption: float) -> float | None:
90
+ """Calculate the amount of burned fuel since last update."""
91
+ current_time = time.monotonic()
92
+ time_since_update = current_time - self._last_update_time
93
+ self._last_update_time = current_time
94
+ if time_since_update < MAX_TIME_SINCE_LAST_FUEL_UPDATE:
95
+ return fuel_consumption * (time_since_update / 3600)
96
+
97
+ _LOGGER.warning(
98
+ "Skipping outdated fuel consumption data (was %f seconds old)",
99
+ time_since_update,
100
+ )
101
+ return None
102
+
103
+
104
+ REQUIRED: tuple[DataFrameDescription, ...] = (
72
105
  DataFrameDescription(
73
106
  frame_type=FrameType.REQUEST_UID,
74
107
  provides=ATTR_PRODUCT,
@@ -103,28 +136,22 @@ SETUP_FRAME_TYPES: tuple[DataFrameDescription, ...] = (
103
136
  ),
104
137
  )
105
138
 
106
- _LOGGER = logging.getLogger(__name__)
139
+ REQUIRED_TYPES = [description.frame_type for description in REQUIRED]
107
140
 
108
141
 
109
142
  class EcoMAX(PhysicalDevice):
110
143
  """Represents an ecoMAX controller."""
111
144
 
112
- __slots__ = ("_fuel_burned_time_ns",)
145
+ __slots__ = ("_fuel_meter",)
113
146
 
114
- address = DeviceType.ECOMAX
115
- _setup_frames = SETUP_FRAME_TYPES
147
+ _fuel_meter: FuelMeter
116
148
 
117
- _fuel_burned_time_ns: int
149
+ address = DeviceType.ECOMAX
118
150
 
119
151
  def __init__(self, queue: asyncio.Queue[Frame], network: NetworkInfo) -> None:
120
152
  """Initialize a new ecoMAX controller."""
121
153
  super().__init__(queue, network)
122
- self._fuel_burned_time_ns = time.perf_counter_ns()
123
-
124
- async def async_setup(self) -> bool:
125
- """Set up an ecoMAX controller."""
126
- await self.wait_for(ATTR_SENSORS)
127
- return await super().async_setup()
154
+ self._fuel_meter = FuelMeter()
128
155
 
129
156
  def handle_frame(self, frame: Frame) -> None:
130
157
  """Handle frame received from the ecoMAX device."""
@@ -162,6 +189,14 @@ class EcoMAX(PhysicalDevice):
162
189
 
163
190
  return self.dispatch_nowait(ATTR_THERMOSTATS, thermostats)
164
191
 
192
+ async def _request_frame_version(
193
+ self, frame_type: FrameType | int, version: int
194
+ ) -> None:
195
+ """Request frame version from the device."""
196
+ setup_done = self.get_nowait(ATTR_SETUP, False)
197
+ if setup_done or frame_type not in REQUIRED_TYPES:
198
+ await super()._request_frame_version(frame_type, version)
199
+
165
200
  async def _set_ecomax_state(self, state: State) -> bool:
166
201
  """Try to set the ecoMAX control state."""
167
202
  try:
@@ -196,16 +231,39 @@ class EcoMAX(PhysicalDevice):
196
231
  await asyncio.gather(*(device.shutdown() for device in devices))
197
232
  await super().shutdown()
198
233
 
199
- @event_listener(ATTR_ECOMAX_PARAMETERS)
234
+ @event_listener
235
+ async def on_event_setup(self, setup: bool) -> None:
236
+ """Request frames required to set up an ecoMAX entry."""
237
+ _LOGGER.info("Setting up device entry")
238
+ await self.wait_for(ATTR_SENSORS)
239
+ results = await asyncio.gather(
240
+ *(
241
+ self.request(description.provides, description.frame_type)
242
+ for description in REQUIRED
243
+ ),
244
+ return_exceptions=True,
245
+ )
246
+
247
+ errors = [
248
+ result.frame_type for result in results if isinstance(result, RequestError)
249
+ ]
250
+
251
+ if errors:
252
+ self.dispatch_nowait(ATTR_FRAME_ERRORS, errors)
253
+
254
+ _LOGGER.info("Device entry setup done")
255
+
256
+ @event_listener
200
257
  async def on_event_ecomax_parameters(
201
- self, parameters: Sequence[tuple[int, ParameterValues]]
258
+ self, parameters: list[tuple[int, ParameterValues]]
202
259
  ) -> bool:
203
260
  """Update ecoMAX parameters and dispatch the events."""
261
+ _LOGGER.info("Received device parameters")
204
262
  product_info: ProductInfo = await self.get(ATTR_PRODUCT)
263
+ parameter_types = await get_ecomax_parameter_types(product_info)
205
264
 
206
- def _ecomax_parameter_events() -> Generator[Coroutine, Any, None]:
265
+ def _ecomax_parameter_events() -> Generator[Coroutine]:
207
266
  """Get dispatch calls for ecoMAX parameter events."""
208
- parameter_types = get_ecomax_parameter_types(product_info)
209
267
  for index, values in parameters:
210
268
  try:
211
269
  description = parameter_types[index]
@@ -236,37 +294,20 @@ class EcoMAX(PhysicalDevice):
236
294
  await asyncio.gather(*_ecomax_parameter_events())
237
295
  return True
238
296
 
239
- @event_listener(ATTR_FUEL_CONSUMPTION)
297
+ @event_listener
240
298
  async def on_event_fuel_consumption(self, fuel_consumption: float) -> None:
241
- """Update the amount of burned fuel.
242
-
243
- This method calculates the fuel burned based on the time
244
- elapsed since the last sensor message, which contains fuel
245
- consumption data. If the elapsed time is within the acceptable
246
- range, it dispatches the fuel burned data. Otherwise, it logs a
247
- warning and skips the outdated data.
248
- """
249
- time_ns = time.perf_counter_ns()
250
- nanoseconds_passed = time_ns - self._fuel_burned_time_ns
251
- self._fuel_burned_time_ns = time_ns
252
- if nanoseconds_passed < MAX_TIME_SINCE_LAST_FUEL_UPDATE_NS:
253
- return self.dispatch_nowait(
254
- ATTR_FUEL_BURNED,
255
- fuel_consumption * nanoseconds_passed / (3600 * NANOSECONDS_IN_SECOND),
256
- )
257
-
258
- _LOGGER.warning(
259
- "Skipping outdated fuel consumption data: %f (was %i seconds old)",
260
- fuel_consumption,
261
- nanoseconds_passed / NANOSECONDS_IN_SECOND,
262
- )
299
+ """Update the amount of burned fuel."""
300
+ fuel_burned = self._fuel_meter.calculate(fuel_consumption)
301
+ if fuel_burned is not None:
302
+ self.dispatch_nowait(ATTR_FUEL_BURNED, fuel_burned)
263
303
 
264
- @event_listener(ATTR_MIXER_PARAMETERS)
304
+ @event_listener
265
305
  async def on_event_mixer_parameters(
266
306
  self,
267
- parameters: dict[int, Sequence[tuple[int, ParameterValues]]] | None,
307
+ parameters: dict[int, list[tuple[int, ParameterValues]]] | None,
268
308
  ) -> bool:
269
309
  """Handle mixer parameters and dispatch the events."""
310
+ _LOGGER.info("Received mixer parameters")
270
311
  if parameters:
271
312
  await asyncio.gather(
272
313
  *(
@@ -278,11 +319,12 @@ class EcoMAX(PhysicalDevice):
278
319
 
279
320
  return False
280
321
 
281
- @event_listener(ATTR_MIXER_SENSORS)
322
+ @event_listener
282
323
  async def on_event_mixer_sensors(
283
324
  self, sensors: dict[int, dict[str, Any]] | None
284
325
  ) -> bool:
285
326
  """Update mixer sensors and dispatch the events."""
327
+ _LOGGER.info("Received mixer sensors")
286
328
  if sensors:
287
329
  await asyncio.gather(
288
330
  *(
@@ -294,9 +336,9 @@ class EcoMAX(PhysicalDevice):
294
336
 
295
337
  return False
296
338
 
297
- @event_listener(ATTR_SCHEDULE_PARAMETERS)
339
+ @event_listener
298
340
  async def on_event_schedule_parameters(
299
- self, parameters: Sequence[tuple[int, ParameterValues]]
341
+ self, parameters: list[tuple[int, ParameterValues]]
300
342
  ) -> bool:
301
343
  """Update schedule parameters and dispatch the events."""
302
344
 
@@ -319,20 +361,22 @@ class EcoMAX(PhysicalDevice):
319
361
  await asyncio.gather(*_schedule_parameter_events())
320
362
  return True
321
363
 
322
- @event_listener(ATTR_SENSORS)
364
+ @event_listener
323
365
  async def on_event_sensors(self, sensors: dict[str, Any]) -> bool:
324
366
  """Update ecoMAX sensors and dispatch the events."""
367
+ _LOGGER.info("Received device sensors")
325
368
  await asyncio.gather(
326
369
  *(self.dispatch(name, value) for name, value in sensors.items())
327
370
  )
328
371
  return True
329
372
 
330
- @event_listener(ATTR_THERMOSTAT_PARAMETERS)
373
+ @event_listener
331
374
  async def on_event_thermostat_parameters(
332
375
  self,
333
- parameters: dict[int, Sequence[tuple[int, ParameterValues]]] | None,
376
+ parameters: dict[int, list[tuple[int, ParameterValues]]] | None,
334
377
  ) -> bool:
335
378
  """Handle thermostat parameters and dispatch the events."""
379
+ _LOGGER.info("Received thermostat parameters")
336
380
  if parameters:
337
381
  await asyncio.gather(
338
382
  *(
@@ -346,7 +390,7 @@ class EcoMAX(PhysicalDevice):
346
390
 
347
391
  return False
348
392
 
349
- @event_listener(ATTR_THERMOSTAT_PROFILE)
393
+ @event_listener
350
394
  async def on_event_thermostat_profile(
351
395
  self, values: ParameterValues | None
352
396
  ) -> EcomaxNumber | None:
@@ -358,11 +402,12 @@ class EcoMAX(PhysicalDevice):
358
402
 
359
403
  return None
360
404
 
361
- @event_listener(ATTR_THERMOSTAT_SENSORS)
405
+ @event_listener
362
406
  async def on_event_thermostat_sensors(
363
407
  self, sensors: dict[int, dict[str, Any]] | None
364
408
  ) -> bool:
365
409
  """Update thermostat sensors and dispatch the events."""
410
+ _LOGGER.info("Received thermostat sensors")
366
411
  if sensors:
367
412
  await asyncio.gather(
368
413
  *(
@@ -377,11 +422,12 @@ class EcoMAX(PhysicalDevice):
377
422
 
378
423
  return False
379
424
 
380
- @event_listener(ATTR_SCHEDULES)
425
+ @event_listener
381
426
  async def on_event_schedules(
382
427
  self, schedules: list[tuple[int, list[list[bool]]]]
383
428
  ) -> dict[str, Schedule]:
384
429
  """Update schedules."""
430
+ _LOGGER.info("Received device schedules")
385
431
  return {
386
432
  SCHEDULES[index]: Schedule(
387
433
  name=SCHEDULES[index],
@@ -397,7 +443,7 @@ class EcoMAX(PhysicalDevice):
397
443
  for index, schedule in schedules
398
444
  }
399
445
 
400
- @event_listener(ATTR_STATE, on_change)
446
+ @event_listener(filter=on_change)
401
447
  async def on_event_state(self, state: DeviceState) -> None:
402
448
  """Update the ecoMAX control parameter."""
403
449
  await self.dispatch(