PyPlumIO 0.5.48__tar.gz → 0.5.50__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 (164) hide show
  1. {pyplumio-0.5.48 → pyplumio-0.5.50}/PKG-INFO +1 -1
  2. {pyplumio-0.5.48 → pyplumio-0.5.50}/PyPlumIO.egg-info/PKG-INFO +1 -1
  3. {pyplumio-0.5.48 → pyplumio-0.5.50}/PyPlumIO.egg-info/SOURCES.txt +2 -0
  4. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/_version.py +2 -2
  5. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/const.py +2 -0
  6. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/devices/__init__.py +10 -5
  7. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/devices/ecomax.py +9 -9
  8. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/devices/mixer.py +2 -2
  9. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/devices/thermostat.py +2 -2
  10. pyplumio-0.5.50/pyplumio/parameters/custom/ecomax_860d3_hb.py +80 -0
  11. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/protocol.py +10 -13
  12. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/conftest.py +42 -7
  13. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/devices/test_ecomax.py +42 -26
  14. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/devices/test_init.py +20 -5
  15. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/helpers/test_schedule.py +2 -3
  16. pyplumio-0.5.50/tests/parameters/custom/test_ecomax_860d3_hb.py +87 -0
  17. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/parameters/test_init.py +5 -7
  18. pyplumio-0.5.50/tests/testdata/parameters/ecomax_860d3_hb.json +20 -0
  19. pyplumio-0.5.48/pyplumio/parameters/custom/ecomax_860d3_hb.py +0 -38
  20. {pyplumio-0.5.48 → pyplumio-0.5.50}/.gitattributes +0 -0
  21. {pyplumio-0.5.48 → pyplumio-0.5.50}/.github/CODE_OF_CONDUCT.md +0 -0
  22. {pyplumio-0.5.48 → pyplumio-0.5.50}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  23. {pyplumio-0.5.48 → pyplumio-0.5.50}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  24. {pyplumio-0.5.48 → pyplumio-0.5.50}/.github/dependabot.yml +0 -0
  25. {pyplumio-0.5.48 → pyplumio-0.5.50}/.github/workflows/ci.yml +0 -0
  26. {pyplumio-0.5.48 → pyplumio-0.5.50}/.github/workflows/codeql.yml +0 -0
  27. {pyplumio-0.5.48 → pyplumio-0.5.50}/.github/workflows/deploy.yml +0 -0
  28. {pyplumio-0.5.48 → pyplumio-0.5.50}/.github/workflows/documentation.yml +0 -0
  29. {pyplumio-0.5.48 → pyplumio-0.5.50}/.gitignore +0 -0
  30. {pyplumio-0.5.48 → pyplumio-0.5.50}/.pre-commit-config.yaml +0 -0
  31. {pyplumio-0.5.48 → pyplumio-0.5.50}/.qlty/qlty.toml +0 -0
  32. {pyplumio-0.5.48 → pyplumio-0.5.50}/.vscode/settings.json +0 -0
  33. {pyplumio-0.5.48 → pyplumio-0.5.50}/LICENSE +0 -0
  34. {pyplumio-0.5.48 → pyplumio-0.5.50}/MANIFEST.in +0 -0
  35. {pyplumio-0.5.48 → pyplumio-0.5.50}/PyPlumIO.egg-info/dependency_links.txt +0 -0
  36. {pyplumio-0.5.48 → pyplumio-0.5.50}/PyPlumIO.egg-info/requires.txt +0 -0
  37. {pyplumio-0.5.48 → pyplumio-0.5.50}/PyPlumIO.egg-info/top_level.txt +0 -0
  38. {pyplumio-0.5.48 → pyplumio-0.5.50}/README.md +0 -0
  39. {pyplumio-0.5.48 → pyplumio-0.5.50}/docs/Makefile +0 -0
  40. {pyplumio-0.5.48 → pyplumio-0.5.50}/docs/make.bat +0 -0
  41. {pyplumio-0.5.48 → pyplumio-0.5.50}/docs/source/callbacks.rst +0 -0
  42. {pyplumio-0.5.48 → pyplumio-0.5.50}/docs/source/conf.py +0 -0
  43. {pyplumio-0.5.48 → pyplumio-0.5.50}/docs/source/connecting.rst +0 -0
  44. {pyplumio-0.5.48 → pyplumio-0.5.50}/docs/source/frames.rst +0 -0
  45. {pyplumio-0.5.48 → pyplumio-0.5.50}/docs/source/index.rst +0 -0
  46. {pyplumio-0.5.48 → pyplumio-0.5.50}/docs/source/mixers_thermostats.rst +0 -0
  47. {pyplumio-0.5.48 → pyplumio-0.5.50}/docs/source/protocol.rst +0 -0
  48. {pyplumio-0.5.48 → pyplumio-0.5.50}/docs/source/reading.rst +0 -0
  49. {pyplumio-0.5.48 → pyplumio-0.5.50}/docs/source/schedules.rst +0 -0
  50. {pyplumio-0.5.48 → pyplumio-0.5.50}/docs/source/writing.rst +0 -0
  51. {pyplumio-0.5.48 → pyplumio-0.5.50}/images/ecomax.png +0 -0
  52. {pyplumio-0.5.48 → pyplumio-0.5.50}/images/rs485.png +0 -0
  53. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/__init__.py +0 -0
  54. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/__main__.py +0 -0
  55. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/connection.py +0 -0
  56. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/data_types.py +0 -0
  57. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/devices/ecoster.py +0 -0
  58. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/exceptions.py +0 -0
  59. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/filters.py +0 -0
  60. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/frames/__init__.py +0 -0
  61. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/frames/messages.py +0 -0
  62. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/frames/requests.py +0 -0
  63. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/frames/responses.py +0 -0
  64. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/helpers/__init__.py +0 -0
  65. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/helpers/async_cache.py +0 -0
  66. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/helpers/event_manager.py +0 -0
  67. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/helpers/factory.py +0 -0
  68. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/helpers/schedule.py +0 -0
  69. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/helpers/task_manager.py +0 -0
  70. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/helpers/timeout.py +0 -0
  71. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/helpers/uid.py +0 -0
  72. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/parameters/__init__.py +0 -0
  73. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/parameters/custom/__init__.py +0 -0
  74. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/parameters/ecomax.py +0 -0
  75. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/parameters/mixer.py +0 -0
  76. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/parameters/thermostat.py +0 -0
  77. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/py.typed +0 -0
  78. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/stream.py +0 -0
  79. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/structures/__init__.py +0 -0
  80. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/structures/alerts.py +0 -0
  81. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/structures/boiler_load.py +0 -0
  82. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/structures/boiler_power.py +0 -0
  83. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/structures/ecomax_parameters.py +0 -0
  84. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/structures/fan_power.py +0 -0
  85. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/structures/frame_versions.py +0 -0
  86. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/structures/fuel_consumption.py +0 -0
  87. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/structures/fuel_level.py +0 -0
  88. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/structures/lambda_sensor.py +0 -0
  89. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/structures/mixer_parameters.py +0 -0
  90. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/structures/mixer_sensors.py +0 -0
  91. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/structures/modules.py +0 -0
  92. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/structures/network_info.py +0 -0
  93. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/structures/output_flags.py +0 -0
  94. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/structures/outputs.py +0 -0
  95. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/structures/pending_alerts.py +0 -0
  96. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/structures/product_info.py +0 -0
  97. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/structures/program_version.py +0 -0
  98. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/structures/regulator_data.py +0 -0
  99. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/structures/regulator_data_schema.py +0 -0
  100. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/structures/schedules.py +0 -0
  101. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/structures/statuses.py +0 -0
  102. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/structures/temperatures.py +0 -0
  103. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/structures/thermostat_parameters.py +0 -0
  104. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/structures/thermostat_sensors.py +0 -0
  105. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyplumio/utils.py +0 -0
  106. {pyplumio-0.5.48 → pyplumio-0.5.50}/pyproject.toml +0 -0
  107. {pyplumio-0.5.48 → pyplumio-0.5.50}/requirements.txt +0 -0
  108. {pyplumio-0.5.48 → pyplumio-0.5.50}/requirements_docs.txt +0 -0
  109. {pyplumio-0.5.48 → pyplumio-0.5.50}/requirements_test.txt +0 -0
  110. {pyplumio-0.5.48 → pyplumio-0.5.50}/setup.cfg +0 -0
  111. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/__init__.py +0 -0
  112. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/devices/__init__.py +0 -0
  113. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/devices/test_ecoster.py +0 -0
  114. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/devices/test_mixer.py +0 -0
  115. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/devices/test_thermostat.py +0 -0
  116. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/frames/test_init.py +0 -0
  117. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/frames/test_messages.py +0 -0
  118. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/frames/test_requests.py +0 -0
  119. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/frames/test_responses.py +0 -0
  120. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/helpers/__init__.py +0 -0
  121. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/helpers/test_async_cache.py +0 -0
  122. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/helpers/test_event_manager.py +0 -0
  123. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/helpers/test_factory.py +0 -0
  124. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/helpers/test_task_manager.py +0 -0
  125. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/helpers/test_timeout.py +0 -0
  126. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/helpers/test_uid.py +0 -0
  127. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/parameters/__init__.py +0 -0
  128. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/parameters/custom/__init__.py +0 -0
  129. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/parameters/custom/test_init.py +0 -0
  130. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/parameters/test_ecomax.py +0 -0
  131. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/parameters/test_mixers.py +0 -0
  132. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/parameters/test_thermostats.py +0 -0
  133. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/ruff.toml +0 -0
  134. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/test_connection.py +0 -0
  135. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/test_data_types.py +0 -0
  136. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/test_filters.py +0 -0
  137. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/test_init.py +0 -0
  138. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/test_main.py +0 -0
  139. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/test_protocol.py +0 -0
  140. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/test_stream.py +0 -0
  141. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/test_utils.py +0 -0
  142. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/testdata/messages/regulator_data.json +0 -0
  143. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/testdata/messages/sensor_data.json +0 -0
  144. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/testdata/requests/alerts.json +0 -0
  145. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/testdata/requests/ecomax_control.json +0 -0
  146. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/testdata/requests/ecomax_parameters.json +0 -0
  147. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/testdata/requests/mixer_parameters.json +0 -0
  148. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/testdata/requests/set_ecomax_parameter.json +0 -0
  149. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/testdata/requests/set_mixer_parameter.json +0 -0
  150. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/testdata/requests/set_schedule.json +0 -0
  151. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/testdata/requests/set_thermostat_parameter.json +0 -0
  152. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/testdata/requests/thermostat_parameters.json +0 -0
  153. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/testdata/responses/alerts.json +0 -0
  154. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/testdata/responses/device_available.json +0 -0
  155. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/testdata/responses/ecomax_parameters.json +0 -0
  156. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/testdata/responses/mixer_parameters.json +0 -0
  157. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/testdata/responses/password.json +0 -0
  158. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/testdata/responses/program_version.json +0 -0
  159. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/testdata/responses/regulator_data_schema.json +0 -0
  160. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/testdata/responses/schedules.json +0 -0
  161. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/testdata/responses/thermostat_parameters.json +0 -0
  162. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/testdata/responses/uid.json +0 -0
  163. {pyplumio-0.5.48 → pyplumio-0.5.50}/tests/testdata/unknown/unknown_ecomax_parameter.json +0 -0
  164. {pyplumio-0.5.48 → pyplumio-0.5.50}/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.48
