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.
- pyplumio/__init__.py +3 -2
- pyplumio/_version.py +2 -2
- pyplumio/connection.py +14 -14
- pyplumio/const.py +8 -3
- pyplumio/{helpers/data_types.py → data_types.py} +23 -21
- pyplumio/devices/__init__.py +42 -41
- pyplumio/devices/ecomax.py +202 -174
- pyplumio/devices/ecoster.py +5 -0
- pyplumio/devices/mixer.py +24 -34
- pyplumio/devices/thermostat.py +24 -31
- pyplumio/filters.py +188 -147
- pyplumio/frames/__init__.py +20 -8
- pyplumio/frames/messages.py +3 -0
- pyplumio/frames/requests.py +21 -0
- pyplumio/frames/responses.py +18 -0
- pyplumio/helpers/async_cache.py +48 -0
- pyplumio/helpers/event_manager.py +58 -3
- pyplumio/helpers/factory.py +5 -2
- pyplumio/helpers/schedule.py +8 -5
- pyplumio/helpers/task_manager.py +3 -0
- pyplumio/helpers/timeout.py +7 -6
- pyplumio/helpers/uid.py +8 -5
- pyplumio/{helpers/parameter.py → parameters/__init__.py} +105 -5
- pyplumio/parameters/ecomax.py +868 -0
- pyplumio/parameters/mixer.py +245 -0
- pyplumio/parameters/thermostat.py +197 -0
- pyplumio/protocol.py +21 -10
- pyplumio/stream.py +3 -0
- pyplumio/structures/__init__.py +3 -0
- pyplumio/structures/alerts.py +9 -6
- pyplumio/structures/boiler_load.py +3 -0
- pyplumio/structures/boiler_power.py +4 -1
- pyplumio/structures/ecomax_parameters.py +6 -800
- pyplumio/structures/fan_power.py +4 -1
- pyplumio/structures/frame_versions.py +4 -1
- pyplumio/structures/fuel_consumption.py +4 -1
- pyplumio/structures/fuel_level.py +3 -0
- pyplumio/structures/lambda_sensor.py +9 -1
- pyplumio/structures/mixer_parameters.py +8 -230
- pyplumio/structures/mixer_sensors.py +10 -1
- pyplumio/structures/modules.py +14 -0
- pyplumio/structures/network_info.py +12 -1
- pyplumio/structures/output_flags.py +10 -1
- pyplumio/structures/outputs.py +22 -1
- pyplumio/structures/pending_alerts.py +3 -0
- pyplumio/structures/product_info.py +6 -3
- pyplumio/structures/program_version.py +3 -0
- pyplumio/structures/regulator_data.py +5 -2
- pyplumio/structures/regulator_data_schema.py +4 -1
- pyplumio/structures/schedules.py +18 -1
- pyplumio/structures/statuses.py +9 -0
- pyplumio/structures/temperatures.py +23 -1
- pyplumio/structures/thermostat_parameters.py +18 -184
- pyplumio/structures/thermostat_sensors.py +10 -1
- pyplumio/utils.py +14 -12
- {pyplumio-0.5.42.dist-info → pyplumio-0.5.44.dist-info}/METADATA +32 -17
- pyplumio-0.5.44.dist-info/RECORD +64 -0
- {pyplumio-0.5.42.dist-info → pyplumio-0.5.44.dist-info}/WHEEL +1 -1
- pyplumio-0.5.42.dist-info/RECORD +0 -60
- {pyplumio-0.5.42.dist-info → pyplumio-0.5.44.dist-info}/licenses/LICENSE +0 -0
- {pyplumio-0.5.42.dist-info → pyplumio-0.5.44.dist-info}/top_level.txt +0 -0
pyplumio/devices/thermostat.py
CHANGED
@@ -3,59 +3,49 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
5
|
import asyncio
|
6
|
-
from collections.abc import Coroutine, Generator
|
7
|
-
|
6
|
+
from collections.abc import Coroutine, Generator
|
7
|
+
import logging
|
8
|
+
from typing import Any
|
8
9
|
|
9
|
-
from pyplumio.devices import
|
10
|
-
from pyplumio.helpers.
|
11
|
-
from pyplumio.
|
12
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
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
|
-
|
47
|
-
|
37
|
+
@event_listener
|
38
|
+
async def on_event_thermostat_parameters(
|
39
|
+
self, parameters: list[tuple[int, ParameterValues]]
|
48
40
|
) -> bool:
|
49
|
-
"""
|
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 =
|
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.
|
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
|
68
|
+
def is_close(old: Parameter, new: Parameter) -> bool: ...
|
54
69
|
|
55
70
|
|
56
71
|
@overload
|
57
|
-
def
|
72
|
+
def is_close(old: SupportsFloat, new: SupportsFloat) -> bool: ...
|
58
73
|
|
59
74
|
|
60
75
|
@overload
|
61
|
-
def
|
62
|
-
old: SupportsComparison, new: SupportsComparison
|
63
|
-
) -> bool: ...
|
76
|
+
def is_close(old: SupportsComparison, new: SupportsComparison) -> bool: ...
|
64
77
|
|
65
78
|
|
66
|
-
def
|
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
|
91
|
+
def diffence_between(old: list, new: list) -> list: ...
|
79
92
|
|
80
93
|
|
81
94
|
@overload
|
82
|
-
def
|
95
|
+
def diffence_between(
|
83
96
|
old: SupportsSubtraction, new: SupportsSubtraction
|
84
97
|
) -> SupportsSubtraction: ...
|
85
98
|
|
86
99
|
|
87
|
-
def
|
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
|
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
|
-
"""
|
234
|
+
"""Create a new clamp filter.
|
158
235
|
|
159
|
-
A callback function will be called
|
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
|
-
|
175
|
-
"""Represents a value changed filter.
|
251
|
+
_FilterT: TypeAlias = Callable[[Any], bool]
|
176
252
|
|
177
|
-
|
178
|
-
|
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.
|
186
|
-
self.
|
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
|
193
|
-
"""
|
277
|
+
def custom(callback: Callback, filter_fn: _FilterT) -> _Custom:
|
278
|
+
"""Create a new custom filter.
|
194
279
|
|
195
|
-
A callback function will
|
196
|
-
|
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
|
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:
|
291
|
+
:rtype: _Custom
|
202
292
|
"""
|
203
|
-
return
|
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
|
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
|
-
"""
|
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
|
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 :=
|
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
|
-
"""
|
369
|
+
"""Create a new difference filter.
|
327
370
|
|
328
371
|
A callback function will be called with a difference between two
|
329
|
-
subsequent
|
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
|
341
|
-
"""Represents
|
383
|
+
class _OnChange(Filter):
|
384
|
+
"""Represents a value changed filter.
|
342
385
|
|
343
|
-
Calls a callback
|
344
|
-
|
386
|
+
Calls a callback only when value is changed from the
|
387
|
+
previous callback call.
|
345
388
|
"""
|
346
389
|
|
347
|
-
__slots__ = (
|
348
|
-
|
349
|
-
_sum: complex
|
350
|
-
_last_update: float
|
351
|
-
_timeout: float
|
390
|
+
__slots__ = ()
|
352
391
|
|
353
|
-
def __init__(self, callback: Callback
|
354
|
-
"""Initialize a new
|
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
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
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
|
378
|
-
"""
|
405
|
+
def on_change(callback: Callback) -> _OnChange:
|
406
|
+
"""Create a new value changed filter.
|
379
407
|
|
380
|
-
A callback function will be called
|
381
|
-
|
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
|
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:
|
414
|
+
:rtype: _OnChange
|
391
415
|
"""
|
392
|
-
return
|
416
|
+
return _OnChange(callback)
|
393
417
|
|
394
418
|
|
395
|
-
class
|
396
|
-
"""Represents a
|
419
|
+
class _Throttle(Filter):
|
420
|
+
"""Represents a throttle filter.
|
397
421
|
|
398
|
-
Calls a callback
|
399
|
-
|
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__ = ("
|
426
|
+
__slots__ = ("_last_called", "_timeout")
|
404
427
|
|
405
|
-
|
428
|
+
_last_called: float | None
|
429
|
+
_timeout: float
|
406
430
|
|
407
|
-
def __init__(self, callback: Callback,
|
408
|
-
"""Initialize a new
|
431
|
+
def __init__(self, callback: Callback, seconds: float) -> None:
|
432
|
+
"""Initialize a new throttle filter."""
|
409
433
|
super().__init__(callback)
|
410
|
-
self.
|
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
|
-
|
415
|
-
|
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
|
419
|
-
"""
|
448
|
+
def throttle(callback: Callback, seconds: float) -> _Throttle:
|
449
|
+
"""Create a new throttle filter.
|
420
450
|
|
421
|
-
A callback function will be called
|
422
|
-
|
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
|
426
|
-
filter
|
454
|
+
:param callback: A callback function that will be awaited once
|
455
|
+
filter conditions are fulfilled
|
427
456
|
:type callback: Callback
|
428
|
-
:param
|
429
|
-
|
430
|
-
:type
|
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:
|
461
|
+
:rtype: _Throttle
|
433
462
|
"""
|
434
|
-
return
|
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
|
+
]
|
pyplumio/frames/__init__.py
CHANGED
@@ -30,9 +30,9 @@ if TYPE_CHECKING:
|
|
30
30
|
from pyplumio.devices import PhysicalDevice
|
31
31
|
|
32
32
|
|
33
|
-
def bcc(
|
33
|
+
def bcc(buffer: bytes) -> int:
|
34
34
|
"""Return a block check character."""
|
35
|
-
return reduce(lambda x, y: x ^ y,
|
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
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
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
|
+
]
|
pyplumio/frames/messages.py
CHANGED
pyplumio/frames/requests.py
CHANGED
@@ -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
|
+
]
|