PyPlumIO 0.6.0__py3-none-any.whl → 0.6.2__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 (49) hide show
  1. pyplumio/__init__.py +3 -1
  2. pyplumio/_version.py +2 -2
  3. pyplumio/connection.py +0 -36
  4. pyplumio/const.py +0 -5
  5. pyplumio/data_types.py +2 -2
  6. pyplumio/devices/__init__.py +23 -5
  7. pyplumio/devices/ecomax.py +30 -53
  8. pyplumio/devices/ecoster.py +2 -3
  9. pyplumio/filters.py +199 -136
  10. pyplumio/frames/__init__.py +101 -15
  11. pyplumio/frames/messages.py +8 -65
  12. pyplumio/frames/requests.py +38 -38
  13. pyplumio/frames/responses.py +30 -86
  14. pyplumio/helpers/async_cache.py +13 -8
  15. pyplumio/helpers/event_manager.py +24 -18
  16. pyplumio/helpers/factory.py +0 -3
  17. pyplumio/parameters/__init__.py +38 -35
  18. pyplumio/protocol.py +63 -47
  19. pyplumio/structures/alerts.py +2 -2
  20. pyplumio/structures/ecomax_parameters.py +1 -1
  21. pyplumio/structures/frame_versions.py +3 -2
  22. pyplumio/structures/mixer_parameters.py +5 -3
  23. pyplumio/structures/network_info.py +1 -0
  24. pyplumio/structures/product_info.py +1 -1
  25. pyplumio/structures/program_version.py +2 -2
  26. pyplumio/structures/schedules.py +8 -40
  27. pyplumio/structures/sensor_data.py +498 -0
  28. pyplumio/structures/thermostat_parameters.py +7 -4
  29. pyplumio/utils.py +41 -4
  30. {pyplumio-0.6.0.dist-info → pyplumio-0.6.2.dist-info}/METADATA +7 -8
  31. pyplumio-0.6.2.dist-info/RECORD +50 -0
  32. pyplumio/structures/boiler_load.py +0 -32
  33. pyplumio/structures/boiler_power.py +0 -33
  34. pyplumio/structures/fan_power.py +0 -33
  35. pyplumio/structures/fuel_consumption.py +0 -36
  36. pyplumio/structures/fuel_level.py +0 -39
  37. pyplumio/structures/lambda_sensor.py +0 -57
  38. pyplumio/structures/mixer_sensors.py +0 -80
  39. pyplumio/structures/modules.py +0 -102
  40. pyplumio/structures/output_flags.py +0 -47
  41. pyplumio/structures/outputs.py +0 -88
  42. pyplumio/structures/pending_alerts.py +0 -28
  43. pyplumio/structures/statuses.py +0 -52
  44. pyplumio/structures/temperatures.py +0 -94
  45. pyplumio/structures/thermostat_sensors.py +0 -106
  46. pyplumio-0.6.0.dist-info/RECORD +0 -63
  47. {pyplumio-0.6.0.dist-info → pyplumio-0.6.2.dist-info}/WHEEL +0 -0
  48. {pyplumio-0.6.0.dist-info → pyplumio-0.6.2.dist-info}/licenses/LICENSE +0 -0
  49. {pyplumio-0.6.0.dist-info → pyplumio-0.6.2.dist-info}/top_level.txt +0 -0
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from abc import ABC, abstractmethod
6
- from dataclasses import dataclass
6
+ from collections.abc import Callable
7
7
  from functools import cache, reduce
8
8
  import struct
9
9
  from typing import TYPE_CHECKING, Any, ClassVar, Final, TypeVar
@@ -13,6 +13,9 @@ from pyplumio.exceptions import UnknownFrameError
13
13
  from pyplumio.helpers.factory import create_instance
14
14
  from pyplumio.utils import ensure_dict, to_camelcase
15
15
 
16
+ if TYPE_CHECKING:
17
+ from pyplumio.structures import Structure
18
+
16
19
  FRAME_START: Final = 0x68
17
20
  FRAME_END: Final = 0x16
18
21
 
@@ -60,15 +63,7 @@ def get_frame_handler(frame_type: int) -> str:
60
63
  return f"frames.{module.lower()}s.{type_name}{module.capitalize()}"
61
64
 
62
65
 
