PyPlumIO 0.5.40__tar.gz → 0.5.41__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 (142) hide show
  1. {pyplumio-0.5.40 → pyplumio-0.5.41}/PKG-INFO +1 -1
  2. {pyplumio-0.5.40 → pyplumio-0.5.41}/PyPlumIO.egg-info/PKG-INFO +1 -1
  3. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/_version.py +2 -2
  4. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/helpers/parameter.py +57 -29
  5. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/ecomax_parameters.py +26 -31
  6. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/mixer_parameters.py +23 -28
  7. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/thermostat_parameters.py +24 -30
  8. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/utils.py +11 -0
  9. {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/test_devices.py +25 -13
  10. {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/test_utils.py +12 -0
  11. {pyplumio-0.5.40 → pyplumio-0.5.41}/.gitattributes +0 -0
  12. {pyplumio-0.5.40 → pyplumio-0.5.41}/.github/CODE_OF_CONDUCT.md +0 -0
  13. {pyplumio-0.5.40 → pyplumio-0.5.41}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  14. {pyplumio-0.5.40 → pyplumio-0.5.41}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  15. {pyplumio-0.5.40 → pyplumio-0.5.41}/.github/dependabot.yml +0 -0
  16. {pyplumio-0.5.40 → pyplumio-0.5.41}/.github/workflows/ci.yml +0 -0
  17. {pyplumio-0.5.40 → pyplumio-0.5.41}/.github/workflows/codeql-analysis.yml +0 -0
  18. {pyplumio-0.5.40 → pyplumio-0.5.41}/.github/workflows/deploy.yml +0 -0
  19. {pyplumio-0.5.40 → pyplumio-0.5.41}/.github/workflows/documentation.yml +0 -0
  20. {pyplumio-0.5.40 → pyplumio-0.5.41}/.gitignore +0 -0
  21. {pyplumio-0.5.40 → pyplumio-0.5.41}/.pre-commit-config.yaml +0 -0
  22. {pyplumio-0.5.40 → pyplumio-0.5.41}/.vscode/settings.json +0 -0
  23. {pyplumio-0.5.40 → pyplumio-0.5.41}/LICENSE +0 -0
  24. {pyplumio-0.5.40 → pyplumio-0.5.41}/MANIFEST.in +0 -0
  25. {pyplumio-0.5.40 → pyplumio-0.5.41}/PyPlumIO.egg-info/SOURCES.txt +0 -0
  26. {pyplumio-0.5.40 → pyplumio-0.5.41}/PyPlumIO.egg-info/dependency_links.txt +0 -0
  27. {pyplumio-0.5.40 → pyplumio-0.5.41}/PyPlumIO.egg-info/requires.txt +0 -0
  28. {pyplumio-0.5.40 → pyplumio-0.5.41}/PyPlumIO.egg-info/top_level.txt +0 -0
  29. {pyplumio-0.5.40 → pyplumio-0.5.41}/README.md +0 -0
  30. {pyplumio-0.5.40 → pyplumio-0.5.41}/docs/Makefile +0 -0
  31. {pyplumio-0.5.40 → pyplumio-0.5.41}/docs/make.bat +0 -0
  32. {pyplumio-0.5.40 → pyplumio-0.5.41}/docs/source/callbacks.rst +0 -0
  33. {pyplumio-0.5.40 → pyplumio-0.5.41}/docs/source/conf.py +0 -0
  34. {pyplumio-0.5.40 → pyplumio-0.5.41}/docs/source/connecting.rst +0 -0
  35. {pyplumio-0.5.40 → pyplumio-0.5.41}/docs/source/frames.rst +0 -0
  36. {pyplumio-0.5.40 → pyplumio-0.5.41}/docs/source/index.rst +0 -0
  37. {pyplumio-0.5.40 → pyplumio-0.5.41}/docs/source/mixers_thermostats.rst +0 -0
  38. {pyplumio-0.5.40 → pyplumio-0.5.41}/docs/source/protocol.rst +0 -0
  39. {pyplumio-0.5.40 → pyplumio-0.5.41}/docs/source/reading.rst +0 -0
  40. {pyplumio-0.5.40 → pyplumio-0.5.41}/docs/source/schedules.rst +0 -0
  41. {pyplumio-0.5.40 → pyplumio-0.5.41}/docs/source/writing.rst +0 -0
  42. {pyplumio-0.5.40 → pyplumio-0.5.41}/images/ecomax.png +0 -0
  43. {pyplumio-0.5.40 → pyplumio-0.5.41}/images/rs485.png +0 -0
  44. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/__init__.py +0 -0
  45. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/__main__.py +0 -0
  46. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/connection.py +0 -0
  47. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/const.py +0 -0
  48. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/devices/__init__.py +0 -0
  49. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/devices/ecomax.py +0 -0
  50. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/devices/ecoster.py +0 -0
  51. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/devices/mixer.py +0 -0
  52. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/devices/thermostat.py +0 -0
  53. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/exceptions.py +0 -0
  54. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/filters.py +0 -0
  55. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/frames/__init__.py +0 -0
  56. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/frames/messages.py +0 -0
  57. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/frames/requests.py +0 -0
  58. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/frames/responses.py +0 -0
  59. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/helpers/__init__.py +0 -0
  60. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/helpers/data_types.py +0 -0
  61. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/helpers/event_manager.py +0 -0
  62. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/helpers/factory.py +0 -0
  63. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/helpers/schedule.py +0 -0
  64. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/helpers/task_manager.py +0 -0
  65. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/helpers/timeout.py +0 -0
  66. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/helpers/uid.py +0 -0
  67. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/protocol.py +0 -0
  68. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/py.typed +0 -0
  69. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/stream.py +0 -0
  70. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/__init__.py +0 -0
  71. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/alerts.py +0 -0
  72. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/boiler_load.py +0 -0
  73. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/boiler_power.py +0 -0
  74. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/fan_power.py +0 -0
  75. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/frame_versions.py +0 -0
  76. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/fuel_consumption.py +0 -0
  77. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/fuel_level.py +0 -0
  78. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/lambda_sensor.py +0 -0
  79. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/mixer_sensors.py +0 -0
  80. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/modules.py +0 -0
  81. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/network_info.py +0 -0
  82. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/output_flags.py +0 -0
  83. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/outputs.py +0 -0
  84. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/pending_alerts.py +0 -0
  85. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/product_info.py +0 -0
  86. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/program_version.py +0 -0
  87. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/regulator_data.py +0 -0
  88. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/regulator_data_schema.py +0 -0
  89. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/schedules.py +0 -0
  90. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/statuses.py +0 -0
  91. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/temperatures.py +0 -0
  92. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/thermostat_sensors.py +0 -0
  93. {pyplumio-0.5.40 → pyplumio-0.5.41}/pyproject.toml +0 -0
  94. {pyplumio-0.5.40 → pyplumio-0.5.41}/requirements.txt +0 -0
  95. {pyplumio-0.5.40 → pyplumio-0.5.41}/requirements_docs.txt +0 -0
  96. {pyplumio-0.5.40 → pyplumio-0.5.41}/requirements_test.txt +0 -0
  97. {pyplumio-0.5.40 → pyplumio-0.5.41}/setup.cfg +0 -0
  98. {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/__init__.py +0 -0
  99. {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/conftest.py +0 -0
  100. {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/frames/test_init.py +0 -0
  101. {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/frames/test_messages.py +0 -0
  102. {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/frames/test_requests.py +0 -0
  103. {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/frames/test_responses.py +0 -0
  104. {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/helpers/__init__.py +0 -0
  105. {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/helpers/test_data_types.py +0 -0
  106. {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/helpers/test_event_manager.py +0 -0
  107. {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/helpers/test_factory.py +0 -0
  108. {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/helpers/test_parameter.py +0 -0
  109. {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/helpers/test_schedule.py +0 -0
  110. {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/helpers/test_task_manager.py +0 -0
  111. {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/helpers/test_timeout.py +0 -0
  112. {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/helpers/test_uid.py +0 -0
  113. {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/ruff.toml +0 -0
  114. {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/test_connection.py +0 -0
  115. {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/test_filters.py +0 -0
  116. {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/test_init.py +0 -0
  117. {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/test_main.py +0 -0
  118. {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/test_protocol.py +0 -0
  119. {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/test_stream.py +0 -0
  120. {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/testdata/messages/regulator_data.json +0 -0
  121. {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/testdata/messages/sensor_data.json +0 -0
  122. {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/testdata/requests/alerts.json +0 -0
  123. {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/testdata/requests/ecomax_control.json +0 -0
  124. {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/testdata/requests/ecomax_parameters.json +0 -0
  125. {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/testdata/requests/mixer_parameters.json +0 -0
  126. {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/testdata/requests/set_ecomax_parameter.json +0 -0
  127. {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/testdata/requests/set_mixer_parameter.json +0 -0
  128. {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/testdata/requests/set_schedule.json +0 -0
  129. {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/testdata/requests/set_thermostat_parameter.json +0 -0
  130. {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/testdata/requests/thermostat_parameters.json +0 -0
  131. {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/testdata/responses/alerts.json +0 -0
  132. {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/testdata/responses/device_available.json +0 -0
  133. {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/testdata/responses/ecomax_parameters.json +0 -0
  134. {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/testdata/responses/mixer_parameters.json +0 -0
  135. {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/testdata/responses/password.json +0 -0
  136. {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/testdata/responses/program_version.json +0 -0
  137. {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/testdata/responses/regulator_data_schema.json +0 -0
  138. {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/testdata/responses/schedules.json +0 -0
  139. {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/testdata/responses/thermostat_parameters.json +0 -0
  140. {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/testdata/responses/uid.json +0 -0
  141. {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/testdata/unknown/unknown_ecomax_parameter.json +0 -0
  142. {pyplumio-0.5.40 → pyplumio-0.5.41}/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.40
3
+ Version: 0.5.41
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.40
3
+ Version: 0.5.41
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
@@ -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.40'
21
- __version_tuple__ = version_tuple = (0, 5, 40)
20
+ __version__ = version = '0.5.41'
21
+ __version_tuple__ = version_tuple = (0, 5, 41)
@@ -47,17 +47,6 @@ def is_valid_parameter(data: bytearray) -> bool:
47
47
  return any(x for x in data if x != BYTE_UNDEFINED)
48
48
 
49
49
 
50
- def parameter_value_to_int(value: NumericType | State | bool) -> int:
51
- """Convert a parameter value to an integer.
52
-
53
- If the value is STATE_OFF or STATE_ON, it returns 0 or 1 respectively.
54
- """
55
- if value in get_args(State):
56
- return 1 if value == STATE_ON else 0
57
-
58
- return int(value)
59
-
60
-
61
50
  @dataclass
62
51
  class ParameterValues:
63
52
  """Represents a parameter values."""
@@ -125,7 +114,7 @@ class Parameter(ABC):
125
114
 
126
115
  if isinstance(other, (int, float, bool)) or other in get_args(State):
127
116
  handler = getattr(self.values.value, method_to_call)
128
- return handler(parameter_value_to_int(other))
117
+ return handler(self._pack_value(other))
129
118
  else:
130
119
  return NotImplemented
131
120
 
@@ -180,25 +169,16 @@ class Parameter(ABC):
180
169
  )
181
170
  return type(self)(self.device, self.description, values)
182
171
 
183
- def validate(self, value: NumericType | State | bool) -> int:
184
- """Validate a parameter value."""
185
- int_value = parameter_value_to_int(value)
186
- if int_value < self.values.min_value or int_value > self.values.max_value:
187
- raise ValueError(
188
- f"Invalid value: {value}. Must be between "
189
- f"{self.min_value} and {self.max_value}."
190
- )
191
-
192
- return int_value
193
-
194
172
  async def set(self, value: Any, retries: int = 5, timeout: float = 5.0) -> bool:
195
173
  """Set a parameter value."""
196
- return await self._attempt_update(self.validate(value), retries, timeout)
174
+ self.validate(value)
175
+ return await self._attempt_update(self._pack_value(value), retries, timeout)
197
176
 
198
177
  def set_nowait(self, value: Any, retries: int = 5, timeout: float = 5.0) -> None:
199
178
  """Set a parameter value without waiting."""
179
+ self.validate(value)
200
180
  self.device.create_task(
201
- self._attempt_update(self.validate(value), retries, timeout)
181
+ self._attempt_update(self._pack_value(value), retries, timeout)
202
182
  )
203
183
 
204
184
  async def _attempt_update(
@@ -269,6 +249,18 @@ class Parameter(ABC):
269
249
 
270
250
  return parameter
271
251
 
252
+ @abstractmethod
253
+ def _pack_value(self, value: Any) -> int:
254
+ """Pack the parameter value."""
255
+
256
+ @abstractmethod
257
+ def _unpack_value(self, value: int) -> Any:
258
+ """Unpack the parameter value."""
259
+
260
+ @abstractmethod
261
+ def validate(self, value: Any) -> bool:
262
+ """Validate a parameter value."""
263
+
272
264
  @property
273
265
  @abstractmethod
274
266
  def value(self) -> NumericType | State | bool:
@@ -304,6 +296,24 @@ class Number(Parameter):
304
296
 
305
297
  description: NumberDescription
306
298
 
299
+ def _pack_value(self, value: NumericType) -> int:
300
+ """Pack the parameter value."""
301
+ return int(value)
302
+
303
+ def _unpack_value(self, value: int) -> NumericType:
304
+ """Unpack the parameter value."""
305
+ return value
306
+
307
+ def validate(self, value: Any) -> bool:
308
+ """Validate a parameter value."""
309
+ if value < self.min_value or value > self.max_value:
310
+ raise ValueError(
311
+ f"Invalid number value: {value}. Must be between "
312
+ f"{self.min_value} and {self.max_value}."
313
+ )
314
+
315
+ return True
316
+
307
317
  async def set(
308
318
  self, value: NumericType, retries: int = 5, timeout: float = 5.0
309
319
  ) -> bool:
@@ -323,17 +333,17 @@ class Number(Parameter):
323
333
  @property
324
334
  def value(self) -> NumericType:
325
335
  """Return the value."""
326
- return self.values.value
336
+ return self._unpack_value(self.values.value)
327
337
 
328
338
  @property
329
339
  def min_value(self) -> NumericType:
330
340
  """Return the minimum allowed value."""
331
- return self.values.min_value
341
+ return self._unpack_value(self.values.min_value)
332
342
 
333
343
  @property
334
344
  def max_value(self) -> NumericType:
335
345
  """Return the maximum allowed value."""
336
- return self.values.max_value
346
+ return self._unpack_value(self.values.max_value)
337
347
 
338
348
  @property
339
349
  def unit_of_measurement(self) -> UnitOfMeasurement | Literal["%"] | None:
@@ -354,6 +364,24 @@ class Switch(Parameter):
354
364
 
355
365
  description: SwitchDescription
356
366
 
367
+ def _pack_value(self, value: State | bool) -> int:
368
+ """Pack the parameter value."""
369
+ if value in get_args(State):
370
+ return 1 if value == STATE_ON else 0
371
+
372
+ return int(value)
373
+
374
+ def _unpack_value(self, value: int) -> State:
375
+ """Unpack the parameter value."""
376
+ return STATE_ON if value == 1 else STATE_OFF
377
+
378
+ def validate(self, value: Any) -> bool:
379
+ """Validate a parameter value."""
380
+ if not isinstance(value, bool) and value not in get_args(State):
381
+ raise ValueError(f"Invalid switch value: {value}. Must be 'on' or 'off'.")
382
+
383
+ return True
384
+
357
385
  async def set(
358
386
  self, value: State | bool, retries: int = 5, timeout: float = 5.0
359
387
  ) -> bool:
@@ -399,7 +427,7 @@ class Switch(Parameter):
399
427
  @property
400
428
  def value(self) -> State:
401
429
  """Return the value."""
402
- return STATE_ON if self.values.value == 1 else STATE_OFF
430
+ return self._unpack_value(self.values.value)
403
431
 
404
432
  @property
405
433
  def min_value(self) -> Literal["off"]:
@@ -23,6 +23,7 @@ from pyplumio.frames import Request
23
23
  from pyplumio.helpers.parameter import (
24
24
  Number,
25
25
  NumberDescription,
26
+ NumericType,
26
27
  Parameter,
27
28
  ParameterDescription,
28
29
  ParameterValues,
@@ -32,7 +33,7 @@ from pyplumio.helpers.parameter import (
32
33
  )
33
34
  from pyplumio.structures import StructureDecoder
34
35
  from pyplumio.structures.thermostat_parameters import ATTR_THERMOSTAT_PROFILE
35
- from pyplumio.utils import ensure_dict
36
+ from pyplumio.utils import ensure_dict, is_divisible
36
37
 
37
38
  if TYPE_CHECKING:
38
39
  from pyplumio.devices.ecomax import EcoMAX
@@ -89,7 +90,7 @@ class EcomaxParameter(Parameter):
89
90
  class EcomaxNumberDescription(EcomaxParameterDescription, NumberDescription):
90
91
  """Represents an ecoMAX number description."""
91
92
 
92
- multiplier: float = 1.0
93
+ step: float = 1.0
93
94
  offset: int = 0
94
95
  precision: int = 6
95
96
 
@@ -101,31 +102,25 @@ class EcomaxNumber(EcomaxParameter, Number):
101
102
 
102
103
  description: EcomaxNumberDescription
103
104
 
104
- async def set(
105
- self, value: float | int, retries: int = 5, timeout: float = 5.0
106
- ) -> bool:
107
- """Set a parameter value."""
108
- value += self.description.offset
109
- value = round(value / self.description.multiplier, self.description.precision)
110
- return await super().set(value, retries, timeout)
105
+ def validate(self, value: NumericType) -> bool:
106
+ """Validate the parameter value."""
107
+ if not is_divisible(value, self.description.step, self.description.precision):
108
+ raise ValueError(
109
+ f"Invalid value: {value}. The value must be adjusted in increments of "
110
+ f"{self.description.step}."
111
+ )
111
112
 
112
- @property
113
- def value(self) -> float:
114
- """Return the value."""
115
- value = self.values.value - self.description.offset
116
- return round(value * self.description.multiplier, self.description.precision)
113
+ return super().validate(value)
117
114
 
118
- @property
119
- def min_value(self) -> float:
120
- """Return the minimum allowed value."""
121
- value = self.values.min_value - self.description.offset
122
- return round(value * self.description.multiplier, self.description.precision)
115
+ def _pack_value(self, value: NumericType) -> int:
116
+ """Pack the parameter value."""
117
+ value += self.description.offset
118
+ return round(value / self.description.step)
123
119
 
124
- @property
125
- def max_value(self) -> float:
126
- """Return the maximum allowed value."""
127
- value = self.values.max_value - self.description.offset
128
- return round(value * self.description.multiplier, self.description.precision)
120
+ def _unpack_value(self, value: int) -> NumericType:
121
+ """Unpack the parameter value."""
122
+ value -= self.description.offset
123
+ return round(value * self.description.step, self.description.precision)
129
124
 
130
125
 
131
126
  @dataslots
@@ -468,8 +463,8 @@ ECOMAX_PARAMETERS: dict[ProductType, tuple[EcomaxParameterDescription, ...]] = {
468
463
  ),
469
464
  EcomaxNumberDescription(
470
465
  name="max_fuel_flow",
471
- multiplier=20,
472
- unit_of_measurement=UnitOfMeasurement.GRAMS,
466
+ step=0.2,
467
+ unit_of_measurement=UnitOfMeasurement.KILOGRAMS_PER_HOUR,
473
468
  ),
474
469
  EcomaxNumberDescription(
475
470
  name="feeder_calibration",
@@ -479,7 +474,7 @@ ECOMAX_PARAMETERS: dict[ProductType, tuple[EcomaxParameterDescription, ...]] = {
479
474
  ),
480
475
  EcomaxNumberDescription(
481
476
  name="fuel_calorific_value",
482
- multiplier=0.1,
477
+ step=0.1,
483
478
  unit_of_measurement=UnitOfMeasurement.KILO_WATT_HOUR_PER_KILOGRAM,
484
479
  ),
485
480
  EcomaxNumberDescription(
@@ -546,7 +541,7 @@ ECOMAX_PARAMETERS: dict[ProductType, tuple[EcomaxParameterDescription, ...]] = {
546
541
  ),
547
542
  EcomaxNumberDescription(
548
543
  name="heating_curve",
549
- multiplier=0.1,
544
+ step=0.1,
550
545
  ),
551
546
  EcomaxNumberDescription(
552
547
  name="heating_curve_shift",
@@ -703,12 +698,12 @@ ECOMAX_PARAMETERS: dict[ProductType, tuple[EcomaxParameterDescription, ...]] = {
703
698
  ),
704
699
  EcomaxNumberDescription(
705
700
  name="solar_pump_on_delta_temp",
706
- multiplier=0.1,
701
+ step=0.1,
707
702
  unit_of_measurement=UnitOfMeasurement.CELSIUS,
708
703
  ),
709
704
  EcomaxNumberDescription(
710
705
  name="solar_pump_off_delta_temp",
711
- multiplier=0.1,
706
+ step=0.1,
712
707
  unit_of_measurement=UnitOfMeasurement.CELSIUS,
713
708
  ),
714
709
  EcomaxNumberDescription(
@@ -801,7 +796,7 @@ ECOMAX_PARAMETERS: dict[ProductType, tuple[EcomaxParameterDescription, ...]] = {
801
796
  ),
802
797
  EcomaxNumberDescription(
803
798
  name="thermostat_hysteresis",
804
- multiplier=0.1,
799
+ step=0.1,
805
800
  unit_of_measurement=UnitOfMeasurement.CELSIUS,
806
801
  ),
807
802
  EcomaxNumberDescription(
@@ -20,6 +20,7 @@ from pyplumio.frames import Request
20
20
  from pyplumio.helpers.parameter import (
21
21
  Number,
22
22
  NumberDescription,
23
+ NumericType,
23
24
  Parameter,
24
25
  ParameterDescription,
25
26
  ParameterValues,
@@ -28,7 +29,7 @@ from pyplumio.helpers.parameter import (
28
29
  unpack_parameter,
29
30
  )
30
31
  from pyplumio.structures import StructureDecoder
31
- from pyplumio.utils import ensure_dict
32
+ from pyplumio.utils import ensure_dict, is_divisible
32
33
 
33
34
  if TYPE_CHECKING:
34
35
  from pyplumio.devices.mixer import Mixer
@@ -71,7 +72,7 @@ class MixerParameter(Parameter):
71
72
  class MixerNumberDescription(MixerParameterDescription, NumberDescription):
72
73
  """Represent a mixer number description."""
73
74
 
74
- multiplier: float = 1.0
75
+ step: float = 1.0
75
76
  offset: int = 0
76
77
  precision: int = 6
77
78
 
@@ -83,31 +84,25 @@ class MixerNumber(MixerParameter, Number):
83
84
 
84
85
  description: MixerNumberDescription
85
86
 
86
- async def set(
87
- self, value: int | float, retries: int = 5, timeout: float = 5.0
88
- ) -> bool:
89
- """Set a parameter value."""
90
- value += self.description.offset
91
- value = round(value / self.description.multiplier, self.description.precision)
92
- return await super().set(value, retries, timeout)
87
+ def validate(self, value: NumericType) -> bool:
88
+ """Validate the parameter value."""
89
+ if not is_divisible(value, self.description.step, self.description.precision):
90
+ raise ValueError(
91
+ f"Invalid value: {value}. The value must be adjusted in increments of "
92
+ f"{self.description.step}."
93
+ )
93
94
 
94
- @property
95
- def value(self) -> float:
96
- """Return the parameter value."""
97
- value = self.values.value - self.description.offset
98
- return round(value * self.description.multiplier, self.description.precision)
95
+ return super().validate(value)
99
96
 
100
- @property
101
- def min_value(self) -> float:
102
- """Return the minimum allowed value."""
103
- value = self.values.min_value - self.description.offset
104
- return round(value * self.description.multiplier, self.description.precision)
97
+ def _pack_value(self, value: NumericType) -> int:
98
+ """Pack the parameter value."""
99
+ value += self.description.offset
100
+ return round(value / self.description.step)
105
101
 
106
- @property
107
- def max_value(self) -> float:
108
- """Return the maximum allowed value."""
109
- value = self.values.max_value - self.description.offset
110
- return round(value * self.description.multiplier, self.description.precision)
102
+ def _unpack_value(self, value: int) -> NumericType:
103
+ """Unpack the parameter value."""
104
+ value -= self.description.offset
105
+ return round(value * self.description.step, self.description.precision)
111
106
 
112
107
 
113
108
  @dataslots
@@ -147,7 +142,7 @@ MIXER_PARAMETERS: dict[ProductType, tuple[MixerParameterDescription, ...]] = {
147
142
  ),
148
143
  MixerNumberDescription(
149
144
  name="heating_curve",
150
- multiplier=0.1,
145
+ step=0.1,
151
146
  ),
152
147
  MixerNumberDescription(
153
148
  name="heating_curve_shift",
@@ -162,7 +157,7 @@ MIXER_PARAMETERS: dict[ProductType, tuple[MixerParameterDescription, ...]] = {
162
157
  ),
163
158
  MixerNumberDescription(
164
159
  name="mixer_input_dead_zone",
165
- multiplier=0.1,
160
+ step=0.1,
166
161
  unit_of_measurement=UnitOfMeasurement.CELSIUS,
167
162
  ),
168
163
  MixerSwitchDescription(
@@ -232,7 +227,7 @@ MIXER_PARAMETERS: dict[ProductType, tuple[MixerParameterDescription, ...]] = {
232
227
  ),
233
228
  MixerNumberDescription(
234
229
  name="mixer_input_dead_zone",
235
- multiplier=0.1,
230
+ step=0.1,
236
231
  unit_of_measurement=UnitOfMeasurement.CELSIUS,
237
232
  ),
238
233
  MixerNumberDescription(
@@ -243,7 +238,7 @@ MIXER_PARAMETERS: dict[ProductType, tuple[MixerParameterDescription, ...]] = {
243
238
  ),
244
239
  MixerNumberDescription(
245
240
  name="heating_curve",
246
- multiplier=0.1,
241
+ step=0.1,
247
242
  ),
248
243
  MixerNumberDescription(
249
244
  name="heating_curve_shift",
@@ -20,6 +20,7 @@ from pyplumio.frames import Request
20
20
  from pyplumio.helpers.parameter import (
21
21
  Number,
22
22
  NumberDescription,
23
+ NumericType,
23
24
  Parameter,
24
25
  ParameterDescription,
25
26
  ParameterValues,
@@ -29,7 +30,7 @@ from pyplumio.helpers.parameter import (
29
30
  )
30
31
  from pyplumio.structures import StructureDecoder
31
32
  from pyplumio.structures.thermostat_sensors import ATTR_THERMOSTATS_AVAILABLE
32
- from pyplumio.utils import ensure_dict
33
+ from pyplumio.utils import ensure_dict, is_divisible
33
34
 
34
35
  if TYPE_CHECKING:
35
36
  from pyplumio.devices.thermostat import Thermostat
@@ -92,7 +93,7 @@ class ThermostatParameter(Parameter):
92
93
  class ThermostatNumberDescription(ThermostatParameterDescription, NumberDescription):
93
94
  """Represent a thermostat number description."""
94
95
 
95
- multiplier: float = 1.0
96
+ step: float = 1.0
96
97
  precision: int = 6
97
98
 
98
99
 
@@ -103,30 +104,23 @@ class ThermostatNumber(ThermostatParameter, Number):
103
104
 
104
105
  description: ThermostatNumberDescription
105
106
 
106
- async def set(
107
- self, value: int | float, retries: int = 5, timeout: float = 5.0
108
- ) -> bool:
109
- """Set a parameter value."""
110
- value = round(value / self.description.multiplier, self.description.precision)
111
- return await super().set(value, retries, timeout)
107
+ def validate(self, value: NumericType) -> bool:
108
+ """Validate the parameter value."""
109
+ if not is_divisible(value, self.description.step, self.description.precision):
110
+ raise ValueError(
111
+ f"Invalid value: {value}. The value must be adjusted in increments of "
112
+ f"{self.description.step}."
113
+ )
112
114
 
113
- @property
114
- def value(self) -> float:
115
- """Return the value."""
116
- value = self.values.value * self.description.multiplier
117
- return round(value, self.description.precision)
115
+ return super().validate(value)
118
116
 
119
- @property
120
- def min_value(self) -> float:
121
- """Return the minimum allowed value."""
122
- value = self.values.min_value * self.description.multiplier
123
- return round(value, self.description.precision)
117
+ def _pack_value(self, value: NumericType) -> int:
118
+ """Pack the parameter value."""
119
+ return round(value / self.description.step)
124
120
 
125
- @property
126
- def max_value(self) -> float:
127
- """Return the maximum allowed value."""
128
- value = self.values.max_value * self.description.multiplier
129
- return round(value, self.description.precision)
121
+ def _unpack_value(self, value: int) -> NumericType:
122
+ """Unpack the parameter value."""
123
+ return round(value * self.description.step, self.description.precision)
130
124
 
131
125
 
132
126
  @dataslots
@@ -150,13 +144,13 @@ THERMOSTAT_PARAMETERS: tuple[ThermostatParameterDescription, ...] = (
150
144
  ThermostatNumberDescription(
151
145
  name="party_target_temp",
152
146
  size=2,
153
- multiplier=0.1,
147
+ step=0.1,
154
148
  unit_of_measurement=UnitOfMeasurement.CELSIUS,
155
149
  ),
156
150
  ThermostatNumberDescription(
157
151
  name="holidays_target_temp",
158
152
  size=2,
159
- multiplier=0.1,
153
+ step=0.1,
160
154
  unit_of_measurement=UnitOfMeasurement.CELSIUS,
161
155
  ),
162
156
  ThermostatNumberDescription(
@@ -181,31 +175,31 @@ THERMOSTAT_PARAMETERS: tuple[ThermostatParameterDescription, ...] = (
181
175
  ),
182
176
  ThermostatNumberDescription(
183
177
  name="hysteresis",
184
- multiplier=0.1,
178
+ step=0.1,
185
179
  unit_of_measurement=UnitOfMeasurement.CELSIUS,
186
180
  ),
187
181
  ThermostatNumberDescription(
188
182
  name="day_target_temp",
189
183
  size=2,
190
- multiplier=0.1,
184
+ step=0.1,
191
185
  unit_of_measurement=UnitOfMeasurement.CELSIUS,
192
186
  ),
193
187
  ThermostatNumberDescription(
194
188
  name="night_target_temp",
195
189
  size=2,
196
- multiplier=0.1,
190
+ step=0.1,
197
191
  unit_of_measurement=UnitOfMeasurement.CELSIUS,
198
192
  ),
199
193
  ThermostatNumberDescription(
200
194
  name="antifreeze_target_temp",
201
195
  size=2,
202
- multiplier=0.1,
196
+ step=0.1,
203
197
  unit_of_measurement=UnitOfMeasurement.CELSIUS,
204
198
  ),
205
199
  ThermostatNumberDescription(
206
200
  name="heating_target_temp",
207
201
  size=2,
208
- multiplier=0.1,
202
+ step=0.1,
209
203
  unit_of_measurement=UnitOfMeasurement.CELSIUS,
210
204
  ),
211
205
  ThermostatNumberDescription(
@@ -28,3 +28,14 @@ def ensure_dict(initial: dict[KT, VT] | None, *args: dict[KT, VT]) -> dict[KT, V
28
28
  data |= extra
29
29
 
30
30
  return data
31
+
32
+
33
+ def is_divisible(a: float, b: float, precision: int = 6) -> bool:
34
+ """Check if a is divisible by b."""
35
+ scale: int = 10**precision
36
+ b_scaled = round(b * scale)
37
+ if b_scaled == 0:
38
+ raise ValueError("Division by zero is not allowed.")
39
+
40
+ a_scaled = round(a * scale)
41
+ return a_scaled % b_scaled == 0
@@ -241,19 +241,19 @@ async def test_ecomax_parameters_callbacks(ecomax: EcoMAX) -> None:
241
241
  await ecomax.wait_until_done()
242
242
  assert await ecomax.get("fuzzy_logic") is fuzzy_logic
243
243
 
244
- # Test parameter with the multiplier (heating_heat_curve)
244
+ # Test parameter with the step (heating_heat_curve)
245
245
  fuel_calorific_value = await ecomax.get("fuel_calorific_value")
246
246
  assert fuel_calorific_value.value == 4.6
247
247
  assert fuel_calorific_value.min_value == 0.1
248
248
  assert fuel_calorific_value.max_value == 25.0
249
249
 
250
- # Test setting parameter with the multiplier.
251
- with patch(
252
- "pyplumio.structures.ecomax_parameters.Parameter.set", new_callable=AsyncMock
253
- ) as mock_set:
250
+ # Test setting parameter with the step.
251
+ with patch("asyncio.Queue.put", new_callable=AsyncMock) as mock_put:
254
252
  await fuel_calorific_value.set(2.5)
255
253
 
256
- mock_set.assert_awaited_once_with(25, 5, 5.0)
254
+ request = mock_put.call_args[0][0]
255
+ assert isinstance(request, SetEcomaxParameterRequest)
256
+ assert request.data[ATTR_VALUE] == 25
257
257
 
258
258
  # Test parameter with the offset (heating_heat_curve_shift)
259
259
  heating_heat_curve_shift = await ecomax.get("heating_curve_shift")
@@ -264,12 +264,12 @@ async def test_ecomax_parameters_callbacks(ecomax: EcoMAX) -> None:
264
264
  assert heating_heat_curve_shift.unit_of_measurement == UnitOfMeasurement.CELSIUS
265
265
 
266
266
  # Test setting the parameter with the offset.
267
- with patch(
268
- "pyplumio.structures.ecomax_parameters.Parameter.set", new_callable=AsyncMock
269
- ) as mock_set:
267
+ with patch("asyncio.Queue.put", new_callable=AsyncMock) as mock_put:
270
268
  await heating_heat_curve_shift.set(1)
271
269
 
272
- mock_set.assert_awaited_once_with(21, 5, 5.0)
270
+ request = mock_put.call_args[0][0]
271
+ assert isinstance(request, SetEcomaxParameterRequest)
272
+ assert request.data[ATTR_VALUE] == 21
273
273
 
274
274
 
275
275
  async def test_unknown_ecomax_parameter(ecomax: EcoMAX, caplog) -> None:
@@ -629,9 +629,13 @@ async def test_set(ecomax: EcoMAX) -> None:
629
629
  await ecomax.wait_until_done()
630
630
 
631
631
  # Test setting an ecomax parameter.
632
- assert await ecomax.set("max_fuel_flow", 2600)
632
+ assert await ecomax.set("max_fuel_flow", 26.0)
633
633
  max_fuel_flow = await ecomax.get("max_fuel_flow")
634
- assert max_fuel_flow.value == 2600
634
+ assert max_fuel_flow.value == 26.0
635
+
636
+ # Test setting an ecomax parameter with invalid step.
637
+ with pytest.raises(ValueError):
638
+ await ecomax.set("max_fuel_flow", 26.1)
635
639
 
636
640
  # Test setting an ecomax parameter without blocking.
637
641
  with (
@@ -646,16 +650,24 @@ async def test_set(ecomax: EcoMAX) -> None:
646
650
 
647
651
  # Test setting a thermostat parameter.
648
652
  thermostat = ecomax.data[ATTR_THERMOSTATS][0]
649
- assert await thermostat.set("party_target_temp", 21.0)
653
+ assert await thermostat.set("party_target_temp", 21)
650
654
  target_party_temp = await thermostat.get("party_target_temp")
651
655
  assert target_party_temp.value == 21.0
652
656
 
657
+ # Test setting a thermostat parameter with invalid step.
658
+ with pytest.raises(ValueError):
659
+ await thermostat.set("party_target_temp", 26.01)
660
+
653
661
  # Test setting a mixer parameter.
654
662
  mixer = ecomax.data[ATTR_MIXERS][0]
655
663
  assert await mixer.set("mixer_target_temp", 35.0)
656
664
  mixer_target_temp = await mixer.get("mixer_target_temp")
657
665
  assert mixer_target_temp.value == 35.0
658
666
 
667
+ # Test setting a mixer parameter with invalid step.
668
+ with pytest.raises(ValueError):
669
+ await mixer.set("mixer_target_temp", 35.01)
670
+
659
671
  # Test with invalid parameter.
660
672
  ecomax.data["bar"] = Mock()
661
673
  with pytest.raises(TypeError):
@@ -1,5 +1,7 @@
1
1
  """Contains tests for the utility functions."""
2
2
 
3
+ import pytest
4
+
3
5
  from pyplumio import utils
4
6
 
5
7
 
@@ -20,3 +22,13 @@ def test_ensure_dict() -> None:
20
22
  "foo": "bar",
21
23
  "baz": "foobar",
22
24
  }
25
+
26
+
27
+ def test_is_divisible() -> None:
28
+ """Test divisibility check."""
29
+ assert utils.is_divisible(10.0, 0.2)
30
+ assert utils.is_divisible(0.0, 1.0)
31
+ assert not utils.is_divisible(10.0, 3.0)
32
+
33
+ with pytest.raises(ValueError, match="Division by zero is not allowed."):
34
+ utils.is_divisible(10.0, 0.0)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes