python-dali 0.11__tar.gz → 0.12__tar.gz
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.
- {python-dali-0.11/python_dali.egg-info → python_dali-0.12}/PKG-INFO +20 -14
- {python-dali-0.11 → python_dali-0.12}/README.rst +6 -2
- {python-dali-0.11 → python_dali-0.12}/dali/driver/base.py +0 -114
- {python-dali-0.11 → python_dali-0.12}/dali/driver/hid.py +51 -27
- {python-dali-0.11 → python_dali-0.12}/dali/driver/serial.py +40 -4
- {python-dali-0.11 → python_dali-0.12}/dali/driver/tridonic.py +2 -2
- {python-dali-0.11 → python_dali-0.12}/dali/driver/unipi.py +18 -8
- python_dali-0.12/dali/driver/usb.py +114 -0
- {python-dali-0.11 → python_dali-0.12}/dali/gear/__init__.py +1 -0
- python_dali-0.12/dali/gear/converter.py +173 -0
- {python-dali-0.11 → python_dali-0.12}/dali/gear/sequences.py +1 -0
- {python-dali-0.11 → python_dali-0.12/python_dali.egg-info}/PKG-INFO +20 -14
- {python-dali-0.11 → python_dali-0.12}/python_dali.egg-info/SOURCES.txt +2 -1
- {python-dali-0.11 → python_dali-0.12}/setup.cfg +3 -5
- python-dali-0.11/dali/version.py +0 -1
- {python-dali-0.11 → python_dali-0.12}/COPYING +0 -0
- {python-dali-0.11 → python_dali-0.12}/COPYING.LESSER +0 -0
- {python-dali-0.11 → python_dali-0.12}/LICENSE +0 -0
- {python-dali-0.11 → python_dali-0.12}/dali/__init__.py +0 -0
- {python-dali-0.11 → python_dali-0.12}/dali/address.py +0 -0
- {python-dali-0.11 → python_dali-0.12}/dali/command.py +0 -0
- {python-dali-0.11 → python_dali-0.12}/dali/device/__init__.py +0 -0
- {python-dali-0.11 → python_dali-0.12}/dali/device/general.py +0 -0
- {python-dali-0.11 → python_dali-0.12}/dali/device/helpers.py +0 -0
- {python-dali-0.11 → python_dali-0.12}/dali/device/light.py +0 -0
- {python-dali-0.11 → python_dali-0.12}/dali/device/occupancy.py +0 -0
- {python-dali-0.11 → python_dali-0.12}/dali/device/pushbutton.py +0 -0
- {python-dali-0.11 → python_dali-0.12}/dali/device/sequences.py +0 -0
- {python-dali-0.11 → python_dali-0.12}/dali/driver/__init__.py +0 -0
- {python-dali-0.11 → python_dali-0.12}/dali/driver/atxled.py +0 -0
- {python-dali-0.11 → python_dali-0.12}/dali/driver/daliserver.py +0 -0
- {python-dali-0.11 → python_dali-0.12}/dali/driver/hasseb.py +0 -0
- {python-dali-0.11 → python_dali-0.12}/dali/driver/trace_logging.py +0 -0
- {python-dali-0.11 → python_dali-0.12}/dali/exceptions.py +0 -0
- {python-dali-0.11 → python_dali-0.12}/dali/frame.py +0 -0
- {python-dali-0.11 → python_dali-0.12}/dali/gear/colour.py +0 -0
- {python-dali-0.11 → python_dali-0.12}/dali/gear/emergency.py +0 -0
- {python-dali-0.11 → python_dali-0.12}/dali/gear/general.py +0 -0
- {python-dali-0.11 → python_dali-0.12}/dali/gear/incandescent.py +0 -0
- {python-dali-0.11 → python_dali-0.12}/dali/gear/led.py +0 -0
- {python-dali-0.11 → python_dali-0.12}/dali/memory/__init__.py +0 -0
- {python-dali-0.11 → python_dali-0.12}/dali/memory/diagnostics.py +0 -0
- {python-dali-0.11 → python_dali-0.12}/dali/memory/energy.py +0 -0
- {python-dali-0.11 → python_dali-0.12}/dali/memory/info.py +0 -0
- {python-dali-0.11 → python_dali-0.12}/dali/memory/location.py +0 -0
- {python-dali-0.11 → python_dali-0.12}/dali/memory/maintenance.py +0 -0
- {python-dali-0.11 → python_dali-0.12}/dali/memory/oem.py +0 -0
- {python-dali-0.11 → python_dali-0.12}/dali/sequences.py +0 -0
- {python-dali-0.11 → python_dali-0.12}/dali/tests/__init__.py +0 -0
- {python-dali-0.11 → python_dali-0.12}/dali/tests/fakes.py +0 -0
- {python-dali-0.11 → python_dali-0.12}/dali/tests/fakes_serial.py +0 -0
- {python-dali-0.11 → python_dali-0.12}/dali/tests/test_colour.py +0 -0
- {python-dali-0.11 → python_dali-0.12}/dali/tests/test_command.py +0 -0
- {python-dali-0.11 → python_dali-0.12}/dali/tests/test_device_sequences.py +0 -0
- {python-dali-0.11 → python_dali-0.12}/dali/tests/test_dummy.py +0 -0
- {python-dali-0.11 → python_dali-0.12}/dali/tests/test_events.py +0 -0
- {python-dali-0.11 → python_dali-0.12}/dali/tests/test_exceptions.py +0 -0
- {python-dali-0.11 → python_dali-0.12}/dali/tests/test_fakes.py +0 -0
- {python-dali-0.11 → python_dali-0.12}/dali/tests/test_frame.py +0 -0
- {python-dali-0.11 → python_dali-0.12}/dali/tests/test_memory.py +0 -0
- {python-dali-0.11 → python_dali-0.12}/dali/tests/test_sequences.py +0 -0
- {python-dali-0.11 → python_dali-0.12}/pyproject.toml +0 -0
- {python-dali-0.11 → python_dali-0.12}/python_dali.egg-info/dependency_links.txt +0 -0
- {python-dali-0.11 → python_dali-0.12}/python_dali.egg-info/requires.txt +2 -2
- {python-dali-0.11 → python_dali-0.12}/python_dali.egg-info/top_level.txt +0 -0
- {python-dali-0.11 → python_dali-0.12}/setup.py +0 -0
|
@@ -1,25 +1,29 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: python-dali
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.12
|
|
4
4
|
Summary: Interface to DALI lighting systems
|
|
5
5
|
Home-page: https://github.com/sde1000/python-dali
|
|
6
6
|
Author: Stephen Early
|
|
7
7
|
Author-email: steve@assorted.org.uk
|
|
8
|
-
License:
|
|
9
|
-
Platform: UNKNOWN
|
|
8
|
+
License: "LGPL-3.0-or-later"
|
|
10
9
|
Classifier: Development Status :: 3 - Alpha
|
|
11
10
|
Classifier: Intended Audience :: Developers
|
|
12
|
-
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
13
|
-
Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)
|
|
14
11
|
Classifier: Programming Language :: Python :: 3
|
|
15
|
-
Requires-Python: >=3.
|
|
12
|
+
Requires-Python: >=3.8
|
|
16
13
|
Description-Content-Type: text/x-rst
|
|
17
|
-
Provides-Extra: driver-serial
|
|
18
|
-
Provides-Extra: driver-unipi
|
|
19
|
-
Provides-Extra: test
|
|
20
14
|
License-File: COPYING
|
|
21
15
|
License-File: COPYING.LESSER
|
|
22
16
|
License-File: LICENSE
|
|
17
|
+
Provides-Extra: driver-unipi
|
|
18
|
+
Requires-Dist: pyusb; extra == "driver-unipi"
|
|
19
|
+
Requires-Dist: pymodbus; extra == "driver-unipi"
|
|
20
|
+
Provides-Extra: driver-serial
|
|
21
|
+
Requires-Dist: pyserial-asyncio; extra == "driver-serial"
|
|
22
|
+
Provides-Extra: test
|
|
23
|
+
Requires-Dist: pytest; extra == "test"
|
|
24
|
+
Requires-Dist: pytest-asyncio; extra == "test"
|
|
25
|
+
Requires-Dist: pyserial-asyncio; extra == "test"
|
|
26
|
+
Dynamic: license-file
|
|
23
27
|
|
|
24
28
|
python-dali — lighting control interface
|
|
25
29
|
========================================
|
|
@@ -44,7 +48,9 @@ This library has been written with reference to the following documents:
|
|
|
44
48
|
- IEC 62386-201:2009 (fluorescent lamps)
|
|
45
49
|
- IEC 62386-202:2009 (self-contained emergency lighting)
|
|
46
50
|
- IEC 62386-205:2009 (supply voltage controller for incandescent lamps)
|
|
51
|
+
- IEC 62386-206:2009 (conversion from digital signal into DC voltage)
|
|
47
52
|
- IEC 62386-207:2009 (LED modules)
|
|
53
|
+
- IEC 62386-209:2012 (colour control)
|
|
48
54
|
- IEC 62386-301:2017 (particular requirements for push button input devices)
|
|
49
55
|
- IEC 62386-303:2017 (particular requirements for occupancy sensor input devices)
|
|
50
56
|
|
|
@@ -64,7 +70,7 @@ Interface Alliance`_:
|
|
|
64
70
|
Python versions supported
|
|
65
71
|
-------------------------
|
|
66
72
|
|
|
67
|
-
This library currently requires Python version 3.
|
|
73
|
+
This library currently requires Python version 3.8 or later.
|
|
68
74
|
|
|
69
75
|
Stability
|
|
70
76
|
---------
|
|
@@ -160,11 +166,13 @@ Contributors
|
|
|
160
166
|
|
|
161
167
|
- David Russell
|
|
162
168
|
|
|
169
|
+
- Nikolay Korotkiy
|
|
170
|
+
|
|
163
171
|
|
|
164
172
|
Copyright
|
|
165
173
|
---------
|
|
166
174
|
|
|
167
|
-
python-dali is Copyright (C) 2013–
|
|
175
|
+
python-dali is Copyright (C) 2013–2026 Stephen Early <steve@assorted.org.uk>
|
|
168
176
|
and other contributors.
|
|
169
177
|
|
|
170
178
|
It is distributed under the terms of the GNU Lesser General Public
|
|
@@ -182,5 +190,3 @@ not, see `this link`_.
|
|
|
182
190
|
|
|
183
191
|
.. _this link: https://www.gnu.org/licenses/
|
|
184
192
|
.. _Digital Illumination Interface Alliance: https://www.dali-alliance.org/dali/data.html
|
|
185
|
-
|
|
186
|
-
|
|
@@ -21,7 +21,9 @@ This library has been written with reference to the following documents:
|
|
|
21
21
|
- IEC 62386-201:2009 (fluorescent lamps)
|
|
22
22
|
- IEC 62386-202:2009 (self-contained emergency lighting)
|
|
23
23
|
- IEC 62386-205:2009 (supply voltage controller for incandescent lamps)
|
|
24
|
+
- IEC 62386-206:2009 (conversion from digital signal into DC voltage)
|
|
24
25
|
- IEC 62386-207:2009 (LED modules)
|
|
26
|
+
- IEC 62386-209:2012 (colour control)
|
|
25
27
|
- IEC 62386-301:2017 (particular requirements for push button input devices)
|
|
26
28
|
- IEC 62386-303:2017 (particular requirements for occupancy sensor input devices)
|
|
27
29
|
|
|
@@ -41,7 +43,7 @@ Interface Alliance`_:
|
|
|
41
43
|
Python versions supported
|
|
42
44
|
-------------------------
|
|
43
45
|
|
|
44
|
-
This library currently requires Python version 3.
|
|
46
|
+
This library currently requires Python version 3.8 or later.
|
|
45
47
|
|
|
46
48
|
Stability
|
|
47
49
|
---------
|
|
@@ -137,11 +139,13 @@ Contributors
|
|
|
137
139
|
|
|
138
140
|
- David Russell
|
|
139
141
|
|
|
142
|
+
- Nikolay Korotkiy
|
|
143
|
+
|
|
140
144
|
|
|
141
145
|
Copyright
|
|
142
146
|
---------
|
|
143
147
|
|
|
144
|
-
python-dali is Copyright (C) 2013–
|
|
148
|
+
python-dali is Copyright (C) 2013–2026 Stephen Early <steve@assorted.org.uk>
|
|
145
149
|
and other contributors.
|
|
146
150
|
|
|
147
151
|
It is distributed under the terms of the GNU Lesser General Public
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
import threading
|
|
2
|
-
import usb
|
|
3
|
-
|
|
4
|
-
|
|
5
1
|
###############################################################################
|
|
6
2
|
# driver contracts
|
|
7
3
|
###############################################################################
|
|
@@ -124,113 +120,3 @@ class Listener(Backend):
|
|
|
124
120
|
"""
|
|
125
121
|
raise NotImplementedError(
|
|
126
122
|
'Abstract ``Listener`` does not implement ``listen``')
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
###############################################################################
|
|
130
|
-
# USB backends
|
|
131
|
-
###############################################################################
|
|
132
|
-
|
|
133
|
-
class USBBackend(Backend):
|
|
134
|
-
"""Backend implementation for communicating with USB devices.
|
|
135
|
-
"""
|
|
136
|
-
|
|
137
|
-
def __init__(self, vendor, product, bus=None,
|
|
138
|
-
port_numbers=None, interface=0):
|
|
139
|
-
self._device = None
|
|
140
|
-
# lookup devices by vendor and product
|
|
141
|
-
devices = [dev for dev in usb.core.find(
|
|
142
|
-
find_all=True,
|
|
143
|
-
idVendor=vendor,
|
|
144
|
-
idProduct=product
|
|
145
|
-
)]
|
|
146
|
-
# use first device if bus or port_numers not defined
|
|
147
|
-
if bus is None or port_numbers is None:
|
|
148
|
-
self._device = devices[0]
|
|
149
|
-
else:
|
|
150
|
-
for dev in devices:
|
|
151
|
-
if dev.bus == bus and dev.port_numbers == port_numbers:
|
|
152
|
-
self._device = dev
|
|
153
|
-
break
|
|
154
|
-
# if queried device not found, raise
|
|
155
|
-
if not self._device:
|
|
156
|
-
raise usb.core.USBError('Device not found')
|
|
157
|
-
# detach kernel driver if necessary
|
|
158
|
-
if self._device.is_kernel_driver_active(interface) is True:
|
|
159
|
-
self._device.detach_kernel_driver(interface)
|
|
160
|
-
# set device configuration
|
|
161
|
-
self._device.set_configuration()
|
|
162
|
-
# claim interface
|
|
163
|
-
usb.util.claim_interface(self._device, interface)
|
|
164
|
-
# get active configuration
|
|
165
|
-
cfg = self._device.get_active_configuration()
|
|
166
|
-
intf = cfg[(0, 0)]
|
|
167
|
-
# get write end point
|
|
168
|
-
def match_ep_out(e):
|
|
169
|
-
return usb.util.endpoint_direction(e.bEndpointAddress) == \
|
|
170
|
-
usb.util.ENDPOINT_OUT
|
|
171
|
-
self._ep_write = usb.util.find_descriptor(
|
|
172
|
-
intf, custom_match=match_ep_out)
|
|
173
|
-
# get read end point
|
|
174
|
-
def match_ep_in(e):
|
|
175
|
-
return usb.util.endpoint_direction(e.bEndpointAddress) == \
|
|
176
|
-
usb.util.ENDPOINT_IN
|
|
177
|
-
self._ep_read = usb.util.find_descriptor(
|
|
178
|
-
intf, custom_match=match_ep_in)
|
|
179
|
-
|
|
180
|
-
def read(self, timeout=None):
|
|
181
|
-
"""Read data from USB device.
|
|
182
|
-
"""
|
|
183
|
-
return self._ep_read.read(self._ep_read.wMaxPacketSize, timeout=timeout)
|
|
184
|
-
|
|
185
|
-
def write(self, data):
|
|
186
|
-
"""Write data to USB device.
|
|
187
|
-
"""
|
|
188
|
-
return self._ep_write.write(data)
|
|
189
|
-
|
|
190
|
-
def close(self):
|
|
191
|
-
"""Close connection to USB device.
|
|
192
|
-
"""
|
|
193
|
-
usb.util.dispose_resources(self._device)
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
class USBListener(USBBackend, Listener):
|
|
197
|
-
"""Listener implementation for communicating with USB devices.
|
|
198
|
-
"""
|
|
199
|
-
|
|
200
|
-
def __init__(self, driver, vendor, product, bus=None,
|
|
201
|
-
port_numbers=None, interface=0):
|
|
202
|
-
super(USBListener, self).__init__(
|
|
203
|
-
vendor,
|
|
204
|
-
product,
|
|
205
|
-
bus=bus,
|
|
206
|
-
port_numbers=port_numbers,
|
|
207
|
-
interface=interface
|
|
208
|
-
)
|
|
209
|
-
self.driver = driver
|
|
210
|
-
# flag whether actually disconnecting from device
|
|
211
|
-
self._disconnecting = False
|
|
212
|
-
# event to stop listening
|
|
213
|
-
self._stop_listening = threading.Event()
|
|
214
|
-
# create and start listener thread
|
|
215
|
-
self._listener = threading.Thread(target=self.listen)
|
|
216
|
-
self._listener.start()
|
|
217
|
-
|
|
218
|
-
def listen(self):
|
|
219
|
-
"""Poll data from USB device.
|
|
220
|
-
"""
|
|
221
|
-
while not self._stop_listening.is_set():
|
|
222
|
-
try:
|
|
223
|
-
self.driver.receive(self.read())
|
|
224
|
-
except usb.core.USBError as e:
|
|
225
|
-
# read timeout
|
|
226
|
-
if e.errno == 110:
|
|
227
|
-
continue
|
|
228
|
-
if not self._disconnecting:
|
|
229
|
-
self.close()
|
|
230
|
-
|
|
231
|
-
def close(self):
|
|
232
|
-
"""Close connection to USB device.
|
|
233
|
-
"""
|
|
234
|
-
self._disconnecting = True
|
|
235
|
-
self._stop_listening.set()
|
|
236
|
-
super(USBListener, self).close()
|
|
@@ -293,6 +293,8 @@ class tridonic(hid):
|
|
|
293
293
|
# Commands sent to the interface
|
|
294
294
|
# cmd, seq, ctrl, mode, frame (4 bytes), dtr, prio, devtype
|
|
295
295
|
_cmdtmpl = struct.Struct(">4B4s3B53x")
|
|
296
|
+
# length of the "useful part", without the trailing padding
|
|
297
|
+
_cmdtmpl_useful_size = _cmdtmpl.size - 53
|
|
296
298
|
|
|
297
299
|
# _CMD send in byte 0
|
|
298
300
|
_CMD_INIT = 0x01
|
|
@@ -332,6 +334,8 @@ class tridonic(hid):
|
|
|
332
334
|
# Responses received from the interface
|
|
333
335
|
# Decodes to mode, response type, frame, interval, seq
|
|
334
336
|
_resptmpl = struct.Struct(">BB4sHB55x")
|
|
337
|
+
# length of the "useful part", without the trailing padding
|
|
338
|
+
_resptmpl_useful_size = _resptmpl.size - 55
|
|
335
339
|
_MODE_INFO = 0x01 # Response to an init command
|
|
336
340
|
_MODE_OBSERVE = 0x11 # Other traffic observed on the bus
|
|
337
341
|
_MODE_RESPONSE = 0x12 # Response to a send command
|
|
@@ -384,9 +388,13 @@ class tridonic(hid):
|
|
|
384
388
|
self._bus_watch_data_available = asyncio.Event()
|
|
385
389
|
self._bus_watch_data = []
|
|
386
390
|
|
|
391
|
+
def _raw_write(self, buf: bytes):
|
|
392
|
+
self._log.debug(f"_raw_write: {_hex(buf[0:self._cmdtmpl_useful_size])}")
|
|
393
|
+
os.write(self._f, buf)
|
|
394
|
+
|
|
387
395
|
def _initialise_device(self):
|
|
388
396
|
# Read firmware version; pick up the reply in _handle_read
|
|
389
|
-
|
|
397
|
+
self._raw_write(self._cmd(
|
|
390
398
|
tridonic._CMD_INIT, tridonic._CMD_INIT_READVERSION))
|
|
391
399
|
|
|
392
400
|
async def _power_supply(self, supply_on):
|
|
@@ -396,7 +404,7 @@ class tridonic(hid):
|
|
|
396
404
|
self._POWER_SUPPLY_ON if supply_on else self._POWER_SUPPLY_OFF)
|
|
397
405
|
|
|
398
406
|
try:
|
|
399
|
-
|
|
407
|
+
self._raw_write(data)
|
|
400
408
|
except OSError:
|
|
401
409
|
# The device has failed. Disconnect, schedule a
|
|
402
410
|
# reconnection, and report this command as failed.
|
|
@@ -421,7 +429,7 @@ class tridonic(hid):
|
|
|
421
429
|
await self.connected.wait()
|
|
422
430
|
async with self._command_semaphore:
|
|
423
431
|
seq = next(self._cmd_seq)
|
|
424
|
-
self._log.debug("Sending with seq
|
|
432
|
+
self._log.debug(f"Sending with {seq=:02x}")
|
|
425
433
|
event = asyncio.Event()
|
|
426
434
|
messages = []
|
|
427
435
|
# If seq is in self._outstanding this means we've wrapped
|
|
@@ -435,7 +443,7 @@ class tridonic(hid):
|
|
|
435
443
|
mode=self._command_mode(frame),
|
|
436
444
|
frame=frame.pack_len(4))
|
|
437
445
|
try:
|
|
438
|
-
|
|
446
|
+
self._raw_write(data)
|
|
439
447
|
except OSError:
|
|
440
448
|
# The device has failed. Disconnect, schedule a
|
|
441
449
|
# reconnection, and report this command as failed.
|
|
@@ -446,11 +454,17 @@ class tridonic(hid):
|
|
|
446
454
|
outstanding_transmissions = 2 if command.sendtwice else 1
|
|
447
455
|
response = None
|
|
448
456
|
while outstanding_transmissions or response is None:
|
|
449
|
-
self._log.debug(f"waiting for {outstanding_transmissions=} "
|
|
450
|
-
"{response
|
|
457
|
+
self._log.debug(f"_send_raw waiting for {outstanding_transmissions=} "
|
|
458
|
+
f"{response=!s}")
|
|
451
459
|
if len(messages) == 0:
|
|
452
|
-
|
|
453
|
-
|
|
460
|
+
try:
|
|
461
|
+
await asyncio.wait_for(event.wait(), timeout=0.2)
|
|
462
|
+
except TimeoutError:
|
|
463
|
+
self._log.error(f"_send_raw timed out {seq=:02x}, faking an error response")
|
|
464
|
+
response = dali.frame.BackwardFrameError(255)
|
|
465
|
+
break
|
|
466
|
+
finally:
|
|
467
|
+
event.clear()
|
|
454
468
|
message = messages.pop(0)
|
|
455
469
|
if message == "fail":
|
|
456
470
|
# The device has gone away, possibly in the middle
|
|
@@ -461,11 +475,12 @@ class tridonic(hid):
|
|
|
461
475
|
# The message mode is guaranteed to be _MODE_RESPONSE
|
|
462
476
|
mode, rtype, frame, interval, seq = self._resptmpl.unpack(
|
|
463
477
|
message)
|
|
464
|
-
self._log.debug(f"message
|
|
478
|
+
self._log.debug(f"_send_raw got message: {mode=:02x} {rtype=:02x} frame={_hex(frame)} {interval=:04x} {seq=:02x}")
|
|
465
479
|
if rtype in (self._RESPONSE_FRAME_DALI16,
|
|
466
480
|
self._RESPONSE_FRAME_DALI24):
|
|
467
481
|
# XXX check the frame contents?
|
|
468
482
|
outstanding_transmissions -= 1
|
|
483
|
+
self._log.debug(f"_send_raw rtype DALI16 or DALI24, new {outstanding_transmissions=}")
|
|
469
484
|
elif rtype == self._RESPONSE_FRAME_DALI8:
|
|
470
485
|
response = dali.frame.BackwardFrame(frame)
|
|
471
486
|
elif rtype == self._RESPONSE_INFO \
|
|
@@ -474,7 +489,8 @@ class tridonic(hid):
|
|
|
474
489
|
elif rtype == self._RESPONSE_NO_FRAME:
|
|
475
490
|
response = "no"
|
|
476
491
|
else:
|
|
477
|
-
self._log.debug(f"didn't understand {rtype
|
|
492
|
+
self._log.debug(f"didn't understand {rtype=:02x}")
|
|
493
|
+
self._log.debug(f"_send_raw finished {seq=:02x}")
|
|
478
494
|
del self._outstanding[seq], event, messages
|
|
479
495
|
if command.response:
|
|
480
496
|
# Construct response and return it
|
|
@@ -498,13 +514,13 @@ class tridonic(hid):
|
|
|
498
514
|
# Wait for data
|
|
499
515
|
if len(self._bus_watch_data) == 0:
|
|
500
516
|
if current_command:
|
|
501
|
-
self._log.debug("
|
|
517
|
+
self._log.debug("bus_watch waiting with timeout")
|
|
502
518
|
try:
|
|
503
519
|
await asyncio.wait_for(self._bus_watch_data_available.wait(), 0.2)
|
|
504
520
|
except asyncio.TimeoutError:
|
|
505
521
|
pass
|
|
506
522
|
else:
|
|
507
|
-
self._log.debug("
|
|
523
|
+
self._log.debug("bus_watch waiting for data, no timeout")
|
|
508
524
|
await self._bus_watch_data_available.wait()
|
|
509
525
|
self._bus_watch_data_available.clear()
|
|
510
526
|
|
|
@@ -515,10 +531,10 @@ class tridonic(hid):
|
|
|
515
531
|
else:
|
|
516
532
|
timeout = False
|
|
517
533
|
message = self._bus_watch_data.pop(0)
|
|
518
|
-
self._log.debug("bus_watch message %s", _hex(message[0:
|
|
534
|
+
self._log.debug("bus_watch message %s", _hex(message[0:self._resptmpl_useful_size]))
|
|
519
535
|
origin, rtype, raw_frame, interval, seq = self._resptmpl.unpack(message)
|
|
520
536
|
if origin not in (self._MODE_OBSERVE, self._MODE_RESPONSE):
|
|
521
|
-
self._log.
|
|
537
|
+
self._log.warning("bus_watch: unexpected packet mode, ignoring")
|
|
522
538
|
continue
|
|
523
539
|
if rtype == self._RESPONSE_FRAME_DALI16:
|
|
524
540
|
frame = dali.frame.ForwardFrame(16, raw_frame)
|
|
@@ -545,7 +561,7 @@ class tridonic(hid):
|
|
|
545
561
|
# We are waiting for a repeat of the command
|
|
546
562
|
if timeout:
|
|
547
563
|
# We didn't get it: report a failed command
|
|
548
|
-
self._log.
|
|
564
|
+
self._log.warning("Failed sendtwice command: %s", current_command)
|
|
549
565
|
self.bus_traffic._invoke(current_command, None, True)
|
|
550
566
|
current_command = None
|
|
551
567
|
continue
|
|
@@ -557,18 +573,23 @@ class tridonic(hid):
|
|
|
557
573
|
current_command = None
|
|
558
574
|
continue
|
|
559
575
|
else:
|
|
560
|
-
self._log.
|
|
576
|
+
self._log.warning("Failed config command (second frame didn't match): %s", current_command)
|
|
561
577
|
self.bus_traffic._invoke(current_command, None, True)
|
|
562
578
|
current_command = None
|
|
563
579
|
# Fall through to continue processing frame
|
|
564
580
|
elif isinstance(frame, dali.frame.BackwardFrame):
|
|
565
581
|
# Error: config commands don't get backward frames.
|
|
566
|
-
self._log.
|
|
582
|
+
self._log.error("Failed config command %s with backward frame",
|
|
567
583
|
current_command)
|
|
568
584
|
self.bus_traffic._invoke(current_command, None, True)
|
|
569
585
|
current_command = None
|
|
586
|
+
elif frame == "no":
|
|
587
|
+
self._log.warning("No frame in response to a sendtwice command: %s", current_command)
|
|
588
|
+
self.bus_traffic._invoke(current_command, None, True)
|
|
589
|
+
current_command = None
|
|
590
|
+
continue
|
|
570
591
|
else:
|
|
571
|
-
self._log.
|
|
592
|
+
self._log.error("Unexpected response waiting for retransmit of config command, frame = %s", frame)
|
|
572
593
|
elif current_command.response:
|
|
573
594
|
# We are waiting for a response
|
|
574
595
|
if timeout or frame == "no":
|
|
@@ -620,13 +641,13 @@ class tridonic(hid):
|
|
|
620
641
|
# self._log.debug("End of loop")
|
|
621
642
|
|
|
622
643
|
def _handle_read(self, data):
|
|
623
|
-
self._log.debug("_handle_read %s", _hex(data[0:
|
|
644
|
+
self._log.debug("_handle_read %s", _hex(data[0:self._resptmpl_useful_size]))
|
|
624
645
|
if data[0] == self._MODE_INFO:
|
|
625
646
|
# Response to initialisation command
|
|
626
647
|
if not self.firmware_version:
|
|
627
648
|
self.firmware_version = f"{data[3]}.{data[4]}"
|
|
628
649
|
# Now read the serial number
|
|
629
|
-
|
|
650
|
+
self._raw_write(self._cmd(
|
|
630
651
|
tridonic._CMD_INIT, tridonic._CMD_INIT_READSERIAL))
|
|
631
652
|
elif not self.serial:
|
|
632
653
|
self.serial = _hex(data[1:5])
|
|
@@ -644,17 +665,20 @@ class tridonic(hid):
|
|
|
644
665
|
self._bus_watch_data.append(data)
|
|
645
666
|
self._bus_watch_data_available.set()
|
|
646
667
|
seq = data[8]
|
|
647
|
-
# The Tridonic DALI USB has a firmware bug. When it
|
|
648
|
-
# observes a frame on the bus, not generated by itself,
|
|
649
|
-
# that matches the most recent frame it transmitted, it
|
|
650
|
-
# reports it as if it had transmitted it itself. We
|
|
651
|
-
# ignore this - the sequence number will have been removed
|
|
652
|
-
# from self._outstanding.
|
|
653
668
|
if seq in self._outstanding:
|
|
669
|
+
self._log.debug(f"_handle_read: signaling for {seq=:02x}")
|
|
654
670
|
event, messages = self._outstanding[seq]
|
|
655
671
|
messages.append(data)
|
|
656
672
|
event.set()
|
|
657
673
|
del event, messages
|
|
674
|
+
else:
|
|
675
|
+
# The Tridonic DALI USB has a firmware bug. When it
|
|
676
|
+
# observes a frame on the bus, not generated by itself,
|
|
677
|
+
# that matches the most recent frame it transmitted, it
|
|
678
|
+
# reports it as if it had transmitted it itself. We
|
|
679
|
+
# ignore this - the sequence number will have been removed
|
|
680
|
+
# from self._outstanding.
|
|
681
|
+
self._log.debug(f"_handle_read: nothing to do for {seq=:02x}")
|
|
658
682
|
|
|
659
683
|
else:
|
|
660
684
|
self._log.debug("Unknown response mode %x", data[0])
|
|
@@ -717,7 +741,7 @@ class hasseb(hid):
|
|
|
717
741
|
async with self._command_lock:
|
|
718
742
|
times = 2 if command.sendtwice else 1
|
|
719
743
|
for rep in range(times):
|
|
720
|
-
|
|
744
|
+
self._raw_write(frame.pack_len(2))
|
|
721
745
|
# Earlier commands may have left a response available that
|
|
722
746
|
# we need to ignore. We're only interested in responses
|
|
723
747
|
# that become available in the future.
|
|
@@ -590,13 +590,29 @@ class DriverLubaRs232(DriverSerialBase):
|
|
|
590
590
|
async with self._tx_lock:
|
|
591
591
|
_LOG.debug("Sending LUBA device settings")
|
|
592
592
|
mode_settings = 0b00000000
|
|
593
|
+
# 7: 1 = activates the DALI ping
|
|
594
|
+
# 6: 1 = Deactivates sending of DALI frames during "Initialize" mode
|
|
595
|
+
# 5: 1 = Deactivates sending of DALI frames during "Quiescent" mode
|
|
596
|
+
# 4..0: Reserved
|
|
593
597
|
event_settings = 0b00010010
|
|
598
|
+
# 7: 1 = deactivates all events
|
|
599
|
+
# 6: 1 = deactivates events for successfully sending a DALI frame
|
|
600
|
+
# 5: 1 = deactivates events for receiving a DALI frame
|
|
601
|
+
# 4: 1 = deactivate events for the send-buffer (full/empty)
|
|
602
|
+
# 3: 1 = deactivates including the tick in events
|
|
603
|
+
# 2: 1 = deactivates including the line number in events
|
|
604
|
+
# 1: 1 = Deactivates events for macros
|
|
605
|
+
# 0: reserved
|
|
606
|
+
hardware_settings = 0b00000000
|
|
607
|
+
# 7: 1 = turn on bus power supply
|
|
608
|
+
# 6..0: Reserved
|
|
594
609
|
tx_ints = [
|
|
595
610
|
0x59, # ASCII 'Y'
|
|
596
611
|
DriverLubaRs232.LubaCmd.READ_WRITE_SETTINGS_CMD.value, # LUBA Command
|
|
597
|
-
|
|
612
|
+
3, # Length
|
|
598
613
|
mode_settings,
|
|
599
614
|
event_settings,
|
|
615
|
+
hardware_settings,
|
|
600
616
|
None, # Checksum
|
|
601
617
|
]
|
|
602
618
|
# Fill in the checksum
|
|
@@ -910,9 +926,29 @@ class DriverLubaRs232(DriverSerialBase):
|
|
|
910
926
|
f"Wrong event type 0x{self._buffer[1]:02x}, expected 0x21"
|
|
911
927
|
)
|
|
912
928
|
|
|
913
|
-
# QUERY DEVICE INFO supports two "sets" of data,
|
|
914
|
-
#
|
|
915
|
-
#
|
|
929
|
+
# QUERY DEVICE INFO supports two "sets" of data, and the only way
|
|
930
|
+
# differentiate between them is to check for response length.
|
|
931
|
+
# When querying request set "0", response length should be 20 bytes,
|
|
932
|
+
# and for request set "1", response length should be 18 bytes.
|
|
933
|
+
# Currently this driver only ever requests set "0", therefore the
|
|
934
|
+
# harcoded response handling.
|
|
935
|
+
try:
|
|
936
|
+
payload_length = self._buffer[2]
|
|
937
|
+
except IndexError:
|
|
938
|
+
raise ValueError(
|
|
939
|
+
f"Invalid data for QUERY DEVICE INFO response packet"
|
|
940
|
+
)
|
|
941
|
+
|
|
942
|
+
if payload_length not in [20, 18]:
|
|
943
|
+
raise ValueError(
|
|
944
|
+
f"Unexpected payload length {payload_length} for QUERY DEVICE INFO response"
|
|
945
|
+
)
|
|
946
|
+
|
|
947
|
+
if payload_length == 18:
|
|
948
|
+
raise NotImplementedError(
|
|
949
|
+
f"QUERY DEVICE INFO with set \"1\" is not yet implemented"
|
|
950
|
+
)
|
|
951
|
+
|
|
916
952
|
info = DriverLubaRs232.LubaDeviceInfo(
|
|
917
953
|
gtin=int.from_bytes(bytes(received_data[3:9]), byteorder="big"),
|
|
918
954
|
id=int.from_bytes(bytes(received_data[9:17]), byteorder="big"),
|
|
@@ -2,8 +2,8 @@ from dali.command import from_frame
|
|
|
2
2
|
from dali.driver.base import AsyncDALIDriver
|
|
3
3
|
from dali.driver.base import DALIDriver
|
|
4
4
|
from dali.driver.base import SyncDALIDriver
|
|
5
|
-
from dali.driver.
|
|
6
|
-
from dali.driver.
|
|
5
|
+
from dali.driver.usb import USBBackend
|
|
6
|
+
from dali.driver.usb import USBListener
|
|
7
7
|
from dali.frame import BackwardFrame
|
|
8
8
|
from dali.frame import ForwardFrame
|
|
9
9
|
import logging
|
|
@@ -8,10 +8,15 @@
|
|
|
8
8
|
import logging
|
|
9
9
|
from time import sleep
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
)
|
|
11
|
+
import pymodbus
|
|
12
|
+
from packaging import version
|
|
13
|
+
|
|
14
|
+
PYMODBUS_VERSION = version.parse(pymodbus.__version__)
|
|
15
|
+
|
|
16
|
+
if PYMODBUS_VERSION >= version.parse("3.0.0"):
|
|
17
|
+
from pymodbus.client import ModbusSerialClient, ModbusTcpClient
|
|
18
|
+
else:
|
|
19
|
+
from pymodbus.client.sync import ModbusSerialClient, ModbusTcpClient
|
|
15
20
|
|
|
16
21
|
from dali.driver.base import DALIDriver, SyncDALIDriver
|
|
17
22
|
from dali.frame import BackwardFrame, ForwardFrame
|
|
@@ -40,11 +45,16 @@ class RemoteArm:
|
|
|
40
45
|
|
|
41
46
|
def __init__(self, host, unit=1, baud=19200, timeout=1):
|
|
42
47
|
if host.startswith("/"):
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
if PYMODBUS_VERSION >= version.parse("3.0.0"):
|
|
49
|
+
self.pymc = ModbusSerialClient(
|
|
50
|
+
port=host, baudrate=baud, timeout=timeout
|
|
51
|
+
)
|
|
52
|
+
else:
|
|
53
|
+
self.pymc = ModbusSerialClient(
|
|
54
|
+
method="rtu", port=host, baudrate=baud, timeout=timeout
|
|
55
|
+
)
|
|
46
56
|
else:
|
|
47
|
-
self.pymc =
|
|
57
|
+
self.pymc = ModbusTcpClient(host=host, port=502)
|
|
48
58
|
self.unit = unit
|
|
49
59
|
|
|
50
60
|
def close(self):
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
import usb
|
|
3
|
+
|
|
4
|
+
from dali.driver.base import Backend, Listener
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
###############################################################################
|
|
8
|
+
# USB backends
|
|
9
|
+
###############################################################################
|
|
10
|
+
|
|
11
|
+
class USBBackend(Backend):
|
|
12
|
+
"""Backend implementation for communicating with USB devices.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, vendor, product, bus=None,
|
|
16
|
+
port_numbers=None, interface=0):
|
|
17
|
+
self._device = None
|
|
18
|
+
# lookup devices by vendor and product
|
|
19
|
+
devices = [dev for dev in usb.core.find(
|
|
20
|
+
find_all=True,
|
|
21
|
+
idVendor=vendor,
|
|
22
|
+
idProduct=product
|
|
23
|
+
)]
|
|
24
|
+
# use first device if bus or port_numers not defined
|
|
25
|
+
if bus is None or port_numbers is None:
|
|
26
|
+
self._device = devices[0]
|
|
27
|
+
else:
|
|
28
|
+
for dev in devices:
|
|
29
|
+
if dev.bus == bus and dev.port_numbers == port_numbers:
|
|
30
|
+
self._device = dev
|
|
31
|
+
break
|
|
32
|
+
# if queried device not found, raise
|
|
33
|
+
if not self._device:
|
|
34
|
+
raise usb.core.USBError('Device not found')
|
|
35
|
+
# detach kernel driver if necessary
|
|
36
|
+
if self._device.is_kernel_driver_active(interface) is True:
|
|
37
|
+
self._device.detach_kernel_driver(interface)
|
|
38
|
+
# set device configuration
|
|
39
|
+
self._device.set_configuration()
|
|
40
|
+
# claim interface
|
|
41
|
+
usb.util.claim_interface(self._device, interface)
|
|
42
|
+
# get active configuration
|
|
43
|
+
cfg = self._device.get_active_configuration()
|
|
44
|
+
intf = cfg[(0, 0)]
|
|
45
|
+
# get write end point
|
|
46
|
+
def match_ep_out(e):
|
|
47
|
+
return usb.util.endpoint_direction(e.bEndpointAddress) == \
|
|
48
|
+
usb.util.ENDPOINT_OUT
|
|
49
|
+
self._ep_write = usb.util.find_descriptor(
|
|
50
|
+
intf, custom_match=match_ep_out)
|
|
51
|
+
# get read end point
|
|
52
|
+
def match_ep_in(e):
|
|
53
|
+
return usb.util.endpoint_direction(e.bEndpointAddress) == \
|
|
54
|
+
usb.util.ENDPOINT_IN
|
|
55
|
+
self._ep_read = usb.util.find_descriptor(
|
|
56
|
+
intf, custom_match=match_ep_in)
|
|
57
|
+
|
|
58
|
+
def read(self, timeout=None):
|
|
59
|
+
"""Read data from USB device.
|
|
60
|
+
"""
|
|
61
|
+
return self._ep_read.read(self._ep_read.wMaxPacketSize, timeout=timeout)
|
|
62
|
+
|
|
63
|
+
def write(self, data):
|
|
64
|
+
"""Write data to USB device.
|
|
65
|
+
"""
|
|
66
|
+
return self._ep_write.write(data)
|
|
67
|
+
|
|
68
|
+
def close(self):
|
|
69
|
+
"""Close connection to USB device.
|
|
70
|
+
"""
|
|
71
|
+
usb.util.dispose_resources(self._device)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class USBListener(USBBackend, Listener):
|
|
75
|
+
"""Listener implementation for communicating with USB devices.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def __init__(self, driver, vendor, product, bus=None,
|
|
79
|
+
port_numbers=None, interface=0):
|
|
80
|
+
super(USBListener, self).__init__(
|
|
81
|
+
vendor,
|
|
82
|
+
product,
|
|
83
|
+
bus=bus,
|
|
84
|
+
port_numbers=port_numbers,
|
|
85
|
+
interface=interface
|
|
86
|
+
)
|
|
87
|
+
self.driver = driver
|
|
88
|
+
# flag whether actually disconnecting from device
|
|
89
|
+
self._disconnecting = False
|
|
90
|
+
# event to stop listening
|
|
91
|
+
self._stop_listening = threading.Event()
|
|
92
|
+
# create and start listener thread
|
|
93
|
+
self._listener = threading.Thread(target=self.listen)
|
|
94
|
+
self._listener.start()
|
|
95
|
+
|
|
96
|
+
def listen(self):
|
|
97
|
+
"""Poll data from USB device.
|
|
98
|
+
"""
|
|
99
|
+
while not self._stop_listening.is_set():
|
|
100
|
+
try:
|
|
101
|
+
self.driver.receive(self.read())
|
|
102
|
+
except usb.core.USBError as e:
|
|
103
|
+
# read timeout
|
|
104
|
+
if e.errno == 110:
|
|
105
|
+
continue
|
|
106
|
+
if not self._disconnecting:
|
|
107
|
+
self.close()
|
|
108
|
+
|
|
109
|
+
def close(self):
|
|
110
|
+
"""Close connection to USB device.
|
|
111
|
+
"""
|
|
112
|
+
self._disconnecting = True
|
|
113
|
+
self._stop_listening.set()
|
|
114
|
+
super(USBListener, self).close()
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Commands and responses from IEC 62386 part 206.
|
|
2
|
+
|
|
3
|
+
This part covers conversion from digital signal into DC voltage.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dali import command
|
|
7
|
+
from dali.gear.general import _StandardCommand, QueryExtendedVersionNumberMixin
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class _ConversionCommand(_StandardCommand):
|
|
11
|
+
devicetype = 5
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class _ConversionConfigCommand(_ConversionCommand):
|
|
15
|
+
"""An incandescent lighting configuration command as defined in
|
|
16
|
+
section 11.3.4.1 of IEC 62386-206:2009.
|
|
17
|
+
"""
|
|
18
|
+
sendtwice = True
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
###############################################################################
|
|
22
|
+
# Commands from IEC 62386-206 section 11.3.4.1
|
|
23
|
+
###############################################################################
|
|
24
|
+
|
|
25
|
+
class SetOutputRange1To10V(_ConversionConfigCommand):
|
|
26
|
+
"""Set the output range to 1V - 10V
|
|
27
|
+
|
|
28
|
+
Converters without this feature shall not react.
|
|
29
|
+
"""
|
|
30
|
+
_cmdval = 224
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SetOutputRange0To10V(_ConversionConfigCommand):
|
|
34
|
+
"""Set the output range to 0V - 10V
|
|
35
|
+
|
|
36
|
+
Converters without this feature shall not react.
|
|
37
|
+
"""
|
|
38
|
+
_cmdval = 225
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class SwitchOnInternalPullUp(_ConversionConfigCommand):
|
|
42
|
+
"""Switch on the internal pull-up resistor at the control voltage output
|
|
43
|
+
|
|
44
|
+
Converters without this feature shall not react.
|
|
45
|
+
"""
|
|
46
|
+
_cmdval = 226
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class SwitchOffInternalPullUp(_ConversionConfigCommand):
|
|
50
|
+
"""Switch off the internal pull-up resistor at the control voltage output
|
|
51
|
+
|
|
52
|
+
Converters without this feature shall not react.
|
|
53
|
+
"""
|
|
54
|
+
_cmdval = 227
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class StoreDtrAsPhysicalMinimum(_ConversionConfigCommand):
|
|
58
|
+
"""The physical minimum level shall be changed to the value given in the DTR"""
|
|
59
|
+
_cmdval = 228
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class SelectDimmingCurve(_ConversionConfigCommand):
|
|
63
|
+
"""Select Dimming Curve
|
|
64
|
+
|
|
65
|
+
If DTR0 is 0 then selects the standard logarithmic curve
|
|
66
|
+
|
|
67
|
+
If DTR0 is 1 then selects a linear dimming curve
|
|
68
|
+
|
|
69
|
+
Other values of DTR0 are reserved and will not change the dimming
|
|
70
|
+
curve. The setting is stored in non-volatile memory and is not
|
|
71
|
+
cleared by the Reset command.
|
|
72
|
+
"""
|
|
73
|
+
_cmdval = 229
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class ResetConverterSettings(_ConversionConfigCommand):
|
|
77
|
+
"""Reset parameters which are not affected by RESET
|
|
78
|
+
|
|
79
|
+
All converter settings not influenced by the RESET command shall be set to the
|
|
80
|
+
default values given in clause 10 of IEC 62386-206:2009.
|
|
81
|
+
"""
|
|
82
|
+
_cmdval = 230
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
###############################################################################
|
|
86
|
+
# Commands from IEC 62386-205 section 11.3.4.2
|
|
87
|
+
###############################################################################
|
|
88
|
+
|
|
89
|
+
class QueryDimmingCurve(_ConversionCommand):
|
|
90
|
+
"""Query Dimming Curve
|
|
91
|
+
|
|
92
|
+
0 = standard logarithmic
|
|
93
|
+
1 = linear
|
|
94
|
+
2-255 = reserved for future use
|
|
95
|
+
"""
|
|
96
|
+
_cmdval = 238
|
|
97
|
+
response = command.Response
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class OutputLevelResponse(command.NumericResponse):
|
|
101
|
+
def __str__(self):
|
|
102
|
+
if isinstance(self.value, int):
|
|
103
|
+
if self.value == 254:
|
|
104
|
+
return "10.16V or more"
|
|
105
|
+
elif self.value == 255:
|
|
106
|
+
return "unknown"
|
|
107
|
+
else:
|
|
108
|
+
return f"{self.value * 0.04} V"
|
|
109
|
+
return self.value
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class QueryOutputLevel(_ConversionCommand):
|
|
113
|
+
"""Query the output level
|
|
114
|
+
|
|
115
|
+
The answer shall be the analog output level in units of 0.04V, fiving a range of 0V to 10.16V.
|
|
116
|
+
|
|
117
|
+
Raw value 254 maps to 10.16V or higher.
|
|
118
|
+
Raw value of 255 means "the output level is not known".
|
|
119
|
+
|
|
120
|
+
Converters without this feature shall not react.
|
|
121
|
+
"""
|
|
122
|
+
_cmdval = 239
|
|
123
|
+
response = OutputLevelResponse
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class QueryConverterFeaturesResponse(command.BitmapResponse):
|
|
127
|
+
bits = [
|
|
128
|
+
"0V - 10V output selectable",
|
|
129
|
+
"internal pull-up selectable",
|
|
130
|
+
"detection of output fault selectable",
|
|
131
|
+
"mains relay",
|
|
132
|
+
"output level can be queried",
|
|
133
|
+
"non-logarithmic dimming curve supported",
|
|
134
|
+
"physical selection / lamp fail detection by loss out output supported",
|
|
135
|
+
"physical selection switch supported",
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class QueryConverterFeatures(_ConversionCommand):
|
|
140
|
+
"""Query hardware features of the converter"""
|
|
141
|
+
_cmdval = 240
|
|
142
|
+
response = QueryConverterFeaturesResponse
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class QueryFailureStatusResponse(command.BitmapResponse):
|
|
146
|
+
bits = [
|
|
147
|
+
"output fault detected",
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class QueryFailureStatus(_ConversionCommand):
|
|
152
|
+
"""Query failure status register"""
|
|
153
|
+
_cmdval = 241
|
|
154
|
+
response = QueryFailureStatusResponse
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class QueryConverterStatusResponse(command.BitmapResponse):
|
|
158
|
+
bits = [
|
|
159
|
+
"0-10V operation",
|
|
160
|
+
"internal pull-up on",
|
|
161
|
+
"non-logarithmic dimming curve active",
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class QueryConverterStatus(_ConversionCommand):
|
|
166
|
+
"""Query the current status of the converter"""
|
|
167
|
+
_cmdval = 242
|
|
168
|
+
response = QueryConverterStatusResponse
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class QueryExtendedVersionNumber(QueryExtendedVersionNumberMixin,
|
|
172
|
+
_ConversionCommand):
|
|
173
|
+
pass
|
|
@@ -69,6 +69,7 @@ def QueryDT8ColourValue(
|
|
|
69
69
|
"'query' must be a value from QueryColourValueDTR enumerator"
|
|
70
70
|
)
|
|
71
71
|
|
|
72
|
+
# 62386-209, command 250, Note 2: start by sending "QUERY ACTUAL LEVEL"
|
|
72
73
|
yield QueryActualLevel(address)
|
|
73
74
|
yield DTR0(query.value)
|
|
74
75
|
msb = yield QueryColourValue(address)
|
|
@@ -1,25 +1,29 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: python-dali
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.12
|
|
4
4
|
Summary: Interface to DALI lighting systems
|
|
5
5
|
Home-page: https://github.com/sde1000/python-dali
|
|
6
6
|
Author: Stephen Early
|
|
7
7
|
Author-email: steve@assorted.org.uk
|
|
8
|
-
License:
|
|
9
|
-
Platform: UNKNOWN
|
|
8
|
+
License: "LGPL-3.0-or-later"
|
|
10
9
|
Classifier: Development Status :: 3 - Alpha
|
|
11
10
|
Classifier: Intended Audience :: Developers
|
|
12
|
-
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
13
|
-
Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)
|
|
14
11
|
Classifier: Programming Language :: Python :: 3
|
|
15
|
-
Requires-Python: >=3.
|
|
12
|
+
Requires-Python: >=3.8
|
|
16
13
|
Description-Content-Type: text/x-rst
|
|
17
|
-
Provides-Extra: driver-serial
|
|
18
|
-
Provides-Extra: driver-unipi
|
|
19
|
-
Provides-Extra: test
|
|
20
14
|
License-File: COPYING
|
|
21
15
|
License-File: COPYING.LESSER
|
|
22
16
|
License-File: LICENSE
|
|
17
|
+
Provides-Extra: driver-unipi
|
|
18
|
+
Requires-Dist: pyusb; extra == "driver-unipi"
|
|
19
|
+
Requires-Dist: pymodbus; extra == "driver-unipi"
|
|
20
|
+
Provides-Extra: driver-serial
|
|
21
|
+
Requires-Dist: pyserial-asyncio; extra == "driver-serial"
|
|
22
|
+
Provides-Extra: test
|
|
23
|
+
Requires-Dist: pytest; extra == "test"
|
|
24
|
+
Requires-Dist: pytest-asyncio; extra == "test"
|
|
25
|
+
Requires-Dist: pyserial-asyncio; extra == "test"
|
|
26
|
+
Dynamic: license-file
|
|
23
27
|
|
|
24
28
|
python-dali — lighting control interface
|
|
25
29
|
========================================
|
|
@@ -44,7 +48,9 @@ This library has been written with reference to the following documents:
|
|
|
44
48
|
- IEC 62386-201:2009 (fluorescent lamps)
|
|
45
49
|
- IEC 62386-202:2009 (self-contained emergency lighting)
|
|
46
50
|
- IEC 62386-205:2009 (supply voltage controller for incandescent lamps)
|
|
51
|
+
- IEC 62386-206:2009 (conversion from digital signal into DC voltage)
|
|
47
52
|
- IEC 62386-207:2009 (LED modules)
|
|
53
|
+
- IEC 62386-209:2012 (colour control)
|
|
48
54
|
- IEC 62386-301:2017 (particular requirements for push button input devices)
|
|
49
55
|
- IEC 62386-303:2017 (particular requirements for occupancy sensor input devices)
|
|
50
56
|
|
|
@@ -64,7 +70,7 @@ Interface Alliance`_:
|
|
|
64
70
|
Python versions supported
|
|
65
71
|
-------------------------
|
|
66
72
|
|
|
67
|
-
This library currently requires Python version 3.
|
|
73
|
+
This library currently requires Python version 3.8 or later.
|
|
68
74
|
|
|
69
75
|
Stability
|
|
70
76
|
---------
|
|
@@ -160,11 +166,13 @@ Contributors
|
|
|
160
166
|
|
|
161
167
|
- David Russell
|
|
162
168
|
|
|
169
|
+
- Nikolay Korotkiy
|
|
170
|
+
|
|
163
171
|
|
|
164
172
|
Copyright
|
|
165
173
|
---------
|
|
166
174
|
|
|
167
|
-
python-dali is Copyright (C) 2013–
|
|
175
|
+
python-dali is Copyright (C) 2013–2026 Stephen Early <steve@assorted.org.uk>
|
|
168
176
|
and other contributors.
|
|
169
177
|
|
|
170
178
|
It is distributed under the terms of the GNU Lesser General Public
|
|
@@ -182,5 +190,3 @@ not, see `this link`_.
|
|
|
182
190
|
|
|
183
191
|
.. _this link: https://www.gnu.org/licenses/
|
|
184
192
|
.. _Digital Illumination Interface Alliance: https://www.dali-alliance.org/dali/data.html
|
|
185
|
-
|
|
186
|
-
|
|
@@ -11,7 +11,6 @@ dali/command.py
|
|
|
11
11
|
dali/exceptions.py
|
|
12
12
|
dali/frame.py
|
|
13
13
|
dali/sequences.py
|
|
14
|
-
dali/version.py
|
|
15
14
|
dali/device/__init__.py
|
|
16
15
|
dali/device/general.py
|
|
17
16
|
dali/device/helpers.py
|
|
@@ -29,8 +28,10 @@ dali/driver/serial.py
|
|
|
29
28
|
dali/driver/trace_logging.py
|
|
30
29
|
dali/driver/tridonic.py
|
|
31
30
|
dali/driver/unipi.py
|
|
31
|
+
dali/driver/usb.py
|
|
32
32
|
dali/gear/__init__.py
|
|
33
33
|
dali/gear/colour.py
|
|
34
|
+
dali/gear/converter.py
|
|
34
35
|
dali/gear/emergency.py
|
|
35
36
|
dali/gear/general.py
|
|
36
37
|
dali/gear/incandescent.py
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
[metadata]
|
|
2
2
|
name = python-dali
|
|
3
|
-
version = 0.
|
|
3
|
+
version = 0.12
|
|
4
4
|
description = Interface to DALI lighting systems
|
|
5
5
|
long_description = file: README.rst
|
|
6
6
|
long_description_content_type = text/x-rst
|
|
7
7
|
author = Stephen Early
|
|
8
8
|
author_email = steve@assorted.org.uk
|
|
9
9
|
url = https://github.com/sde1000/python-dali
|
|
10
|
-
license =
|
|
10
|
+
license = "LGPL-3.0-or-later"
|
|
11
11
|
license_files =
|
|
12
12
|
COPYING
|
|
13
13
|
COPYING.LESSER
|
|
@@ -15,12 +15,10 @@ license_files =
|
|
|
15
15
|
classifiers =
|
|
16
16
|
Development Status :: 3 - Alpha
|
|
17
17
|
Intended Audience :: Developers
|
|
18
|
-
License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
19
|
-
License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)
|
|
20
18
|
Programming Language :: Python :: 3
|
|
21
19
|
|
|
22
20
|
[options]
|
|
23
|
-
python_requires = >=3.
|
|
21
|
+
python_requires = >=3.8
|
|
24
22
|
packages =
|
|
25
23
|
dali
|
|
26
24
|
dali.device
|
python-dali-0.11/dali/version.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
version="0.11 (Fri, 03 May 2024 12:49:40 +0100)"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|