3
+ Version: 0.5.50
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPlumIO
3
- Version: 0.5.48
3
+ Version: 0.5.50
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
@@ -133,9 +133,11 @@ tests/parameters/test_init.py
133
133
  tests/parameters/test_mixers.py
134
134
  tests/parameters/test_thermostats.py
135
135
  tests/parameters/custom/__init__.py
136
+ tests/parameters/custom/test_ecomax_860d3_hb.py
136
137
  tests/parameters/custom/test_init.py
137
138
  tests/testdata/messages/regulator_data.json
138
139
  tests/testdata/messages/sensor_data.json
140
+ tests/testdata/parameters/ecomax_860d3_hb.json
139
141
  tests/testdata/requests/alerts.json
140
142
  tests/testdata/requests/ecomax_control.json
141
143
  tests/testdata/requests/ecomax_parameters.json
@@ -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.48'
21
- __version_tuple__ = version_tuple = (0, 5, 48)
20
+ __version__ = version = '0.5.50'
21
+ __version_tuple__ = version_tuple = (0, 5, 50)
@@ -184,6 +184,7 @@ class FrameType(IntEnum):
184
184
  REQUEST_SET_MIXER_PARAMETER = 52
185
185
  REQUEST_SCHEDULES = 54
186
186
  REQUEST_SET_SCHEDULE = 55
