PyPlumIO 0.5.42__py3-none-any.whl → 0.5.44__py3-none-any.whl

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 (61) hide show
  1. pyplumio/__init__.py +3 -2
  2. pyplumio/_version.py +2 -2
  3. pyplumio/connection.py +14 -14
  4. pyplumio/const.py +8 -3
  5. pyplumio/{helpers/data_types.py → data_types.py} +23 -21
  6. pyplumio/devices/__init__.py +42 -41
  7. pyplumio/devices/ecomax.py +202 -174
  8. pyplumio/devices/ecoster.py +5 -0
  9. pyplumio/devices/mixer.py +24 -34
  10. pyplumio/devices/thermostat.py +24 -31
  11. pyplumio/filters.py +188 -147
  12. pyplumio/frames/__init__.py +20 -8
  13. pyplumio/frames/messages.py +3 -0
  14. pyplumio/frames/requests.py +21 -0
  15. pyplumio/frames/responses.py +18 -0
  16. pyplumio/helpers/async_cache.py +48 -0
  17. pyplumio/helpers/event_manager.py +58 -3
  18. pyplumio/helpers/factory.py +5 -2
  19. pyplumio/helpers/schedule.py +8 -5
  20. pyplumio/helpers/task_manager.py +3 -0
  21. pyplumio/helpers/timeout.py +7 -6
  22. pyplumio/helpers/uid.py +8 -5
  23. pyplumio/{helpers/parameter.py → parameters/__init__.py} +105 -5
  24. pyplumio/parameters/ecomax.py +868 -0
  25. pyplumio/parameters/mixer.py +245 -0
  26. pyplumio/parameters/thermostat.py +197 -0
  27. pyplumio/protocol.py +21 -10
  28. pyplumio/stream.py +3 -0
  29. pyplumio/structures/__init__.py +3 -0
  30. pyplumio/structures/alerts.py +9 -6
  31. pyplumio/structures/boiler_load.py +3 -0
  32. pyplumio/structures/boiler_power.py +4 -1
  33. pyplumio/structures/ecomax_parameters.py +6 -800
  34. pyplumio/structures/fan_power.py +4 -1
  35. pyplumio/structures/frame_versions.py +4 -1
  36. pyplumio/structures/fuel_consumption.py +4 -1
  37. pyplumio/structures/fuel_level.py +3 -0
  38. pyplumio/structures/lambda_sensor.py +9 -1
  39. pyplumio/structures/mixer_parameters.py +8 -230
  40. pyplumio/structures/mixer_sensors.py +10 -1
  41. pyplumio/structures/modules.py +14 -0
  42. pyplumio/structures/network_info.py +12 -1
  43. pyplumio/structures/output_flags.py +10 -1
  44. pyplumio/structures/outputs.py +22 -1
  45. pyplumio/structures/pending_alerts.py +3 -0
  46. pyplumio/structures/product_info.py +6 -3
  47. pyplumio/structures/program_version.py +3 -0
  48. pyplumio/structures/regulator_data.py +5 -2
  49. pyplumio/structures/regulator_data_schema.py +4 -1
  50. pyplumio/structures/schedules.py +18 -1
  51. pyplumio/structures/statuses.py +9 -0
  52. pyplumio/structures/temperatures.py +23 -1
  53. pyplumio/structures/thermostat_parameters.py +18 -184
  54. pyplumio/structures/thermostat_sensors.py +10 -1
  55. pyplumio/utils.py +14 -12
  56. {pyplumio-0.5.42.dist-info → pyplumio-0.5.44.dist-info}/METADATA +32 -17
  57. pyplumio-0.5.44.dist-info/RECORD +64 -0
  58. {pyplumio-0.5.42.dist-info → pyplumio-0.5.44.dist-info}/WHEEL +1 -1
  59. pyplumio-0.5.42.dist-info/RECORD +0 -60
  60. {pyplumio-0.5.42.dist-info → pyplumio-0.5.44.dist-info}/licenses/LICENSE +0 -0
  61. {pyplumio-0.5.42.dist-info → pyplumio-0.5.44.dist-info}/top_level.txt +0 -0
@@ -3,59 +3,49 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
- from collections.abc import Coroutine, Generator, Sequence
7
- from typing import TYPE_CHECKING, Any
6
+ from collections.abc import Coroutine, Generator
7
+ import logging
8
+ from typing import Any
8
9
 
