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/helpers/parameter.py
CHANGED
@@ -2,15 +2,17 @@
|
|
2
2
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
|
-
from abc import ABC
|
5
|
+
from abc import ABC, abstractmethod
|
6
6
|
import asyncio
|
7
7
|
from dataclasses import dataclass
|
8
8
|
import logging
|
9
|
-
from typing import TYPE_CHECKING, Any, Final, Literal
|
9
|
+
from typing import TYPE_CHECKING, Any, Final, Literal, TypeVar, Union
|
10
|
+
|
11
|
+
from dataslots import dataslots
|
12
|
+
from typing_extensions import TypeAlias
|
10
13
|
|
11
14
|
from pyplumio.const import BYTE_UNDEFINED, STATE_OFF, STATE_ON, UnitOfMeasurement
|
12
15
|
from pyplumio.frames import Request
|
13
|
-
from pyplumio.helpers.typing import ParameterValueType
|
14
16
|
|
15
17
|
if TYPE_CHECKING:
|
16
18
|
from pyplumio.devices import Device
|
@@ -20,6 +22,9 @@ _LOGGER = logging.getLogger(__name__)
|
|
20
22
|
SET_TIMEOUT: Final = 5
|
21
23
|
SET_RETRIES: Final = 5
|
22
24
|
|
25
|
+
ParameterValueType: TypeAlias = Union[int, float, bool, Literal["off", "on"]]
|
26
|
+
ParameterT = TypeVar("ParameterT", bound="Parameter")
|
27
|
+
|
23
28
|
|
24
29
|
def unpack_parameter(
|
25
30
|
data: bytearray, offset: int = 0, size: int = 1
|
@@ -45,13 +50,10 @@ def check_parameter(data: bytearray) -> bool:
|
|
45
50
|
|
46
51
|
|
47
52
|
def _normalize_parameter_value(value: ParameterValueType) -> int:
|
48
|
-
"""Normalize a parameter value
|
49
|
-
if
|
53
|
+
"""Normalize a parameter value."""
|
54
|
+
if value in (STATE_OFF, STATE_ON):
|
50
55
|
return 1 if value == STATE_ON else 0
|
51
56
|
|
52
|
-
if isinstance(value, ParameterValues):
|
53
|
-
value = value.value
|
54
|
-
|
55
57
|
return int(value)
|
56
58
|
|
57
59
|
|
@@ -66,58 +68,54 @@ class ParameterValues:
|
|
66
68
|
max_value: int
|
67
69
|
|
68
70
|
|
71
|
+
@dataslots
|
69
72
|
@dataclass
|
70
|
-
class ParameterDescription
|
73
|
+
class ParameterDescription:
|
71
74
|
"""Represents a parameter description."""
|
72
75
|
|
73
76
|
name: str
|
74
|
-
unit_of_measurement: UnitOfMeasurement | Literal["%"] | None = None
|
75
|
-
|
76
|
-
|
77
|
-
@dataclass
|
78
|
-
class BinaryParameterDescription(ParameterDescription, ABC):
|
79
|
-
"""Represent a binary parameter description."""
|
80
77
|
|
81
78
|
|
82
79
|
class Parameter(ABC):
|
83
|
-
"""Represents a parameter."""
|
80
|
+
"""Represents a base parameter."""
|
84
81
|
|
85
|
-
__slots__ = ("device", "
|
82
|
+
__slots__ = ("device", "description", "_pending_update", "_index", "_values")
|
86
83
|
|
87
84
|
device: Device
|
88
|
-
values: ParameterValues
|
89
85
|
description: ParameterDescription
|
90
86
|
_pending_update: bool
|
91
87
|
_index: int
|
88
|
+
_values: ParameterValues
|
92
89
|
|
93
90
|
def __init__(
|
94
91
|
self,
|
95
92
|
device: Device,
|
96
|
-
values: ParameterValues,
|
97
93
|
description: ParameterDescription,
|
94
|
+
values: ParameterValues | None = None,
|
98
95
|
index: int = 0,
|
99
96
|
):
|
100
97
|
"""Initialize a new parameter."""
|
101
98
|
self.device = device
|
102
|
-
self.values = values
|
103
99
|
self.description = description
|
104
100
|
self._pending_update = False
|
105
101
|
self._index = index
|
102
|
+
self._values = values if values else ParameterValues(0, 0, 0)
|
106
103
|
|
107
104
|
def __repr__(self) -> str:
|
108
105
|
"""Return a serializable string representation."""
|
109
106
|
return (
|
110
107
|
f"{self.__class__.__name__}("
|
111
108
|
f"device={self.device.__class__.__name__}, "
|
112
|
-
f"values={self.values}, "
|
113
109
|
f"description={self.description}, "
|
110
|
+
f"values={self.values}, "
|
114
111
|
f"index={self._index})"
|
115
112
|
)
|
116
113
|
|
117
114
|
def _call_relational_method(self, method_to_call: str, other: Any) -> Any:
|
118
115
|
"""Call a specified relational method."""
|
119
|
-
|
120
|
-
|
116
|
+
handler = getattr(self.values.value, method_to_call)
|
117
|
+
other = other.value if isinstance(other, ParameterValues) else other
|
118
|
+
return handler(_normalize_parameter_value(other))
|
121
119
|
|
122
120
|
def __int__(self) -> int:
|
123
121
|
"""Return an integer representation of parameter's value."""
|
@@ -163,15 +161,7 @@ class Parameter(ABC):
|
|
163
161
|
"""Compare if parameter value is less that other."""
|
164
162
|
return self._call_relational_method("__lt__", other)
|
165
163
|
|
166
|
-
async def
|
167
|
-
"""Set parameter as no longer pending update."""
|
168
|
-
self._pending_update = False
|
169
|
-
|
170
|
-
async def create_request(self) -> Request:
|
171
|
-
"""Create a request to change the parameter."""
|
172
|
-
raise NotImplementedError
|
173
|
-
|
174
|
-
async def set(self, value: ParameterValueType, retries: int = SET_RETRIES) -> bool:
|
164
|
+
async def set(self, value: Any, retries: int = SET_RETRIES) -> bool:
|
175
165
|
"""Set a parameter value."""
|
176
166
|
if (value := _normalize_parameter_value(value)) == self.values.value:
|
177
167
|
return True
|
@@ -181,16 +171,14 @@ class Parameter(ABC):
|
|
181
171
|
f"Value must be between '{self.min_value}' and '{self.max_value}'"
|
182
172
|
)
|
183
173
|
|
184
|
-
self.
|
174
|
+
self._values.value = value
|
185
175
|
self._pending_update = True
|
186
|
-
self.device.subscribe_once(self.description.name, self._confirm_update)
|
187
176
|
while self.pending_update:
|
188
177
|
if retries <= 0:
|
189
178
|
_LOGGER.error(
|
190
179
|
"Timed out while trying to set '%s' parameter",
|
191
180
|
self.description.name,
|
192
181
|
)
|
193
|
-
self.device.unsubscribe(self.description.name, self._confirm_update)
|
194
182
|
return False
|
195
183
|
|
196
184
|
await self.device.queue.put(await self.create_request())
|
@@ -199,9 +187,10 @@ class Parameter(ABC):
|
|
199
187
|
|
200
188
|
return True
|
201
189
|
|
202
|
-
def
|
203
|
-
"""
|
204
|
-
self.
|
190
|
+
def update(self, values: ParameterValues) -> None:
|
191
|
+
"""Update the parameter values."""
|
192
|
+
self._values = values
|
193
|
+
self._pending_update = False
|
205
194
|
|
206
195
|
@property
|
207
196
|
def pending_update(self) -> bool:
|
@@ -209,17 +198,88 @@ class Parameter(ABC):
|
|
209
198
|
return self._pending_update
|
210
199
|
|
211
200
|
@property
|
212
|
-
def
|
213
|
-
"""Return the parameter
|
201
|
+
def values(self) -> ParameterValues:
|
202
|
+
"""Return the parameter values."""
|
203
|
+
return self._values
|
204
|
+
|
205
|
+
@classmethod
|
206
|
+
def create_or_update(
|
207
|
+
cls: type[ParameterT],
|
208
|
+
device: Device,
|
209
|
+
description: ParameterDescription,
|
210
|
+
values: ParameterValues,
|
211
|
+
**kwargs: Any,
|
212
|
+
) -> ParameterT:
|
213
|
+
"""Create new parameter or update parameter values."""
|
214
|
+
parameter: ParameterT | None = device.get_nowait(description.name, None)
|
215
|
+
if parameter and isinstance(parameter, cls):
|
216
|
+
parameter.update(values)
|
217
|
+
else:
|
218
|
+
parameter = cls(
|
219
|
+
device=device, description=description, values=values, **kwargs
|
220
|
+
)
|
221
|
+
|
222
|
+
return parameter
|
223
|
+
|
224
|
+
@property
|
225
|
+
@abstractmethod
|
226
|
+
def value(self) -> Any:
|
227
|
+
"""Return the value."""
|
228
|
+
|
229
|
+
@property
|
230
|
+
@abstractmethod
|
231
|
+
def min_value(self) -> Any:
|
232
|
+
"""Return the minimum allowed value."""
|
233
|
+
|
234
|
+
@property
|
235
|
+
@abstractmethod
|
236
|
+
def max_value(self) -> Any:
|
237
|
+
"""Return the maximum allowed value."""
|
238
|
+
|
239
|
+
@abstractmethod
|
240
|
+
async def create_request(self) -> Request:
|
241
|
+
"""Create a request to change the parameter."""
|
242
|
+
|
243
|
+
|
244
|
+
@dataslots
|
245
|
+
@dataclass
|
246
|
+
class NumberDescription(ParameterDescription):
|
247
|
+
"""Represents a parameter description."""
|
248
|
+
|
249
|
+
unit_of_measurement: UnitOfMeasurement | Literal["%"] | None = None
|
250
|
+
|
251
|
+
|
252
|
+
class Number(Parameter):
|
253
|
+
"""Represents a number."""
|
254
|
+
|
255
|
+
__slots__ = ()
|
256
|
+
|
257
|
+
description: NumberDescription
|
258
|
+
|
259
|
+
async def set(self, value: int | float, retries: int = SET_RETRIES) -> bool:
|
260
|
+
"""Set a parameter value."""
|
261
|
+
return await super().set(value, retries)
|
262
|
+
|
263
|
+
def set_nowait(self, value: int | float, retries: int = SET_RETRIES) -> None:
|
264
|
+
"""Set a parameter value without waiting."""
|
265
|
+
self.device.create_task(self.set(value, retries))
|
266
|
+
|
267
|
+
async def create_request(self) -> Request:
|
268
|
+
"""Create a request to change the number."""
|
269
|
+
return Request()
|
270
|
+
|
271
|
+
@property
|
272
|
+
def value(self) -> int | float:
|
273
|
+
"""Return the value."""
|
214
274
|
return self.values.value
|
215
275
|
|
216
276
|
@property
|
217
|
-
def min_value(self) ->
|
277
|
+
def min_value(self) -> int | float:
|
218
278
|
"""Return the minimum allowed value."""
|
219
279
|
return self.values.min_value
|
220
280
|
|
221
281
|
@property
|
222
|
-
def max_value(self) ->
|
282
|
+
def max_value(self) -> int | float:
|
223
283
|
"""Return the maximum allowed value."""
|
224
284
|
return self.values.max_value
|
225
285
|
|
@@ -229,11 +289,33 @@ class Parameter(ABC):
|
|
229
289
|
return self.description.unit_of_measurement
|
230
290
|
|
231
291
|
|
232
|
-
|
233
|
-
|
292
|
+
@dataslots
|
293
|
+
@dataclass
|
294
|
+
class SwitchDescription(ParameterDescription):
|
295
|
+
"""Represents a switch description."""
|
296
|
+
|
297
|
+
|
298
|
+
class Switch(Parameter):
|
299
|
+
"""Represents a switch."""
|
300
|
+
|
301
|
+
__slots__ = ()
|
302
|
+
|
303
|
+
description: SwitchDescription
|
304
|
+
|
305
|
+
async def set(
|
306
|
+
self, value: bool | Literal["off", "on"], retries: int = SET_RETRIES
|
307
|
+
) -> bool:
|
308
|
+
"""Set a parameter value."""
|
309
|
+
return await super().set(value, retries)
|
310
|
+
|
311
|
+
def set_nowait(
|
312
|
+
self, value: bool | Literal["off", "on"], retries: int = SET_RETRIES
|
313
|
+
) -> None:
|
314
|
+
"""Set a switch value without waiting."""
|
315
|
+
self.device.create_task(self.set(value, retries))
|
234
316
|
|
235
317
|
async def turn_on(self) -> bool:
|
236
|
-
"""Set a
|
318
|
+
"""Set a switch value to 'on'.
|
237
319
|
|
238
320
|
:return: `True` if parameter was successfully turned on, `False`
|
239
321
|
otherwise.
|
@@ -242,7 +324,7 @@ class BinaryParameter(Parameter):
|
|
242
324
|
return await self.set(STATE_ON)
|
243
325
|
|
244
326
|
async def turn_off(self) -> bool:
|
245
|
-
"""Set a
|
327
|
+
"""Set a switch value to 'off'.
|
246
328
|
|
247
329
|
:return: `True` if parameter was successfully turned off, `False`
|
248
330
|
otherwise.
|
@@ -251,24 +333,28 @@ class BinaryParameter(Parameter):
|
|
251
333
|
return await self.set(STATE_OFF)
|
252
334
|
|
253
335
|
def turn_on_nowait(self) -> None:
|
254
|
-
"""Set a
|
336
|
+
"""Set a switch value to 'on' without waiting."""
|
255
337
|
self.set_nowait(STATE_ON)
|
256
338
|
|
257
339
|
def turn_off_nowait(self) -> None:
|
258
|
-
"""Set a
|
340
|
+
"""Set a switch value to 'off' without waiting."""
|
259
341
|
self.set_nowait(STATE_OFF)
|
260
342
|
|
343
|
+
async def create_request(self) -> Request:
|
344
|
+
"""Create a request to change the switch."""
|
345
|
+
return Request()
|
346
|
+
|
261
347
|
@property
|
262
|
-
def value(self) ->
|
263
|
-
"""Return the
|
348
|
+
def value(self) -> Literal["off", "on"]:
|
349
|
+
"""Return the value."""
|
264
350
|
return STATE_ON if self.values.value == 1 else STATE_OFF
|
265
351
|
|
266
352
|
@property
|
267
|
-
def min_value(self) ->
|
353
|
+
def min_value(self) -> Literal["off"]:
|
268
354
|
"""Return the minimum allowed value."""
|
269
355
|
return STATE_OFF
|
270
356
|
|
271
357
|
@property
|
272
|
-
def max_value(self) ->
|
358
|
+
def max_value(self) -> Literal["on"]:
|
273
359
|
"""Return the maximum allowed value."""
|
274
360
|
return STATE_ON
|
pyplumio/helpers/task_manager.py
CHANGED
@@ -10,15 +10,20 @@ from typing import Any
|
|
10
10
|
class TaskManager:
|
11
11
|
"""Represents a task manager."""
|
12
12
|
|
13
|
+
__slots__ = ("_tasks",)
|
14
|
+
|
13
15
|
_tasks: set[asyncio.Task]
|
14
16
|
|
15
17
|
def __init__(self) -> None:
|
16
18
|
"""Initialize a new task manager."""
|
19
|
+
super().__init__()
|
17
20
|
self._tasks = set()
|
18
21
|
|
19
|
-
def create_task(
|
22
|
+
def create_task(
|
23
|
+
self, coro: Coroutine[Any, Any, Any], name: str | None = None
|
24
|
+
) -> asyncio.Task:
|
20
25
|
"""Create asyncio task and store a reference for it."""
|
21
|
-
task = asyncio.create_task(coro)
|
26
|
+
task = asyncio.create_task(coro, name=name)
|
22
27
|
self._tasks.add(task)
|
23
28
|
task.add_done_callback(self._tasks.discard)
|
24
29
|
return task
|
pyplumio/helpers/timeout.py
CHANGED
@@ -5,7 +5,6 @@ from __future__ import annotations
|
|
5
5
|
import asyncio
|
6
6
|
from collections.abc import Awaitable, Callable, Coroutine
|
7
7
|
from functools import wraps
|
8
|
-
import logging
|
9
8
|
from typing import Any, TypeVar
|
10
9
|
|
11
10
|
from typing_extensions import ParamSpec
|
@@ -13,8 +12,6 @@ from typing_extensions import ParamSpec
|
|
13
12
|
T = TypeVar("T")
|
14
13
|
P = ParamSpec("P")
|
15
14
|
|
16
|
-
_LOGGER = logging.getLogger(__name__)
|
17
|
-
|
18
15
|
|
19
16
|
def timeout(
|
20
17
|
seconds: int,
|
pyplumio/helpers/uid.py
CHANGED
@@ -7,6 +7,7 @@ from typing import Final
|
|
7
7
|
|
8
8
|
CRC: Final = 0xA3A3
|
9
9
|
POLYNOMIAL: Final = 0xA001
|
10
|
+
BASE5_KEY: Final = "0123456789ABCDEFGHIJKLMNZPQRSTUV"
|
10
11
|
|
11
12
|
|
12
13
|
def decode_uid(buffer: bytes) -> str:
|
@@ -16,11 +17,10 @@ def decode_uid(buffer: bytes) -> str:
|
|
16
17
|
|
17
18
|
def _base5(buffer: bytes) -> str:
|
18
19
|
"""Encode bytes to a base5 encoded string."""
|
19
|
-
key_string = "0123456789ABCDEFGHIJKLMNZPQRSTUV"
|
20
20
|
number = int.from_bytes(buffer, byteorder="little")
|
21
21
|
output = ""
|
22
22
|
while number:
|
23
|
-
output =
|
23
|
+
output = BASE5_KEY[number & 0b00011111] + output
|
24
24
|
number >>= 5
|
25
25
|
|
26
26
|
return output
|
pyplumio/protocol.py
CHANGED
@@ -8,9 +8,11 @@ from collections.abc import Awaitable, Callable
|
|
8
8
|
from dataclasses import dataclass
|
9
9
|
import logging
|
10
10
|
|
11
|
+
from typing_extensions import TypeAlias
|
12
|
+
|
11
13
|
from pyplumio.const import ATTR_CONNECTED, DeviceType
|
12
14
|
from pyplumio.devices import AddressableDevice
|
13
|
-
from pyplumio.exceptions import
|
15
|
+
from pyplumio.exceptions import ProtocolError
|
14
16
|
from pyplumio.frames import Frame
|
15
17
|
from pyplumio.frames.requests import StartMasterRequest
|
16
18
|
from pyplumio.helpers.event_manager import EventManager
|
@@ -23,6 +25,8 @@ from pyplumio.structures.network_info import (
|
|
23
25
|
|
24
26
|
_LOGGER = logging.getLogger(__name__)
|
25
27
|
|
28
|
+
_Callback: TypeAlias = Callable[[], Awaitable[None]]
|
29
|
+
|
26
30
|
|
27
31
|
class Protocol(ABC):
|
28
32
|
"""Represents a protocol."""
|
@@ -30,7 +34,7 @@ class Protocol(ABC):
|
|
30
34
|
connected: asyncio.Event
|
31
35
|
reader: FrameReader | None
|
32
36
|
writer: FrameWriter | None
|
33
|
-
_on_connection_lost: set[
|
37
|
+
_on_connection_lost: set[_Callback]
|
34
38
|
|
35
39
|
def __init__(self) -> None:
|
36
40
|
"""Initialize a new protocol."""
|
@@ -47,7 +51,7 @@ class Protocol(ABC):
|
|
47
51
|
self.writer = None
|
48
52
|
|
49
53
|
@property
|
50
|
-
def on_connection_lost(self) -> set[
|
54
|
+
def on_connection_lost(self) -> set[_Callback]:
|
51
55
|
"""Return the callbacks that'll be called on connection lost."""
|
52
56
|
return self._on_connection_lost
|
53
57
|
|
@@ -86,8 +90,7 @@ class DummyProtocol(Protocol):
|
|
86
90
|
if self.connected.is_set():
|
87
91
|
self.connected.clear()
|
88
92
|
await self.close_writer()
|
89
|
-
for callback in self.on_connection_lost
|
90
|
-
await callback()
|
93
|
+
await asyncio.gather(*(callback() for callback in self.on_connection_lost))
|
91
94
|
|
92
95
|
async def shutdown(self) -> None:
|
93
96
|
"""Shutdown the protocol."""
|
@@ -102,15 +105,15 @@ class Queues:
|
|
102
105
|
|
103
106
|
__slots__ = ("read", "write")
|
104
107
|
|
105
|
-
read: asyncio.Queue
|
106
|
-
write: asyncio.Queue
|
108
|
+
read: asyncio.Queue[Frame]
|
109
|
+
write: asyncio.Queue[Frame]
|
107
110
|
|
108
111
|
async def join(self) -> None:
|
109
112
|
"""Wait for queues to finish."""
|
110
113
|
await asyncio.gather(self.read.join(), self.write.join())
|
111
114
|
|
112
115
|
|
113
|
-
class AsyncProtocol(Protocol, EventManager):
|
116
|
+
class AsyncProtocol(Protocol, EventManager[AddressableDevice]):
|
114
117
|
"""Represents an async protocol.
|
115
118
|
|
116
119
|
This protocol implements producer-consumers pattern using
|
@@ -127,7 +130,6 @@ class AsyncProtocol(Protocol, EventManager):
|
|
127
130
|
"""
|
128
131
|
|
129
132
|
consumers_count: int
|
130
|
-
data: dict[str, AddressableDevice]
|
131
133
|
_network: NetworkInfo
|
132
134
|
_queues: Queues
|
133
135
|
|
@@ -154,37 +156,42 @@ class AsyncProtocol(Protocol, EventManager):
|
|
154
156
|
self.writer = FrameWriter(writer)
|
155
157
|
self._queues.write.put_nowait(StartMasterRequest(recipient=DeviceType.ECOMAX))
|
156
158
|
self.create_task(
|
157
|
-
self.frame_producer(self._queues, reader=self.reader, writer=self.writer)
|
159
|
+
self.frame_producer(self._queues, reader=self.reader, writer=self.writer),
|
160
|
+
name="frame_producer_task",
|
158
161
|
)
|
159
|
-
for
|
160
|
-
self.create_task(
|
162
|
+
for consumer in range(self.consumers_count):
|
163
|
+
self.create_task(
|
164
|
+
self.frame_consumer(self._queues.read),
|
165
|
+
name=f"frame_consumer_task ({consumer})",
|
166
|
+
)
|
161
167
|
|
162
168
|
for device in self.data.values():
|
163
169
|
device.dispatch_nowait(ATTR_CONNECTED, True)
|
164
170
|
|
165
171
|
self.connected.set()
|
166
172
|
|
167
|
-
async def
|
168
|
-
"""Close the
|
169
|
-
if not self.connected.is_set():
|
170
|
-
return
|
171
|
-
|
173
|
+
async def _connection_close(self) -> None:
|
174
|
+
"""Close the connection if it is established."""
|
172
175
|
self.connected.clear()
|
173
|
-
await self.close_writer()
|
174
176
|
await asyncio.gather(
|
175
|
-
*
|
177
|
+
*(device.dispatch(ATTR_CONNECTED, False) for device in self.data.values())
|
176
178
|
)
|
177
|
-
await
|
179
|
+
await self.close_writer()
|
180
|
+
|
181
|
+
async def connection_lost(self) -> None:
|
182
|
+
"""Close the connection and call connection lost callbacks."""
|
183
|
+
if self.connected.is_set():
|
184
|
+
await self._connection_close()
|
185
|
+
await asyncio.gather(*(callback() for callback in self.on_connection_lost))
|
178
186
|
|
179
187
|
async def shutdown(self) -> None:
|
180
|
-
"""Shutdown protocol
|
188
|
+
"""Shutdown the protocol and close the connection."""
|
181
189
|
await self._queues.join()
|
182
190
|
self.cancel_tasks()
|
183
191
|
await self.wait_until_done()
|
184
|
-
await asyncio.gather(*[device.shutdown() for device in self.data.values()])
|
185
192
|
if self.connected.is_set():
|
186
|
-
self.
|
187
|
-
await self.
|
193
|
+
await self._connection_close()
|
194
|
+
await asyncio.gather(*(device.shutdown() for device in self.data.values()))
|
188
195
|
|
189
196
|
async def frame_producer(
|
190
197
|
self, queues: Queues, reader: FrameReader, writer: FrameWriter
|
@@ -200,7 +207,7 @@ class AsyncProtocol(Protocol, EventManager):
|
|
200
207
|
if (response := await reader.read()) is not None:
|
201
208
|
queues.read.put_nowait(response)
|
202
209
|
|
203
|
-
except
|
210
|
+
except ProtocolError as e:
|
204
211
|
_LOGGER.debug("Can't process received frame: %s", e)
|
205
212
|
except (OSError, asyncio.TimeoutError):
|
206
213
|
self.create_task(self.connection_lost())
|
@@ -208,11 +215,11 @@ class AsyncProtocol(Protocol, EventManager):
|
|
208
215
|
except Exception:
|
209
216
|
_LOGGER.exception("Unexpected exception")
|
210
217
|
|
211
|
-
async def frame_consumer(self, queue: asyncio.Queue) -> None:
|
218
|
+
async def frame_consumer(self, queue: asyncio.Queue[Frame]) -> None:
|
212
219
|
"""Handle frame processing."""
|
213
220
|
await self.connected.wait()
|
214
221
|
while self.connected.is_set():
|
215
|
-
frame
|
222
|
+
frame = await queue.get()
|
216
223
|
device = await self.get_device_entry(frame.sender)
|
217
224
|
device.handle_frame(frame)
|
218
225
|
queue.task_done()
|
@@ -225,7 +232,7 @@ class AsyncProtocol(Protocol, EventManager):
|
|
225
232
|
device_type, queue=self._queues.write, network=self._network
|
226
233
|
)
|
227
234
|
device.dispatch_nowait(ATTR_CONNECTED, True)
|
228
|
-
self.create_task(device.async_setup())
|
235
|
+
self.create_task(device.async_setup(), name=f"device_setup_task ({name})")
|
229
236
|
self.set_event(name)
|
230
237
|
self.data[name] = device
|
231
238
|
|
pyplumio/stream.py
CHANGED
@@ -100,8 +100,8 @@ class FrameReader:
|
|
100
100
|
"""Read the frame and return corresponding handler object.
|
101
101
|
|
102
102
|
Raise pyplumio.ReadError on unexpected frame length or
|
103
|
-
incomplete frame and pyplumio.ChecksumError on incorrect
|
104
|
-
checksum.
|
103
|
+
incomplete frame and pyplumio. Raise ChecksumError on incorrect
|
104
|
+
frame checksum.
|
105
105
|
"""
|
106
106
|
(
|
107
107
|
header_bytes,
|