187
+ REQUEST_ECOMAX_PARAMETER_CHANGES = 56
187
188
  REQUEST_UID = 57
188
189
  REQUEST_PASSWORD = 58
189
190
  REQUEST_ECOMAX_CONTROL = 59
@@ -203,6 +204,7 @@ class FrameType(IntEnum):
203
204
  RESPONSE_ECOMAX_CONTROL = 187
204
205
  RESPONSE_ALERTS = 189
205
206
  RESPONSE_PROGRAM_VERSION = 192
207
+ RESPONSE_ECOMAX_PARAMETER_CHANGES = 212
206
208
  RESPONSE_REGULATOR_DATA_SCHEMA = 213
207
209
  RESPONSE_THERMOSTAT_PARAMETERS = 220
208
210
  RESPONSE_SET_THERMOSTAT_PARAMETER = 221
@@ -72,7 +72,7 @@ class Device(ABC, EventManager):
72
72
  :param name: Name of the parameter
73
73
  :type name: str
74
74
  :param value: New value for the parameter
75
- :type value: int | float | bool | Literal["off", "on"]
75
+ :type value: int | float | bool | Literal["on", "off"]
76
76
  :param retries: Try setting parameter for this amount of
77
77
  times, defaults to 5
78
78
  :type retries: int, optional
@@ -104,7 +104,7 @@ class Device(ABC, EventManager):
104
104
  :param name: Name of the parameter
105
105
  :type name: str
106
106
  :param value: New value for the parameter
107
- :type value: int | float | bool | Literal["off", "on"]
107
+ :type value: int | float | bool | Literal["on", "off"]
108
108
  :param retries: Try setting parameter for this amount of
109
109
  times, defaults to 5
110
110
  :type retries: int, optional
@@ -145,21 +145,26 @@ class PhysicalDevice(Device, ABC):
145
145
  @event_listener(filter=on_change)
146
146
  async def on_event_frame_versions(self, versions: dict[int, int]) -> None:
147
147
  """Check frame versions and update outdated frames."""
148
- _LOGGER.info("Received frame version table")
148
+ _LOGGER.debug("Received frame version table")
149
149
  for frame_type, version in versions.items():
150
150
  if (
151
151
  is_known_frame_type(frame_type)
152
152
  and self.supports_frame_type(frame_type)
153
153
  and not self.has_frame_version(frame_type, version)
154
154
  ):
155
- await self._request_frame_version(frame_type, version)
155
+ request_frame_type = (
156
+ FrameType.REQUEST_ECOMAX_PARAMETERS
157
+ if frame_type == FrameType.REQUEST_ECOMAX_PARAMETER_CHANGES
158
+ else frame_type
159
+ )
160
+ await self._request_frame_version(request_frame_type, version)
156
161
  self._frame_versions[frame_type] = version
157
162
 
158
163
  async def _request_frame_version(
159
164
  self, frame_type: FrameType | int, version: int
160
165
  ) -> None:
161
166
  """Request frame version from the device."""
162
- _LOGGER.info("Updating frame %s to version %i", repr(frame_type), version)
167
+ _LOGGER.debug("Updating frame %s to version %i", repr(frame_type), version)
163
168
  request = await Request.create(frame_type, recipient=self.address)
164
169
  self.queue.put_nowait(request)
165
170
 
@@ -234,7 +234,7 @@ class EcoMAX(PhysicalDevice):
234
234
  @event_listener
235
235
  async def on_event_setup(self, setup: bool) -> None:
236
236
  """Request frames required to set up an ecoMAX entry."""
237
- _LOGGER.info("Setting up device entry")
237
+ _LOGGER.debug("Setting up device entry")
238
238
  await self.wait_for(ATTR_SENSORS)
