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 +1 -0
- dali/device/general_sensor.py +237 -0
- dali/device/helpers.py +33 -5
- dali/device/sequences.py +175 -2
- dali/driver/serial.py +355 -54
- dali/exceptions.py +13 -0
- dali/gear/sequences.py +112 -0
- dali/memory/__init__.py +1 -0
- dali/memory/emergency.py +334 -0
- dali/tests/fakes.py +116 -0
- dali/tests/test_device_sequences.py +171 -0
- dali/tests/test_emergency.py +71 -0
- dali/tests/test_general_sensor.py +53 -0
- dali/tests/test_memory.py +77 -2
- dali/tests/test_serial_driver.py +211 -0
- {python_dali-0.12.dist-info → python_dali-0.13.dist-info}/METADATA +11 -6
- {python_dali-0.12.dist-info → python_dali-0.13.dist-info}/RECORD +22 -17
- {python_dali-0.12.dist-info → python_dali-0.13.dist-info}/WHEEL +0 -0
- {python_dali-0.12.dist-info → python_dali-0.13.dist-info}/licenses/COPYING +0 -0
- {python_dali-0.12.dist-info → python_dali-0.13.dist-info}/licenses/COPYING.LESSER +0 -0
- {python_dali-0.12.dist-info → python_dali-0.13.dist-info}/licenses/LICENSE +0 -0
- {python_dali-0.12.dist-info → python_dali-0.13.dist-info}/top_level.txt +0 -0
dali/device/__init__.py
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|