python-dali 0.9__py3-none-any.whl → 0.11__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 (39) hide show
  1. dali/address.py +198 -57
  2. dali/command.py +24 -3
  3. dali/device/__init__.py +5 -0
  4. dali/device/general.py +650 -118
  5. dali/device/helpers.py +238 -0
  6. dali/device/light.py +209 -0
  7. dali/device/occupancy.py +322 -0
  8. dali/device/pushbutton.py +325 -0
  9. dali/device/sequences.py +277 -0
  10. dali/driver/atxled.py +154 -0
  11. dali/driver/hid.py +150 -56
  12. dali/driver/serial.py +1668 -0
  13. dali/driver/trace_logging.py +64 -0
  14. dali/frame.py +7 -35
  15. dali/gear/__init__.py +1 -0
  16. dali/gear/colour.py +432 -0
  17. dali/gear/general.py +29 -23
  18. dali/gear/led.py +19 -2
  19. dali/gear/sequences.py +113 -0
  20. dali/memory/location.py +139 -38
  21. dali/tests/fakes.py +392 -56
  22. dali/tests/fakes_serial.py +369 -0
  23. dali/tests/test_colour.py +69 -0
  24. dali/tests/test_command.py +12 -1
  25. dali/tests/test_device_sequences.py +365 -0
  26. dali/tests/test_dummy.py +194 -0
  27. dali/tests/test_events.py +359 -0
  28. dali/tests/test_fakes.py +266 -0
  29. dali/tests/test_frame.py +0 -42
  30. dali/tests/test_memory.py +36 -9
  31. dali/version.py +1 -1
  32. {python_dali-0.9.dist-info → python_dali-0.11.dist-info}/METADATA +39 -4
  33. python_dali-0.11.dist-info/RECORD +58 -0
  34. {python_dali-0.9.dist-info → python_dali-0.11.dist-info}/WHEEL +1 -1
  35. python_dali-0.9.dist-info/RECORD +0 -42
  36. {python_dali-0.9.dist-info → python_dali-0.11.dist-info}/COPYING +0 -0
  37. {python_dali-0.9.dist-info → python_dali-0.11.dist-info}/COPYING.LESSER +0 -0
  38. {python_dali-0.9.dist-info → python_dali-0.11.dist-info}/LICENSE +0 -0
  39. {python_dali-0.9.dist-info → python_dali-0.11.dist-info}/top_level.txt +0 -0
dali/address.py CHANGED
@@ -30,11 +30,12 @@ _bad_frame_length = IncompatibleFrame("Unsupported frame size")
30
30
  # Address bytes
31
31
  ###############################################################################
32
32
 
33
+
33
34
  class AddressTracker(type):
34
35
  """Metaclass keeping track of all the types of Address we understand."""
35
36
 
36
37
  def __init__(cls, name, bases, attrs):
37
- if not hasattr(cls, '_addrtypes'):
38
+ if not hasattr(cls, "_addrtypes"):
38
39
  cls._addrtypes = []
39
40
  else:
40
41
  cls._addrtypes.append(cls)
@@ -43,6 +44,8 @@ class AddressTracker(type):
43
44
  class Address(metaclass=AddressTracker):
44
45
  """An address for one or more ballasts."""
45
46
 
47
+ required_frame_size = None
48
+
46
49
  @classmethod
47
50
  def from_frame(cls, f):