9
- from pyplumio.devices import PhysicalDevice, VirtualDevice
10
- from pyplumio.helpers.parameter import ParameterValues
11
- from pyplumio.structures.thermostat_parameters import (
12
- ATTR_THERMOSTAT_PARAMETERS,
13
- THERMOSTAT_PARAMETERS,
10
+ from pyplumio.devices import VirtualDevice
11
+ from pyplumio.helpers.event_manager import event_listener
12
+ from pyplumio.parameters import ParameterValues
13
+ from pyplumio.parameters.thermostat import (
14
14
  ThermostatNumber,
15
15
  ThermostatSwitch,
16
16
  ThermostatSwitchDescription,
17
+ get_thermostat_parameter_types,
17
18
  )
18
- from pyplumio.structures.thermostat_sensors import ATTR_THERMOSTAT_SENSORS
19
19
 
20
- if TYPE_CHECKING:
21
- from pyplumio.frames import Frame
20
+ _LOGGER = logging.getLogger()
22
21
 
23
22
 
24
23
  class Thermostat(VirtualDevice):
25
24
  """Represents a thermostat."""
26
25
 
27
- def __init__(
28
- self, queue: asyncio.Queue[Frame], parent: PhysicalDevice, index: int = 0
29
- ) -> None:
30
- """Initialize a new thermostat."""
31
- super().__init__(queue, parent, index)
32
- self.subscribe(ATTR_THERMOSTAT_SENSORS, self._handle_thermostat_sensors)
33
- self.subscribe(ATTR_THERMOSTAT_PARAMETERS, self._handle_thermostat_parameters)
26
+ __slots__ = ()
34
27
 
35
- async def _handle_thermostat_sensors(self, sensors: dict[str, Any]) -> bool:
36
- """Handle thermostat sensors.
37
-
38
- For each sensor dispatch an event with the
39
- sensor's name and value.
40
- """
28
+ @event_listener
29
+ async def on_event_thermostat_sensors(self, sensors: dict[str, Any]) -> bool:
30
+ """Update thermostat sensors and dispatch the events."""
31
+ _LOGGER.info("Received thermostat %i sensors", self.index)
41
32
  await asyncio.gather(
42
33
  *(self.dispatch(name, value) for name, value in sensors.items())
43
34
  )
44
35
  return True
45
36
 
46
- async def _handle_thermostat_parameters(
47
- self, parameters: Sequence[tuple[int, ParameterValues]]
37
+ @event_listener
38
+ async def on_event_thermostat_parameters(
39
+ self, parameters: list[tuple[int, ParameterValues]]
48
40
  ) -> bool:
49
- """Handle thermostat parameters.
50
-
51
- For each parameter dispatch an event with the
52
- parameter's name and value.
53
- """
41
+ """Update thermostat parameters and dispatch the events."""
42
+ _LOGGER.info("Received thermostat %i parameters", self.index)
54
43
 
55
44
  def _thermostat_parameter_events() -> Generator[Coroutine, Any, None]:
56
45
  """Get dispatch calls for thermostat parameter events."""
46
+ parameter_types = get_thermostat_parameter_types()
57
47
  for index, values in parameters:
58
- description = THERMOSTAT_PARAMETERS[index]
48
+ description = parameter_types[index]
59
49
  handler = (
60
50
  ThermostatSwitch
61
51
  if isinstance(description, ThermostatSwitchDescription)
@@ -74,3 +64,6 @@ class Thermostat(VirtualDevice):
74
64
 
75
65
  await asyncio.gather(*_thermostat_parameter_events())
76
66
  return True
67
+
68
+
69
+ __all__ = ["Thermostat"]
pyplumio/filters.py CHANGED
@@ -4,7 +4,10 @@ from __future__ import annotations
4
4
 
5
5
  from abc import ABC, abstractmethod
6
6
  from collections.abc import Callable
7
+ from contextlib import suppress
7
8
  from copy import copy
9
+ from decimal import Decimal
10
+ import logging
8
11
  import math
9
12
  import time
10
13
  from typing import (
@@ -17,8 +20,20 @@ from typing import (
17
20
  runtime_checkable,
18
21
  )
19
22
 
23
+ from typing_extensions import TypeAlias
24
+
20
25
  from pyplumio.helpers.event_manager import Callback
21
- from pyplumio.helpers.parameter import Parameter
26
+ from pyplumio.parameters import Parameter
27
+
28
+ _LOGGER = logging.getLogger(__name__)
29
+
30
+ numpy_installed = False
31
+ with suppress(ImportError):
32
+ import numpy as np
33
+
34
+ _LOGGER.info("Using numpy for improved float precision")
35
+ numpy_installed = True
36
+
22
37
 
23
38
  UNDEFINED: Final = "undefined"
24
39
  TOLERANCE: Final = 0.1
@@ -50,20 +65,18 @@ Comparable = TypeVar("Comparable", Parameter, SupportsFloat, SupportsComparison)
50
65
 
51
66
 
52
67
  @overload
53
- def _significantly_changed(old: Parameter, new: Parameter) -> bool: ...
68
+ def is_close(old: Parameter, new: Parameter) -> bool: ...
54
69
 
55
70
 
56
71
  @overload
57
- def _significantly_changed(old: SupportsFloat, new: SupportsFloat) -> bool: ...
72
+ def is_close(old: SupportsFloat, new: SupportsFloat) -> bool: ...
58
73
 
59
74
 
60
75
  @overload
61
- def _significantly_changed(
62
- old: SupportsComparison, new: SupportsComparison
63
- ) -> bool: ...
76
+ def is_close(old: SupportsComparison, new: SupportsComparison) -> bool: ...
64
77
 
65
78
 
66
- def _significantly_changed(old: Comparable, new: Comparable) -> bool:
79
+ def is_close(old: Comparable, new: Comparable) -> bool:
67
80
  """Check if value is significantly changed."""
68
81
  if isinstance(old, Parameter) and isinstance(new, Parameter):
69
82
  return new.pending_update or old.values.__ne__(new.values)
@@ -75,16 +88,16 @@ def _significantly_changed(old: Comparable, new: Comparable) -> bool:
75
88
 
76
89
 
77
90
  @overload
78
- def _diffence_between(old: list, new: list) -> list: ...
91
+ def diffence_between(old: list, new: list) -> list: ...
79
92
 
80
93
 
81
94
  @overload
82
- def _diffence_between(
95
+ def diffence_between(
83
96
  old: SupportsSubtraction, new: SupportsSubtraction
84
97
  ) -> SupportsSubtraction: ...
85
98
 
86
99
 
87
- def _diffence_between(
100
+ def diffence_between(
88
101
  old: SupportsSubtraction | list, new: SupportsSubtraction | list
89
102
  ) -> SupportsSubtraction | list | None:
90
103
  """Return a difference between values."""
@@ -125,6 +138,70 @@ class Filter(ABC):
125
138
  """Set a new value for the callback."""
126
139
 
127
140
 
141
+ class _Aggregate(Filter):
142
+ """Represents an aggregate filter.
143
+
144
+ Calls a callback with a sum of values collected over a specified
145
+ time period or when sample size limit reached.
146
+ """
147
+
148
+ __slots__ = ("_values", "_sample_size", "_timeout", "_last_call_time")
149
+
150
+ _values: list[float | int | Decimal]
151
+ _sample_size: int
152
+ _timeout: float
153
+ _last_call_time: float
154
+
155
+ def __init__(self, callback: Callback, seconds: float, sample_size: int) -> None:
156
+ """Initialize a new aggregate filter."""
157
+ super().__init__(callback)
158
+ self._last_call_time = time.monotonic()
159
+ self._timeout = seconds
160
+ self._sample_size = sample_size
161
+ self._values = []
162
+
163
+ async def __call__(self, new_value: Any) -> Any:
164
+ """Set a new value for the callback."""
165
+ if not isinstance(new_value, (float, int, Decimal)):
166
+ raise TypeError(
167
+ "Aggregate filter can only be used with numeric values, got "
168
+ f"{type(new_value).__name__}: {new_value}"
169
+ )
170
+
171
+ current_time = time.monotonic()
172
+ self._values.append(new_value)
173
+ time_since_call = current_time - self._last_call_time
174
+ if time_since_call >= self._timeout or len(self._values) >= self._sample_size:
175
+ result = await self._callback(
176
+ np.sum(self._values) if numpy_installed else sum(self._values)
177
+ )
178
+ self._last_call_time = current_time
179
+ self._values = []
180
+ return result
181
+
182
+
183
+ def aggregate(callback: Callback, seconds: float, sample_size: int) -> _Aggregate:
184
+ """Create a new aggregate filter.
185
+
186
+ A callback function will be called with a sum of values collected
187
+ over a specified time period or when sample size limit reached.
188
+ Can only be used with numeric values.
189
+
190
+ :param callback: A callback function to be awaited once filter
191
+ conditions are fulfilled
192
+ :type callback: Callback
193
+ :param seconds: A callback will be awaited with a sum of values
194
+ aggregated over this amount of seconds.
195
+ :type seconds: float
196
+ :param sample_size: The maximum number of values to aggregate
197
+ before calling the callback
198
+ :type sample_size: int
199
+ :return: An instance of callable filter
200
+ :rtype: _Aggregate
201
+ """
202
+ return _Aggregate(callback, seconds, sample_size)
203
+
204
+
128
205
  class _Clamp(Filter):
129
206
  """Represents a clamp filter.
130
207
 
@@ -137,7 +214,7 @@ class _Clamp(Filter):
137
214
  _max_value: float
138
215
 
139
216
  def __init__(self, callback: Callback, min_value: float, max_value: float) -> None:
140
- """Initialize a new clamp filter."""
217
+ """Initialize a new Clamp filter."""
141
218
  super().__init__(callback)
142
219
  self._min_value = min_value
143
220
  self._max_value = max_value
@@ -154,10 +231,10 @@ class _Clamp(Filter):
154
231
 
155
232
 
156
233
  def clamp(callback: Callback, min_value: float, max_value: float) -> _Clamp:
157
- """Return a clamp filter.
234
+ """Create a new clamp filter.
158
235
 
159
- A callback function will be called with value clamped between
160
- specified boundaries.
236
+ A callback function will be called and passed value clamped
237
+ between specified boundaries.
161
238
 
162
239
  :param callback: A callback function to be awaited on new value
163
240
  :type callback: Callback
@@ -171,36 +248,49 @@ def clamp(callback: Callback, min_value: float, max_value: float) -> _Clamp:
171
248
  return _Clamp(callback, min_value, max_value)
172
249
 
173
250
 
174
- class _OnChange(Filter):
175
- """Represents a value changed filter.
251
+ _FilterT: TypeAlias = Callable[[Any], bool]
176
252
 
177
- Calls a callback only when value is changed from the
178
- previous callback call.
253
+
254
+ class _Custom(Filter):
255
+ """Represents a custom filter.
256
+
257
+ Calls a callback with value, if user-defined filter function
258
+ that's called by this class with the value as an argument
259
+ returns true.
179
260
  """
180
261
 
181
- __slots__ = ()
262
+ __slots__ = ("_filter_fn",)
263
+
264
+ _filter_fn: _FilterT
265
+
266
+ def __init__(self, callback: Callback, filter_fn: _FilterT) -> None:
267
+ """Initialize a new custom filter."""
268
+ super().__init__(callback)
269
+ self._filter_fn = filter_fn
182
270
 
183
271
  async def __call__(self, new_value: Any) -> Any:
184
272
  """Set a new value for the callback."""
185
- if self._value == UNDEFINED or _significantly_changed(self._value, new_value):
186
- self._value = (
187
- copy(new_value) if isinstance(new_value, Parameter) else new_value
188
- )
189
- return await self._callback(new_value)
273
+ if self._filter_fn(new_value):
274
+ await self._callback(new_value)
190
275
 
191
276
 
192
- def on_change(callback: Callback) -> _OnChange:
193
- """Return a value changed filter.
277
+ def custom(callback: Callback, filter_fn: _FilterT) -> _Custom:
278
+ """Create a new custom filter.
194
279
 
195
- A callback function will only be called if value is changed from the
196
- previous call.
280
+ A callback function will be called when a user-defined filter
281
+ function, that's being called with the value as an argument,
282
+ returns true.
197
283
 
198
- :param callback: A callback function to be awaited on value change
284
+ :param callback: A callback function to be awaited when
285
+ filter function return true
199
286
  :type callback: Callback
287
+ :param filter_fn: Filter function, that will be called with a
288
+ value and should return `True` to await filter's callback
289
+ :type filter_fn: Callable[[Any], bool]
200
290
  :return: An instance of callable filter
201
- :rtype: _OnChange
291
+ :rtype: _Custom
202
292
  """
203
- return _OnChange(callback)
293
+ return _Custom(callback, filter_fn)
204
294
 
205
295
 
206
296
  class _Debounce(Filter):
@@ -223,7 +313,7 @@ class _Debounce(Filter):
223
313
 
224
314
  async def __call__(self, new_value: Any) -> Any:
225
315
  """Set a new value for the callback."""
226
- if self._value == UNDEFINED or _significantly_changed(self._value, new_value):
316
+ if self._value == UNDEFINED or is_close(self._value, new_value):
227
317
  self._calls += 1
228
318
  else:
229
319
  self._calls = 0
@@ -237,9 +327,9 @@ class _Debounce(Filter):
237
327
 
238
328
 
239
329
  def debounce(callback: Callback, min_calls: int) -> _Debounce:
240
- """Return a debounce filter.
330
+ """Create a new debounce filter.
241
331
 
242
- A callback function will only called once value is stabilized
332
+ A callback function will only be called once the value is stabilized
243
333
  across multiple filter calls.
244
334
 
245
335
  :param callback: A callback function to be awaited on value change
@@ -253,53 +343,6 @@ def debounce(callback: Callback, min_calls: int) -> _Debounce:
253
343
  return _Debounce(callback, min_calls)
254
344
 
255
345
 
256
- class _Throttle(Filter):
257
- """Represents a throttle filter.
258
-
259
- Calls a callback only when certain amount of seconds passed
260
- since the last call.
261
- """
262
-
263
- __slots__ = ("_last_called", "_timeout")
264
-
265
- _last_called: float | None
266
- _timeout: float
267
-
268
- def __init__(self, callback: Callback, seconds: float) -> None:
269
- """Initialize a new throttle filter."""
270
- super().__init__(callback)
271
- self._last_called = None
272
- self._timeout = seconds
273
-
274
- async def __call__(self, new_value: Any) -> Any:
275
- """Set a new value for the callback."""
276
- current_timestamp = time.monotonic()
277
- if (
278
- self._last_called is None
279
- or (current_timestamp - self._last_called) >= self._timeout
280
- ):
281
- self._last_called = current_timestamp
282
- return await self._callback(new_value)
283
-
284
-
285
- def throttle(callback: Callback, seconds: float) -> _Throttle:
286
- """Return a throttle filter.
287
-
288
- A callback function will only be called once a certain amount of
289
- seconds passed since the last call.
290
-
291
- :param callback: A callback function that will be awaited once
292
- filter conditions are fulfilled
293
- :type callback: Callback
294
- :param seconds: A callback will be awaited at most once per
295
- this amount of seconds
296
- :type seconds: float
297
- :return: An instance of callable filter
298
- :rtype: _Throttle
299
- """
300
- return _Throttle(callback, seconds)
301
-
302
-
303
346
  class _Delta(Filter):
304
347
  """Represents a difference filter.
305
348
 
@@ -310,23 +353,23 @@ class _Delta(Filter):
310
353
 
311
354
  async def __call__(self, new_value: Any) -> Any:
312
355
  """Set a new value for the callback."""
313
- if self._value == UNDEFINED or _significantly_changed(self._value, new_value):
356
+ if self._value == UNDEFINED or is_close(self._value, new_value):
314
357
  old_value = self._value
315
358
  self._value = (
316
359
  copy(new_value) if isinstance(new_value, Parameter) else new_value
317
360
  )
318
361
  if (
319
362
  self._value != UNDEFINED
320
- and (difference := _diffence_between(old_value, new_value)) is not None
363
+ and (difference := diffence_between(old_value, new_value)) is not None
321
364
  ):
322
365
  return await self._callback(difference)
323
366
 
324
367
 
325
368
  def delta(callback: Callback) -> _Delta:
326
- """Return a difference filter.
369
+ """Create a new difference filter.
327
370
 
328
371
  A callback function will be called with a difference between two
329
- subsequent value.
372
+ subsequent values.
330
373
 
331
374
  :param callback: A callback function that will be awaited with
332
375
  difference between values in two subsequent calls
@@ -337,98 +380,96 @@ def delta(callback: Callback) -> _Delta:
337
380
  return _Delta(callback)
338
381
 
339
382
 
340
- class _Aggregate(Filter):
341
- """Represents an aggregate filter.
383
+ class _OnChange(Filter):
384
+ """Represents a value changed filter.
342
385
 
343
- Calls a callback with a sum of values collected over a specified
344
- time period.
386
+ Calls a callback only when value is changed from the
387
+ previous callback call.
345
388
  """
346
389
 
347
- __slots__ = ("_sum", "_last_update", "_timeout")
348
-
349
- _sum: complex
350
- _last_update: float
351
- _timeout: float
390
+ __slots__ = ()
352
391
 
353
- def __init__(self, callback: Callback, seconds: float) -> None:
354
- """Initialize a new aggregate filter."""
392
+ def __init__(self, callback: Callback) -> None:
393
+ """Initialize a new value changed filter."""
355
394
  super().__init__(callback)
356
- self._last_update = time.monotonic()
357
- self._timeout = seconds
358
- self._sum = 0.0
359
395
 
360
396
  async def __call__(self, new_value: Any) -> Any:
361
397
  """Set a new value for the callback."""
362
- current_timestamp = time.monotonic()
363
- try:
364
- self._sum += new_value
365
- except TypeError as e:
366
- raise ValueError(
367
- "Aggregate filter can only be used with numeric values"
368
- ) from e
369
-
370
- if current_timestamp - self._last_update >= self._timeout:
371
- result = await self._callback(self._sum)
372
- self._last_update = current_timestamp
373
- self._sum = 0.0
374
- return result
398
+ if self._value == UNDEFINED or is_close(self._value, new_value):
399
+ self._value = (
400
+ copy(new_value) if isinstance(new_value, Parameter) else new_value
401
+ )
402
+ return await self._callback(new_value)
375
403
 
376
404
 
377
- def aggregate(callback: Callback, seconds: float) -> _Aggregate:
378
- """Return an aggregate filter.
405
+ def on_change(callback: Callback) -> _OnChange:
406
+ """Create a new value changed filter.
379
407
 
380
- A callback function will be called with a sum of values collected
381
- over a specified time period. Can only be used with numeric values.
408
+ A callback function will only be called if the value is changed from the
409
+ previous call.
382
410
 
383
- :param callback: A callback function to be awaited once filter
384
- conditions are fulfilled
411
+ :param callback: A callback function to be awaited on value change
385
412
  :type callback: Callback
386
- :param seconds: A callback will be awaited with a sum of values
387
- aggregated over this amount of seconds.
388
- :type seconds: float
389
413
  :return: An instance of callable filter
390
- :rtype: _Aggregate
414
+ :rtype: _OnChange
391
415
  """
392
- return _Aggregate(callback, seconds)
416
+ return _OnChange(callback)
393
417
 
394
418
 
395
- class _Custom(Filter):
396
- """Represents a custom filter.
419
+ class _Throttle(Filter):
420
+ """Represents a throttle filter.
397
421
 
398
- Calls a callback with value, if user-defined filter function
399
- that's called by this class with the value as an argument
400
- returns true.
422
+ Calls a callback only when certain amount of seconds passed
423
+ since the last call.
401
424
  """
402
425
 
403
- __slots__ = ("_filter_fn",)
426
+ __slots__ = ("_last_called", "_timeout")
404
427
 
405
- filter_fn: Callable[[Any], bool]
428
+ _last_called: float | None
429
+ _timeout: float
406
430
 
407
- def __init__(self, callback: Callback, filter_fn: Callable[[Any], bool]) -> None:
408
- """Initialize a new custom filter."""
431
+ def __init__(self, callback: Callback, seconds: float) -> None:
432
+ """Initialize a new throttle filter."""
409
433
  super().__init__(callback)
410
- self._filter_fn = filter_fn
434
+ self._last_called = None
435
+ self._timeout = seconds
411
436
 
412
437
  async def __call__(self, new_value: Any) -> Any:
413
438
  """Set a new value for the callback."""
414
- if self._filter_fn(new_value):
415
- await self._callback(new_value)
439
+ current_timestamp = time.monotonic()
440
+ if (
441
+ self._last_called is None
442
+ or (current_timestamp - self._last_called) >= self._timeout
443
+ ):
444
+ self._last_called = current_timestamp
445
+ return await self._callback(new_value)
416
446
 
417
447
 
418
- def custom(callback: Callback, filter_fn: Callable[[Any], bool]) -> _Custom:
419
- """Return a custom filter.
448
+ def throttle(callback: Callback, seconds: float) -> _Throttle:
449
+ """Create a new throttle filter.
420
450
 
421
- A callback function will be called when user-defined filter
422
- function, that's being called with the value as an argument,
423
- returns true.
451
+ A callback function will only be called once a certain amount of
452
+ seconds passed since the last call.
424
453
 
425
- :param callback: A callback function to be awaited when
426
- filter function return true
454
+ :param callback: A callback function that will be awaited once
455
+ filter conditions are fulfilled
427
456
  :type callback: Callback
428
- :param filter_fn: Filter function, that will be called with a
429
- value and should return `True` to await filter's callback
430
- :type filter_fn: Callable[[Any], bool]
457
+ :param seconds: A callback will be awaited at most once per
458
+ this amount of seconds
459
+ :type seconds: float
431
460
  :return: An instance of callable filter
432
- :rtype: _Custom
461
+ :rtype: _Throttle
433
462
  """
434
- return _Custom(callback, filter_fn)
463
+ return _Throttle(callback, seconds)
464
+
465
+
466
+ __all__ = [
467
+ "Filter",
468
+ "aggregate",
469
+ "clamp",
470
+ "custom",
471
+ "debounce",
472
+ "delta",
473
+ "on_change",
474
+ "throttle",
475
+ ]
@@ -30,9 +30,9 @@ if TYPE_CHECKING:
30
30
  from pyplumio.devices import PhysicalDevice
31
31
 
32
32
 
33
- def bcc(data: bytes) -> int:
33
+ def bcc(buffer: bytes) -> int:
34
34
  """Return a block check character."""
35
- return reduce(lambda x, y: x ^ y, data)
35
+ return reduce(lambda x, y: x ^ y, buffer)
36
36
 
37
37
 
38
38
  @cache
@@ -121,12 +121,12 @@ class Frame(ABC):
121
121
  self._message,
122
122
  self._data,
123
123
  ) == (
124
- self.recipient,
125
- self.sender,
126
- self.econet_type,
127
- self.econet_version,
128
- self._message,
129
- self._data,
124
+ other.recipient,
125
+ other.sender,
126
+ other.econet_type,
127
+ other.econet_version,
128
+ other._message,
129
+ other._data,
130
130
  )
131
131
 
132
132
  return NotImplemented
@@ -280,3 +280,15 @@ class Message(Response):
280
280
  """Represents a message."""
281
281
 
282
282
  __slots__ = ()
283
+
284
+
285
+ __all__ = [
286
+ "Frame",
287
+ "Request",
288
+ "Response",
289
+ "Message",
290
+ "DataFrameDescription",
291
+ "bcc",
292
+ "is_known_frame_type",
293
+ "get_frame_handler",
294
+ ]
@@ -79,3 +79,6 @@ class SensorDataMessage(Message):
79
79
  sensors[ATTR_STATE] = DeviceState(sensors[ATTR_STATE])
80
80
 
81
81
  return {ATTR_SENSORS: sensors}
82
+
83
+
84
+ __all__ = ["RegulatorDataMessage", "SensorDataMessage"]
@@ -260,3 +260,24 @@ class UIDRequest(Request):
260
260
  __slots__ = ()
261
261
 
262
262
  frame_type = FrameType.REQUEST_UID
263
+
264
+
265
+ __all__ = [
266
+ "AlertsRequest",
267
+ "CheckDeviceRequest",
268
+ "EcomaxControlRequest",
269
+ "EcomaxParametersRequest",
270
+ "MixerParametersRequest",
271
+ "PasswordRequest",
272
+ "ProgramVersionRequest",
273
+ "RegulatorDataSchemaRequest",
274
+ "SchedulesRequest",
275
+ "SetEcomaxParameterRequest",
276
+ "SetMixerParameterRequest",
277
+ "SetScheduleRequest",
278
+ "SetThermostatParameterRequest",
279
+ "StartMasterRequest",
280
+ "StopMasterRequest",
281
+ "ThermostatParametersRequest",
282
+ "UIDRequest",
283
+ ]