63
- @dataclass(slots=True)
64
- class DataFrameDescription:
65
- """Describes what data is provided by the frame."""
66
-
67
- frame_type: FrameType
68
- provides: str
69
-
70
-
71
- FrameT = TypeVar("FrameT", bound="Frame")
66
+ _FrameT = TypeVar("_FrameT", bound="Frame")
72
67
 
73
68
 
74
69
  class Frame(ABC):
@@ -235,17 +230,17 @@ class Frame(ABC):
235
230
  return bytes(data)
236
231
 
237
232
  @classmethod
238
- async def create(cls: type[FrameT], frame_type: int, **kwargs: Any) -> FrameT:
233
+ async def create(cls: type[_FrameT], frame_type: int, **kwargs: Any) -> _FrameT:
239
234
  """Create a frame handler object from frame type."""
240
235
  return await create_instance(get_frame_handler(frame_type), cls=cls, **kwargs)
241
236
 
242
237
  @abstractmethod
243
238
  def create_message(self, data: dict[str, Any]) -> bytearray:
244
- """Create frame message."""
239
+ """Create a frame message."""
245
240
 
246
241
  @abstractmethod
247
242
  def decode_message(self, message: bytearray) -> dict[str, Any]:
248
- """Decode frame message."""
243
+ """Decode a frame message."""
249
244
 
250
245
 
251
246
  class Request(Frame):
@@ -286,13 +281,104 @@ class Message(Response):
286
281
  __slots__ = ()
287
282
 
288
283
 
284
+ class Structured(Frame):
285
+ """Represents a frame that has known structure."""
286
+
287
+ __slots__ = ("_structures",)
288
+
289
+ container: ClassVar[str]
290
+ structures: ClassVar[list[type[Structure]]]
291
+
292
+ _structures: list[Structure]
293
+
294
+ def __init__(
295
+ self,
296
+ recipient: DeviceType = DeviceType.ALL,
297
+ sender: DeviceType = DeviceType.ECONET,
298
+ econet_type: int = ECONET_TYPE,
299
+ econet_version: int = ECONET_VERSION,
300
+ message: bytearray | None = None,
301
+ data: dict[str, Any] | None = None,
302
+ **kwargs: Any,
303
+ ) -> None:
304
+ """Initialize a new structured frame."""
305
+ if hasattr(self, "structures"):
306
+ self._structures = [structure(self) for structure in self.structures]
307
+
308
+ super().__init__(
309
+ recipient, sender, econet_type, econet_version, message, data, **kwargs
310
+ )
311
+
312
+ def create_message(self, data: dict[str, Any]) -> bytearray:
313
+ """Create frame message."""
314
+ message = bytearray()
315
+ for structure in self._structures:
316
+ message += structure.encode(data)
317
+
318
+ return message
319
+
320
+ def decode_message(self, message: bytearray) -> dict[str, Any]:
321
+ """Decode frame message."""
322
+ data: dict[str, Any] = {}
323
+ offset = 0
324
+ for structure in self._structures:
325
+ data, offset = structure.decode(message, offset, data)
326
+
327
+ if hasattr(self, "container"):
328
+ return {self.container: data}
329
+
330
+ return data
331
+
332
+
333
+ def frame_handler(
334
+ frame_type: FrameType, structure: type[Structure] | None = None
335
+ ) -> Callable[[type[_FrameT]], type[_FrameT]]:
336
+ """Specify frame type for the frame class."""
337
+
338
+ def wrapper(cls: type[_FrameT]) -> type[_FrameT]:
339
+ """Wrap the frame class."""
340
+ setattr(cls, "frame_type", frame_type)
341
+ if structure and issubclass(cls, Structured):
342
+ setattr(cls, "structures", (structure,))
343
+
344
+ return cls
345
+
346
+ return wrapper
347
+
348
+
349
+ _StructuredT = TypeVar("_StructuredT", bound=Structured)
350
+
351
+
352
+ def contains(
353
+ *structures: type[Structure], container: str | None = None
354
+ ) -> Callable[[type[_StructuredT]], type[_StructuredT]]:
355
+ """Decorate frame class with structure.
356
+
357
+ Indicate which structures need to be used for encoding/decoding
358
+ the frame as well as provide a way to wrap output data under
359
+ a single key.
360
+ """
361
+
362
+ def wrapper(cls: type[_StructuredT]) -> type[_StructuredT]:
363
+ """Wrap the frame class."""
364
+ setattr(cls, "structures", structures)
365
+ if container:
366
+ setattr(cls, "container", container)
367
+
368
+ return cls
369
+
370
+ return wrapper
371
+
372
+
289
373
  __all__ = [
290
374
  "Frame",
291
375
  "Request",
292
376
  "Response",
293
377
  "Message",
294
- "DataFrameDescription",
378
+ "Structured",
295
379
  "bcc",
296
- "is_known_frame_type",
380
+ "contains",
381
+ "frame_handler",
297
382
  "get_frame_handler",
383
+ "is_known_frame_type",
298
384
  ]
