PyPlumIO 0.5.43__tar.gz → 0.5.45__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 (152) hide show
  1. {pyplumio-0.5.43 → pyplumio-0.5.45}/PKG-INFO +3 -1
  2. {pyplumio-0.5.43 → pyplumio-0.5.45}/PyPlumIO.egg-info/PKG-INFO +3 -1
  3. {pyplumio-0.5.43 → pyplumio-0.5.45}/PyPlumIO.egg-info/SOURCES.txt +4 -2
  4. {pyplumio-0.5.43 → pyplumio-0.5.45}/PyPlumIO.egg-info/requires.txt +2 -0
  5. {pyplumio-0.5.43 → pyplumio-0.5.45}/docs/source/callbacks.rst +6 -3
  6. {pyplumio-0.5.43 → pyplumio-0.5.45}/docs/source/frames.rst +1 -1
  7. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/_version.py +2 -2
  8. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/const.py +1 -3
  9. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/devices/__init__.py +16 -28
  10. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/devices/ecomax.py +112 -59
  11. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/devices/mixer.py +6 -6
  12. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/devices/thermostat.py +9 -6
  13. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/filters.py +44 -22
  14. pyplumio-0.5.45/pyplumio/helpers/async_cache.py +48 -0
  15. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/helpers/event_manager.py +20 -2
  16. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/helpers/timeout.py +6 -5
  17. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/parameters/__init__.py +9 -3
  18. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/protocol.py +15 -7
  19. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/structures/alerts.py +1 -1
  20. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/structures/boiler_power.py +1 -1
  21. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/structures/fan_power.py +1 -1
  22. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/structures/frame_versions.py +1 -1
  23. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/structures/fuel_consumption.py +1 -1
  24. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/structures/lambda_sensor.py +1 -1
  25. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/structures/mixer_sensors.py +1 -1
  26. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/structures/network_info.py +1 -1
  27. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/structures/output_flags.py +1 -1
  28. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/structures/outputs.py +1 -1
  29. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/structures/product_info.py +1 -1
  30. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/structures/regulator_data.py +1 -1
  31. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/structures/regulator_data_schema.py +1 -1
  32. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/structures/temperatures.py +1 -1
  33. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/structures/thermostat_parameters.py +5 -7
  34. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/structures/thermostat_sensors.py +1 -1
  35. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyproject.toml +2 -0
  36. {pyplumio-0.5.43 → pyplumio-0.5.45}/requirements_test.txt +2 -0
  37. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/conftest.py +8 -0
  38. pyplumio-0.5.45/tests/helpers/test_async_cache.py +49 -0
  39. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/helpers/test_event_manager.py +17 -2
  40. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/parameters/test_init.py +9 -2
  41. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/test_connection.py +5 -1
  42. {pyplumio-0.5.43/tests/helpers → pyplumio-0.5.45/tests}/test_data_types.py +1 -1
  43. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/test_devices.py +45 -17
  44. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/test_filters.py +63 -11
  45. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/test_protocol.py +9 -2
  46. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/testdata/responses/regulator_data_schema.json +44 -44
  47. {pyplumio-0.5.43 → pyplumio-0.5.45}/.gitattributes +0 -0
  48. {pyplumio-0.5.43 → pyplumio-0.5.45}/.github/CODE_OF_CONDUCT.md +0 -0
  49. {pyplumio-0.5.43 → pyplumio-0.5.45}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  50. {pyplumio-0.5.43 → pyplumio-0.5.45}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  51. {pyplumio-0.5.43 → pyplumio-0.5.45}/.github/dependabot.yml +0 -0
  52. {pyplumio-0.5.43 → pyplumio-0.5.45}/.github/workflows/ci.yml +0 -0
  53. {pyplumio-0.5.43 → pyplumio-0.5.45}/.github/workflows/codeql.yml +0 -0
  54. {pyplumio-0.5.43 → pyplumio-0.5.45}/.github/workflows/deploy.yml +0 -0
  55. {pyplumio-0.5.43 → pyplumio-0.5.45}/.github/workflows/documentation.yml +0 -0
  56. {pyplumio-0.5.43 → pyplumio-0.5.45}/.gitignore +0 -0
  57. {pyplumio-0.5.43 → pyplumio-0.5.45}/.pre-commit-config.yaml +0 -0
  58. {pyplumio-0.5.43 → pyplumio-0.5.45}/.qlty/qlty.toml +0 -0
  59. {pyplumio-0.5.43 → pyplumio-0.5.45}/.vscode/settings.json +0 -0
  60. {pyplumio-0.5.43 → pyplumio-0.5.45}/LICENSE +0 -0
  61. {pyplumio-0.5.43 → pyplumio-0.5.45}/MANIFEST.in +0 -0
  62. {pyplumio-0.5.43 → pyplumio-0.5.45}/PyPlumIO.egg-info/dependency_links.txt +0 -0
  63. {pyplumio-0.5.43 → pyplumio-0.5.45}/PyPlumIO.egg-info/top_level.txt +0 -0
  64. {pyplumio-0.5.43 → pyplumio-0.5.45}/README.md +0 -0
  65. {pyplumio-0.5.43 → pyplumio-0.5.45}/docs/Makefile +0 -0
  66. {pyplumio-0.5.43 → pyplumio-0.5.45}/docs/make.bat +0 -0
  67. {pyplumio-0.5.43 → pyplumio-0.5.45}/docs/source/conf.py +0 -0
  68. {pyplumio-0.5.43 → pyplumio-0.5.45}/docs/source/connecting.rst +0 -0
  69. {pyplumio-0.5.43 → pyplumio-0.5.45}/docs/source/index.rst +0 -0
  70. {pyplumio-0.5.43 → pyplumio-0.5.45}/docs/source/mixers_thermostats.rst +0 -0
  71. {pyplumio-0.5.43 → pyplumio-0.5.45}/docs/source/protocol.rst +0 -0
  72. {pyplumio-0.5.43 → pyplumio-0.5.45}/docs/source/reading.rst +0 -0
  73. {pyplumio-0.5.43 → pyplumio-0.5.45}/docs/source/schedules.rst +0 -0
  74. {pyplumio-0.5.43 → pyplumio-0.5.45}/docs/source/writing.rst +0 -0
  75. {pyplumio-0.5.43 → pyplumio-0.5.45}/images/ecomax.png +0 -0
  76. {pyplumio-0.5.43 → pyplumio-0.5.45}/images/rs485.png +0 -0
  77. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/__init__.py +0 -0
  78. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/__main__.py +0 -0
  79. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/connection.py +0 -0
  80. {pyplumio-0.5.43/pyplumio/helpers → pyplumio-0.5.45/pyplumio}/data_types.py +0 -0
  81. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/devices/ecoster.py +0 -0
  82. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/exceptions.py +0 -0
  83. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/frames/__init__.py +0 -0
  84. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/frames/messages.py +0 -0
  85. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/frames/requests.py +0 -0
  86. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/frames/responses.py +0 -0
  87. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/helpers/__init__.py +0 -0
  88. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/helpers/factory.py +0 -0
  89. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/helpers/schedule.py +0 -0
  90. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/helpers/task_manager.py +0 -0
  91. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/helpers/uid.py +0 -0
  92. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/parameters/ecomax.py +0 -0
  93. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/parameters/mixer.py +0 -0
  94. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/parameters/thermostat.py +0 -0
  95. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/py.typed +0 -0
  96. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/stream.py +0 -0
  97. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/structures/__init__.py +0 -0
  98. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/structures/boiler_load.py +0 -0
  99. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/structures/ecomax_parameters.py +0 -0
  100. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/structures/fuel_level.py +0 -0
  101. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/structures/mixer_parameters.py +0 -0
  102. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/structures/modules.py +0 -0
  103. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/structures/pending_alerts.py +0 -0
  104. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/structures/program_version.py +0 -0
  105. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/structures/schedules.py +0 -0
  106. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/structures/statuses.py +0 -0
  107. {pyplumio-0.5.43 → pyplumio-0.5.45}/pyplumio/utils.py +0 -0
  108. {pyplumio-0.5.43 → pyplumio-0.5.45}/requirements.txt +0 -0
  109. {pyplumio-0.5.43 → pyplumio-0.5.45}/requirements_docs.txt +0 -0
  110. {pyplumio-0.5.43 → pyplumio-0.5.45}/setup.cfg +0 -0
  111. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/__init__.py +0 -0
  112. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/frames/test_init.py +0 -0
  113. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/frames/test_messages.py +0 -0
  114. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/frames/test_requests.py +0 -0
  115. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/frames/test_responses.py +0 -0
  116. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/helpers/__init__.py +0 -0
  117. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/helpers/test_factory.py +0 -0
  118. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/helpers/test_schedule.py +0 -0
  119. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/helpers/test_task_manager.py +0 -0
  120. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/helpers/test_timeout.py +0 -0
  121. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/helpers/test_uid.py +0 -0
  122. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/parameters/__init__.py +0 -0
  123. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/parameters/test_ecomax.py +0 -0
  124. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/parameters/test_mixers.py +0 -0
  125. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/parameters/test_thermostats.py +0 -0
  126. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/ruff.toml +0 -0
  127. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/test_init.py +0 -0
  128. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/test_main.py +0 -0
  129. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/test_stream.py +0 -0
  130. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/test_utils.py +0 -0
  131. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/testdata/messages/regulator_data.json +0 -0
  132. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/testdata/messages/sensor_data.json +0 -0
  133. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/testdata/requests/alerts.json +0 -0
  134. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/testdata/requests/ecomax_control.json +0 -0
  135. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/testdata/requests/ecomax_parameters.json +0 -0
  136. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/testdata/requests/mixer_parameters.json +0 -0
  137. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/testdata/requests/set_ecomax_parameter.json +0 -0
  138. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/testdata/requests/set_mixer_parameter.json +0 -0
  139. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/testdata/requests/set_schedule.json +0 -0
  140. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/testdata/requests/set_thermostat_parameter.json +0 -0
  141. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/testdata/requests/thermostat_parameters.json +0 -0
  142. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/testdata/responses/alerts.json +0 -0
  143. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/testdata/responses/device_available.json +0 -0
  144. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/testdata/responses/ecomax_parameters.json +0 -0
  145. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/testdata/responses/mixer_parameters.json +0 -0
  146. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/testdata/responses/password.json +0 -0
  147. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/testdata/responses/program_version.json +0 -0
  148. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/testdata/responses/schedules.json +0 -0
  149. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/testdata/responses/thermostat_parameters.json +0 -0
  150. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/testdata/responses/uid.json +0 -0
  151. {pyplumio-0.5.43 → pyplumio-0.5.45}/tests/testdata/unknown/unknown_ecomax_parameter.json +0 -0
  152. {pyplumio-0.5.43 → pyplumio-0.5.45}/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.43
