PyPlumIO 0.6.1__py3-none-any.whl → 0.6.2__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 (48) hide show
  1. pyplumio/__init__.py +3 -1
  2. pyplumio/_version.py +2 -2
  3. pyplumio/const.py +0 -5
  4. pyplumio/data_types.py +2 -2
  5. pyplumio/devices/__init__.py +23 -5
  6. pyplumio/devices/ecomax.py +30 -53
  7. pyplumio/devices/ecoster.py +2 -3
  8. pyplumio/filters.py +199 -136
  9. pyplumio/frames/__init__.py +101 -15
  10. pyplumio/frames/messages.py +8 -65
  11. pyplumio/frames/requests.py +38 -38
  12. pyplumio/frames/responses.py +30 -86
  13. pyplumio/helpers/async_cache.py +13 -8
  14. pyplumio/helpers/event_manager.py +24 -18
  15. pyplumio/helpers/factory.py +0 -3
  16. pyplumio/parameters/__init__.py +38 -35
  17. pyplumio/protocol.py +14 -8
  18. pyplumio/structures/alerts.py +2 -2
  19. pyplumio/structures/ecomax_parameters.py +1 -1
  20. pyplumio/structures/frame_versions.py +3 -2
  21. pyplumio/structures/mixer_parameters.py +5 -3
  22. pyplumio/structures/network_info.py +1 -0
  23. pyplumio/structures/product_info.py +1 -1
  24. pyplumio/structures/program_version.py +2 -2
  25. pyplumio/structures/schedules.py +8 -40
  26. pyplumio/structures/sensor_data.py +498 -0
  27. pyplumio/structures/thermostat_parameters.py +7 -4
  28. pyplumio/utils.py +41 -4
  29. {pyplumio-0.6.1.dist-info → pyplumio-0.6.2.dist-info}/METADATA +4 -4
  30. pyplumio-0.6.2.dist-info/RECORD +50 -0
  31. pyplumio/structures/boiler_load.py +0 -32
  32. pyplumio/structures/boiler_power.py +0 -33
  33. pyplumio/structures/fan_power.py +0 -33
  34. pyplumio/structures/fuel_consumption.py +0 -36
  35. pyplumio/structures/fuel_level.py +0 -39
  36. pyplumio/structures/lambda_sensor.py +0 -57
  37. pyplumio/structures/mixer_sensors.py +0 -80
  38. pyplumio/structures/modules.py +0 -102
  39. pyplumio/structures/output_flags.py +0 -47
  40. pyplumio/structures/outputs.py +0 -88
  41. pyplumio/structures/pending_alerts.py +0 -28
  42. pyplumio/structures/statuses.py +0 -52
  43. pyplumio/structures/temperatures.py +0 -94
  44. pyplumio/structures/thermostat_sensors.py +0 -106
  45. pyplumio-0.6.1.dist-info/RECORD +0 -63
  46. {pyplumio-0.6.1.dist-info → pyplumio-0.6.2.dist-info}/WHEEL +0 -0
  47. {pyplumio-0.6.1.dist-info → pyplumio-0.6.2.dist-info}/licenses/LICENSE +0 -0
  48. {pyplumio-0.6.1.dist-info → pyplumio-0.6.2.dist-info}/top_level.txt +0 -0
pyplumio/filters.py CHANGED
@@ -7,6 +7,7 @@ from collections.abc import Callable
7
7
  from contextlib import suppress
8
8
  from copy import copy
9
9
  from decimal import Decimal
10
+ from functools import wraps
10
11
  import logging
11
12
  import math
12
13
  import time
@@ -21,8 +22,8 @@ from typing import (
21
22
  runtime_checkable,
22
23
  )
23
24
 
24
- from pyplumio.helpers.event_manager import Callback
25
- from pyplumio.parameters import Parameter
25
+ from pyplumio.helpers.event_manager import EventCallback
26
+ from pyplumio.parameters import Number, Parameter
26
27
 