48
51
  """Return an Address object
@@ -64,72 +67,131 @@ class Address(metaclass=AddressTracker):
64
67
  return "<no address>"
65
68
 
66
69
 
67
- class Broadcast(Address):
68
- """All control gear or devices connected to the network."""
70
+ class GearAddress(Address):
71
+ """Base class for control gear addresses"""
72
+
73
+ required_frame_size = 16
74
+
75
+
76
+ class DeviceAddress(Address):
77
+ """Base class for control device addresses"""
78
+
79
+ required_frame_size = 24
80
+
81
+
82
+ class GearBroadcast(GearAddress):
83
+ """All control gear connected to the network"""
69
84
 
70
85
  @classmethod
71
86
  def from_frame(cls, f):
72
87
  if len(f) == 16:
73
- if f[15:9] == 0x7f:
74
- return cls()
75
- elif len(f) == 24:
76
- if f[16] and f[23:17] == 0x7f:
88
+ if f[15:9] == 0x7F:
77
89
  return cls()
78
90
 
79
91
  def add_to_frame(self, f):
80
92
  if len(f) == 16:
81
- f[15:9] = 0x7f
82
- elif len(f) == 24:
83
- f[23:17] = 0x7f
93
+ f[15:9] = 0x7F
94
+ else:
95
+ raise _bad_frame_length
96
+
97
+ def __eq__(self, other):
98
+ return isinstance(other, GearBroadcast)
99
+
100
+ def __str__(self):
101
+ return "<broadcast (control gear)>"
102
+
103
+
104
+ # Broadcast alias provided for legacy purposes, new code should avoid
105
+ # ambiguity by using either GearBroadcast or DeviceBroadcast
106
+ Broadcast = GearBroadcast
107
+
108
+
109
+ class DeviceBroadcast(DeviceAddress):
110
+ """All control devices connected to the network"""
111
+
112
+ @classmethod
113
+ def from_frame(cls, f):
114
+ if len(f) == 24:
115
+ if f[16] and f[23:17] == 0x7F:
116
+ return cls()
117
+
118
+ def add_to_frame(self, f):
119
+ if len(f) == 24:
120
+ f[23:17] = 0x7F
84
121
  else:
85
122
  raise _bad_frame_length
86
123
 
87
124
  def __eq__(self, other):
88
- return isinstance(other, Broadcast)
125
+ return isinstance(other, DeviceBroadcast)
89
126
 
90
127
  def __str__(self):
91
- return "<broadcast>"
128
+ return "<broadcast (control device)>"
92
129
 
93
130
 
94
- class BroadcastUnaddressed(Address):
95
- """All unaddressed control gear or devices
131
+ class GearBroadcastUnaddressed(GearAddress):
132
+ """All unaddressed control gear
96
133
 
97
- All control gear or devices in the system that have no short
98
- address assigned.
134
+ All control gear in the system that have no short address assigned.
99
135
  """
100
136
 
101
137
  @classmethod
102
138
  def from_frame(cls, f):
103
139
  if len(f) == 16:
104
- if f[15:9] == 0x7e:
105
- return cls()
106
- elif len(f) == 24:
107
- if f[16] and f[23:17] == 0x7e:
140
+ if f[15:9] == 0x7E:
108
141
  return cls()
109
142
 
110
143
  def add_to_frame(self, f):
111
144
  if len(f) == 16:
112
- f[15:9] = 0x7e
113
- elif len(f) == 24:
114
- f[23:17] = 0x7e
145
+ f[15:9] = 0x7E
146
+ else:
147
+ raise _bad_frame_length
148
+
149
+ def __eq__(self, other):
150
+ return isinstance(other, GearBroadcastUnaddressed)
151
+
152
+ def __str__(self):
153
+ return "<broadcast unaddressed (control gear)>"
154
+
155
+
156
+ # BroadcastUnaddressed alias provided for legacy purposes, new code should
157
+ # avoid ambiguity by using either GearBroadcastUnaddressed or
158
+ # DeviceBroadcastUnaddressed
159
+ BroadcastUnaddressed = GearBroadcastUnaddressed
160
+
161
+
162
+ class DeviceBroadcastUnaddressed(DeviceAddress):
163
+ """All unaddressed control devices
164
+
165
+ All control devices in the system that have no short address assigned.
166
+ """
167
+
168
+ @classmethod
169
+ def from_frame(cls, f):
170
+ if len(f) == 24:
171
+ if f[16] and f[23:17] == 0x7E:
172
+ return cls()
173
+
174
+ def add_to_frame(self, f):
175
+ if len(f) == 24:
176
+ f[23:17] = 0x7E
115
177
  else:
116
178
  raise _bad_frame_length
117
179
 
118
180
  def __eq__(self, other):