@@ -2,83 +2,26 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from contextlib import suppress
6
- from typing import Any
7
-
8
- from pyplumio.const import (
9
- ATTR_SENSORS,
10
- ATTR_STATE,
11
- ATTR_THERMOSTAT,
12
- ATTR_TRANSMISSION,
13
- DeviceState,
14
- FrameType,
15
- )
16
- from pyplumio.frames import Message
17
- from pyplumio.structures.boiler_load import BoilerLoadStructure
18
- from pyplumio.structures.boiler_power import BoilerPowerStructure
19
- from pyplumio.structures.fan_power import FanPowerStructure
5
+ from pyplumio.const import ATTR_SENSORS, FrameType
6
+ from pyplumio.frames import Message, Structured, contains, frame_handler
20
7
  from pyplumio.structures.frame_versions import FrameVersionsStructure
21
- from pyplumio.structures.fuel_consumption import FuelConsumptionStructure
22
- from pyplumio.structures.fuel_level import FuelLevelStructure
23
- from pyplumio.structures.lambda_sensor import LambdaSensorStructure
24
- from pyplumio.structures.mixer_sensors import MixerSensorsStructure
25
- from pyplumio.structures.modules import ModulesStructure
26
- from pyplumio.structures.output_flags import OutputFlagsStructure
27
- from pyplumio.structures.outputs import OutputsStructure
28
- from pyplumio.structures.pending_alerts import PendingAlertsStructure
29
8
  from pyplumio.structures.regulator_data import RegulatorDataStructure
30
- from pyplumio.structures.statuses import StatusesStructure
31
- from pyplumio.structures.temperatures import TemperaturesStructure
32
- from pyplumio.structures.thermostat_sensors import ThermostatSensorsStructure
9
+ from pyplumio.structures.sensor_data import SensorDataStructure
33
10
 
34
11
 
35
- class RegulatorDataMessage(Message):
12
+ @frame_handler(FrameType.MESSAGE_REGULATOR_DATA, structure=RegulatorDataStructure)
13
+ class RegulatorDataMessage(Structured, Message):
36
14
  """Represents a regulator data message."""
37
15
 
38
16
  __slots__ = ()
39
17
 
40
- frame_type = FrameType.MESSAGE_REGULATOR_DATA
41
-
42
- def decode_message(self, message: bytearray) -> dict[str, Any]:
43
- """Decode a frame message."""
44
- return RegulatorDataStructure(self).decode(message)[0]
45
-
46
18
 
47
- class SensorDataMessage(Message):
19
+ @frame_handler(FrameType.MESSAGE_SENSOR_DATA)
20
+ @contains(FrameVersionsStructure, SensorDataStructure, container=ATTR_SENSORS)
21
+ class SensorDataMessage(Structured, Message):
48
22
  """Represents a sensor data message."""
49
23
 
50
24
  __slots__ = ()
51
25
 
