PyPlumIO 0.5.21__py3-none-any.whl → 0.5.23__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-0.5.21.dist-info → PyPlumIO-0.5.23.dist-info}/METADATA +12 -10
- PyPlumIO-0.5.23.dist-info/RECORD +60 -0
- {PyPlumIO-0.5.21.dist-info → PyPlumIO-0.5.23.dist-info}/WHEEL +1 -1
- pyplumio/__init__.py +2 -2
- pyplumio/_version.py +2 -2
- pyplumio/connection.py +3 -12
- pyplumio/devices/__init__.py +16 -16
- pyplumio/devices/ecomax.py +126 -126
- pyplumio/devices/mixer.py +50 -44
- pyplumio/devices/thermostat.py +36 -35
- pyplumio/exceptions.py +9 -9
- pyplumio/filters.py +56 -37
- pyplumio/frames/__init__.py +6 -6
- pyplumio/frames/messages.py +4 -6
- pyplumio/helpers/data_types.py +8 -7
- pyplumio/helpers/event_manager.py +53 -33
- pyplumio/helpers/parameter.py +138 -52
- pyplumio/helpers/task_manager.py +7 -2
- pyplumio/helpers/timeout.py +0 -3
- pyplumio/helpers/uid.py +2 -2
- pyplumio/protocol.py +35 -28
- pyplumio/stream.py +2 -2
- pyplumio/structures/alerts.py +40 -31
- pyplumio/structures/ecomax_parameters.py +493 -282
- pyplumio/structures/frame_versions.py +5 -6
- pyplumio/structures/lambda_sensor.py +6 -6
- pyplumio/structures/mixer_parameters.py +136 -71
- pyplumio/structures/network_info.py +2 -3
- pyplumio/structures/product_info.py +0 -4
- pyplumio/structures/program_version.py +24 -17
- pyplumio/structures/schedules.py +35 -15
- pyplumio/structures/thermostat_parameters.py +82 -50
- pyplumio/utils.py +12 -7
- PyPlumIO-0.5.21.dist-info/RECORD +0 -61
- pyplumio/helpers/typing.py +0 -29
- {PyPlumIO-0.5.21.dist-info → PyPlumIO-0.5.23.dist-info}/LICENSE +0 -0
- {PyPlumIO-0.5.21.dist-info → PyPlumIO-0.5.23.dist-info}/top_level.txt +0 -0
pyplumio/devices/thermostat.py
CHANGED
@@ -3,43 +3,47 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
5
|
import asyncio
|
6
|
-
from collections.abc import Sequence
|
7
|
-
from typing import Any
|
6
|
+
from collections.abc import Coroutine, Generator, Sequence
|
7
|
+
from typing import TYPE_CHECKING, Any
|
8
8
|
|
9
9
|
from pyplumio.devices import AddressableDevice, SubDevice
|
10
10
|
from pyplumio.helpers.parameter import ParameterValues
|
11
11
|
from pyplumio.structures.thermostat_parameters import (
|
12
12
|
ATTR_THERMOSTAT_PARAMETERS,
|
13
13
|
THERMOSTAT_PARAMETERS,
|
14
|
-
|
15
|
-
|
16
|
-
|
14
|
+
ThermostatNumber,
|
15
|
+
ThermostatSwitch,
|
16
|
+
ThermostatSwitchDescription,
|
17
17
|
)
|
18
18
|
from pyplumio.structures.thermostat_sensors import ATTR_THERMOSTAT_SENSORS
|
19
19
|
|
20
|
+
if TYPE_CHECKING:
|
21
|
+
from pyplumio.frames import Frame
|
22
|
+
|
20
23
|
|
21
24
|
class Thermostat(SubDevice):
|
22
25
|
"""Represents a thermostat."""
|
23
26
|
|
24
|
-
def __init__(
|
27
|
+
def __init__(
|
28
|
+
self, queue: asyncio.Queue[Frame], parent: AddressableDevice, index: int = 0
|
29
|
+
):
|
25
30
|
"""Initialize a new thermostat."""
|
26
31
|
super().__init__(queue, parent, index)
|
27
|
-
self.subscribe(ATTR_THERMOSTAT_SENSORS, self.
|
28
|
-
self.subscribe(ATTR_THERMOSTAT_PARAMETERS, self.
|
32
|
+
self.subscribe(ATTR_THERMOSTAT_SENSORS, self._handle_thermostat_sensors)
|
33
|
+
self.subscribe(ATTR_THERMOSTAT_PARAMETERS, self._handle_thermostat_parameters)
|
29
34
|
|
30
|
-
async def
|
35
|
+
async def _handle_thermostat_sensors(self, sensors: dict[str, Any]) -> bool:
|
31
36
|
"""Handle thermostat sensors.
|
32
37
|
|
33
38
|
For each sensor dispatch an event with the
|
34
39
|
sensor's name and value.
|
35
40
|
"""
|
36
41
|
await asyncio.gather(
|
37
|
-
*
|
42
|
+
*(self.dispatch(name, value) for name, value in sensors.items())
|
38
43
|
)
|
39
|
-
|
40
44
|
return True
|
41
45
|
|
42
|
-
async def
|
46
|
+
async def _handle_thermostat_parameters(
|
43
47
|
self, parameters: Sequence[tuple[int, ParameterValues]]
|
44
48
|
) -> bool:
|
45
49
|
"""Handle thermostat parameters.
|
@@ -47,29 +51,26 @@ class Thermostat(SubDevice):
|
|
47
51
|
For each parameter dispatch an event with the
|
48
52
|
parameter's name and value.
|
49
53
|
"""
|
50
|
-
for index, values in parameters:
|
51
|
-
description = THERMOSTAT_PARAMETERS[index]
|
52
|
-
name = description.name
|
53
|
-
if name in self.data:
|
54
|
-
parameter: ThermostatParameter = self.data[name]
|
55
|
-
parameter.values = values
|
56
|
-
await self.dispatch(name, parameter)
|
57
|
-
continue
|
58
54
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
description
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
55
|
+
def _thermostat_parameter_events() -> Generator[Coroutine, Any, None]:
|
56
|
+
"""Get dispatch calls for thermostat parameter events."""
|
57
|
+
for index, values in parameters:
|
58
|
+
description = THERMOSTAT_PARAMETERS[index]
|
59
|
+
handler = (
|
60
|
+
ThermostatSwitch
|
61
|
+
if isinstance(description, ThermostatSwitchDescription)
|
62
|
+
else ThermostatNumber
|
63
|
+
)
|
64
|
+
yield self.dispatch(
|
65
|
+
description.name,
|
66
|
+
handler.create_or_update(
|
67
|
+
device=self,
|
68
|
+
description=description,
|
69
|
+
values=values,
|
70
|
+
index=index,
|
71
|
+
offset=(self.index * len(parameters)),
|
72
|
+
),
|
73
|
+
)
|
74
74
|
|
75
|
+
await asyncio.gather(*_thermostat_parameter_events())
|
75
76
|
return True
|
pyplumio/exceptions.py
CHANGED
@@ -11,25 +11,25 @@ class ConnectionFailedError(PyPlumIOError):
|
|
11
11
|
"""Raised on connection failure."""
|
12
12
|
|
13
13
|
|
14
|
-
class
|
15
|
-
"""
|
14
|
+
class ProtocolError(PyPlumIOError):
|
15
|
+
"""Base class for protocol-related errors."""
|
16
16
|
|
17
17
|
|
18
|
-
class ReadError(
|
18
|
+
class ReadError(ProtocolError):
|
19
19
|
"""Raised on read error."""
|
20
20
|
|
21
21
|
|
22
|
-
class
|
23
|
-
"""
|
22
|
+
class ChecksumError(ProtocolError):
|
23
|
+
"""Raised on incorrect frame checksum."""
|
24
24
|
|
25
25
|
|
26
|
-
class
|
27
|
-
"""Raised on
|
26
|
+
class UnknownDeviceError(ProtocolError):
|
27
|
+
"""Raised on unknown device."""
|
28
28
|
|
29
29
|
|
30
|
-
class UnknownFrameError(
|
30
|
+
class UnknownFrameError(ProtocolError):
|
31
31
|
"""Raised on unknown frame type."""
|
32
32
|
|
33
33
|
|
34
|
-
class FrameDataError(
|
34
|
+
class FrameDataError(ProtocolError):
|
35
35
|
"""Raised on incorrect frame data."""
|
pyplumio/filters.py
CHANGED
@@ -3,18 +3,49 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
5
|
from abc import ABC, abstractmethod
|
6
|
-
from collections.abc import
|
6
|
+
from collections.abc import Callable
|
7
7
|
from copy import copy
|
8
8
|
import math
|
9
9
|
import time
|
10
|
-
from typing import
|
11
|
-
|
10
|
+
from typing import (
|
11
|
+
Any,
|
12
|
+
Final,
|
13
|
+
Protocol,
|
14
|
+
SupportsFloat,
|
15
|
+
TypeVar,
|
16
|
+
overload,
|
17
|
+
runtime_checkable,
|
18
|
+
)
|
19
|
+
|
20
|
+
from pyplumio.helpers.event_manager import Callback
|
12
21
|
from pyplumio.helpers.parameter import Parameter
|
13
|
-
from pyplumio.helpers.typing import SupportsComparison, SupportsSubtraction
|
14
22
|
|
15
23
|
UNDEFINED: Final = "undefined"
|
16
24
|
TOLERANCE: Final = 0.1
|
17
25
|
|
26
|
+
|
27
|
+
@runtime_checkable
|
28
|
+
class SupportsSubtraction(Protocol):
|
29
|
+
"""Supports subtraction operation."""
|
30
|
+
|
31
|
+
__slots__ = ()
|
32
|
+
|
33
|
+
def __sub__(
|
34
|
+
self: SupportsSubtraction, other: SupportsSubtraction
|
35
|
+
) -> SupportsSubtraction:
|
36
|
+
"""Subtract a value."""
|
37
|
+
|
38
|
+
|
39
|
+
@runtime_checkable
|
40
|
+
class SupportsComparison(Protocol):
|
41
|
+
"""Supports comparison."""
|
42
|
+
|
43
|
+
__slots__ = ()
|
44
|
+
|
45
|
+
def __eq__(self: SupportsComparison, other: SupportsComparison) -> bool:
|
46
|
+
"""Compare a value."""
|
47
|
+
|
48
|
+
|
18
49
|
Comparable = TypeVar("Comparable", Parameter, SupportsFloat, SupportsComparison)
|
19
50
|
|
20
51
|
|
@@ -71,23 +102,23 @@ class Filter(ABC):
|
|
71
102
|
|
72
103
|
__slots__ = ("_callback", "_value")
|
73
104
|
|
74
|
-
_callback:
|
105
|
+
_callback: Callback
|
75
106
|
_value: Any
|
76
107
|
|
77
|
-
def __init__(self, callback:
|
108
|
+
def __init__(self, callback: Callback) -> None:
|
78
109
|
"""Initialize a new filter."""
|
79
110
|
self._callback = callback
|
80
111
|
self._value = UNDEFINED
|
81
112
|
|
82
|
-
def __eq__(self, other: Any) ->
|
113
|
+
def __eq__(self, other: Any) -> bool:
|
83
114
|
"""Compare callbacks."""
|
84
115
|
if isinstance(other, Filter):
|
85
116
|
return self._callback == other._callback
|
86
117
|
|
87
118
|
if callable(other):
|
88
|
-
return self._callback == other
|
119
|
+
return bool(self._callback == other)
|
89
120
|
|
90
|
-
|
121
|
+
return NotImplemented
|
91
122
|
|
92
123
|
@abstractmethod
|
93
124
|
async def __call__(self, new_value: Any) -> Any:
|
@@ -112,14 +143,14 @@ class _OnChange(Filter):
|
|
112
143
|
return await self._callback(new_value)
|
113
144
|
|
114
145
|
|
115
|
-
def on_change(callback:
|
146
|
+
def on_change(callback: Callback) -> _OnChange:
|
116
147
|
"""Return a value changed filter.
|
117
148
|
|
118
149
|
A callback function will only be called if value is changed from the
|
119
150
|
previous call.
|
120
151
|
|
121
152
|
:param callback: A callback function to be awaited on value change
|
122
|
-
:type callback: Callable[[Any],
|
153
|
+
:type callback: Callable[[Any], Coroutine[Any, Any, Any]]
|
123
154
|
:return: A instance of callable filter
|
124
155
|
:rtype: _OnChange
|
125
156
|
"""
|
@@ -138,9 +169,7 @@ class _Debounce(Filter):
|
|
138
169
|
_calls: int
|
139
170
|
_min_calls: int
|
140
171
|
|
141
|
-
def __init__(
|
142
|
-
self, callback: Callable[[Any], Awaitable[Any]], min_calls: int
|
143
|
-
) -> None:
|
172
|
+
def __init__(self, callback: Callback, min_calls: int) -> None:
|
144
173
|
"""Initialize a new debounce filter."""
|
145
174
|
super().__init__(callback)
|
146
175
|
self._calls = 0
|
@@ -161,14 +190,14 @@ class _Debounce(Filter):
|
|
161
190
|
return await self._callback(new_value)
|
162
191
|
|
163
192
|
|
164
|
-
def debounce(callback:
|
193
|
+
def debounce(callback: Callback, min_calls: int) -> _Debounce:
|
165
194
|
"""Return a debounce filter.
|
166
195
|
|
167
196
|
A callback function will only called once value is stabilized
|
168
197
|
across multiple filter calls.
|
169
198
|
|
170
199
|
:param callback: A callback function to be awaited on value change
|
171
|
-
:type callback: Callable[[Any],
|
200
|
+
:type callback: Callable[[Any], Coroutine[Any, Any, Any]]
|
172
201
|
:param min_calls: Value shouldn't change for this amount of
|
173
202
|
filter calls
|
174
203
|
:type min_calls: int
|
@@ -190,9 +219,7 @@ class _Throttle(Filter):
|
|
190
219
|
_last_called: float | None
|
191
220
|
_timeout: float
|
192
221
|
|
193
|
-
def __init__(
|
194
|
-
self, callback: Callable[[Any], Awaitable[Any]], seconds: float
|
195
|
-
) -> None:
|
222
|
+
def __init__(self, callback: Callback, seconds: float) -> None:
|
196
223
|
"""Initialize a new throttle filter."""
|
197
224
|
super().__init__(callback)
|
198
225
|
self._last_called = None
|
@@ -209,7 +236,7 @@ class _Throttle(Filter):
|
|
209
236
|
return await self._callback(new_value)
|
210
237
|
|
211
238
|
|
212
|
-
def throttle(callback:
|
239
|
+
def throttle(callback: Callback, seconds: float) -> _Throttle:
|
213
240
|
"""Return a throttle filter.
|
214
241
|
|
215
242
|
A callback function will only be called once a certain amount of
|
@@ -217,7 +244,7 @@ def throttle(callback: Callable[[Any], Awaitable[Any]], seconds: float) -> _Thro
|
|
217
244
|
|
218
245
|
:param callback: A callback function that will be awaited once
|
219
246
|
filter conditions are fulfilled
|
220
|
-
:type callback: Callable[[Any],
|
247
|
+
:type callback: Callable[[Any], Coroutine[Any, Any, Any]]
|
221
248
|
:param seconds: A callback will be awaited at most once per
|
222
249
|
this amount of seconds
|
223
250
|
:type seconds: float
|
@@ -249,7 +276,7 @@ class _Delta(Filter):
|
|
249
276
|
return await self._callback(difference)
|
250
277
|
|
251
278
|
|
252
|
-
def delta(callback:
|
279
|
+
def delta(callback: Callback) -> _Delta:
|
253
280
|
"""Return a difference filter.
|
254
281
|
|
255
282
|
A callback function will be called with a difference between two
|
@@ -257,7 +284,7 @@ def delta(callback: Callable[[Any], Awaitable[Any]]) -> _Delta:
|
|
257
284
|
|
258
285
|
:param callback: A callback function that will be awaited with
|
259
286
|
difference between values in two subsequent calls
|
260
|
-
:type callback: Callable[[Any],
|
287
|
+
:type callback: Callable[[Any], Coroutine[Any, Any, Any]]
|
261
288
|
:return: A instance of callable filter
|
262
289
|
:rtype: _Delta
|
263
290
|
"""
|
@@ -277,9 +304,7 @@ class _Aggregate(Filter):
|
|
277
304
|
_last_update: float
|
278
305
|
_timeout: float
|
279
306
|
|
280
|
-
def __init__(
|
281
|
-
self, callback: Callable[[Any], Awaitable[Any]], seconds: float
|
282
|
-
) -> None:
|
307
|
+
def __init__(self, callback: Callback, seconds: float) -> None:
|
283
308
|
"""Initialize a new aggregate filter."""
|
284
309
|
super().__init__(callback)
|
285
310
|
self._last_update = time.monotonic()
|
@@ -303,7 +328,7 @@ class _Aggregate(Filter):
|
|
303
328
|
return result
|
304
329
|
|
305
330
|
|
306
|
-
def aggregate(callback:
|
331
|
+
def aggregate(callback: Callback, seconds: float) -> _Aggregate:
|
307
332
|
"""Return an aggregate filter.
|
308
333
|
|
309
334
|
A callback function will be called with a sum of values collected
|
@@ -311,7 +336,7 @@ def aggregate(callback: Callable[[Any], Awaitable[Any]], seconds: float) -> _Agg
|
|
311
336
|
|
312
337
|
:param callback: A callback function to be awaited once filter
|
313
338
|
conditions are fulfilled
|
314
|
-
:type callback: Callable[[Any],
|
339
|
+
:type callback: Callable[[Any], Coroutine[Any, Any, Any]]
|
315
340
|
:param seconds: A callback will be awaited with a sum of values
|
316
341
|
aggregated over this amount of seconds.
|
317
342
|
:type seconds: float
|
@@ -333,11 +358,7 @@ class _Custom(Filter):
|
|
333
358
|
|
334
359
|
filter_fn: Callable[[Any], bool]
|
335
360
|
|
336
|
-
def __init__(
|
337
|
-
self,
|
338
|
-
callback: Callable[[Any], Awaitable[Any]],
|
339
|
-
filter_fn: Callable[[Any], bool],
|
340
|
-
) -> None:
|
361
|
+
def __init__(self, callback: Callback, filter_fn: Callable[[Any], bool]) -> None:
|
341
362
|
"""Initialize a new custom filter."""
|
342
363
|
super().__init__(callback)
|
343
364
|
self._filter_fn = filter_fn
|
@@ -348,9 +369,7 @@ class _Custom(Filter):
|
|
348
369
|
await self._callback(new_value)
|
349
370
|
|
350
371
|
|
351
|
-
def custom(
|
352
|
-
callback: Callable[[Any], Awaitable[Any]], filter_fn: Callable[[Any], bool]
|
353
|
-
) -> _Custom:
|
372
|
+
def custom(callback: Callback, filter_fn: Callable[[Any], bool]) -> _Custom:
|
354
373
|
"""Return a custom filter.
|
355
374
|
|
356
375
|
A callback function will be called when user-defined filter
|
@@ -359,7 +378,7 @@ def custom(
|
|
359
378
|
|
360
379
|
:param callback: A callback function to be awaited when
|
361
380
|
filter function return true
|
362
|
-
:type callback: Callable[[Any],
|
381
|
+
:type callback: Callable[[Any], Coroutine[Any, Any, Any]]
|
363
382
|
:param filter_fn: Filter function, that will be called with a
|
364
383
|
value and should return `True` to await filter's callback
|
365
384
|
:type filter_fn: Callable[[Any], bool]
|
pyplumio/frames/__init__.py
CHANGED
@@ -29,9 +29,6 @@ if TYPE_CHECKING:
|
|
29
29
|
from pyplumio.devices import AddressableDevice
|
30
30
|
|
31
31
|
|
32
|
-
T = TypeVar("T")
|
33
|
-
|
34
|
-
|
35
32
|
def bcc(data: bytes) -> int:
|
36
33
|
"""Return a block check character."""
|
37
34
|
return reduce(lambda x, y: x ^ y, data)
|
@@ -112,7 +109,7 @@ class Frame(ABC):
|
|
112
109
|
self._data = data if not kwargs else ensure_dict(data, kwargs)
|
113
110
|
self._message = message
|
114
111
|
|
115
|
-
def __eq__(self, other:
|
112
|
+
def __eq__(self, other: object) -> bool:
|
116
113
|
"""Compare if this frame is equal to other."""
|
117
114
|
if isinstance(other, Frame):
|
118
115
|
return (
|
@@ -131,7 +128,7 @@ class Frame(ABC):
|
|
131
128
|
self._data,
|
132
129
|
)
|
133
130
|
|
134
|
-
|
131
|
+
return NotImplemented
|
135
132
|
|
136
133
|
def __repr__(self) -> str:
|
137
134
|
"""Return a serializable string representation."""
|
@@ -224,7 +221,7 @@ class Frame(ABC):
|
|
224
221
|
return bytes(data)
|
225
222
|
|
226
223
|
@classmethod
|
227
|
-
async def create(cls: type[
|
224
|
+
async def create(cls: type[FrameT], frame_type: int, **kwargs: Any) -> FrameT:
|
228
225
|
"""Create a frame handler object from frame type."""
|
229
226
|
return await create_instance(get_frame_handler(frame_type), cls=cls, **kwargs)
|
230
227
|
|
@@ -237,6 +234,9 @@ class Frame(ABC):
|
|
237
234
|
"""Decode frame message."""
|
238
235
|
|
239
236
|
|
237
|
+
FrameT = TypeVar("FrameT", bound=Frame)
|
238
|
+
|
239
|
+
|
240
240
|
class Request(Frame):
|
241
241
|
"""Represents a request."""
|
242
242
|
|
pyplumio/frames/messages.py
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
|
+
from contextlib import suppress
|
5
6
|
from typing import Any, ClassVar
|
6
7
|
|
7
8
|
from pyplumio.const import (
|
@@ -53,12 +54,7 @@ class SensorDataMessage(Message):
|
|
53
54
|
def decode_message(self, message: bytearray) -> dict[str, Any]:
|
54
55
|
"""Decode a frame message."""
|
55
56
|
sensors, offset = FrameVersionsStructure(self).decode(message, offset=0)
|
56
|
-
|
57
|
-
sensors[ATTR_STATE] = message[offset]
|
58
|
-
sensors[ATTR_STATE] = DeviceState(sensors[ATTR_STATE])
|
59
|
-
except ValueError:
|
60
|
-
pass
|
61
|
-
|
57
|
+
sensors[ATTR_STATE] = message[offset]
|
62
58
|
sensors, offset = OutputsStructure(self).decode(message, offset + 1, sensors)
|
63
59
|
sensors, offset = OutputFlagsStructure(self).decode(message, offset, sensors)
|
64
60
|
sensors, offset = TemperaturesStructure(self).decode(message, offset, sensors)
|
@@ -79,5 +75,7 @@ class SensorDataMessage(Message):
|
|
79
75
|
message, offset, sensors
|
80
76
|
)
|
81
77
|
sensors, offset = MixerSensorsStructure(self).decode(message, offset, sensors)
|
78
|
+
with suppress(ValueError):
|
79
|
+
sensors[ATTR_STATE] = DeviceState(sensors[ATTR_STATE])
|
82
80
|
|
83
81
|
return {ATTR_SENSORS: sensors}
|
pyplumio/helpers/data_types.py
CHANGED
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
5
5
|
from abc import ABC, abstractmethod
|
6
6
|
import socket
|
7
7
|
import struct
|
8
|
-
from typing import Any, ClassVar, Final
|
8
|
+
from typing import Any, ClassVar, Final, TypeVar
|
9
9
|
|
10
10
|
|
11
11
|
class DataType(ABC):
|
@@ -25,21 +25,19 @@ class DataType(ABC):
|
|
25
25
|
"""Return serializable string representation of the class."""
|
26
26
|
return f"{self.__class__.__name__}(value={self._value})"
|
27
27
|
|
28
|
-
def __eq__(self, other:
|
28
|
+
def __eq__(self, other: object) -> bool:
|
29
29
|
"""Compare if this data type is equal to other."""
|
30
30
|
if isinstance(other, DataType):
|
31
|
-
|
32
|
-
else:
|
33
|
-
result = self._value == other
|
31
|
+
return bool(self._value == other._value)
|
34
32
|
|
35
|
-
return
|
33
|
+
return bool(self._value == other)
|
36
34
|
|
37
35
|
def _slice_data(self, data: bytes) -> bytes:
|
38
36
|
"""Slice the data to data type size."""
|
39
37
|
return data[: self.size] if self.size is not None else data
|
40
38
|
|
41
39
|
@classmethod
|
42
|
-
def from_bytes(cls, data: bytes, offset: int = 0) ->
|
40
|
+
def from_bytes(cls: type[DataTypeT], data: bytes, offset: int = 0) -> DataTypeT:
|
43
41
|
"""Initialize a new data type from bytes."""
|
44
42
|
data_type = cls()
|
45
43
|
data_type.unpack(data[offset:])
|
@@ -68,6 +66,9 @@ class DataType(ABC):
|
|
68
66
|
"""Unpack the data."""
|
69
67
|
|
70
68
|
|
69
|
+
DataTypeT = TypeVar("DataTypeT", bound=DataType)
|
70
|
+
|
71
|
+
|
71
72
|
class Undefined(DataType):
|
72
73
|
"""Represents an undefined."""
|
73
74
|
|
@@ -3,18 +3,26 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
5
|
import asyncio
|
6
|
-
from collections.abc import
|
7
|
-
from typing import Any
|
6
|
+
from collections.abc import Callable, Coroutine
|
7
|
+
from typing import Any, Generic, TypeVar, overload
|
8
|
+
|
9
|
+
from typing_extensions import TypeAlias
|
8
10
|
|
9
11
|
from pyplumio.helpers.task_manager import TaskManager
|
10
12
|
|
13
|
+
Callback: TypeAlias = Callable[[Any], Coroutine[Any, Any, Any]]
|
14
|
+
CallbackT = TypeVar("CallbackT", bound=Callback)
|
15
|
+
T = TypeVar("T")
|
16
|
+
|
11
17
|
|
12
|
-
class EventManager(TaskManager):
|
18
|
+
class EventManager(TaskManager, Generic[T]):
|
13
19
|
"""Represents an event manager."""
|
14
20
|
|
15
|
-
data
|
21
|
+
__slots__ = ("data", "_events", "_callbacks")
|
22
|
+
|
23
|
+
data: dict[str, T]
|
16
24
|
_events: dict[str, asyncio.Event]
|
17
|
-
_callbacks: dict[str, list[
|
25
|
+
_callbacks: dict[str, list[Callback]]
|
18
26
|
|
19
27
|
def __init__(self) -> None:
|
20
28
|
"""Initialize a new event manager."""
|
@@ -23,7 +31,7 @@ class EventManager(TaskManager):
|
|
23
31
|
self._events = {}
|
24
32
|
self._callbacks = {}
|
25
33
|
|
26
|
-
def __getattr__(self, name: str) ->
|
34
|
+
def __getattr__(self, name: str) -> T:
|
27
35
|
"""Return attributes from the underlying data dictionary."""
|
28
36
|
try:
|
29
37
|
return self.data[name]
|
@@ -43,7 +51,7 @@ class EventManager(TaskManager):
|
|
43
51
|
if name not in self.data:
|
44
52
|
await asyncio.wait_for(self.create_event(name).wait(), timeout=timeout)
|
45
53
|
|
46
|
-
async def get(self, name: str, timeout: float | None = None) ->
|
54
|
+
async def get(self, name: str, timeout: float | None = None) -> T:
|
47
55
|
"""Get the value by name.
|
48
56
|
|
49
57
|
:param name: Event name or ID
|
@@ -57,6 +65,12 @@ class EventManager(TaskManager):
|
|
57
65
|
await self.wait_for(name, timeout=timeout)
|
58
66
|
return self.data[name]
|
59
67
|
|
68
|
+
@overload
|
69
|
+
def get_nowait(self, name: str, default: None = ...) -> T | None: ...
|
70
|
+
|
71
|
+
@overload
|
72
|
+
def get_nowait(self, name: str, default: T) -> T: ...
|
73
|
+
|
60
74
|
def get_nowait(self, name: str, default: Any = None) -> Any:
|
61
75
|
"""Get the value by name without waiting.
|
62
76
|
|
@@ -75,23 +89,23 @@ class EventManager(TaskManager):
|
|
75
89
|
except KeyError:
|
76
90
|
return default
|
77
91
|
|
78
|
-
def subscribe(self, name: str, callback:
|
92
|
+
def subscribe(self, name: str, callback: CallbackT) -> CallbackT:
|
79
93
|
"""Subscribe a callback to the event.
|
80
94
|
|
81
95
|
:param name: Event name or ID
|
82
96
|
:type name: str
|
83
97
|
:param callback: A coroutine callback function, that will be
|
84
98
|
awaited on the with the event data as an argument.
|
85
|
-
:type callback:
|
99
|
+
:type callback: Callback
|
100
|
+
:return: A reference to the callback, that can be used
|
101
|
+
with `EventManager.unsubscribe()`.
|
102
|
+
:rtype: Callback
|
86
103
|
"""
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
self._callbacks[name].append(callback)
|
104
|
+
callbacks = self._callbacks.setdefault(name, [])
|
105
|
+
callbacks.append(callback)
|
106
|
+
return callback
|
91
107
|
|
92
|
-
def subscribe_once(
|
93
|
-
self, name: str, callback: Callable[[Any], Awaitable[Any]]
|
94
|
-
) -> None:
|
108
|
+
def subscribe_once(self, name: str, callback: Callback) -> Callback:
|
95
109
|
"""Subscribe a callback to the event once.
|
96
110
|
|
97
111
|
Callback will be unsubscribed after single event.
|
@@ -100,17 +114,20 @@ class EventManager(TaskManager):
|
|
100
114
|
:type name: str
|
101
115
|
:param callback: A coroutine callback function, that will be
|
102
116
|
awaited on the with the event data as an argument.
|
103
|
-
:type callback:
|
117
|
+
:type callback: Callback
|
118
|
+
:return: A reference to the callback, that can be used
|
119
|
+
with `EventManager.unsubscribe()`.
|
120
|
+
:rtype: Callback
|
104
121
|
"""
|
105
122
|
|
106
|
-
async def
|
123
|
+
async def _call_once(value: Any) -> Any:
|
107
124
|
"""Unsubscribe callback from the event and calls it."""
|
108
|
-
self.unsubscribe(name,
|
125
|
+
self.unsubscribe(name, _call_once)
|
109
126
|
return await callback(value)
|
110
127
|
|
111
|
-
self.subscribe(name,
|
128
|
+
return self.subscribe(name, _call_once)
|
112
129
|
|
113
|
-
def unsubscribe(self, name: str, callback:
|
130
|
+
def unsubscribe(self, name: str, callback: Callback) -> bool:
|
114
131
|
"""Usubscribe a callback from the event.
|
115
132
|
|
116
133
|
:param name: Event name or ID
|
@@ -118,34 +135,37 @@ class EventManager(TaskManager):
|
|
118
135
|
:param callback: A coroutine callback function, previously
|
119
136
|
subscribed to an event using ``subscribe()`` or
|
120
137
|
``subscribe_once()`` methods.
|
121
|
-
:type callback:
|
138
|
+
:type callback: Callback
|
139
|
+
:return: `True` if callback is found, `False` otherwise.
|
140
|
+
:rtype: bool
|
122
141
|
"""
|
123
142
|
if name in self._callbacks and callback in self._callbacks[name]:
|
124
143
|
self._callbacks[name].remove(callback)
|
144
|
+
return True
|
145
|
+
|
146
|
+
return False
|
125
147
|
|
126
|
-
async def dispatch(self, name: str, value:
|
148
|
+
async def dispatch(self, name: str, value: T) -> None:
|
127
149
|
"""Call registered callbacks and dispatch the event."""
|
128
|
-
if
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
value = return_value if return_value is not None else value
|
150
|
+
if callbacks := self._callbacks.get(name, None):
|
151
|
+
for callback in list(callbacks):
|
152
|
+
result = await callback(value)
|
153
|
+
value = result if result is not None else value
|
133
154
|
|
134
155
|
self.data[name] = value
|
135
156
|
self.set_event(name)
|
136
157
|
|
137
|
-
def dispatch_nowait(self, name: str, value:
|
158
|
+
def dispatch_nowait(self, name: str, value: T) -> None:
|
138
159
|
"""Call a registered callbacks and dispatch the event without waiting."""
|
139
160
|
self.create_task(self.dispatch(name, value))
|
140
161
|
|
141
|
-
async def load(self, data: dict[str,
|
162
|
+
async def load(self, data: dict[str, T]) -> None:
|
142
163
|
"""Load event data."""
|
143
|
-
self.data = data
|
144
164
|
await asyncio.gather(
|
145
|
-
*
|
165
|
+
*(self.dispatch(name, value) for name, value in data.items())
|
146
166
|
)
|
147
167
|
|
148
|
-
def load_nowait(self, data: dict[str,
|
168
|
+
def load_nowait(self, data: dict[str, T]) -> None:
|
149
169
|
"""Load event data without waiting."""
|
150
170
|
self.create_task(self.load(data))
|
151
171
|
|