3
+ Version: 0.5.45
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,7 +29,9 @@ 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"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPlumIO
3
- Version: 0.5.43
3
+ Version: 0.5.45
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,7 +29,9 @@ 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"
@@ -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
@@ -99,6 +100,7 @@ tests/__init__.py
99
100
  tests/conftest.py
100
101
  tests/ruff.toml
101
102
  tests/test_connection.py
103
+ tests/test_data_types.py
102
104
  tests/test_devices.py
103
105
  tests/test_filters.py
104
106
  tests/test_init.py
@@ -111,7 +113,7 @@ tests/frames/test_messages.py
111
113
  tests/frames/test_requests.py
112
114
  tests/frames/test_responses.py
113
115
  tests/helpers/__init__.py
114
- tests/helpers/test_data_types.py
116
+ tests/helpers/test_async_cache.py
115
117
  tests/helpers/test_event_manager.py
116
118
  tests/helpers/test_factory.py
117
119
  tests/helpers/test_schedule.py
@@ -15,7 +15,9 @@ 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
@@ -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
 
@@ -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.45'
21
+ __version_tuple__ = version_tuple = (0, 5, 45)
@@ -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
 
@@ -126,11 +125,11 @@ class PhysicalDevice(Device, ABC):
126
125
  virtual devices associated with them via parent property.