52
- frame_type = FrameType.MESSAGE_SENSOR_DATA
53
-
54
- def decode_message(self, message: bytearray) -> dict[str, Any]:
55
- """Decode a frame message."""
56
- sensors, offset = FrameVersionsStructure(self).decode(message, offset=0)
57
- sensors[ATTR_STATE] = message[offset]
58
- sensors, offset = OutputsStructure(self).decode(message, offset + 1, sensors)
59
- sensors, offset = OutputFlagsStructure(self).decode(message, offset, sensors)
60
- sensors, offset = TemperaturesStructure(self).decode(message, offset, sensors)
61
- sensors, offset = StatusesStructure(self).decode(message, offset, sensors)
62
- sensors, offset = PendingAlertsStructure(self).decode(message, offset, sensors)
63
- sensors, offset = FuelLevelStructure(self).decode(message, offset, sensors)
64
- sensors[ATTR_TRANSMISSION] = message[offset]
65
- sensors, offset = FanPowerStructure(self).decode(message, offset + 1, sensors)
66
- sensors, offset = BoilerLoadStructure(self).decode(message, offset, sensors)
67
- sensors, offset = BoilerPowerStructure(self).decode(message, offset, sensors)
68
- sensors, offset = FuelConsumptionStructure(self).decode(
69
- message, offset, sensors
70
- )
71
- sensors[ATTR_THERMOSTAT] = message[offset]
72
- sensors, offset = ModulesStructure(self).decode(message, offset + 1, sensors)
73
- sensors, offset = LambdaSensorStructure(self).decode(message, offset, sensors)
74
- sensors, offset = ThermostatSensorsStructure(self).decode(
75
- message, offset, sensors
76
- )
77
- sensors, offset = MixerSensorsStructure(self).decode(message, offset, sensors)
78
- with suppress(ValueError):
79
- sensors[ATTR_STATE] = DeviceState(sensors[ATTR_STATE])
80
-
81
- return {ATTR_SENSORS: sensors}
82
-
83
26
 
84
27
  __all__ = ["RegulatorDataMessage", "SensorDataMessage"]
@@ -2,24 +2,30 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import Any
5
+ from typing import Any, cast
6
6
 
7
7
  from pyplumio.const import (
8
8
  ATTR_COUNT,
9
9
  ATTR_DEVICE_INDEX,
10
10
  ATTR_INDEX,
11
11
  ATTR_OFFSET,
12
+ ATTR_PARAMETER,
13
+ ATTR_SCHEDULE,
12
14
  ATTR_SIZE,
13
15
  ATTR_START,
16
+ ATTR_SWITCH,
17
+ ATTR_TYPE,
14
18
  ATTR_VALUE,
15
19
  FrameType,
16
20
  )
17
21
  from pyplumio.exceptions import FrameDataError
18
- from pyplumio.frames import Request, Response
22
+ from pyplumio.frames import Request, Response, frame_handler
19
23
  from pyplumio.frames.responses import DeviceAvailableResponse, ProgramVersionResponse
20
- from pyplumio.structures.schedules import SchedulesStructure
24
+ from pyplumio.structures.schedules import SCHEDULES
25
+ from pyplumio.utils import join_bits
21
26
 
22
27
 
28
+ @frame_handler(FrameType.REQUEST_ALERTS)
23
29
  class AlertsRequest(Request):
24
30
  """Represents an alerts request.
25
31
 
@@ -29,25 +35,23 @@ class AlertsRequest(Request):
29
35
 
30
36
  __slots__ = ()
31
37
 
32
- frame_type = FrameType.REQUEST_ALERTS
33
-
34
38
  def create_message(self, data: dict[str, Any]) -> bytearray:
35
39
  """Create a frame message."""
36
40
  return bytearray([data.get(ATTR_START, 0), data.get(ATTR_COUNT, 10)])
37
41
 
38
42
 
43
+ @frame_handler(FrameType.REQUEST_CHECK_DEVICE)
39
44
  class CheckDeviceRequest(Request):
40
45
  """Represents a check device request."""
41
46
 
42
47
  __slots__ = ()
43
48
 
44
- frame_type = FrameType.REQUEST_CHECK_DEVICE
45
-
46
49
  def response(self, **kwargs: Any) -> Response | None:
47
50
  """Return a response frame."""
48
51
  return DeviceAvailableResponse(recipient=self.sender, **kwargs)
49
52
 
50
53
 
54
+ @frame_handler(FrameType.REQUEST_ECOMAX_CONTROL)
51
55
  class EcomaxControlRequest(Request):
52
56
  """Represents an ecoMAX control request.
53
57
 
@@ -57,8 +61,6 @@ class EcomaxControlRequest(Request):
57
61
 
58
62
  __slots__ = ()
59
63
 
60
- frame_type = FrameType.REQUEST_ECOMAX_CONTROL
61
-
62
64
  def create_message(self, data: dict[str, Any]) -> bytearray:
63
65
  """Create a frame message."""
64
66
  try:
@@ -67,6 +69,7 @@ class EcomaxControlRequest(Request):
67
69
  raise FrameDataError from e