119
- return isinstance(other, BroadcastUnaddressed)
181
+ return isinstance(other, DeviceBroadcastUnaddressed)
120
182
 
121
183
  def __str__(self):
122
- return "<broadcast unaddressed>"
184
+ return "<broadcast unaddressed (control device)>"
123
185
 
124
186
 
125
- class Group(Address):
126
- """All control gear or devices that are members of the specified group."""
187
+ class GearGroup(GearAddress):
188
+ """All control gear that are members of the specified group"""
127
189
 
128
- def __init__(self, group):
190
+ def __init__(self, group: int):
129
191
  if not isinstance(group, int):
130
192
  raise ValueError("group must be an integer")
131
- if group < 0 or group > 31:
132
- raise ValueError("group must be in the range 0..31")
193
+ if group < 0 or group > 15:
194
+ raise ValueError("group must be in the range 0..15")
133
195
  self.group = group
134
196
 
135
197
  @classmethod
@@ -137,32 +199,59 @@ class Group(Address):
137
199
  if len(f) == 16:
138
200
  if f[15:13] == 0x4:
139
201
  return cls(f[12:9])
140
- elif len(f) == 24:
141
- if f[16] and f[23:22] == 0x2:
142
- return cls(f[21:17])
143
202
 
144
203
  def add_to_frame(self, f):
145
204
  if len(f) == 16:
146
- if self.group > 15:
147
- raise IncompatibleFrame(
148
- "Groups 16..31 are not supported in 16-bit forward frames")
149
205
  f[15:13] = 0x4
150
206
  f[12:9] = self.group
151
- elif len(f) == 24:
207
+ else:
208
+ raise _bad_frame_length
209
+
210
+ def __eq__(self, other):
211
+ return isinstance(other, GearGroup) and other.group == self.group
212
+
213
+ def __str__(self):
214
+ return f"<group (control gear) {self.group}>"
215
+
216
+
217
+ # Group alias provided for legacy purposes, new code should avoid ambiguity
218
+ # by using either GearGroup or DeviceGroup
219
+ Group = GearGroup
220
+
221
+
222
+ class DeviceGroup(DeviceAddress):
223
+ """All control devices that are members of the specified group"""
224
+
225
+ def __init__(self, group: int):
226
+ if not isinstance(group, int):
227
+ raise ValueError("group must be an integer")
228
+ if group < 0 or group > 31:
229
+ raise ValueError("group must be in the range 0..31")
230
+ self.group = group
231
+
232
+ @classmethod
233
+ def from_frame(cls, f):
234
+ if len(f) == 24:
235
+ if f[16] and f[23:22] == 0x2:
236
+ return cls(f[21:17])
237
+
238
+ def add_to_frame(self, f):
239
+ if len(f) == 24:
152
240
  f[23:22] = 0x2
153
241
  f[21:17] = self.group
154
242
  else:
155
243
  raise _bad_frame_length
156
244
 
157
245
  def __eq__(self, other):
158
- return isinstance(other, Group) and other.group == self.group
246
+ return isinstance(other, DeviceGroup) and other.group == self.group
159
247
 
160
248
  def __str__(self):
161
- return "<group %d>" % self.group
249
+ return f"<group (control device) {self.group}>"
162
250
 
163
251
 
164
- class Short(Address):
165
- """The control gear or device that has this address.
252
+ class GearShort(GearAddress):
253
+ """
254
+ The control gear that has this address.
166
255
 
167
256
  In a correctly configured DALI network, no more than one control
168
257
  gear will have a particular short address, and no more than one
@@ -184,25 +273,63 @@ class Short(Address):
184
273
  if len(f) == 16:
185
274
  if not f[15]:
186
275
  return cls(f[14:9])
187
- elif len(f) == 24:
188
- if f[16] and not f[23]:
189
- return cls(f[22:17])
190
276
 
191
277
  def add_to_frame(self, f):
192
278
  if len(f) == 16:
193
279
  f[15] = False
194
280
  f[14:9] = self.address
