python-dali 0.12__py3-none-any.whl → 0.13__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.
dali/device/__init__.py CHANGED
@@ -9,3 +9,4 @@ import dali.device.sequences
9
9
  import dali.device.pushbutton # noqa: F401
10
10
  import dali.device.occupancy # noqa: F401
11
11
  import dali.device.light # noqa: F401
12
+ import dali.device.general_sensor # noqa: F401
@@ -0,0 +1,237 @@
1
+ """Commands and events from IEC 62386 part 306: General sensors."""
2
+
3
+ from enum import IntEnum
4
+
5
+ from dali import command
6
+ from dali.device import general
7
+
8
+ # IEC 62386-306 instance type.
9
+ instance_type = 6
10
+
11
+
12
+ class MeasurementVariable(IntEnum):
13
+ """DTR0 values for QUERY MEASUREMENT VARIABLE, from Part 306 Table 17.
14
+
15
+ Note: each QUERY MEASUREMENT VARIABLE execution auto-increments DTR0 by 1,
16
+ so DTR0 must be set again before every single-byte read.
17
+ """
18
+
19
+ ALARM_IS_ACTIVATED = 0x00
20
+ ALARM_TYPE = 0x01
21
+ INPUT_SIGNAL_SIGNED = 0x02
22
+ ALARM_0_BYTE_0 = 0x03
23
+ ALARM_0_BYTE_1 = 0x04
24
+ ALARM_0_BYTE_2 = 0x05
25
+ ALARM_1_BYTE_0 = 0x06
26
+ ALARM_1_BYTE_1 = 0x07
27
+ ALARM_1_BYTE_2 = 0x08
28
+ ALARM_2_BYTE_0 = 0x09
29
+ ALARM_2_BYTE_1 = 0x0A
30
+ ALARM_2_BYTE_2 = 0x0B
31
+ ALARM_3_BYTE_0 = 0x0C
32
+ ALARM_3_BYTE_1 = 0x0D
33
+ ALARM_3_BYTE_2 = 0x0E
34
+ ALARM_0_HYSTERESIS_BYTE_0 = 0x0F
35
+ ALARM_0_HYSTERESIS_BYTE_1 = 0x10
36
+ ALARM_0_HYSTERESIS_BYTE_2 = 0x11
37
+ ALARM_1_HYSTERESIS_BYTE_0 = 0x12
38
+ ALARM_1_HYSTERESIS_BYTE_1 = 0x13
39
+ ALARM_1_HYSTERESIS_BYTE_2 = 0x14
40
+ ALARM_2_HYSTERESIS_BYTE_0 = 0x15
41
+ ALARM_2_HYSTERESIS_BYTE_1 = 0x16
42
+ ALARM_2_HYSTERESIS_BYTE_2 = 0x17
43
+ ALARM_3_HYSTERESIS_BYTE_0 = 0x18
44
+ ALARM_3_HYSTERESIS_BYTE_1 = 0x19
45
+ ALARM_3_HYSTERESIS_BYTE_2 = 0x1A
46
+ UNIT_OF_MEASUREMENT = 0x1B
47
+ UNIT_OF_MEASUREMENT_EXTENDED_0 = 0x1C
48
+ UNIT_OF_MEASUREMENT_EXTENDED_1 = 0x1D
49
+ QUANTITY_NAME = 0x1E
50
+ QUANTITY_NAME_EXTENDED_0 = 0x1F
51
+ QUANTITY_NAME_EXTENDED_1 = 0x20
52
+ MAGNITUDE = 0x21
53
+ MAGNITUDE_PHYSICAL_MAX = 0x22
54
+ MAGNITUDE_PHYSICAL_MIN = 0x23
55
+ MAX_MEASURED_VALUE_BYTE_0 = 0x24
56
+ MAX_MEASURED_VALUE_BYTE_1 = 0x25
57
+ MAX_MEASURED_VALUE_BYTE_2 = 0x26
58
+ MAX_MEASURED_VALUE_BYTE_3 = 0x27
59
+ MIN_MEASURED_VALUE_BYTE_0 = 0x28
60
+ MIN_MEASURED_VALUE_BYTE_1 = 0x29
61
+ MIN_MEASURED_VALUE_BYTE_2 = 0x2A
62
+ MIN_MEASURED_VALUE_BYTE_3 = 0x2B
63
+
64
+
65
+ class UnitOfMeasurement(IntEnum):
66
+ """Standard unit-of-measurement values from Part 306 Annex A, Table A.1."""
67
+
68
+ MANUFACTURER_DEFINED = 0
69
+ DIMENSIONLESS = 1
70
+ SECOND = 2
71
+ HERTZ = 3
72
+ METRE = 4
73
+ KILOGRAM = 5
74
+ VOLT = 6
75
+ AMPERE = 7
76
+ KELVIN = 8
77
+ CELSIUS = 9
78
+ FAHRENHEIT = 10
79
+ MOLE = 11
80
+ DEGREE_ANGLE = 12
81
+ RADIAN = 13
82
+ STERADIAN = 14
83
+ NEWTON = 15
84
+ PASCAL = 16
85
+ PSI = 17
86
+ JOULE = 18
87
+ WATT = 19
88
+ COULOMB = 20
89
+ FARAD = 21
90
+ OHM = 22
91
+ SIEMENS = 23
92
+ WEBER = 24
93
+ TESLA = 25
94
+ HENRY = 26
95
+ AMPERE_PER_METRE = 27
96
+ VOLT_PER_METRE = 28
97
+ AMPERE_HOUR = 29
98
+ WATT_HOUR = 30
99
+ LUMEN = 31
100
+ LUX = 32
101
+ CANDELA = 33
102
+ CANDELA_PER_SQUARE_METRE = 34
103
+ BECQUEREL = 35
104
+ GRAY = 36
105
+ SIEVERT = 37
106
+ KATAL = 38
107
+ SQUARE_METRE = 39
108
+ CUBIC_METRE = 40
109
+ METRE_PER_SECOND = 41
110
+ METRE_PER_SECOND_SQUARED = 42
111
+ CUBIC_METRE_PER_SECOND = 43
112
+ KILOGRAM_PER_CUBIC_METRE = 44
113
+ KILOGRAM_PER_SQUARE_METRE = 45
114
+ CUBIC_METRE_PER_KILOGRAM = 46
115
+ FOOT = 47
116
+ NEWTON_METRE = 48
117
+ DECIBEL = 49
118
+ PERCENT = 50
119
+ PART_PER_THOUSAND = 51
120
+ PART_PER_TEN_THOUSAND = 52
121
+ PARTS_PER_MILLION = 53
122
+ DECIBEL_A = 54
123
+
124
+
125
+ class QuantityName(IntEnum):
126
+ """Standard quantity-name values from Part 306 Annex A, Table A.2."""
127
+
128
+ MANUFACTURER_DEFINED = 0
129
+ TIME = 1
130
+ FREQUENCY = 2
131
+ LENGTH = 3
132
+ FORCE = 4
133
+ WEIGHT = 5
134
+ MASS = 6
135
+ VELOCITY = 7
136
+ ACCELERATION = 8
137
+ ANGLE = 9
138
+ AREA = 10
139
+ VOLUME = 11
140
+ TORQUE = 12
141
+ VOLTAGE = 13
142
+ CURRENT = 14
143
+ POWER = 15
144
+ POWER_APPARENT = 16
145
+ POWER_REACTIVE = 17
146
+ ENERGY = 18
147
+ POWER_FACTOR = 19
148
+ SOUND_PRESSURE_LEVEL = 20
149
+ CCT = 21
150
+ CRI = 22
151
+ RED_LIGHT = 23
152
+ GREEN_LIGHT = 24
153
+ BLUE_LIGHT = 25
154
+ TEMPERATURE = 26
155
+ WET_BULB_TEMPERATURE = 27
156
+ ABSOLUTE_HUMIDITY = 28
157
+ RELATIVE_HUMIDITY = 29
158
+ DEW_POINT = 30
159
+ PRESSURE = 31
160
+ FLOW_RATE = 32
161
+ SO2 = 33
162
+ CO2 = 34
163
+ CO = 35
164
+ VOC = 36
165
+ NOX = 37
166
+ N2O = 38
167
+ AMMONIA = 39
168
+ OZONE = 40
169
+ CHLORINE = 41
170
+ METHANE = 42
171
+ ACIDITY = 43
172
+ PARTICULATE_MATTER_PM10 = 44
173
+ PARTICULATE_MATTER_PM2_5 = 45
174
+ AIR_QUALITY_INDEX = 46
175
+ RSSI_IBEACON = 47
176
+ RSSI_EDDYSTONE = 48
177
+ RSSI_ALTBEACON = 49
178
+ GLOBAL_POSITION_LONGITUDE = 50
179
+ GLOBAL_POSITION_LATITUDE = 51
180
+ ALTITUDE = 52
181
+ RELATIVE_POSITION_X = 53
182
+ RELATIVE_POSITION_Y = 54
183
+ RELATIVE_POSITION_Z = 55
184
+ WIND_SPEED = 56
185
+ FLUID_LEVEL = 57
186
+ BATTERY_CHARGE = 58
187
+ # Clarifications and Recommendations for IEC 62386:
188
+ GENERAL_PURPOSE_COUNTER = 59
189
+ PEOPLE_COUNTER = 60
190
+
191
+
192
+ class GeneralSensorEvent(general._Event):
193
+ """A Part 306 measured-value event."""
194
+
195
+ _instance_type = instance_type
196
+ _event_info = 0x20000
197
+
198
+ @classmethod
199
+ def from_event_data(cls, event_data: int):
200
+ """Return this event class for a measurement event."""
201
+ return cls if event_data & 0x200 else None
202
+
203
+ @property
204
+ def measured_value(self) -> int:
205
+ """Return the 9-bit measured-value event payload."""
206
+ return self.event_data & 0x1FF
207
+
208
+ @property
209
+ def event_data(self) -> int:
210
+ """Return the encoded event information."""
211
+ return self._event_info
212
+
213
+ def _set_event_data(self, set_data: int, set_frame) -> None:
214
+ """Encode the measured value into a Part 306 event frame."""
215
+ if not isinstance(set_data, int) or not 0 <= set_data <= 0x3FF:
216
+ raise ValueError("GeneralSensorEvent requires 9-bit event data")
217
+ self._event_info = 0x200 | (set_data & 0x1FF)
218
+ set_frame[9:0] = self._event_info
219
+
220
+
221
+ class InstanceEventFilter(general.InstanceEventFilter):
222
+ """Part 306 event filters."""
223
+
224
+ measured_value = 0x01
225
+
226
+
227
+ class QueryMeasurementVariable(general._StandardInstanceCommand):
228
+ """Query a Part 306 measurement variable selected by DTR0.
229
+
230
+ Executing this command auto-increments DTR0 by 1, so DTR0 must be set
231
+ again before every single-byte read. See MeasurementVariable.
232
+ """
233
+
234
+ inputdev = True
235
+ uses_dtr0 = True
236
+ response = command.NumericResponse
237
+ _opcode = 0x5F
dali/device/helpers.py CHANGED
@@ -19,7 +19,7 @@ from dali.device.general import (
19
19
  )