127
126
  """
128
127
 
129
- __slots__ = ("address", "_network", "_setup_frames", "_frame_versions")
128
+ __slots__ = ("address", "_network", "_frame_versions")
130
129
 
131
130
  address: ClassVar[int]
131
+
132
132
  _network: NetworkInfo
133
- _setup_frames: tuple[DataFrameDescription, ...]
134
133
  _frame_versions: dict[int, int]
135
134
 
136
135
  def __init__(self, queue: asyncio.Queue[Frame], network: NetworkInfo) -> None:
@@ -139,20 +138,27 @@ class PhysicalDevice(Device, ABC):
139
138
  self._network = network
140
139
  self._frame_versions = {}
141
140
 
142
- @event_listener(ATTR_FRAME_VERSIONS, on_change)
141
+ @event_listener(filter=on_change)
143
142
  async def on_event_frame_versions(self, versions: dict[int, int]) -> None:
144
143
  """Check frame versions and update outdated frames."""
144
+ _LOGGER.info("Received frame version table")
145
145
  for frame_type, version in versions.items():
146
146
  if (
147
147
  is_known_frame_type(frame_type)
148
148
  and self.supports_frame_type(frame_type)
149
149
  and not self.has_frame_version(frame_type, version)
150
150
  ):
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)
151
+ await self._request_frame_version(frame_type, version)
154
152
  self._frame_versions[frame_type] = version
155
153
 
154
+ async def _request_frame_version(
155
+ self, frame_type: FrameType | int, version: int
156
+ ) -> None:
157
+ """Request frame version from the device."""
158
+ _LOGGER.info("Updating frame %s to version %i", repr(frame_type), version)
159
+ request = await Request.create(frame_type, recipient=self.address)
160
+ self.queue.put_nowait(request)
161
+
156
162
  def has_frame_version(self, frame_type: FrameType | int, version: int) -> bool:
157
163
  """Return True if frame data is up to date, False otherwise."""
158
164
  return (
@@ -171,25 +177,6 @@ class PhysicalDevice(Device, ABC):
171
177
  for name, value in frame.data.items():
172
178
  self.dispatch_nowait(name, value)
173
179
 
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
180
  async def request(
194
181
  self, name: str, frame_type: FrameType, retries: int = 3, timeout: float = 3.0
195
182
  ) -> Any:
@@ -197,6 +184,7 @@ class PhysicalDevice(Device, ABC):
197
184
 
198
185
  If value is not available before timeout, retry request.
199
186
  """
