PyPlumIO 0.5.21__py3-none-any.whl → 0.5.22__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.22.dist-info}/METADATA +7 -7
- PyPlumIO-0.5.22.dist-info/RECORD +60 -0
- {PyPlumIO-0.5.21.dist-info → PyPlumIO-0.5.22.dist-info}/WHEEL +1 -1
- pyplumio/__init__.py +2 -2
- pyplumio/_version.py +2 -2
- pyplumio/connection.py +2 -10
- pyplumio/devices/__init__.py +14 -14
- pyplumio/devices/ecomax.py +117 -117
- pyplumio/devices/mixer.py +47 -41
- pyplumio/devices/thermostat.py +33 -32
- 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 +36 -25
- pyplumio/helpers/parameter.py +43 -16
- pyplumio/helpers/task_manager.py +7 -2
- pyplumio/helpers/timeout.py +0 -3
- pyplumio/helpers/uid.py +2 -2
- pyplumio/protocol.py +32 -26
- pyplumio/stream.py +2 -2
- pyplumio/structures/alerts.py +40 -31
- pyplumio/structures/ecomax_parameters.py +321 -131
- pyplumio/structures/frame_versions.py +5 -6
- pyplumio/structures/lambda_sensor.py +6 -6
- pyplumio/structures/mixer_parameters.py +74 -28
- pyplumio/structures/network_info.py +2 -3
- pyplumio/structures/product_info.py +0 -4
- pyplumio/structures/program_version.py +24 -17
- pyplumio/structures/thermostat_parameters.py +25 -12
- 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.22.dist-info}/LICENSE +0 -0
- {PyPlumIO-0.5.21.dist-info → PyPlumIO-0.5.22.dist-info}/top_level.txt +0 -0
pyplumio/devices/mixer.py
CHANGED
@@ -3,9 +3,9 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
5
|
import asyncio
|
6
|
-
from collections.abc import Sequence
|
6
|
+
from collections.abc import Coroutine, Generator, Sequence
|
7
7
|
import logging
|
8
|
-
from typing import Any
|
8
|
+
from typing import TYPE_CHECKING, Any
|
9
9
|
|
10
10
|
from pyplumio.devices import AddressableDevice, SubDevice
|
11
11
|
from pyplumio.helpers.parameter import ParameterValues
|
@@ -19,31 +19,35 @@ from pyplumio.structures.mixer_parameters import (
|
|
19
19
|
from pyplumio.structures.mixer_sensors import ATTR_MIXER_SENSORS
|
20
20
|
from pyplumio.structures.product_info import ATTR_PRODUCT, ProductInfo
|
21
21
|
|
22
|
+
if TYPE_CHECKING:
|
23
|
+
from pyplumio.frames import Frame
|
24
|
+
|
22
25
|
_LOGGER = logging.getLogger(__name__)
|
23
26
|
|
24
27
|
|
25
28
|
class Mixer(SubDevice):
|
26
29
|
"""Represents an mixer."""
|
27
30
|
|
28
|
-
def __init__(
|
31
|
+
def __init__(
|
32
|
+
self, queue: asyncio.Queue[Frame], parent: AddressableDevice, index: int = 0
|
33
|
+
):
|
29
34
|
"""Initialize a new mixer."""
|
30
35
|
super().__init__(queue, parent, index)
|
31
|
-
self.subscribe(ATTR_MIXER_SENSORS, self.
|
32
|
-
self.subscribe(ATTR_MIXER_PARAMETERS, self.
|
36
|
+
self.subscribe(ATTR_MIXER_SENSORS, self._handle_mixer_sensors)
|
37
|
+
self.subscribe(ATTR_MIXER_PARAMETERS, self._handle_mixer_parameters)
|
33
38
|
|
34
|
-
async def
|
39
|
+
async def _handle_mixer_sensors(self, sensors: dict[str, Any]) -> bool:
|
35
40
|
"""Handle mixer sensors.
|
36
41
|
|
37
42
|
For each sensor dispatch an event with the
|
38
43
|
sensor's name and value.
|
39
44
|
"""
|
40
45
|
await asyncio.gather(
|
41
|
-
*
|
46
|
+
*(self.dispatch(name, value) for name, value in sensors.items())
|
42
47
|
)
|
43
|
-
|
44
48
|
return True
|
45
49
|
|
46
|
-
async def
|
50
|
+
async def _handle_mixer_parameters(
|
47
51
|
self, parameters: Sequence[tuple[int, ParameterValues]]
|
48
52
|
) -> bool:
|
49
53
|
"""Handle mixer parameters.
|
@@ -52,39 +56,41 @@ class Mixer(SubDevice):
|
|
52
56
|
parameter's name and value.
|
53
57
|
"""
|
54
58
|
product: ProductInfo = await self.parent.get(ATTR_PRODUCT)
|
55
|
-
for index, values in parameters:
|
56
|
-
try:
|
57
|
-
description = MIXER_PARAMETERS[product.type][index]
|
58
|
-
except IndexError:
|
59
|
-
_LOGGER.warning(
|
60
|
-
(
|
61
|
-
"Encountered unknown mixer parameter (%i): %s. "
|
62
|
-
"Your device isn't fully compatible with this software and "
|
63
|
-
"may not work properly. "
|
64
|
-
"Please visit the issue tracker and open a feature "
|
65
|
-
"request to support %s"
|
66
|
-
),
|
67
|
-
index,
|
68
|
-
values,
|
69
|
-
product.model,
|
70
|
-
)
|
71
|
-
return False
|
72
59
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
60
|
+
def _mixer_parameter_events() -> Generator[Coroutine, Any, None]:
|
61
|
+
"""Get dispatch calls for mixer parameter events."""
|
62
|
+
for index, values in parameters:
|
63
|
+
try:
|
64
|
+
description = MIXER_PARAMETERS[product.type][index]
|
65
|
+
except IndexError:
|
66
|
+
_LOGGER.warning(
|
67
|
+
(
|
68
|
+
"Encountered unknown mixer parameter (%i): %s. "
|
69
|
+
"Your device isn't fully compatible with this software and "
|
70
|
+
"may not work properly. "
|
71
|
+
"Please visit the issue tracker and open a feature "
|
72
|
+
"request to support %s"
|
73
|
+
),
|
74
|
+
index,
|
75
|
+
values,
|
76
|
+
product.model,
|
77
|
+
)
|
78
|
+
return
|
79
79
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
80
|
+
handler = (
|
81
|
+
MixerBinaryParameter
|
82
|
+
if isinstance(description, MixerBinaryParameterDescription)
|
83
|
+
else MixerParameter
|
84
|
+
)
|
85
|
+
yield self.dispatch(
|
86
|
+
description.name,
|
87
|
+
handler.create_or_update(
|
88
|
+
device=self,
|
89
|
+
description=description,
|
90
|
+
values=values,
|
91
|
+
index=index,
|
92
|
+
),
|
93
|
+
)
|
89
94
|
|
95
|
+
await asyncio.gather(*_mixer_parameter_events())
|
90
96
|
return True
|
pyplumio/devices/thermostat.py
CHANGED
@@ -3,8 +3,8 @@
|
|
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
|
@@ -17,29 +17,33 @@ from pyplumio.structures.thermostat_parameters import (
|
|
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
|
+
ThermostatBinaryParameter
|
61
|
+
if isinstance(description, ThermostatBinaryParameterDescription)
|
62
|
+
else ThermostatParameter
|
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
|
|