195
- elif len(f) == 24:
281
+ else:
282
+ raise _bad_frame_length
283
+
284
+ def __eq__(self, other):
285
+ return isinstance(other, GearShort) and other.address == self.address
286
+
287
+ def __str__(self):
288
+ return f"<address (control gear) {self.address}>"
289
+
290
+
291
+ # Short alias provided for legacy purposes, new code should avoid ambiguity
292
+ # by using either GearShort or DeviceShort
293
+ Short = GearShort
294
+
295
+
296
+ class DeviceShort(DeviceAddress):
297
+ """
298
+ The control device that has this address.
299
+
300
+ In a correctly configured DALI network, no more than one control
301
+ gear will have a particular short address, and no more than one
302
+ control device will have a particular short address. The short
303
+ address spaces for control gear and control devices are separate;
304
+ it is legal for a control gear and control device to share a short
305
+ address.
306
+ """
307
+
308
+ def __init__(self, address: int):
309
+ if not isinstance(address, int):
310
+ raise ValueError("address must be an integer")
311
+ if address < 0 or address > 63:
312
+ raise ValueError("address must be in the range 0..63")
313
+ self.address = address
314
+
315
+ @classmethod
316
+ def from_frame(cls, f):
317
+ if len(f) == 24:
318
+ if f[16] and not f[23]:
319
+ return cls(f[22:17])
320
+
321
+ def add_to_frame(self, f):
322
+ if len(f) == 24:
196
323
  f[23] = False
197
324
  f[22:17] = self.address
198
325
  else:
199
326
  raise _bad_frame_length
200
327
 
201
328
  def __eq__(self, other):
202
- return isinstance(other, Short) and other.address == self.address
329
+ return isinstance(other, DeviceShort) and other.address == self.address
203
330
 
204
331
  def __str__(self):
205
- return "<address %d>" % self.address
332
+ return f"<address (control device) {self.address}>"
206
333
 
207
334
 
208
335
  from_frame = Address.from_frame
@@ -212,13 +339,27 @@ from_frame = Address.from_frame
212
339
  # Instance bytes
213
340
  ###############################################################################
214
341
 
342
+
215
343
  class Instance:
216
344
  def __init__(self):
217
345
  raise NotImplementedError
218
346
 
347
+ @property
348
+ def value(self):
349
+ if hasattr(self, "_value"):
350
+ return self._value
351
+
219
352
  def add_to_frame(self, f):
220
353
  raise NotImplementedError
221
354
 
355
+ def __eq__(self, other):
356
+ if isinstance(other, self.__class__):
357
+ if hasattr(self, "_value") and hasattr(other, "_value"):
358
+ if self._value == other._value:
359
+ return True
360
+
361
+ return False
362
+
222
363
 
223
364
  class ReservedInstance(Instance):
224
365
  """A reserved instance byte."""
@@ -278,7 +419,7 @@ class InstanceGroup(_AddressedInstance):
278
419
 
279
420
 
280
421
  class InstanceType(_AddressedInstance):
281
- _flags = 0xc0
422
+ _flags = 0xC0
282
423
 
283
424
 
284
425
  class FeatureInstanceNumber(_AddressedInstance):
@@ -286,7 +427,7 @@ class FeatureInstanceNumber(_AddressedInstance):
286
427
 
287
428
 
288
429
  class FeatureInstanceGroup(_AddressedInstance):
289
- _flags = 0xa0
430
+ _flags = 0xA0
290
431
 
291
432
 
292
433
  class FeatureInstanceType(_AddressedInstance):
@@ -294,19 +435,19 @@ class FeatureInstanceType(_AddressedInstance):
294
435
 
295
436
 
296
437
  class FeatureInstanceBroadcast(_UnaddressedInstance):
297
- _val = 0xfd
438
+ _val = 0xFD
298
439
 
299
440
 
300
441
  class InstanceBroadcast(_UnaddressedInstance):
301
- _val = 0xff
442
+ _val = 0xFF
302
443
 
303
444
 
304
445
  class FeatureDevice(_UnaddressedInstance):
305
- _val = 0xfc
446
+ _val = 0xFC
306
447
 
307
448
 
308
449
  class Device(_UnaddressedInstance):
