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.
- pyplumio/__init__.py +3 -1
- pyplumio/_version.py +2 -2
- pyplumio/const.py +0 -5
- pyplumio/data_types.py +2 -2
- pyplumio/devices/__init__.py +23 -5
- pyplumio/devices/ecomax.py +30 -53
- pyplumio/devices/ecoster.py +2 -3
- pyplumio/filters.py +199 -136
- pyplumio/frames/__init__.py +101 -15
- pyplumio/frames/messages.py +8 -65
- pyplumio/frames/requests.py +38 -38
- pyplumio/frames/responses.py +30 -86
- pyplumio/helpers/async_cache.py +13 -8
- pyplumio/helpers/event_manager.py +24 -18
- pyplumio/helpers/factory.py +0 -3
- pyplumio/parameters/__init__.py +38 -35
- pyplumio/protocol.py +14 -8
- pyplumio/structures/alerts.py +2 -2
- pyplumio/structures/ecomax_parameters.py +1 -1
- pyplumio/structures/frame_versions.py +3 -2
- pyplumio/structures/mixer_parameters.py +5 -3
- pyplumio/structures/network_info.py +1 -0
- pyplumio/structures/product_info.py +1 -1
- pyplumio/structures/program_version.py +2 -2
- pyplumio/structures/schedules.py +8 -40
- pyplumio/structures/sensor_data.py +498 -0
- pyplumio/structures/thermostat_parameters.py +7 -4
- pyplumio/utils.py +41 -4
- {pyplumio-0.6.1.dist-info → pyplumio-0.6.2.dist-info}/METADATA +4 -4
- pyplumio-0.6.2.dist-info/RECORD +50 -0
- pyplumio/structures/boiler_load.py +0 -32
- pyplumio/structures/boiler_power.py +0 -33
- pyplumio/structures/fan_power.py +0 -33
- pyplumio/structures/fuel_consumption.py +0 -36
- pyplumio/structures/fuel_level.py +0 -39
- pyplumio/structures/lambda_sensor.py +0 -57
- pyplumio/structures/mixer_sensors.py +0 -80
- pyplumio/structures/modules.py +0 -102
- pyplumio/structures/output_flags.py +0 -47
- pyplumio/structures/outputs.py +0 -88
- pyplumio/structures/pending_alerts.py +0 -28
- pyplumio/structures/statuses.py +0 -52
- pyplumio/structures/temperatures.py +0 -94
- pyplumio/structures/thermostat_sensors.py +0 -106
- pyplumio-0.6.1.dist-info/RECORD +0 -63
- {pyplumio-0.6.1.dist-info → pyplumio-0.6.2.dist-info}/WHEEL +0 -0
- {pyplumio-0.6.1.dist-info → pyplumio-0.6.2.dist-info}/licenses/LICENSE +0 -0
- {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
|
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
|
-
|
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:
|
88
|
+
old: _ComparableT, new: _ComparableT, *, tolerance: float | None = DEFAULT_TOLERANCE
|
88
89
|
) -> bool:
|
89
90
|
"""Check if value is significantly changed."""
|
90
|
-
if isinstance(
|
91
|
-
return
|
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
|
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.
|
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",
|
153
|
+
__slots__ = ("_callback",)
|
126
154
|
|
127
|
-
_callback:
|
128
|
-
_value: Any
|
155
|
+
_callback: EventCallback
|
129
156
|
|
130
|
-
def __init__(self, callback:
|
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[
|
189
|
+
_values: list[_Numeric]
|
164
190
|
_sample_size: int
|
165
191
|
_timeout: float
|
166
192
|
_last_call_time: float
|
167
193
|
|
168
|
-
def __init__(
|
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
|
-
|
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:
|
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:
|
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
|
-
|
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
|
-
|
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(
|
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:
|
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(
|
309
|
+
return _Clamp(
|
310
|
+
callback, min_value, max_value, ignore_out_of_range=ignore_out_of_range
|
311
|
+
)
|
263
312
|
|
264
313
|
|
265
|
-
|
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__ = ("
|
325
|
+
__slots__ = ("_filter_func",)
|
277
326
|
|
278
|
-
|
327
|
+
_filter_func: _FilterFunc
|
279
328
|
|
280
|
-
def __init__(self, callback:
|
329
|
+
def __init__(self, callback: EventCallback, filter_func: _FilterFunc) -> None:
|
281
330
|
"""Initialize a new custom filter."""
|
282
331
|
super().__init__(callback)
|
283
|
-
self.
|
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.
|
336
|
+
if self._filter_func(new_value):
|
288
337
|
await self._callback(new_value)
|
289
338
|
|
290
339
|
|
291
|
-
def custom(callback:
|
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
|
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
|
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,
|
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
|
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:
|
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
|
-
|
452
|
+
@numeric_only
|
453
|
+
async def __call__(self, new_value: _Numeric) -> Any:
|
327
454
|
"""Set a new value for the callback."""
|
328
|
-
if
|
329
|
-
|
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.
|
458
|
+
self.value = new_value
|
338
459
|
return await self._callback(new_value)
|
339
460
|
|
340
461
|
|
341
|
-
def deadband(callback:
|
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:
|
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(
|
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:
|
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.
|
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.
|
383
|
-
self.
|
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:
|
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:
|
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(
|
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.
|
418
|
-
old_value = self.
|
419
|
-
self.
|
420
|
-
|
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:
|
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:
|
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(
|
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.
|
460
|
-
self.
|
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:
|
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",
|