27
28
  _LOGGER = logging.getLogger(__name__)
28
29
 
@@ -62,38 +63,41 @@ class SupportsComparison(Protocol):
62
63
  """Compare a value."""
63
64
 
64
65
 
65
- Comparable = TypeVar("Comparable", Parameter, SupportsFloat, SupportsComparison)
66
+ _ComparableT = TypeVar("_ComparableT", Parameter, SupportsFloat, SupportsComparison)
66
67
 
67
68
  DEFAULT_TOLERANCE: Final = 1e-6
68
69
 
69
70
 
70
71
  @overload
71
- def is_close(old: Parameter, new: Parameter, tolerance: None = None) -> bool: ...
72
+ def is_close(old: Parameter, new: Parameter, *, tolerance: None = None) -> bool: ...
72
73
 
73
74
 
74
75
  @overload
75
76
  def is_close(
76
- old: SupportsFloat, new: SupportsFloat, tolerance: float = DEFAULT_TOLERANCE
77
+ old: SupportsFloat, new: SupportsFloat, *, tolerance: float = DEFAULT_TOLERANCE
77
78
  ) -> bool: ...
78
79
 
79
80
 
80
81
  @overload
81
82
  def is_close(
82
- old: SupportsComparison, new: SupportsComparison, tolerance: None = None
83
+ old: SupportsComparison, new: SupportsComparison, *, tolerance: None = None
83
84
  ) -> bool: ...
84
85
 
85
86
 
86
87
  def is_close(
87
- old: Comparable, new: Comparable, tolerance: float | None = DEFAULT_TOLERANCE
88
+ old: _ComparableT, new: _ComparableT, *, tolerance: float | None = DEFAULT_TOLERANCE
88
89
  ) -> bool:
89
90
  """Check if value is significantly changed."""
90
- if isinstance(old, Parameter) and isinstance(new, Parameter):
91
- return new.update_pending.is_set() or old.values.__ne__(new.values)
91
+ if isinstance(new, Parameter) and new.update_pending.is_set():
92
+ return False
92
93
 
93
94
  if tolerance and isinstance(old, SupportsFloat) and isinstance(new, SupportsFloat):
94
- return not math.isclose(old, new, abs_tol=tolerance)
95
+ return math.isclose(old, new, abs_tol=tolerance)
96
+
97
+ if isinstance(old, Parameter) and isinstance(new, Parameter):
98
+ return old.values.__eq__(new.values)
95
99
 
96
- return old.__ne__(new)
100
+ return old.__eq__(new)
97
101
 
98
102
 
99
103
  @overload