20
20
  from dali.exceptions import MissingResponse, ResponseError
21
21
  from dali.frame import BackwardFrameError
22
- from dali.sequences import progress as seq_progress
22
+ from dali.sequences import progress as seq_progress, sleep
23
23
 
24
24
 
25
25
  def check_bad_rsp(r: Response | None) -> bool:
@@ -85,7 +85,10 @@ class DeviceInstanceTypeMapper:
85
85
  return self._mapping
86
86
 
87
87
  def autodiscover(
88
- self, addresses: int | tuple[int, int] | Iterable[int] = (0, 63)
88
+ self,
89
+ addresses: int | tuple[int, int] | Iterable[int] = (0, 63),
90
+ quiescent_settle: float = 0.2,
91
+ status_attempts: int = 3,
89
92
  ) -> Generator[Command, Response, None]:
90
93
  """
91
94
  A generator sequence to scan a DALI bus for control device instances,
@@ -99,6 +102,11 @@ class DeviceInstanceTypeMapper:
99
102
  which case all addresses between the provided values will be scanned;
100
103
  or finally can be an iterable of ints in which case each address, in
101
104
  the iterator will be scanned.
105
+ :param quiescent_settle: Seconds to wait after entering quiescent mode
106
+ before probing, so an event frame already in flight does not corrupt
107
+ the first address probe.
108
+ :param status_attempts: How many times to probe an address whose status
109
+ reply collides with a spontaneous event before treating it as empty.
102
110
  :return: A generator function, to use with e.g. `driver.run_sequence()`
103
111
 
104
112
  Needs to be used through an appropriate driver, with `run_sequence()`,
@@ -111,6 +119,12 @@ class DeviceInstanceTypeMapper:
111
119
 
112
120
  # Use quiescent mode to reduce bus contention from input devices
113
121
  yield StartQuiescentMode(DeviceBroadcast())
122
+ # Quiescent mode is not instant and an input device may already have an
123
+ # event frame in flight. Let the bus settle so the first probe is not
124
+ # corrupted by a colliding backward frame, which would otherwise make a
125
+ # present device look absent.
126
+ if quiescent_settle:
127
+ yield sleep(quiescent_settle)
114
128
 
115
129
  if isinstance(addresses, int):
116
130
  addresses = (n for n in range(0, addresses))
@@ -120,8 +134,16 @@ class DeviceInstanceTypeMapper:
120
134
  for addr_int in addresses:
121
135
  addr = DeviceShort(addr_int)
122
136
 
123
- # Check that the device exists and responds
124
- rsp = yield QueryDeviceStatus(device=addr)
137
+ # Check that the device exists and responds. A framing error means a
138
+ # device answered but its backward frame collided with a spontaneous
139
+ # event, so retry; a missing response means the address is empty, so
140
+ # stop probing it immediately.
141
+ for _attempt in range(status_attempts):
142
+ rsp = yield QueryDeviceStatus(device=addr)
143
+ if not check_bad_rsp(rsp):
144
+ break
145
+ if not isinstance(getattr(rsp, "raw_value", None), BackwardFrameError):
146
+ break
125
147
  if check_bad_rsp(rsp):
126
148
  continue
127
149
  if isinstance(rsp, QueryDeviceStatusResponse):
@@ -140,7 +162,13 @@ class DeviceInstanceTypeMapper:
140
162
  rsp = yield QueryNumberOfInstances(device=addr)
141
163
  if check_bad_rsp(rsp):
142
164
  continue
143
- num_inst = rsp.value
165
+ # Instance numbers are 0..31 (IEC 62386-103), so a device can have
166
+ # at most 32 instances. Cap the count defensively: a device
167
+ # reporting more, or a value corrupted by a colliding event frame,
168
+ # would otherwise build an out-of-range InstanceNumber and abort the
169
+ # whole scan. Non-existent instances are skipped when they do not
170
+ # respond to QueryInstanceEnabled below.
171
+ num_inst = min(rsp.value, 32)
144
172
 
145
173
  # For each instance, check it is enabled and then query the type
146
174
  for inst_int in range(num_inst):
dali/device/sequences.py CHANGED
@@ -6,14 +6,18 @@ from __future__ import annotations
6
6
  import types
7
7
  from typing import Generator, Optional, Type
8
8
 
9
- from dali.address import DeviceShort, InstanceNumber
9
+ from dali.address import DeviceBroadcast, DeviceShort, InstanceNumber
10
10
  from dali.command import Command, Response
11
11
  from dali.device.general import (
12
12
  DTR0,
13
13
  DTR1,
14
14
  DTR2,
15
+ Compare,
15
16
  EventScheme,
17
+ Initialise,
16
18
  InstanceEventFilter,
19
+ ProgramShortAddress,
20
+ QueryDeviceStatus,
17
21
  QueryResolution,
18
22
  QueryInputValue,
19
23
  QueryInputValueLatch,
@@ -22,11 +26,22 @@ from dali.device.general import (
22
26
  QueryEventFilterM,
23
27
  QueryEventScheme,
24
28
  QueryEventSchemeResponse,
29
+ Randomise,
30
+ SearchAddrH,
31
+ SearchAddrL,
32
+ SearchAddrM,
25
33
  SetEventFilter,
26
34
  SetEventScheme,
35
+ SetShortAddress,
36
+ StartQuiescentMode,
37
+ StopQuiescentMode,
38
+ Terminate,
39
+ VerifyShortAddress,
40
+ Withdraw,
27
41
  )
28
42
  from dali.device.helpers import check_bad_rsp
29
- from dali.exceptions import DALISequenceError
43
+ from dali.exceptions import DALISequenceError, ProgramShortAddressFailure
44
+ from dali.sequences import progress, sleep
30
45
 
31
46
 
32
47
  def SetEventSchemes(
@@ -275,3 +290,161 @@ def query_input_value(
275
290
  value >>= 8 - resolution
276
291
 
277
292
  return value
293
+
294
+
295
+ # INITIALISE data byte values for control devices, from IEC 62386-103:2014
296
+ # Table 21. Unlike control gear (part 102), devices use 0xff for "all" and
297
+ # 0x7f for "no short address" (short address == MASK).
298
+ _INITIALISE_ALL = 0xFF
299
+ _INITIALISE_UNADDRESSED = 0x7F
300
+
301
+
302
+ def _find_next(
303
+ low: int, high: int
304
+ ) -> Generator[Command, Optional[Response], Optional[int | str]]:
305
+ """Binary search the 24-bit random address space for a responding device.
306
+
307
+ Returns the random address of the device with the lowest address in the
308
+ ``low..high`` range, ``"clash"`` if two devices share the current address,
309
+ or ``None`` if no device answers in the range.
310
+ """
311
+ yield SearchAddrH((high >> 16) & 0xFF)
312
+ yield SearchAddrM((high >> 8) & 0xFF)
313
+ yield SearchAddrL(high & 0xFF)
314
+
315
+ r = yield Compare()
316
+
317
+ if low == high:
318
+ if r.value is True:
319
+ return "clash" if r.raw_value.error else low
320
+ return None
321
+
322
+ if r.value is True:
323
+ midpoint = (low + high) // 2
324
+ res = yield from _find_next(low, midpoint)
325
+ if res is not None:
326
+ return res
327
+ return (yield from _find_next(midpoint + 1, high))
328
+
329
+ return None
330
+
331
+
332
+ def Commissioning(
333
+ available_addresses: Optional[list[int]] = None,
334
+ readdress: bool = False,
335
+ dry_run: bool = False,
336
+ ) -> Generator[Command, Optional[Response], list[DeviceShort]]:
337
+ """Assign short addresses to 24-bit (part 103) control devices.
338
+
339
+ This mirrors :func:`dali.sequences.Commissioning` for control gear, using
340
+ the part 103 addressing commands. Use with an appropriate DALI driver
341
+ instance, through its ``run_sequence()`` method.
342
+
343
+ The bus is placed in quiescent mode for the duration so that spontaneous
344
+ event messages do not collide with the addressing backward frames.
345
+
346
+ :param available_addresses: If passed, only these short addresses will be
347
+ assigned; otherwise all 64 short addresses are considered available.
348
+ :param readdress: If set, all existing short addresses are cleared first
349
+ and every device is readdressed; otherwise only devices that are
350
+ currently unaddressed are assigned a short address.
351
+ :param dry_run: If set, no short addresses are actually programmed. Useful
352
+ for testing the search without altering the bus.
353
+ :return: The list of short addresses that are in use once the sequence
354
+ completes.
355
+ """
356
+ used_addresses: list[DeviceShort] = []
357
+ if available_addresses is None:
358
+ available_addresses = list(range(64))
359
+ else:
360
+ available_addresses = list(available_addresses)
361
+
362
+ if readdress:
363
+ if dry_run:
364
+ yield progress(
365
+ message="dry_run is set: not deleting existing short addresses"
366
+ )
367
+ else:
368
+ yield DTR0(255)
369
+ yield SetShortAddress(DeviceBroadcast())
370
+ else:
371
+ # Devices that already have a short address answer QueryDeviceStatus at
372
+ # that address, so those addresses are considered already in use.
373
+ for a in list(available_addresses):
374
+ in_use = yield QueryDeviceStatus(DeviceShort(a))
375
+ if not check_bad_rsp(in_use):
376
+ available_addresses.remove(a)
377
+ used_addresses.append(DeviceShort(a))
378
+ yield progress(message=f"Available addresses: {available_addresses}")
379
+
380
+ yield Terminate()
381
+ yield StartQuiescentMode(DeviceBroadcast())
382
+ yield Initialise(_INITIALISE_ALL if readdress else _INITIALISE_UNADDRESSED)
383
+
384
+ # A verify failure must still leave the bus addressable, so the error is
385
+ # recorded and raised only after quiescent mode has been exited below;
386
+ # yielding during exception unwinding is unsafe because the sequence runner
387
+ # closes the generator via GeneratorExit on failure.
388
+ failed_address: Optional[int] = None
389
+ finished = False
390
+ # Loop to cope with multiple devices picking the same random search
391
+ # address; when that clash is detected we re-randomise and begin again.
392
+ # Devices that already received addresses are unaffected.
393
+ while not finished:
394
+ yield Randomise()
395
+ # Randomise can take up to 200ms to settle.
396
+ yield sleep(0.2)
397
+
398
+ low = 0
399
+ high = 0xFFFFFF
400
+
401
+ while low is not None:
402
+ yield progress(completed=low, size=high)
403
+ low = yield from _find_next(low, high)
404
+ if low == "clash":
405
+ yield progress(
406
+ message="Multiple devices picked the same random "
407
+ "address; restarting"
408
+ )
409
+ break
410
+ if low is None:
411
+ finished = True
412
+ break
413
+ yield progress(message=f"Device found at address {low:#x}")
414
+ if available_addresses:
415
+ new_addr = available_addresses.pop(0)
416
+ if dry_run:
417
+ yield progress(
418
+ message="Not programming short address "
419
+ f"{new_addr} because dry_run is set"
420
+ )
421
+ else:
422
+ yield progress(
423
+ message=f"Programming short address {new_addr}"
424
+ )
425
+ yield ProgramShortAddress(new_addr)
426
+ r = yield VerifyShortAddress(new_addr)
427
+ if r.value is not True:
428
+ failed_address = new_addr
429
+ finished = True
430
+ break
431
+ used_addresses.append(DeviceShort(new_addr))
432
+ else:
433
+ yield progress(
434
+ message="Device found but no short addresses left"
435
+ )
436
+ yield Withdraw()
437
+ if low < high:
438
+ low = low + 1
439
+ else:
440
+ low = None
441
+ finished = True
442
+
443
+ yield Terminate()
444
+ yield StopQuiescentMode(DeviceBroadcast())
445
+
446
+ if failed_address is not None:
447
+ raise ProgramShortAddressFailure(failed_address)
448
+
449
+ yield progress(message="Addressing complete")
450
+ return used_addresses