68
70
 
69
71
 
72
+ @frame_handler(FrameType.REQUEST_ECOMAX_PARAMETERS)
70
73
  class EcomaxParametersRequest(Request):
71
74
  """Represents an ecoMAX parameters request.
72
75
 
@@ -75,13 +78,12 @@ class EcomaxParametersRequest(Request):
75
78
 
76
79
  __slots__ = ()
77
80
 
78
- frame_type = FrameType.REQUEST_ECOMAX_PARAMETERS
79
-
80
81
  def create_message(self, data: dict[str, Any]) -> bytearray:
81
82
  """Create a frame message."""
82
83
  return bytearray([data.get(ATTR_COUNT, 255), data.get(ATTR_START, 0)])
83
84
 
84
85
 
86
+ @frame_handler(FrameType.REQUEST_MIXER_PARAMETERS)
85
87
  class MixerParametersRequest(Request):
86
88
  """Represents a mixer parameters request.
87
89
 
@@ -91,49 +93,44 @@ class MixerParametersRequest(Request):
91
93
 
92
94
  __slots__ = ()
93
95
 
94
- frame_type = FrameType.REQUEST_MIXER_PARAMETERS
95
-
96
96
  def create_message(self, data: dict[str, Any]) -> bytearray:
97
97
  """Create a frame message."""
98
98
  return bytearray([data.get(ATTR_COUNT, 255), data.get(ATTR_START, 0)])
99
99
 
100
100
 
101
+ @frame_handler(FrameType.REQUEST_PASSWORD)
101
102
  class PasswordRequest(Request):
102
103
  """Represents a password request."""
103
104
 
104
105
  __slots__ = ()
105
106
 
106
- frame_type = FrameType.REQUEST_PASSWORD
107
-
108
107
 
108
+ @frame_handler(FrameType.REQUEST_PROGRAM_VERSION)
109
109
  class ProgramVersionRequest(Request):
110
110
  """Represents a program version request."""
111
111
 
112
112
  __slots__ = ()
113
113
 
114
- frame_type = FrameType.REQUEST_PROGRAM_VERSION
115
-
116
114
  def response(self, **kwargs: Any) -> Response | None:
117
115
  """Return a response frame."""
118
116
  return ProgramVersionResponse(recipient=self.sender, **kwargs)
119
117
 
120
118
 
119
+ @frame_handler(FrameType.REQUEST_REGULATOR_DATA_SCHEMA)
121
120
  class RegulatorDataSchemaRequest(Request):
122
121
  """Represents regulator data schema request."""
123
122
 
124
123
  __slots__ = ()
125
124
 
126
- frame_type = FrameType.REQUEST_REGULATOR_DATA_SCHEMA
127
-
128
125
 
126
+ @frame_handler(FrameType.REQUEST_SCHEDULES)
129
127
  class SchedulesRequest(Request):
130
128
  """Represents a schedules request."""
131
129
 
132
130
  __slots__ = ()
133
131
 
134
- frame_type = FrameType.REQUEST_SCHEDULES
135
-
136
132
 
133
+ @frame_handler(FrameType.REQUEST_SET_ECOMAX_PARAMETER)
137
134
  class SetEcomaxParameterRequest(Request):
138
135
  """Represents a request to set an ecoMAX parameter.
139
136
 
@@ -142,8 +139,6 @@ class SetEcomaxParameterRequest(Request):
142
139
 
143
140
  __slots__ = ()
144
141
 
145
- frame_type = FrameType.REQUEST_SET_ECOMAX_PARAMETER
146
-
147
142
  def create_message(self, data: dict[str, Any]) -> bytearray:
148
143
  """Create a frame message."""
149
144
  try:
@@ -152,6 +147,7 @@ class SetEcomaxParameterRequest(Request):
152
147
  raise FrameDataError from e
153
148
 
154
149
 
150
+ @frame_handler(FrameType.REQUEST_SET_MIXER_PARAMETER)
155
151
  class SetMixerParameterRequest(Request):
156
152
  """Represents a request to set a mixer parameter.
157
153
 
@@ -160,8 +156,6 @@ class SetMixerParameterRequest(Request):
160
156
 
161
157
  __slots__ = ()
162
158
 
