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.
- {pyplumio-0.5.40 → pyplumio-0.5.41}/PKG-INFO +1 -1
- {pyplumio-0.5.40 → pyplumio-0.5.41}/PyPlumIO.egg-info/PKG-INFO +1 -1
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/_version.py +2 -2
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/helpers/parameter.py +57 -29
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/ecomax_parameters.py +26 -31
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/mixer_parameters.py +23 -28
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/thermostat_parameters.py +24 -30
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/utils.py +11 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/test_devices.py +25 -13
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/test_utils.py +12 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/.gitattributes +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/.github/CODE_OF_CONDUCT.md +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/.github/dependabot.yml +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/.github/workflows/ci.yml +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/.github/workflows/codeql-analysis.yml +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/.github/workflows/deploy.yml +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/.github/workflows/documentation.yml +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/.gitignore +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/.pre-commit-config.yaml +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/.vscode/settings.json +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/LICENSE +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/MANIFEST.in +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/PyPlumIO.egg-info/SOURCES.txt +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/PyPlumIO.egg-info/dependency_links.txt +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/PyPlumIO.egg-info/requires.txt +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/PyPlumIO.egg-info/top_level.txt +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/README.md +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/docs/Makefile +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/docs/make.bat +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/docs/source/callbacks.rst +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/docs/source/conf.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/docs/source/connecting.rst +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/docs/source/frames.rst +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/docs/source/index.rst +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/docs/source/mixers_thermostats.rst +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/docs/source/protocol.rst +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/docs/source/reading.rst +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/docs/source/schedules.rst +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/docs/source/writing.rst +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/images/ecomax.png +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/images/rs485.png +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/__init__.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/__main__.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/connection.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/const.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/devices/__init__.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/devices/ecomax.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/devices/ecoster.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/devices/mixer.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/devices/thermostat.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/exceptions.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/filters.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/frames/__init__.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/frames/messages.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/frames/requests.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/frames/responses.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/helpers/__init__.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/helpers/data_types.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/helpers/event_manager.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/helpers/factory.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/helpers/schedule.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/helpers/task_manager.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/helpers/timeout.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/helpers/uid.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/protocol.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/py.typed +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/stream.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/__init__.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/alerts.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/boiler_load.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/boiler_power.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/fan_power.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/frame_versions.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/fuel_consumption.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/fuel_level.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/lambda_sensor.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/mixer_sensors.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/modules.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/network_info.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/output_flags.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/outputs.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/pending_alerts.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/product_info.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/program_version.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/regulator_data.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/regulator_data_schema.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/schedules.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/statuses.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/temperatures.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyplumio/structures/thermostat_sensors.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/pyproject.toml +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/requirements.txt +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/requirements_docs.txt +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/requirements_test.txt +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/setup.cfg +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/__init__.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/conftest.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/frames/test_init.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/frames/test_messages.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/frames/test_requests.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/frames/test_responses.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/helpers/__init__.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/helpers/test_data_types.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/helpers/test_event_manager.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/helpers/test_factory.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/helpers/test_parameter.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/helpers/test_schedule.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/helpers/test_task_manager.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/helpers/test_timeout.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/helpers/test_uid.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/ruff.toml +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/test_connection.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/test_filters.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/test_init.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/test_main.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/test_protocol.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/test_stream.py +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/testdata/messages/regulator_data.json +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/testdata/messages/sensor_data.json +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/testdata/requests/alerts.json +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/testdata/requests/ecomax_control.json +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/testdata/requests/ecomax_parameters.json +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/testdata/requests/mixer_parameters.json +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/testdata/requests/set_ecomax_parameter.json +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/testdata/requests/set_mixer_parameter.json +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/testdata/requests/set_schedule.json +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/testdata/requests/set_thermostat_parameter.json +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/testdata/requests/thermostat_parameters.json +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/testdata/responses/alerts.json +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/testdata/responses/device_available.json +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/testdata/responses/ecomax_parameters.json +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/testdata/responses/mixer_parameters.json +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/testdata/responses/password.json +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/testdata/responses/program_version.json +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/testdata/responses/regulator_data_schema.json +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/testdata/responses/schedules.json +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/testdata/responses/thermostat_parameters.json +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/testdata/responses/uid.json +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/testdata/unknown/unknown_ecomax_parameter.json +0 -0
- {pyplumio-0.5.40 → pyplumio-0.5.41}/tests/testdata/unknown/unknown_mixer_parameter.json +0 -0
@@ -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(
|
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
|
-
|
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.
|
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
|
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
|
-
|
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
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
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
|
-
|
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
|
-
|
119
|
-
|
120
|
-
|
121
|
-
value
|
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
|
-
|
125
|
-
|
126
|
-
|
127
|
-
value
|
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
|
-
|
472
|
-
unit_of_measurement=UnitOfMeasurement.
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
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
|
-
|
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
|
-
|
101
|
-
|
102
|
-
|
103
|
-
value
|
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
|
-
|
107
|
-
|
108
|
-
|
109
|
-
value
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
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
|
-
|
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
|
-
|
120
|
-
|
121
|
-
|
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
|
-
|
126
|
-
|
127
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
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
|
-
|
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
|
-
|
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",
|
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 ==
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|