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.
Files changed (37) hide show
  1. {PyPlumIO-0.5.21.dist-info → PyPlumIO-0.5.23.dist-info}/METADATA +12 -10
  2. PyPlumIO-0.5.23.dist-info/RECORD +60 -0
  3. {PyPlumIO-0.5.21.dist-info → PyPlumIO-0.5.23.dist-info}/WHEEL +1 -1
  4. pyplumio/__init__.py +2 -2
  5. pyplumio/_version.py +2 -2
  6. pyplumio/connection.py +3 -12
  7. pyplumio/devices/__init__.py +16 -16
  8. pyplumio/devices/ecomax.py +126 -126
  9. pyplumio/devices/mixer.py +50 -44
  10. pyplumio/devices/thermostat.py +36 -35
  11. pyplumio/exceptions.py +9 -9
  12. pyplumio/filters.py +56 -37
  13. pyplumio/frames/__init__.py +6 -6
  14. pyplumio/frames/messages.py +4 -6
  15. pyplumio/helpers/data_types.py +8 -7
  16. pyplumio/helpers/event_manager.py +53 -33
  17. pyplumio/helpers/parameter.py +138 -52
  18. pyplumio/helpers/task_manager.py +7 -2
  19. pyplumio/helpers/timeout.py +0 -3
  20. pyplumio/helpers/uid.py +2 -2
  21. pyplumio/protocol.py +35 -28
  22. pyplumio/stream.py +2 -2
  23. pyplumio/structures/alerts.py +40 -31
  24. pyplumio/structures/ecomax_parameters.py +493 -282
  25. pyplumio/structures/frame_versions.py +5 -6
  26. pyplumio/structures/lambda_sensor.py +6 -6
  27. pyplumio/structures/mixer_parameters.py +136 -71
  28. pyplumio/structures/network_info.py +2 -3
  29. pyplumio/structures/product_info.py +0 -4
  30. pyplumio/structures/program_version.py +24 -17
  31. pyplumio/structures/schedules.py +35 -15
  32. pyplumio/structures/thermostat_parameters.py +82 -50
  33. pyplumio/utils.py +12 -7
  34. PyPlumIO-0.5.21.dist-info/RECORD +0 -61
  35. pyplumio/helpers/typing.py +0 -29
  36. {PyPlumIO-0.5.21.dist-info → PyPlumIO-0.5.23.dist-info}/LICENSE +0 -0
  37. {PyPlumIO-0.5.21.dist-info → PyPlumIO-0.5.23.dist-info}/top_level.txt +0 -0
@@ -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 to an integer."""
49
- if isinstance(value, str):
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(ABC):
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", "values", "description", "_pending_update", "_index")
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
- func = getattr(self.values.value, method_to_call)
120
- return func(_normalize_parameter_value(other))
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 _confirm_update(self, parameter: Parameter) -> None:
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.values.value = value
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 set_nowait(self, value: ParameterValueType, retries: int = SET_RETRIES) -> None:
203
- """Set a parameter value without waiting."""
204
- self.device.create_task(self.set(value, retries))
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 value(self) -> ParameterValueType:
213
- """Return the parameter value."""
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) -> ParameterValueType:
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) -> ParameterValueType:
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
- class BinaryParameter(Parameter):
233
- """Represents binary device parameter."""
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 parameter value to 'on'.
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 parameter value to 'off'.
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 parameter value to 'on' without waiting."""
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 parameter value to 'off' without waiting."""
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) -> ParameterValueType:
263
- """Return the parameter value."""
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) -> ParameterValueType:
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) -> ParameterValueType:
358
+ def max_value(self) -> Literal["on"]:
273
359
  """Return the maximum allowed value."""
274
360
  return STATE_ON
@@ -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(self, coro: Coroutine[Any, Any, Any]) -> asyncio.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
@@ -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 = key_string[number & 0b00011111] + 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 FrameError, ReadError, UnknownDeviceError
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[Callable[[], Awaitable[None]]]
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[Callable[[], Awaitable[None]]]:
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 _ in range(self.consumers_count):
160
- self.create_task(self.frame_consumer(self._queues.read))
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 connection_lost(self) -> None:
168
- """Close the writer and call connection lost callbacks."""
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
- *[device.dispatch(ATTR_CONNECTED, False) for device in self.data.values()]
177
+ *(device.dispatch(ATTR_CONNECTED, False) for device in self.data.values())
176
178
  )
177
- await asyncio.gather(*[callback() for callback in self.on_connection_lost])
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 tasks."""
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.connected.clear()
187
- await self.close_writer()
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 (ReadError, UnknownDeviceError, FrameError) as e:
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: Frame = await queue.get()
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 frame
104
- checksum.
103
+ incomplete frame and pyplumio. Raise ChecksumError on incorrect
104
+ frame checksum.
105
105
  """
106
106
  (
107
107
  header_bytes,