163
- frame_type = FrameType.REQUEST_SET_MIXER_PARAMETER
164
-
165
159
  def create_message(self, data: dict[str, Any]) -> bytearray:
166
160
  """Create a frame message."""
167
161
  try:
@@ -172,18 +166,30 @@ class SetMixerParameterRequest(Request):
172
166
  raise FrameDataError from e
173
167
 
174
168
 
169
+ @frame_handler(FrameType.REQUEST_SET_SCHEDULE)
175
170
  class SetScheduleRequest(Request):
176
171
  """Represents a request to set a schedule."""
177
172
 
178
173
  __slots__ = ()
179
174
 
180
- frame_type = FrameType.REQUEST_SET_SCHEDULE
181
-
182
175
  def create_message(self, data: dict[str, Any]) -> bytearray:
183
176
  """Create a frame message."""
184
- return SchedulesStructure(self).encode(data)
177
+ message = b"\1"
178
+ try:
179
+ schedule_type = SCHEDULES.index(data[ATTR_TYPE])
180
+ message += schedule_type.to_bytes(length=1, byteorder="little")
181
+ message += int(data[ATTR_SWITCH]).to_bytes(length=1, byteorder="little")
182
+ message += int(data[ATTR_PARAMETER]).to_bytes(length=1, byteorder="little")
183
+ schedule = cast(list[list[bool]], data[ATTR_SCHEDULE])
184
+ except (KeyError, ValueError) as e:
185
+ raise FrameDataError from e
186
+
187
+ return bytearray(message) + bytearray(
188
+ join_bits(day[i : i + 8]) for day in schedule for i in range(0, len(day), 8)
189
+ )
185
190
 
186
191
 
192
+ @frame_handler(FrameType.REQUEST_SET_THERMOSTAT_PARAMETER)
187
193
  class SetThermostatParameterRequest(Request):
188
194
  """Represents a request to set a thermostat parameter.
189
195
 
@@ -200,8 +206,6 @@ class SetThermostatParameterRequest(Request):
200
206
 
201
207
  __slots__ = ()
202
208
 
203
- frame_type = FrameType.REQUEST_SET_THERMOSTAT_PARAMETER
204
-
205
209
  def create_message(self, data: dict[str, Any]) -> bytearray:
206
210
  """Create a frame message."""
207
211
  try:
@@ -214,6 +218,7 @@ class SetThermostatParameterRequest(Request):
214
218
  raise FrameDataError from e
215
219
 
216
220
 
221
+ @frame_handler(FrameType.REQUEST_START_MASTER)
217
222
  class StartMasterRequest(Request):
218
223
  """Represents a request to become a master.
219
224
 
@@ -223,9 +228,8 @@ class StartMasterRequest(Request):
223
228
 
224
229
  __slots__ = ()
225
230
 
226
- frame_type = FrameType.REQUEST_START_MASTER
227
-
228
231
 
232
+ @frame_handler(FrameType.REQUEST_STOP_MASTER)
229
233
  class StopMasterRequest(Request):
230
234
  """Represents a request to stop being a master.
231
235
 
@@ -235,9 +239,8 @@ class StopMasterRequest(Request):
235
239
 
236
240
  __slots__ = ()
237
241
 
238
- frame_type = FrameType.REQUEST_STOP_MASTER
239
-
240
242
 
243
+ @frame_handler(FrameType.REQUEST_THERMOSTAT_PARAMETERS)
241
244
  class ThermostatParametersRequest(Request):
242
245
  """Represents a thermostat parameters request.
243
246
 
@@ -247,20 +250,17 @@ class ThermostatParametersRequest(Request):
247
250
 
248
251
  __slots__ = ()
249
252
 
250
- frame_type = FrameType.REQUEST_THERMOSTAT_PARAMETERS
251
-
252
253
  def create_message(self, data: dict[str, Any]) -> bytearray:
253
254
  """Create a frame message."""
254
255
  return bytearray([data.get(ATTR_COUNT, 255), data.get(ATTR_START, 0)])
255
256
 
256
257
 
258
+ @frame_handler(FrameType.REQUEST_UID)
257
259
  class UIDRequest(Request):
258
260
  """Represents an UID request."""
259
261
 
260
262
  __slots__ = ()
261
263
 
262
- frame_type = FrameType.REQUEST_UID
263
-
264
264
 
265
265
  __all__ = [
266
266
  "AlertsRequest",