187
+ _LOGGER.info("Requesting '%s' with %s", name, repr(frame_type))
200
188
  request = await Request.create(frame_type, recipient=self.address)
201
189
  while retries > 0:
202
190
  try:
@@ -3,15 +3,15 @@
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
15
  STATE_OFF,
16
16
  STATE_ON,
17
17
  DeviceState,
@@ -22,6 +22,7 @@ from pyplumio.const import (
22
22
  from pyplumio.devices import PhysicalDevice
23
23
  from pyplumio.devices.mixer import Mixer
24
24
  from pyplumio.devices.thermostat import Thermostat
25
+ from pyplumio.exceptions import RequestError
25
26
  from pyplumio.filters import on_change
26
27
  from pyplumio.frames import DataFrameDescription, Frame, Request
27
28
  from pyplumio.helpers.event_manager import event_listener
@@ -40,14 +41,12 @@ from pyplumio.structures.ecomax_parameters import (
40
41
  ATTR_ECOMAX_CONTROL,
41
42
  ATTR_ECOMAX_PARAMETERS,
42
43
  )
43
- from pyplumio.structures.fuel_consumption import ATTR_FUEL_CONSUMPTION
44
44
  from pyplumio.structures.mixer_parameters import ATTR_MIXER_PARAMETERS
45
45
  from pyplumio.structures.mixer_sensors import ATTR_MIXER_SENSORS
46
46
  from pyplumio.structures.network_info import ATTR_NETWORK, NetworkInfo
47
47
  from pyplumio.structures.product_info import ATTR_PRODUCT, ProductInfo
48
48
  from pyplumio.structures.regulator_data_schema import ATTR_REGDATA_SCHEMA
49
49
  from pyplumio.structures.schedules import (
50
- ATTR_SCHEDULE_PARAMETERS,
51
50
  ATTR_SCHEDULES,
52
51
  SCHEDULE_PARAMETERS,
53
52
  SCHEDULES,
@@ -55,20 +54,53 @@ from pyplumio.structures.schedules import (
55
54
  ScheduleSwitch,
56
55
  ScheduleSwitchDescription,
57
56
  )
58
- from pyplumio.structures.thermostat_parameters import (
59
- ATTR_THERMOSTAT_PARAMETERS,
60
- ATTR_THERMOSTAT_PROFILE,
61
- )
57
+ from pyplumio.structures.thermostat_parameters import ATTR_THERMOSTAT_PARAMETERS
62
58
  from pyplumio.structures.thermostat_sensors import ATTR_THERMOSTAT_SENSORS
63
59
 
60
+ _LOGGER = logging.getLogger(__name__)
61
+
62
+
64
63
  ATTR_MIXERS: Final = "mixers"
65
64
  ATTR_THERMOSTATS: Final = "thermostats"
66
65
  ATTR_FUEL_BURNED: Final = "fuel_burned"
67
66
 
68
- NANOSECONDS_IN_SECOND: Final = 1_000_000_000
69
- MAX_TIME_SINCE_LAST_FUEL_UPDATE_NS: Final = 300 * NANOSECONDS_IN_SECOND
67
+ MAX_TIME_SINCE_LAST_FUEL_UPDATE: Final = 5 * 60
68
+
69
+
70
+ class FuelMeter:
71
+ """Represents a fuel meter.
72
+
73
+ Calculates the fuel burned based on the time
74
+ elapsed since the last sensor message, which contains fuel
75
+ consumption data. If the elapsed time is within the acceptable
76
+ range, it returns the fuel burned data. Otherwise, it logs a
77
+ warning and returns None.
78
+ """
79
+
80
+ __slots__ = ("_last_update_time",)
81
+
82
+ _last_update_time: float
83
+
84
+ def __init__(self) -> None:
85
+ """Initialize a new fuel meter."""
86
+ self._last_update_time = time.monotonic()
70
87
 
71
- SETUP_FRAME_TYPES: tuple[DataFrameDescription, ...] = (
88
+ def calculate(self, fuel_consumption: float) -> float | None:
89
+ """Calculate the amount of burned fuel since last update."""
90
+ current_time = time.monotonic()
91
+ time_since_update = current_time - self._last_update_time
92
+ self._last_update_time = current_time
93
+ if time_since_update < MAX_TIME_SINCE_LAST_FUEL_UPDATE:
94
+ return fuel_consumption * (time_since_update / 3600)
95
+
96
+ _LOGGER.warning(
97
+ "Skipping outdated fuel consumption data (was %f seconds old)",
98
+ time_since_update,
99
+ )
100
+ return None
101
+
102
+
103
+ REQUIRED: tuple[DataFrameDescription, ...] = (
72
104
  DataFrameDescription(
73
105
  frame_type=FrameType.REQUEST_UID,
74
106
  provides=ATTR_PRODUCT,
@@ -103,28 +135,22 @@ SETUP_FRAME_TYPES: tuple[DataFrameDescription, ...] = (
103
135
  ),
104
136
  )
105
137
 
106
- _LOGGER = logging.getLogger(__name__)
138
+ REQUIRED_TYPES = [description.frame_type for description in REQUIRED]
107
139
 
108
140
 
109
141
  class EcoMAX(PhysicalDevice):
110
142
  """Represents an ecoMAX controller."""
111
143
 
112
- __slots__ = ("_fuel_burned_time_ns",)
144
+ __slots__ = ("_fuel_meter",)
113
145
 
114
- address = DeviceType.ECOMAX
115
- _setup_frames = SETUP_FRAME_TYPES
146
+ _fuel_meter: FuelMeter
116
147
 
117
- _fuel_burned_time_ns: int
148
+ address = DeviceType.ECOMAX
118
149
 
119
150
  def __init__(self, queue: asyncio.Queue[Frame], network: NetworkInfo) -> None:
120
151
  """Initialize a new ecoMAX controller."""
121
152
  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()
153
+ self._fuel_meter = FuelMeter()
128
154
 
129
155
  def handle_frame(self, frame: Frame) -> None:
130
156
  """Handle frame received from the ecoMAX device."""
@@ -162,6 +188,22 @@ class EcoMAX(PhysicalDevice):
162
188
 
163
189
  return self.dispatch_nowait(ATTR_THERMOSTATS, thermostats)
164
190
 
191
+ async def _request_frame_version(
192
+ self, frame_type: FrameType | int, version: int
193
+ ) -> None:
194
+ """Request frame version from the device."""
195
+ request_frame = False
196
+ if frame_type not in REQUIRED_TYPES:
197
+ # Frame not in required, so we'll request it right away.
198
+ request_frame = True
199
+
200
+ if frame_type in self._frame_versions:
201
+ # Frame in required and has been requested once by setup.
202
+ request_frame = True
203
+
204
+ if request_frame:
205
+ await super()._request_frame_version(frame_type, version)
206
+
165
207
  async def _set_ecomax_state(self, state: State) -> bool:
166
208
  """Try to set the ecoMAX control state."""
167
209
  try:
@@ -196,11 +238,34 @@ class EcoMAX(PhysicalDevice):
196
238
  await asyncio.gather(*(device.shutdown() for device in devices))
197
239
  await super().shutdown()
198
240
 
199
- @event_listener(ATTR_ECOMAX_PARAMETERS)
241
+ @event_listener
242
+ async def on_event_setup(self, setup: bool) -> None:
243
+ """Request frames required to set up an ecoMAX entry."""
244
+ _LOGGER.info("Setting up device entry")
245
+ await self.wait_for(ATTR_SENSORS)
246
+ results = await asyncio.gather(
247
+ *(
248
+ self.request(description.provides, description.frame_type)
249
+ for description in REQUIRED
250
+ ),
251
+ return_exceptions=True,
252
+ )
253
+
254
+ errors = [
255
+ result.frame_type for result in results if isinstance(result, RequestError)
256
+ ]
257
+
258
+ if errors:
259
+ self.dispatch_nowait(ATTR_FRAME_ERRORS, errors)
260
+
261
+ _LOGGER.info("Device entry setup done")
262
+
263
+ @event_listener
200
264
  async def on_event_ecomax_parameters(
201
- self, parameters: Sequence[tuple[int, ParameterValues]]
265
+ self, parameters: list[tuple[int, ParameterValues]]
202
266
  ) -> bool:
203
267
  """Update ecoMAX parameters and dispatch the events."""
268
+ _LOGGER.info("Received device parameters")
204
269
  product_info: ProductInfo = await self.get(ATTR_PRODUCT)
205
270
 
206
271
  def _ecomax_parameter_events() -> Generator[Coroutine, Any, None]:
@@ -236,37 +301,20 @@ class EcoMAX(PhysicalDevice):
236
301
  await asyncio.gather(*_ecomax_parameter_events())
237
302
  return True
238
303
 
239
- @event_listener(ATTR_FUEL_CONSUMPTION)
304
+ @event_listener
240
305
  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
- )
306
+ """Update the amount of burned fuel."""
307
+ fuel_burned = self._fuel_meter.calculate(fuel_consumption)
308
+ if fuel_burned is not None:
309
+ self.dispatch_nowait(ATTR_FUEL_BURNED, fuel_burned)
263
310
 
264
- @event_listener(ATTR_MIXER_PARAMETERS)
311
+ @event_listener
265
312
  async def on_event_mixer_parameters(
266
313
  self,
267
- parameters: dict[int, Sequence[tuple[int, ParameterValues]]] | None,
314
+ parameters: dict[int, list[tuple[int, ParameterValues]]] | None,
268
315
  ) -> bool:
269
316
  """Handle mixer parameters and dispatch the events."""
317
+ _LOGGER.info("Received mixer parameters")
270
318
  if parameters:
271
319
  await asyncio.gather(
272
320
  *(
@@ -278,11 +326,12 @@ class EcoMAX(PhysicalDevice):
278
326
 
279
327
  return False
280
328
 
281
- @event_listener(ATTR_MIXER_SENSORS)
329
+ @event_listener
282
330
  async def on_event_mixer_sensors(
283
331
  self, sensors: dict[int, dict[str, Any]] | None
284
332
  ) -> bool:
285
333
  """Update mixer sensors and dispatch the events."""
334
+ _LOGGER.info("Received mixer sensors")
286
335
  if sensors:
287
336
  await asyncio.gather(
288
337
  *(
@@ -294,9 +343,9 @@ class EcoMAX(PhysicalDevice):
294
343
 
295
344
  return False
296
345
 
297
- @event_listener(ATTR_SCHEDULE_PARAMETERS)
346
+ @event_listener
298
347
  async def on_event_schedule_parameters(
299
- self, parameters: Sequence[tuple[int, ParameterValues]]
348
+ self, parameters: list[tuple[int, ParameterValues]]
300
349
  ) -> bool:
301
350
  """Update schedule parameters and dispatch the events."""
302
351
 
@@ -319,20 +368,22 @@ class EcoMAX(PhysicalDevice):
319
368
  await asyncio.gather(*_schedule_parameter_events())
320
369
  return True
321
370
 
322
- @event_listener(ATTR_SENSORS)
371
+ @event_listener
323
372
  async def on_event_sensors(self, sensors: dict[str, Any]) -> bool:
324
373
  """Update ecoMAX sensors and dispatch the events."""
374
+ _LOGGER.info("Received device sensors")
325
375
  await asyncio.gather(
326
376
  *(self.dispatch(name, value) for name, value in sensors.items())
327
377
  )
328
378
  return True
329
379
 
330
- @event_listener(ATTR_THERMOSTAT_PARAMETERS)
380
+ @event_listener
331
381
  async def on_event_thermostat_parameters(
332
382
  self,
333
- parameters: dict[int, Sequence[tuple[int, ParameterValues]]] | None,
383
+ parameters: dict[int, list[tuple[int, ParameterValues]]] | None,
334
384
  ) -> bool:
335
385
  """Handle thermostat parameters and dispatch the events."""
386
+ _LOGGER.info("Received thermostat parameters")
336
387
  if parameters:
337
388
  await asyncio.gather(
338
389
  *(
@@ -346,7 +397,7 @@ class EcoMAX(PhysicalDevice):
346
397
 
347
398
  return False
348
399
 
349
- @event_listener(ATTR_THERMOSTAT_PROFILE)
400
+ @event_listener
350
401
  async def on_event_thermostat_profile(
351
402
  self, values: ParameterValues | None
352
403
  ) -> EcomaxNumber | None:
@@ -358,11 +409,12 @@ class EcoMAX(PhysicalDevice):
358
409
 
359
410
  return None
360
411
 
361
- @event_listener(ATTR_THERMOSTAT_SENSORS)
412
+ @event_listener
362
413
  async def on_event_thermostat_sensors(
363
414
  self, sensors: dict[int, dict[str, Any]] | None
364
415
  ) -> bool:
365
416
  """Update thermostat sensors and dispatch the events."""
417
+ _LOGGER.info("Received thermostat sensors")
366
418
  if sensors:
367
419
  await asyncio.gather(
368
420
  *(
@@ -377,11 +429,12 @@ class EcoMAX(PhysicalDevice):
377
429
 
378
430
  return False
379
431
 
380
- @event_listener(ATTR_SCHEDULES)
432
+ @event_listener
381
433
  async def on_event_schedules(
382
434
  self, schedules: list[tuple[int, list[list[bool]]]]
383
435
  ) -> dict[str, Schedule]:
384
436
  """Update schedules."""
437
+ _LOGGER.info("Received device schedules")
385
438
  return {
386
439
  SCHEDULES[index]: Schedule(
387
440
  name=SCHEDULES[index],
@@ -397,7 +450,7 @@ class EcoMAX(PhysicalDevice):
397
450
  for index, schedule in schedules
398
451
  }
399
452
 
400
- @event_listener(ATTR_STATE, on_change)
453
+ @event_listener(filter=on_change)
401
454
  async def on_event_state(self, state: DeviceState) -> None:
402
455
  """Update the ecoMAX control parameter."""
403
456
  await self.dispatch(
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
- from collections.abc import Coroutine, Generator, Sequence
6
+ from collections.abc import Coroutine, Generator
7
7
  import logging
8
8
  from typing import Any
9
9
 
@@ -16,8 +16,6 @@ from pyplumio.parameters.mixer import (
16
16
  MixerSwitchDescription,
17
17
  get_mixer_parameter_types,
18
18
  )
19
- from pyplumio.structures.mixer_parameters import ATTR_MIXER_PARAMETERS
20
- from pyplumio.structures.mixer_sensors import ATTR_MIXER_SENSORS
21
19
  from pyplumio.structures.product_info import ATTR_PRODUCT, ProductInfo
22
20
 
23
21
  _LOGGER = logging.getLogger(__name__)
@@ -28,19 +26,21 @@ class Mixer(VirtualDevice):
28
26
 
29
27
  __slots__ = ()
30
28
 
31
- @event_listener(ATTR_MIXER_SENSORS)
29
+ @event_listener
32
30
  async def on_event_mixer_sensors(self, sensors: dict[str, Any]) -> bool:
33
31
  """Update mixer sensors and dispatch the events."""
32
+ _LOGGER.info("Received mixer %i sensors", self.index)
34
33
  await asyncio.gather(
35
34
  *(self.dispatch(name, value) for name, value in sensors.items())
36
35
  )
37
36
  return True
38
37
 
39
- @event_listener(ATTR_MIXER_PARAMETERS)
38
+ @event_listener
40
39
  async def on_event_mixer_parameters(
41
- self, parameters: Sequence[tuple[int, ParameterValues]]
40
+ self, parameters: list[tuple[int, ParameterValues]]
42
41
  ) -> bool:
43
42
  """Update mixer parameters and dispatch the events."""
43
+ _LOGGER.info("Received mixer %i parameters", self.index)
44
44
  product_info: ProductInfo = await self.parent.get(ATTR_PRODUCT)
45
45
 
46
46
  def _mixer_parameter_events() -> Generator[Coroutine, Any, None]:
@@ -3,7 +3,8 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
- from collections.abc import Coroutine, Generator, Sequence
6
+ from collections.abc import Coroutine, Generator
7
+ import logging
7
8
  from typing import Any
8
9
 
9
10
  from pyplumio.devices import VirtualDevice
@@ -15,8 +16,8 @@ from pyplumio.parameters.thermostat import (
15
16
  ThermostatSwitchDescription,
16
17
  get_thermostat_parameter_types,
17
18
  )
18
- from pyplumio.structures.thermostat_parameters import ATTR_THERMOSTAT_PARAMETERS
19
- from pyplumio.structures.thermostat_sensors import ATTR_THERMOSTAT_SENSORS
19
+
20
+ _LOGGER = logging.getLogger()
20
21
 
21
22
 
22
23
  class Thermostat(VirtualDevice):
@@ -24,19 +25,21 @@ class Thermostat(VirtualDevice):
24
25
 
25
26
  __slots__ = ()
26
27
 
27
- @event_listener(ATTR_THERMOSTAT_SENSORS)
28
+ @event_listener
28
29
  async def on_event_thermostat_sensors(self, sensors: dict[str, Any]) -> bool:
29
30
  """Update thermostat sensors and dispatch the events."""
31
+ _LOGGER.info("Received thermostat %i sensors", self.index)
30
32
  await asyncio.gather(
31
33
  *(self.dispatch(name, value) for name, value in sensors.items())
32
34
  )
33
35
  return True
34
36
 
35
- @event_listener(ATTR_THERMOSTAT_PARAMETERS)
37
+ @event_listener
36
38
  async def on_event_thermostat_parameters(
37
- self, parameters: Sequence[tuple[int, ParameterValues]]
39
+ self, parameters: list[tuple[int, ParameterValues]]
38
40
  ) -> bool:
39
41
  """Update thermostat parameters and dispatch the events."""
42
+ _LOGGER.info("Received thermostat %i parameters", self.index)
40
43
 
41
44
  def _thermostat_parameter_events() -> Generator[Coroutine, Any, None]:
42
45
  """Get dispatch calls for thermostat parameter events."""