239
239
  results = await asyncio.gather(
240
240
  *(
@@ -251,14 +251,14 @@ class EcoMAX(PhysicalDevice):
251
251
  if errors:
252
252
  self.dispatch_nowait(ATTR_FRAME_ERRORS, errors)
253
253
 
254
- _LOGGER.info("Device entry setup done")
254
+ _LOGGER.debug("Device entry setup done")
255
255
 
256
256
  @event_listener
257
257
  async def on_event_ecomax_parameters(
258
258
  self, parameters: list[tuple[int, ParameterValues]]
259
259
  ) -> bool:
260
260
  """Update ecoMAX parameters and dispatch the events."""
261
- _LOGGER.info("Received device parameters")
261
+ _LOGGER.debug("Received device parameters")
262
262
  product_info: ProductInfo = await self.get(ATTR_PRODUCT)
263
263
  parameter_types = await get_ecomax_parameter_types(product_info)
264
264
 
@@ -307,7 +307,7 @@ class EcoMAX(PhysicalDevice):
307
307
  parameters: dict[int, list[tuple[int, ParameterValues]]] | None,
308
308
  ) -> bool:
309
309
  """Handle mixer parameters and dispatch the events."""
310
- _LOGGER.info("Received mixer parameters")
310
+ _LOGGER.debug("Received mixer parameters")
311
311
  if parameters:
312
312
  await asyncio.gather(
313
313
  *(
@@ -324,7 +324,7 @@ class EcoMAX(PhysicalDevice):
324
324
  self, sensors: dict[int, dict[str, Any]] | None
325
325
  ) -> bool:
326
326
  """Update mixer sensors and dispatch the events."""
327
- _LOGGER.info("Received mixer sensors")
327
+ _LOGGER.debug("Received mixer sensors")
328
328
  if sensors:
329
329
  await asyncio.gather(
330
330
  *(
@@ -364,7 +364,7 @@ class EcoMAX(PhysicalDevice):
364
364
  @event_listener
365
365
  async def on_event_sensors(self, sensors: dict[str, Any]) -> bool:
366
366
  """Update ecoMAX sensors and dispatch the events."""
367
- _LOGGER.info("Received device sensors")
367
+ _LOGGER.debug("Received device sensors")
368
368
  await asyncio.gather(
369
369
  *(self.dispatch(name, value) for name, value in sensors.items())
370
370
  )
@@ -376,7 +376,7 @@ class EcoMAX(PhysicalDevice):
376
376
  parameters: dict[int, list[tuple[int, ParameterValues]]] | None,
377
377
  ) -> bool:
378
378
  """Handle thermostat parameters and dispatch the events."""
379
- _LOGGER.info("Received thermostat parameters")
379
+ _LOGGER.debug("Received thermostat parameters")
380
380
  if parameters:
381
381
  await asyncio.gather(
382
382
  *(
@@ -407,7 +407,7 @@ class EcoMAX(PhysicalDevice):
407
407
  self, sensors: dict[int, dict[str, Any]] | None
408
408
  ) -> bool:
409
409
  """Update thermostat sensors and dispatch the events."""
410
- _LOGGER.info("Received thermostat sensors")
410
+ _LOGGER.debug("Received thermostat sensors")
411
411
  if sensors:
412
412
  await asyncio.gather(
413
413
  *(
@@ -427,7 +427,7 @@ class EcoMAX(PhysicalDevice):
427
427
  self, schedules: list[tuple[int, list[list[bool]]]]
428
428
  ) -> dict[str, Schedule]:
429
429
  """Update schedules."""
430
- _LOGGER.info("Received device schedules")
430
+ _LOGGER.debug("Received device schedules")
431
431
  return {
432
432
  SCHEDULES[index]: Schedule(
433
433
  name=SCHEDULES[index],
@@ -29,7 +29,7 @@ class Mixer(VirtualDevice):
29
29
  @event_listener
30
30
  async def on_event_mixer_sensors(self, sensors: dict[str, Any]) -> bool:
31
31
  """Update mixer sensors and dispatch the events."""
32
- _LOGGER.info("Received mixer %i sensors", self.index)
32
+ _LOGGER.debug("Received mixer %i sensors", self.index)
33
33
  await asyncio.gather(
34
34
  *(self.dispatch(name, value) for name, value in sensors.items())
35
35
  )
@@ -40,7 +40,7 @@ class Mixer(VirtualDevice):
40
40
  self, parameters: list[tuple[int, ParameterValues]]
41
41
  ) -> bool:
42
42
  """Update mixer parameters and dispatch the events."""
43
- _LOGGER.info("Received mixer %i parameters", self.index)
43
+ _LOGGER.debug("Received mixer %i parameters", self.index)
44
44
  product_info: ProductInfo = await self.parent.get(ATTR_PRODUCT)
45
45
  parameter_types = get_mixer_parameter_types(product_info)
46
46
 
@@ -28,7 +28,7 @@ class Thermostat(VirtualDevice):
28
28
  @event_listener
29
29
  async def on_event_thermostat_sensors(self, sensors: dict[str, Any]) -> bool:
30
30
  """Update thermostat sensors and dispatch the events."""
31
- _LOGGER.info("Received thermostat %i sensors", self.index)
31
+ _LOGGER.debug("Received thermostat %i sensors", self.index)
32
32
  await asyncio.gather(
33
33
  *(self.dispatch(name, value) for name, value in sensors.items())
34
34
  )
@@ -39,7 +39,7 @@ class Thermostat(VirtualDevice):
39
39
  self, parameters: list[tuple[int, ParameterValues]]
40
40
  ) -> bool:
41
41
  """Update thermostat parameters and dispatch the events."""
42
- _LOGGER.info("Received thermostat %i parameters", self.index)
42
+ _LOGGER.debug("Received thermostat %i parameters", self.index)
43
43
  parameter_types = get_thermostat_parameter_types()
44
44
 
45
45
  def _thermostat_parameter_events() -> Generator[Coroutine]:
@@ -0,0 +1,80 @@
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, EcomaxSwitchDescription
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
+ # Summer mode
17
+ CustomParameter(
18
+ original="summer_mode_disable_temp",
19
+ replacement=EcomaxNumberDescription(name="__unknown_parameter_1"),
20
+ ),
21
+ CustomParameter(
22
+ original="water_heater_target_temp",
23
+ replacement=EcomaxNumberDescription(name="summer_mode"),
24
+ ),
25
+ CustomParameter(
26
+ original="min_water_heater_target_temp",
27
+ replacement=EcomaxNumberDescription(
28
+ name="summer_mode_enable_temp",
29
+ unit_of_measurement=UnitOfMeasurement.CELSIUS,
30
+ ),
31
+ ),
32
+ CustomParameter(
33
+ original="max_water_heater_target_temp",
34
+ replacement=EcomaxNumberDescription(
35
+ name="summer_mode_disable_temp",
36
+ unit_of_measurement=UnitOfMeasurement.CELSIUS,
37
+ ),
38
+ ),
39
+ # Water heater
40
+ CustomParameter(
41
+ original="disable_pump_on_thermostat",
42
+ replacement=EcomaxNumberDescription(
43
+ name="water_heater_target_temp",
44
+ unit_of_measurement=UnitOfMeasurement.CELSIUS,
45
+ ),
46
+ ),
47
+ CustomParameter(
48
+ original="boiler_alert_temp",
49
+ replacement=EcomaxNumberDescription(
50
+ name="min_water_heater_target_temp",
51
+ unit_of_measurement=UnitOfMeasurement.CELSIUS,
52
+ ),
53
+ ),
54
+ CustomParameter(
55
+ original="max_feeder_temp",
56
+ replacement=EcomaxNumberDescription(
57
+ name="max_water_heater_target_temp",
58
+ unit_of_measurement=UnitOfMeasurement.CELSIUS,
59
+ ),
60
+ ),
61
+ CustomParameter(
62
+ original="water_heater_work_mode",
63
+ replacement=EcomaxNumberDescription(name="water_heater_feeding_extension"),
64
+ ),
65
+ CustomParameter(
66
+ original="external_boiler_temp",
67
+ replacement=EcomaxNumberDescription(name="water_heater_work_mode"),
68
+ ),
69
+ CustomParameter(
70
+ original="alert_notify",
71
+ replacement=EcomaxNumberDescription(
72
+ name="water_heater_hysteresis",
73
+ unit_of_measurement=UnitOfMeasurement.CELSIUS,
74
+ ),
75
+ ),
76
+ CustomParameter(
77
+ original="pump_hysteresis",
78
+ replacement=EcomaxSwitchDescription(name="water_heater_disinfection"),
79
+ ),
80
+ )
@@ -230,20 +230,17 @@ class AsyncProtocol(Protocol, EventManager[PhysicalDevice]):
230
230
  @acache
231
231
  async def get_device_entry(self, device_type: DeviceType) -> PhysicalDevice:
232
232
  """Return the device entry."""
233
-
234
- @acache
235
- async def _setup_device_entry(device_type: DeviceType) -> PhysicalDevice:
236
- """Set up the device entry."""
237
- device = await PhysicalDevice.create(
238
- device_type, queue=self._queues.write, network=self._network
239
- )
240
- device.dispatch_nowait(ATTR_CONNECTED, True)
241
- device.dispatch_nowait(ATTR_SETUP, True)
242
- self.dispatch_nowait(device_type.name.lower(), device)
243
- return device
244
-
245
233
  async with self._entry_lock:
246
- return await _setup_device_entry(device_type)
234
+ name = device_type.name.lower()
235
+ if name not in self.data:
236
+ device = await PhysicalDevice.create(
237
+ device_type, queue=self._queues.write, network=self._network
238
+ )
239
+ device.dispatch_nowait(ATTR_CONNECTED, True)
240
+ device.dispatch_nowait(ATTR_SETUP, True)
241
+ await self.dispatch(device_type.name.lower(), device)
242
+
243
+ return self.data[name]
247
244
 
248
245
 
249
246
  __all__ = ["Protocol", "DummyProtocol", "AsyncProtocol"]
@@ -8,6 +8,7 @@ import functools
8
8
  import importlib
9
9
  import inspect
10
10
  import json
11
+ from math import isclose
11
12
  import os
12
13
  import pathlib
13
14
  from typing import Any, Final, TypeVar
@@ -16,8 +17,9 @@ from unittest.mock import patch
16
17
  from freezegun import freeze_time
17
18
  import pytest
18
19
 
19
- from pyplumio.const import ProductType
20
+ from pyplumio.const import ProductType, State
20
21
  from pyplumio.devices.ecomax import EcoMAX
22
+ from pyplumio.parameters import NumericType
21
23
  from pyplumio.structures.network_info import NetworkInfo
22
24
  from pyplumio.structures.product_info import ATTR_PRODUCT, ProductInfo
23
25
 
@@ -34,7 +36,7 @@ def _create_class_instance(module_name: str, class_name: str, **kwargs):
34
36
  return getattr(importlib.import_module(module_name), class_name)(**kwargs)
35
37
 
36
38
 
37
- def try_int(key: Any) -> Any:
39
+ def _try_int(key: Any) -> Any:
38
40
  """Try to convert key to integer or return key unchanged on error."""
39
41
  try:
40
42
  return int(key)
@@ -55,7 +57,7 @@ def _decode_hinted_objects(d: Any) -> Any:
55
57
  if "__tuple__" in d:
56
58
  return tuple(d["items"])
57
59
 
58
- return {try_int(k): v for k, v in d.items()}
60
+ return {_try_int(k): v for k, v in d.items()}
59
61
 
60
62
 
61
63
  def load_json_test_data(path: str) -> Any:
@@ -83,9 +85,16 @@ def _bypass_pytest_argument_inspection(
83
85
  return wrapper
84
86
 
85
87
 
86
- def json_test_data(json_path: str, selector: str | None = None, dataset: int = 0):
88
+ def json_test_data(
89
+ json_path: str,
90
+ selector: str | None = None,
91
+ dataset: int = 0,
92
+ name: str | None = None,
93
+ ):
87
94
  """Pytest decorator to inject JSON test data as a test argument."""
88
- name = json_path.split("/")[-1].split("\\")[-1].rsplit(".", 1)[0]
95
+ if not name:
96
+ name = json_path.split("/")[-1].split("\\")[-1].rsplit(".", 1)[0]
97
+
89
98
  if selector:
90
99
  name += f"_{selector}"
91
100
 
@@ -104,10 +113,17 @@ def json_test_data(json_path: str, selector: str | None = None, dataset: int = 0
104
113
 
105
114
 
106
115
  def class_from_json(
107
- cls: type, json_path: str, /, dataset: int = 0, arguments: Sequence | None = None
116
+ cls: type,
117
+ json_path: str,
118
+ /,
119
+ dataset: int = 0,
120
+ arguments: Sequence | None = None,
121
+ name: str | None = None,
108
122
  ):
109
123
  """Pytest decorator to inject JSON test data as a test argument."""
110
- name = json_path.split("/")[-1].split("\\")[-1].rsplit(".", 1)[0]
124
+ if not name:
125
+ name = json_path.split("/")[-1].split("\\")[-1].rsplit(".", 1)[0]
126
+
111
127
  init_args = arguments if arguments else ()
112
128
 
113
129
  def decorator(test_func):
@@ -124,6 +140,16 @@ def class_from_json(
124
140
  return decorator
125
141
 
126
142
 
143
+ def equal_parameter_value(
144
+ a: NumericType | State | None, b: NumericType | State | None
145
+ ) -> bool:
146
+ """Compare the parameter values."""
147
+ if isinstance(a, float) and isinstance(b, float):
148
+ return isclose(a, b, rel_tol=DEFAULT_TOLERANCE)
149
+ else:
150
+ return True if a == b else False
151
+
152
+
127
153
  @pytest.fixture(autouse=True)
128
154
  def skip_asyncio_sleep():
129
155
  """Skip an asyncio sleep calls."""
@@ -151,3 +177,12 @@ def fixture_frozen_time():
151
177
  """Get frozen time."""
152
178
  with freeze_time("2012-12-12 12:00:00") as frozen_time:
153
179
  yield frozen_time
180
+
181
+
182
+ __all__ = [
183
+ "class_from_json",
184
+ "equal_parameter_value",
185
+ "json_test_data",
186
+ "load_json_parameters",
187
+ "load_json_test_data",
188
+ ]
@@ -2,8 +2,7 @@
2
2
 
3
3
  from datetime import timedelta
4
4
  import logging
5
- from math import isclose
6
- from typing import Any, Literal, cast
5
+ from typing import Any, cast
7
6
  from unittest.mock import AsyncMock, Mock, patch
8
7
 
9
8
  import pytest
@@ -26,6 +25,7 @@ from pyplumio.const import (
26
25
  DeviceState,
27
26
  DeviceType,
28
27
  FrameType,
28
+ State,
29
29
  UnitOfMeasurement,
30
30
  )
31
31
  from pyplumio.devices.ecomax import (
@@ -43,6 +43,7 @@ from pyplumio.frames.messages import SensorDataMessage
43
43
  from pyplumio.frames.requests import (
44
44
  AlertsRequest,
45
45
  EcomaxControlRequest,
46
+ EcomaxParametersRequest,
46
47
  SetEcomaxParameterRequest,
47
48
  SetScheduleRequest,
48
49
  SetThermostatParameterRequest,
@@ -81,7 +82,12 @@ from pyplumio.structures.thermostat_sensors import (
81
82
  ATTR_THERMOSTATS_AVAILABLE,
82
83
  ATTR_THERMOSTATS_CONNECTED,
83
84
  )
84
- from tests.conftest import DEFAULT_TOLERANCE, UNDEFINED, class_from_json, json_test_data
85
+ from tests.conftest import (
86
+ UNDEFINED,
87
+ class_from_json,
88
+ equal_parameter_value,
89
+ json_test_data,
90
+ )
85
91
 
86
92
 
87
93
  @patch("asyncio.Queue.put_nowait")
@@ -99,10 +105,24 @@ async def test_ecomax_handle_frame(
99
105
  mock_handle_frame.assert_called_once_with(request)
100
106
 
101
107
 
108
+ @pytest.mark.parametrize(
109
+ ("frame_type", "frame_request"),
110
+ [
111
+ (FrameType.REQUEST_ALERTS, AlertsRequest(recipient=DeviceType.ECOMAX)),
112
+ (
113
+ FrameType.REQUEST_ECOMAX_PARAMETER_CHANGES,
114
+ EcomaxParametersRequest(recipient=DeviceType.ECOMAX),
115
+ ),
116
+ ],
117
+ )
102
118
  @patch("asyncio.Queue.put_nowait")
103
119
  @class_from_json(SensorDataMessage, "messages/sensor_data.json", arguments=("message",))
104
120
  async def test_frame_versions_tracker(
105
- mock_put_nowait, ecomax: EcoMAX, sensor_data: SensorDataMessage
121
+ mock_put_nowait,
122
+ ecomax: EcoMAX,
123
+ sensor_data: SensorDataMessage,
124
+ frame_type: FrameType,
125
+ frame_request: Request,
106
126
  ) -> None:
107
127
  """Test frame version tracker."""
108
128
 
@@ -111,7 +131,7 @@ async def test_frame_versions_tracker(
111
131
  return {ATTR_SENSORS: {ATTR_FRAME_VERSIONS: {frame: version}}}
112
132
 
113
133
  # Test with frame type that are handled during setup.
114
- sensor_data.data = _frame_version_data(FrameType.REQUEST_ALERTS, 1)
134
+ sensor_data.data = _frame_version_data(frame_type, 1)
115
135
  ecomax.data.clear()
116
136
  ecomax.handle_frame(sensor_data)
117
137
  await ecomax.wait_until_done()
@@ -119,18 +139,16 @@ async def test_frame_versions_tracker(
119
139
 
120
140
  # Test with same frame type after setup done.
121
141
  mock_put_nowait.reset_mock()
122
- sensor_data.data = _frame_version_data(FrameType.REQUEST_ALERTS, 2)
142
+ sensor_data.data = _frame_version_data(frame_type, 2)
123
143
  ecomax.data.clear()
124
144
  ecomax.data[ATTR_SETUP] = True
125
145
  ecomax.handle_frame(sensor_data)
126
146
  await ecomax.wait_until_done()
127
- mock_put_nowait.assert_called_once_with(AlertsRequest(recipient=DeviceType.ECOMAX))
147
+ mock_put_nowait.assert_called_once_with(frame_request)
128
148
 
129
149
 
130
150
  @pytest.mark.parametrize("state", [STATE_ON, STATE_OFF])
131
- async def test_ecomax_control(
132
- state: Literal["on", "off"], ecomax: EcoMAX, caplog
133
- ) -> None:
151
+ async def test_ecomax_control(state: State, ecomax: EcoMAX, caplog) -> None:
134
152
  """Test ecoMAX control."""
135
153
  coro = getattr(ecomax, f"turn_{state}")
136
154
  await coro()
@@ -143,9 +161,7 @@ async def test_ecomax_control(
143
161
 
144
162
  @pytest.mark.parametrize("state", [STATE_ON, STATE_OFF])
145
163
  @patch("pyplumio.devices.ecomax.EcoMAX.create_task")
146
- def test_ecomax_control_nowait(
147
- mock_create_task, ecomax: EcoMAX, state: Literal["on", "off"]
148
- ) -> None:
164
+ def test_ecomax_control_nowait(mock_create_task, ecomax: EcoMAX, state: State) -> None:
149
165
  """Test ecoMAX control without waiting for result."""
150
166
  func = getattr(ecomax, f"turn_{state}_nowait")
151
167
  with patch(
@@ -237,9 +253,9 @@ async def test_ecomax_parameters_event_listener(
237
253
  # Test parameter with the step (heating_heat_curve)
238
254
  fuel_calorific_value = ecomax.get_nowait("fuel_calorific_value")
239
255
  assert isinstance(fuel_calorific_value, EcomaxNumber)
240
- assert isclose(fuel_calorific_value.value, 4.6, rel_tol=DEFAULT_TOLERANCE)
241
- assert isclose(fuel_calorific_value.min_value, 0.1, rel_tol=DEFAULT_TOLERANCE)
242
- assert isclose(fuel_calorific_value.max_value, 25.0, rel_tol=DEFAULT_TOLERANCE)
256
+ assert equal_parameter_value(fuel_calorific_value.value, 4.6)
257
+ assert equal_parameter_value(fuel_calorific_value.min_value, 0.1)
258
+ assert equal_parameter_value(fuel_calorific_value.max_value, 25.0)
243
259
 
244
260
  # Test setting parameter with the step.
245
261
  await fuel_calorific_value.set(2.5)
@@ -250,9 +266,9 @@ async def test_ecomax_parameters_event_listener(
250
266
  # Test parameter with the offset (heating_heat_curve_shift)
251
267
  heating_heat_curve_shift = ecomax.get_nowait("heating_curve_shift")
252
268
  assert isinstance(heating_heat_curve_shift, EcomaxNumber)
253
- assert isclose(heating_heat_curve_shift.value, 0.0, rel_tol=DEFAULT_TOLERANCE)
254
- assert isclose(heating_heat_curve_shift.min_value, -20.0, rel_tol=DEFAULT_TOLERANCE)
255
- assert isclose(heating_heat_curve_shift.max_value, 20.0, rel_tol=DEFAULT_TOLERANCE)
269
+ assert equal_parameter_value(heating_heat_curve_shift.value, 0.0)
270
+ assert equal_parameter_value(heating_heat_curve_shift.min_value, -20.0)
271
+ assert equal_parameter_value(heating_heat_curve_shift.max_value, 20.0)
256
272
  assert heating_heat_curve_shift.unit_of_measurement == UnitOfMeasurement.CELSIUS
257
273
 
258
274
  # Test setting the parameter with the offset.
@@ -302,7 +318,7 @@ async def test_fuel_consumption_event_listener(
302
318
  ecomax.handle_frame(Response(data={ATTR_FUEL_CONSUMPTION: 3.6}))
303
319
  await ecomax.wait_until_done()
304
320
  fuel_burned = cast(float, ecomax.get_nowait(ATTR_FUEL_BURNED))
305
- assert isclose(fuel_burned, 0.01, rel_tol=DEFAULT_TOLERANCE)
321
+ assert equal_parameter_value(fuel_burned, 0.01)
306
322
 
307
323
  # Test with outdated data.
308
324
  message = "Skipping outdated fuel consumption"
@@ -310,7 +326,7 @@ async def test_fuel_consumption_event_listener(
310
326
  ecomax.handle_frame(Response(data={ATTR_FUEL_CONSUMPTION: 1}))
311
327
  await ecomax.wait_until_done()
312
328
  fuel_burned = cast(float, ecomax.get_nowait(ATTR_FUEL_BURNED))
313
- assert isclose(fuel_burned, 0.01, rel_tol=DEFAULT_TOLERANCE)
329
+ assert equal_parameter_value(fuel_burned, 0.01)
314
330
  assert message in caplog.text
315
331
  caplog.clear()
316
332
 
@@ -319,7 +335,7 @@ async def test_fuel_consumption_event_listener(
319
335
  await ecomax.wait_until_done()
320
336
  assert message not in caplog.text
321
337
  fuel_burned = cast(float, ecomax.get_nowait(ATTR_FUEL_BURNED))
322
- assert isclose(fuel_burned, 0.02, rel_tol=DEFAULT_TOLERANCE)
338
+ assert equal_parameter_value(fuel_burned, 0.02)
323
339
 
324
340
 
325
341
  @class_from_json(
@@ -376,7 +392,7 @@ async def test_ecomax_sensors_event_listener(
376
392
  ecomax.handle_frame(sensor_data)
377
393
  await ecomax.wait_until_done()
378
394
  heating_target = cast(float, ecomax.get_nowait("heating_target"))
379
- assert isclose(heating_target, 41.0, rel_tol=DEFAULT_TOLERANCE)
395
+ assert equal_parameter_value(heating_target, 41.0)
380
396
 
381
397
  ecomax_control = ecomax.get_nowait(ATTR_ECOMAX_CONTROL)
382
398
  assert isinstance(ecomax_control, EcomaxSwitch)
@@ -447,9 +463,9 @@ async def test_thermostat_profile_event_listener(
447
463
  await ecomax.wait_until_done()
448
464
  thermostat_profile = ecomax.get_nowait(ATTR_THERMOSTAT_PROFILE)
449
465
  assert isinstance(thermostat_profile, EcomaxNumber)
450
- assert isclose(thermostat_profile.value, 0.0, rel_tol=DEFAULT_TOLERANCE)
451
- assert isclose(thermostat_profile.min_value, 0.0, rel_tol=DEFAULT_TOLERANCE)
452
- assert isclose(thermostat_profile.max_value, 5.0, rel_tol=DEFAULT_TOLERANCE)
466
+ assert equal_parameter_value(thermostat_profile.value, 0.0)
467
+ assert equal_parameter_value(thermostat_profile.min_value, 0.0)
468
+ assert equal_parameter_value(thermostat_profile.max_value, 5.0)
453
469
  thermostat_profile_request = await thermostat_profile.create_request()
454
470
  assert isinstance(thermostat_profile_request, SetThermostatParameterRequest)
455
471
  assert thermostat_profile_request.data == {
@@ -102,19 +102,34 @@ def fixture_physical_device() -> PhysicalDevice:
102
102
  class TestPhysicalDevice:
103
103
  """Contains tests for PhysicalDevice class."""
104
104
 
105
+ @pytest.mark.parametrize(
106
+ ("frame_type", "requested_frame_type"),
107
+ [
108
+ (FrameType.REQUEST_ALERTS, FrameType.REQUEST_ALERTS),
109
+ (
110
+ FrameType.REQUEST_ECOMAX_PARAMETER_CHANGES,
111
+ FrameType.REQUEST_ECOMAX_PARAMETERS,
112
+ ),
113
+ ],
114
+ )
105
115
  @patch("pyplumio.frames.Request.create", autospec=True)
106
116
  @patch("asyncio.Queue.put_nowait")
107
117
  async def test_frame_versions_event_listener(
108
- self, mock_put_nowait, mock_request_create, physical_device: PhysicalDevice
118
+ self,
119
+ mock_put_nowait,
120
+ mock_request_create,
121
+ physical_device: PhysicalDevice,
122
+ frame_type: FrameType,
123
+ requested_frame_type: FrameType,
109
124
  ) -> None:
110
125
  """Test event listener for frame versions."""
111
- assert physical_device.has_frame_version(FrameType.REQUEST_ALERTS, 1) is False
112
- await physical_device.on_event_frame_versions({FrameType.REQUEST_ALERTS: 1})
126
+ assert physical_device.has_frame_version(frame_type, 1) is False
127
+ await physical_device.on_event_frame_versions({frame_type: 1})
113
128
  mock_request_create.assert_awaited_once_with(
114
- FrameType.REQUEST_ALERTS, recipient=DummyPhysicalDevice.address
129
+ requested_frame_type, recipient=DummyPhysicalDevice.address
115
130
  )
116
131
  mock_put_nowait.assert_called_once_with(mock_request_create.return_value)
117
- assert physical_device.has_frame_version(FrameType.REQUEST_ALERTS, 1) is True
132
+ assert physical_device.has_frame_version(frame_type, 1) is True
118
133
 
119
134
  def test_frame_versions_event_listener_decorator(self) -> None:
120
135
  """Test decorator for the frame version event listener."""