309
- _val = 0xfe
450
+ _val = 0xFE
310
451
 
311
452
 
312
453
  def instance_from_frame(f):
@@ -327,12 +468,12 @@ def instance_from_frame(f):
327
468
  return FeatureInstanceGroup(p)
328
469
  elif flags == 3:
329
470
  return FeatureInstanceType(p)
330
- elif b == 0xfd:
471
+ elif b == 0xFD:
331
472
  return FeatureInstanceBroadcast()
332
- elif b == 0xff:
473
+ elif b == 0xFF:
333
474
  return InstanceBroadcast()
334
- elif b == 0xfc:
475
+ elif b == 0xFC:
335
476
  return FeatureDevice()
336
- elif b == 0xfe:
477
+ elif b == 0xFE:
337
478
  return Device()
338
479
  return ReservedInstance(b)
dali/command.py CHANGED
@@ -1,5 +1,6 @@
1
1
  """Declaration of base types for dali commands and their responses."""
2
2
 
3
+ from enum import IntEnum
3
4
  from dali import address
4
5
  from dali import frame
5
6
  from dali.exceptions import MissingResponse
@@ -158,6 +159,21 @@ class BitmapResponse(Response, metaclass=BitmapResponseBitDict):
158
159
  return "{}".format(e)
159
160
 
160
161
 
162
+ class EnumResponse(Response):
163
+ """
164
+ A response that consists of a number from a pre-defined enumerator
165
+ """
166
+
167
+ enumerator: IntEnum = None
168
+
169
+ @property
170
+ def value(self):
171
+ _value = super().value
172
+ if _value:
173
+ return self.enumerator(_value.as_integer) # noqa
174
+ return _value
175
+
176
+
161
177
  class Command(metaclass=_CommandTracker):
162
178
  """A command frame.
163
179
 
@@ -191,7 +207,10 @@ class Command(metaclass=_CommandTracker):
191
207
  devicetype = 0
192
208
 
193
209
  def __init__(self, f):
194
- assert isinstance(f, frame.ForwardFrame)
210
+ if not isinstance(f, frame.ForwardFrame):
211
+ raise TypeError(
212
+ f"{self.__class__.__name__} expected a ForwardFrame, not a {type(f)}"
213
+ )
195
214
  self._data = f
196
215
 
197
216
  _framesizes = {}
@@ -202,7 +221,7 @@ class Command(metaclass=_CommandTracker):
202
221
  subs.append(subclass)
203
222
 
204
223
  @classmethod
205
- def from_frame(cls, f, devicetype=0):
224
+ def from_frame(cls, f, devicetype=0, dev_inst_map=None):
206
225
  """Return a Command instance corresponding to the supplied frame.
207
226
 
208
227
  If the device type the command is intended for is known
@@ -211,6 +230,8 @@ class Command(metaclass=_CommandTracker):
211
230
 
212
231
  :parameter frame: a forward frame
213
232
  :parameter devicetype: type of device frame is intended for
233
+ :param dev_inst_map: An instance of DeviceInstanceTypeMapper to
234
+ assist with DALI events
214
235
 
215
236
  :returns: Return a Command instance corresponding to the
216
237
  frame. Returns None if there is no match.
@@ -222,7 +243,7 @@ class Command(metaclass=_CommandTracker):
222
243
  subs = cls._framesizes.get(len(f), [])
223
244
 
224
245
  for c in subs:
225
- r = c.from_frame(f, devicetype=devicetype)
246
+ r = c.from_frame(f, devicetype=devicetype, dev_inst_map=dev_inst_map)
226
247
  if r:
227
248
  return r
228
249
 
dali/device/__init__.py CHANGED
@@ -4,3 +4,8 @@ This module declares commands, responses and event messages defined in
4
4
  part 103 ("General requirements - Control devices") and parts 3xx
5
5
  (particular requirements for control devices).
6
6
  """
7
+ import dali.device.general
8
+ import dali.device.sequences
9
+ import dali.device.pushbutton # noqa: F401
10
+ import dali.device.occupancy # noqa: F401
11
+ import dali.device.light # noqa: F401