@@ -119,18 +123,40 @@ def diffence_between(
119
123
  return None
120
124
 
121
125
 
126
+ _Numeric: TypeAlias = float | int | Decimal | Number
127
+
128
+
129
+ def numeric_only(func: Callable) -> Callable:
130
+ """Mark filter as numeric only.
131
+
132
+ Ensure that value passed to filter when used as callable is numeric
133
+ otherwise raise TypeError.
134
+ """
135
+
136
+ @wraps(func)
137
+ def wrapper(instance: Filter, new_value: _Numeric) -> Any:
138
+ """Wrap __call__ method and add numeric check to it."""
139
+ if not isinstance(new_value, _Numeric):
140
+ raise TypeError(
141
+ f"{instance.__class__.__name__.capitalize()} filter can only be used "
142
+ f"with numeric values, got {type(new_value).__name__}: {new_value}"
143
+ )
144
+
145
+ return func(instance, new_value)
146
+
147
+ return wrapper
148
+
149
+
122
150
  class Filter(ABC):
123
151
  """Represents a filter."""
124
152
 
125
- __slots__ = ("_callback", "_value")
153
+ __slots__ = ("_callback",)
126
154
 
127
- _callback: Callback
128
- _value: Any
155
+ _callback: EventCallback
129
156
 
130
- def __init__(self, callback: Callback) -> None:
157
+ def __init__(self, callback: EventCallback) -> None:
131
158
  """Initialize a new filter."""
132
159
  self._callback = callback
133
- self._value = UNDEFINED
134
160
 
135
161
  def __hash__(self) -> int:
136
162
  """Return a hash of the filter based on its callback."""
@@ -160,12 +186,14 @@ class _Aggregate(Filter):
160
186
 
161
187
  __slots__ = ("_values", "_sample_size", "_timeout", "_last_call_time")
162
188
 
163
- _values: list[float | int | Decimal]
189
+ _values: list[_Numeric]
164
190
  _sample_size: int
165
191
  _timeout: float
166
192
  _last_call_time: float
167
193
 
168
- def __init__(self, callback: Callback, seconds: float, sample_size: int) -> None:
194
+ def __init__(
195
+ self, callback: EventCallback, seconds: float, sample_size: int
196
+ ) -> None:
169
197
  """Initialize a new aggregate filter."""
170
198
  super().__init__(callback)
171
199
  self._last_call_time = time.monotonic()
@@ -173,14 +201,9 @@ class _Aggregate(Filter):
173
201
  self._sample_size = sample_size
174
202
  self._values = []
175
203
 
176
- async def __call__(self, new_value: Any) -> Any:
204
+ @numeric_only
205
+ async def __call__(self, new_value: _Numeric) -> Any:
177
206
  """Set a new value for the callback."""
178
- if not isinstance(new_value, float | int | Decimal):
179
- raise TypeError(
180
- "Aggregate filter can only be used with numeric values, got "
181
- f"{type(new_value).__name__}: {new_value}"
182
- )
183
-
184
207
  current_time = time.monotonic()
185
208
  self._values.append(new_value)
186
209
  time_since_call = current_time - self._last_call_time
@@ -194,7 +217,7 @@ class _Aggregate(Filter):
194
217
  return result
195
218
 
196
219
 
197
- def aggregate(callback: Callback, seconds: float, sample_size: int) -> _Aggregate:
220
+ def aggregate(callback: EventCallback, seconds: float, sample_size: int) -> _Aggregate:
198
221
  """Create a new aggregate filter.
199
222
 
200
223
  A callback function will be called with a sum of values collected
@@ -203,7 +226,7 @@ def aggregate(callback: Callback, seconds: float, sample_size: int) -> _Aggregat
203
226
 
204
227
  :param callback: A callback function to be awaited once filter
205
228
  conditions are fulfilled
206
- :type callback: Callback
229
+ :type callback: EventCallback
207
230
  :param seconds: A callback will be awaited with a sum of values
208
231
  aggregated over this amount of seconds.
209
232
  :type seconds: float
@@ -222,19 +245,34 @@ class _Clamp(Filter):
222
245
  Calls callback with a value clamped between specified boundaries.
223
246
  """
224
247
 
225
- __slots__ = ("_min_value", "_max_value")
248
+ __slots__ = ("_min_value", "_max_value", "_ignore_out_of_range")
226
249
 
227
250
  _min_value: float
228
251
  _max_value: float
229
-
230
- def __init__(self, callback: Callback, min_value: float, max_value: float) -> None:
252
+ _ignore_out_of_range: bool
253
+
254
+ def __init__(
255
+ self,
256
+ callback: EventCallback,
257
+ min_value: float,
258
+ max_value: float,
259
+ *,
260
+ ignore_out_of_range: bool = False,
261
+ ) -> None:
231
262
  """Initialize a new Clamp filter."""
232
263
  super().__init__(callback)
233
264
  self._min_value = min_value
234
265
  self._max_value = max_value
266
+ self._ignore_out_of_range = ignore_out_of_range
235
267
 
236
- async def __call__(self, new_value: Any) -> Any:
268
+ @numeric_only
269
+ async def __call__(self, new_value: _Numeric) -> Any:
237
270
  """Set a new value for the callback."""
271
+ if self._ignore_out_of_range and (
272
+ new_value < self._min_value or new_value > self._max_value
273
+ ):
274
+ return
275
+
238
276
  if new_value < self._min_value:
239
277
  return await self._callback(self._min_value)
240
278
 
@@ -244,25 +282,36 @@ class _Clamp(Filter):
244
282
  return await self._callback(new_value)
245
283
 
246
284
 
247
- def clamp(callback: Callback, min_value: float, max_value: float) -> _Clamp:
285
+ def clamp(
286
+ callback: EventCallback,
287
+ min_value: float,
288
+ max_value: float,
289
+ *,
290
+ ignore_out_of_range: bool = False,
291
+ ) -> _Clamp:
248
292
  """Create a new clamp filter.
249
293
 
250
294
  A callback function will be called and passed value clamped
251
295
  between specified boundaries.
252
296
 
253
297
  :param callback: A callback function to be awaited on new value
254
- :type callback: Callback
298
+ :type callback: EventCallback
255
299
  :param min_value: A lower boundary
256
300
  :type min_value: float
257
301
  :param max_value: An upper boundary
258
302
  :type max_value: float
303
+ :param ignore_out_of_range: If `True`, values outside of
304
+ specified boundaries will be ignored, defaults to `False`
305
+ :type ignore_out_of_range: bool, optional
259
306
  :return: An instance of callable filter
260
307
  :rtype: _Clamp
261
308
  """
262
- return _Clamp(callback, min_value, max_value)
309
+ return _Clamp(
310
+ callback, min_value, max_value, ignore_out_of_range=ignore_out_of_range
311
+ )
263
312
 
264
313
 
265
- _FilterT: TypeAlias = Callable[[Any], bool]
314
+ _FilterFunc: TypeAlias = Callable[[Any], bool]
266
315
 
267
316
 
268
317
  class _Custom(Filter):
@@ -273,22 +322,22 @@ class _Custom(Filter):
273
322
  returns true.
274
323
  """
275
324
 
276
- __slots__ = ("_filter_fn",)
325
+ __slots__ = ("_filter_func",)
277
326
 
278
- _filter_fn: _FilterT
327
+ _filter_func: _FilterFunc
279
328
 
280
- def __init__(self, callback: Callback, filter_fn: _FilterT) -> None:
329
+ def __init__(self, callback: EventCallback, filter_func: _FilterFunc) -> None:
281
330
  """Initialize a new custom filter."""
282
331
  super().__init__(callback)
283
- self._filter_fn = filter_fn
332
+ self._filter_func = filter_func
284
333
 
285
334
  async def __call__(self, new_value: Any) -> Any:
286
335
  """Set a new value for the callback."""
287
- if self._filter_fn(new_value):
336
+ if self._filter_func(new_value):
288
337
  await self._callback(new_value)
289
338
 
290
339
 
291
- def custom(callback: Callback, filter_fn: _FilterT) -> _Custom:
340
+ def custom(callback: EventCallback, filter_func: _FilterFunc) -> _Custom:
292
341
  """Create a new custom filter.
293
342
 
294
343
  A callback function will be called when a user-defined filter
@@ -298,16 +347,93 @@ def custom(callback: Callback, filter_fn: _FilterT) -> _Custom:
298
347
  :param callback: A callback function to be awaited when
299
348
  filter function return true
300
349
  :type callback: Callback
301
- :param filter_fn: Filter function, that will be called with a
350
+ :param filter_func: Filter function, that will be called with a
302
351
  value and should return `True` to await filter's callback
303
- :type filter_fn: Callable[[Any], bool]
352
+ :type filter_func: Callable[[Any], bool]
304
353
  :return: An instance of callable filter
305
354
  :rtype: _Custom
306
355
  """
307
- return _Custom(callback, filter_fn)
356
+ return _Custom(callback, filter_func)
357
+
358
+
359
+ class _Throttle(Filter):
360
+ """Represents a throttle filter.
361
+
362
+ Calls a callback only when certain amount of seconds passed
363
+ since the last call.
364
+ """
365
+
366
+ __slots__ = ("_last_called", "_timeout")
367
+
368
+ _last_called: float | None
369
+ _timeout: float
370
+
371
+ def __init__(self, callback: EventCallback, seconds: float) -> None:
372
+ """Initialize a new throttle filter."""
373
+ super().__init__(callback)
374
+ self._last_called = None
375
+ self._timeout = seconds
376
+
377
+ async def __call__(self, new_value: Any) -> Any:
378
+ """Set a new value for the callback."""
379
+ current_timestamp = time.monotonic()
380
+ if (
381
+ self._last_called is None
382
+ or (current_timestamp - self._last_called) >= self._timeout
383
+ ):
384
+ self._last_called = current_timestamp
385
+ return await self._callback(new_value)
386
+
387
+
388
+ def throttle(callback: EventCallback, seconds: float) -> _Throttle:
389
+ """Create a new throttle filter.
390
+
391
+ A callback function will only be called once a certain amount of
392
+ seconds passed since the last call.
393
+
394
+ :param callback: A callback function that will be awaited once
395
+ filter conditions are fulfilled
396
+ :type callback: EventCallback
397
+ :param seconds: A callback will be awaited at most once per
398
+ this amount of seconds
399
+ :type seconds: float
400
+ :return: An instance of callable filter
401
+ :rtype: _Throttle
402
+ """
403
+ return _Throttle(callback, seconds)
308
404
 
309
405
 
310
- class _Deadband(Filter):
406
+ class ComparisonFilter(Filter):
407
+ """Represents a filter that compares current and previous values."""
408
+
409
+ __slots__ = ("_value",)
410
+
411
+ _value: Any
412
+
413
+ def __init__(self, callback: EventCallback) -> None:
414
+ """Initialize a new comparison filter."""
415
+ self._value = UNDEFINED
416
+ super().__init__(callback)
417
+
418
+ def is_undefined(self) -> bool:
419
+ """Check if current value is undefined."""
420
+ return True if self._value == UNDEFINED else False
421
+
422
+ @property
423
+ def value(self) -> Any:
424
+ """Return filter value."""
425
+ return self._value
426
+
427
+ @value.setter
428
+ def value(self, value: Any) -> None:
429
+ """Set filter value."""
430
+ if isinstance(value, Parameter):
431
+ self._value = copy(value)
432
+ else:
433
+ self._value = value
434
+
435
+
436
+ class _Deadband(ComparisonFilter):
311
437
  """Represents a deadband filter.
312
438
 
313
439
  Calls a callback only when value is significantly changed from the
@@ -318,34 +444,29 @@ class _Deadband(Filter):
318
444
 
319
445
  _tolerance: float
320
446
 
321
- def __init__(self, callback: Callback, tolerance: float) -> None:
447
+ def __init__(self, callback: EventCallback, tolerance: float) -> None:
322
448
  """Initialize a new value changed filter."""
323
449
  self._tolerance = tolerance
324
450
  super().__init__(callback)
325
451
 
326
- async def __call__(self, new_value: Any) -> Any:
452
+ @numeric_only
453
+ async def __call__(self, new_value: _Numeric) -> Any:
327
454
  """Set a new value for the callback."""
328
- if not isinstance(new_value, float | int | Decimal):
329
- raise TypeError(
330
- "Deadband filter can only be used with numeric values, got "
331
- f"{type(new_value).__name__}: {new_value}"
332
- )
333
-
334
- if self._value == UNDEFINED or is_close(
335
- self._value, new_value, tolerance=self._tolerance
455
+ if self.is_undefined() or not is_close(
456
+ self.value, new_value, tolerance=self._tolerance
336
457
  ):
337
- self._value = new_value
458
+ self.value = new_value
338
459
  return await self._callback(new_value)
339
460
 
340
461
 
341
- def deadband(callback: Callback, tolerance: float) -> _Deadband:
462
+ def deadband(callback: EventCallback, tolerance: float) -> _Deadband:
342
463
  """Create a new deadband filter.
343
464
 
344
465
  A callback function will only be called when the value is significantly changed
345
466
  from the previous callback call.
346
467
 
347
468
  :param callback: A callback function to be awaited on significant value change
348
- :type callback: Callback
469
+ :type callback: EventCallback
349
470
  :param tolerance: The minimum difference required to trigger the callback
350
471
  :type tolerance: float
351
472
  :return: An instance of callable filter
@@ -354,7 +475,7 @@ def deadband(callback: Callback, tolerance: float) -> _Deadband:
354
475
  return _Deadband(callback, tolerance)
355
476
 
356
477
 
357
- class _Debounce(Filter):
478
+ class _Debounce(ComparisonFilter):
358
479
  """Represents a debounce filter.
359
480
 
360
481
  Calls a callback only when value is stabilized across multiple
@@ -366,7 +487,7 @@ class _Debounce(Filter):
366
487
  _calls: int
367
488
  _min_calls: int
368
489
 
369
- def __init__(self, callback: Callback, min_calls: int) -> None:
490
+ def __init__(self, callback: EventCallback, min_calls: int) -> None:
370
491
  """Initialize a new debounce filter."""
371
492
  super().__init__(callback)
372
493
  self._calls = 0
@@ -374,27 +495,25 @@ class _Debounce(Filter):
374
495
 
375
496
  async def __call__(self, new_value: Any) -> Any:
376
497
  """Set a new value for the callback."""
377
- if self._value == UNDEFINED or is_close(self._value, new_value):
498
+ if self.is_undefined() or not is_close(self.value, new_value):
378
499
  self._calls += 1
379
500
  else:
380
501
  self._calls = 0
381
502
 
382
- if self._value == UNDEFINED or self._calls >= self._min_calls:
383
- self._value = (
384
- copy(new_value) if isinstance(new_value, Parameter) else new_value
385
- )
503
+ if self.is_undefined() or self._calls >= self._min_calls:
504
+ self.value = new_value
386
505
  self._calls = 0
387
506
  return await self._callback(new_value)
388
507
 
389
508
 
390
- def debounce(callback: Callback, min_calls: int) -> _Debounce:
509
+ def debounce(callback: EventCallback, min_calls: int) -> _Debounce:
391
510
  """Create a new debounce filter.
392
511
 
393
512
  A callback function will only be called once the value is stabilized
394
513
  across multiple filter calls.
395
514
 
396
515
  :param callback: A callback function to be awaited on value change
397
- :type callback: Callback
516
+ :type callback: EventCallback
398
517
  :param min_calls: Value shouldn't change for this amount of
399
518
  filter calls
400
519
  :type min_calls: int
@@ -404,7 +523,7 @@ def debounce(callback: Callback, min_calls: int) -> _Debounce:
404
523
  return _Debounce(callback, min_calls)
405
524
 
406
525
 
407
- class _Delta(Filter):
526
+ class _Delta(ComparisonFilter):
408
527
  """Represents a difference filter.
409
528
 
410
529
  Calls a callback with a difference between two subsequent values.
@@ -414,19 +533,14 @@ class _Delta(Filter):
414
533
 
415
534
  async def __call__(self, new_value: Any) -> Any:
416
535
  """Set a new value for the callback."""
417
- if self._value == UNDEFINED or is_close(self._value, new_value):
418
- old_value = self._value
419
- self._value = (
420
- copy(new_value) if isinstance(new_value, Parameter) else new_value
421
- )
422
- if (
423
- self._value != UNDEFINED
424
- and (difference := diffence_between(old_value, new_value)) is not None
425
- ):
536
+ if self.is_undefined() or not is_close(self.value, new_value):
537
+ old_value = self.value
538
+ self.value = new_value
539
+ if (difference := diffence_between(old_value, new_value)) is not None:
426
540
  return await self._callback(difference)
427
541
 
428
542
 
429
- def delta(callback: Callback) -> _Delta:
543
+ def delta(callback: EventCallback) -> _Delta:
430
544
  """Create a new difference filter.
431
545
 
432
546
  A callback function will be called with a difference between two
@@ -434,14 +548,14 @@ def delta(callback: Callback) -> _Delta:
434
548
 
435
549
  :param callback: A callback function that will be awaited with
436
550
  difference between values in two subsequent calls
437
- :type callback: Callback
551
+ :type callback: EventCallback
438
552
  :return: An instance of callable filter
439
553
  :rtype: _Delta
440
554
  """
441
555
  return _Delta(callback)
442
556
 
443
557
 
444
- class _OnChange(Filter):
558
+ class _OnChange(ComparisonFilter):
445
559
  """Represents a value changed filter.
446
560
 
447
561
  Calls a callback only when value is changed from the
@@ -450,20 +564,14 @@ class _OnChange(Filter):
450
564
 
451
565
  __slots__ = ()
452
566
 
453
- def __init__(self, callback: Callback) -> None:
454
- """Initialize a new value changed filter."""
455
- super().__init__(callback)
456
-
457
567
  async def __call__(self, new_value: Any) -> Any:
458
568
  """Set a new value for the callback."""
459
- if self._value == UNDEFINED or is_close(self._value, new_value):
460
- self._value = (
461
- copy(new_value) if isinstance(new_value, Parameter) else new_value
462
- )
569
+ if self.is_undefined() or not is_close(self.value, new_value):
570
+ self.value = new_value
463
571
  return await self._callback(new_value)
464
572
 
465
573
 
466
- def on_change(callback: Callback) -> _OnChange:
574
+ def on_change(callback: EventCallback) -> _OnChange:
467
575
  """Create a new value changed filter.
468
576
 
469
577
  A callback function will only be called if the value is changed from the
@@ -477,55 +585,10 @@ def on_change(callback: Callback) -> _OnChange:
477
585
  return _OnChange(callback)
478
586
 
479
587
 
480
- class _Throttle(Filter):
481
- """Represents a throttle filter.
482
-
483
- Calls a callback only when certain amount of seconds passed
484
- since the last call.
485
- """
486
-
487
- __slots__ = ("_last_called", "_timeout")
488
-
489
- _last_called: float | None
490
- _timeout: float
491
-
492
- def __init__(self, callback: Callback, seconds: float) -> None:
493
- """Initialize a new throttle filter."""
494
- super().__init__(callback)
495
- self._last_called = None
496
- self._timeout = seconds
497
-
498
- async def __call__(self, new_value: Any) -> Any:
499
- """Set a new value for the callback."""
500
- current_timestamp = time.monotonic()
501
- if (
502
- self._last_called is None
503
- or (current_timestamp - self._last_called) >= self._timeout
504
- ):
505
- self._last_called = current_timestamp
506
- return await self._callback(new_value)
507
-
508
-
509
- def throttle(callback: Callback, seconds: float) -> _Throttle:
510
- """Create a new throttle filter.
511
-
512
- A callback function will only be called once a certain amount of
513
- seconds passed since the last call.
514
-
515
- :param callback: A callback function that will be awaited once
516
- filter conditions are fulfilled
517
- :type callback: Callback
518
- :param seconds: A callback will be awaited at most once per
519
- this amount of seconds
520
- :type seconds: float
521
- :return: An instance of callable filter
522
- :rtype: _Throttle
523
- """
524
- return _Throttle(callback, seconds)
525
-
526
-
527
588
  __all__ = [
528
589
  "Filter",
590
+ "ComparisonFilter",
591
+ "numeric_only",
529
592
  "aggregate",
530
593
  "clamp",
531
594
  "custom",