python-dali 0.10__tar.gz → 0.11__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.
Files changed (65) hide show
  1. {python-dali-0.10/python_dali.egg-info → python-dali-0.11}/PKG-INFO +12 -2
  2. {python-dali-0.10 → python-dali-0.11}/README.rst +11 -1
  3. {python-dali-0.10 → python-dali-0.11}/dali/device/__init__.py +2 -0
  4. {python-dali-0.10 → python-dali-0.11}/dali/device/helpers.py +1 -2
  5. {python-dali-0.10 → python-dali-0.11}/dali/device/sequences.py +50 -0
  6. python-dali-0.11/dali/driver/atxled.py +154 -0
  7. {python-dali-0.10 → python-dali-0.11}/dali/driver/hid.py +61 -6
  8. {python-dali-0.10 → python-dali-0.11}/dali/driver/serial.py +596 -3
  9. {python-dali-0.10 → python-dali-0.11}/dali/frame.py +5 -20
  10. {python-dali-0.10 → python-dali-0.11}/dali/gear/colour.py +31 -0
  11. {python-dali-0.10 → python-dali-0.11}/dali/gear/general.py +4 -0
  12. {python-dali-0.10 → python-dali-0.11}/dali/gear/led.py +19 -2
  13. {python-dali-0.10 → python-dali-0.11}/dali/gear/sequences.py +29 -2
  14. {python-dali-0.10 → python-dali-0.11}/dali/tests/test_device_sequences.py +97 -3
  15. {python-dali-0.10 → python-dali-0.11}/dali/tests/test_dummy.py +3 -2
  16. {python-dali-0.10 → python-dali-0.11}/dali/tests/test_frame.py +0 -27
  17. python-dali-0.11/dali/version.py +1 -0
  18. {python-dali-0.10 → python-dali-0.11/python_dali.egg-info}/PKG-INFO +12 -2
  19. {python-dali-0.10 → python-dali-0.11}/python_dali.egg-info/SOURCES.txt +1 -0
  20. {python-dali-0.10 → python-dali-0.11}/setup.cfg +1 -1
  21. python-dali-0.10/dali/version.py +0 -1
  22. {python-dali-0.10 → python-dali-0.11}/COPYING +0 -0
  23. {python-dali-0.10 → python-dali-0.11}/COPYING.LESSER +0 -0
  24. {python-dali-0.10 → python-dali-0.11}/LICENSE +0 -0
  25. {python-dali-0.10 → python-dali-0.11}/dali/__init__.py +0 -0
  26. {python-dali-0.10 → python-dali-0.11}/dali/address.py +0 -0
  27. {python-dali-0.10 → python-dali-0.11}/dali/command.py +0 -0
  28. {python-dali-0.10 → python-dali-0.11}/dali/device/general.py +0 -0
  29. {python-dali-0.10 → python-dali-0.11}/dali/device/light.py +0 -0
  30. {python-dali-0.10 → python-dali-0.11}/dali/device/occupancy.py +0 -0
  31. {python-dali-0.10 → python-dali-0.11}/dali/device/pushbutton.py +0 -0
  32. {python-dali-0.10 → python-dali-0.11}/dali/driver/__init__.py +0 -0
  33. {python-dali-0.10 → python-dali-0.11}/dali/driver/base.py +0 -0
  34. {python-dali-0.10 → python-dali-0.11}/dali/driver/daliserver.py +0 -0
  35. {python-dali-0.10 → python-dali-0.11}/dali/driver/hasseb.py +0 -0
  36. {python-dali-0.10 → python-dali-0.11}/dali/driver/trace_logging.py +0 -0
  37. {python-dali-0.10 → python-dali-0.11}/dali/driver/tridonic.py +0 -0
  38. {python-dali-0.10 → python-dali-0.11}/dali/driver/unipi.py +0 -0
  39. {python-dali-0.10 → python-dali-0.11}/dali/exceptions.py +0 -0
  40. {python-dali-0.10 → python-dali-0.11}/dali/gear/__init__.py +0 -0
  41. {python-dali-0.10 → python-dali-0.11}/dali/gear/emergency.py +0 -0
  42. {python-dali-0.10 → python-dali-0.11}/dali/gear/incandescent.py +0 -0
  43. {python-dali-0.10 → python-dali-0.11}/dali/memory/__init__.py +0 -0
  44. {python-dali-0.10 → python-dali-0.11}/dali/memory/diagnostics.py +0 -0
  45. {python-dali-0.10 → python-dali-0.11}/dali/memory/energy.py +0 -0
  46. {python-dali-0.10 → python-dali-0.11}/dali/memory/info.py +0 -0
  47. {python-dali-0.10 → python-dali-0.11}/dali/memory/location.py +0 -0
  48. {python-dali-0.10 → python-dali-0.11}/dali/memory/maintenance.py +0 -0
  49. {python-dali-0.10 → python-dali-0.11}/dali/memory/oem.py +0 -0
  50. {python-dali-0.10 → python-dali-0.11}/dali/sequences.py +0 -0
  51. {python-dali-0.10 → python-dali-0.11}/dali/tests/__init__.py +0 -0
  52. {python-dali-0.10 → python-dali-0.11}/dali/tests/fakes.py +0 -0
  53. {python-dali-0.10 → python-dali-0.11}/dali/tests/fakes_serial.py +0 -0
  54. {python-dali-0.10 → python-dali-0.11}/dali/tests/test_colour.py +0 -0
  55. {python-dali-0.10 → python-dali-0.11}/dali/tests/test_command.py +0 -0
  56. {python-dali-0.10 → python-dali-0.11}/dali/tests/test_events.py +0 -0
  57. {python-dali-0.10 → python-dali-0.11}/dali/tests/test_exceptions.py +0 -0
  58. {python-dali-0.10 → python-dali-0.11}/dali/tests/test_fakes.py +0 -0
  59. {python-dali-0.10 → python-dali-0.11}/dali/tests/test_memory.py +0 -0
  60. {python-dali-0.10 → python-dali-0.11}/dali/tests/test_sequences.py +0 -0
  61. {python-dali-0.10 → python-dali-0.11}/pyproject.toml +0 -0
  62. {python-dali-0.10 → python-dali-0.11}/python_dali.egg-info/dependency_links.txt +0 -0
  63. {python-dali-0.10 → python-dali-0.11}/python_dali.egg-info/requires.txt +0 -0
  64. {python-dali-0.10 → python-dali-0.11}/python_dali.egg-info/top_level.txt +0 -0
  65. {python-dali-0.10 → python-dali-0.11}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-dali
3
- Version: 0.10
3
+ Version: 0.11
4
4
  Summary: Interface to DALI lighting systems
5
5
  Home-page: https://github.com/sde1000/python-dali
6
6
  Author: Stephen Early
@@ -108,6 +108,8 @@ Library structure
108
108
 
109
109
  - ``serial`` - asyncio-based driver for Lunatone LUBA RS232 interfaces
110
110
 
111
+ - ``atxled`` - Driver for ATX LED SERIAL DALI HAT
112
+
111
113
  - ``exceptions`` - DALI related exceptions
112
114
 
113
115
  - ``frame`` - Forward and backward frames; stable
@@ -150,11 +152,19 @@ Contributors
150
152
 
151
153
  - Sean Lanigan, Wallace Building Systems Pty Ltd
152
154
 
155
+ - Markus Becker
156
+
157
+ - Jan Kundrát
158
+
159
+ - Olivier Pieters
160
+
161
+ - David Russell
162
+
153
163
 
154
164
  Copyright
155
165
  ---------
156
166
 
157
- python-dali is Copyright (C) 2013–2022 Stephen Early <steve@assorted.org.uk>
167
+ python-dali is Copyright (C) 2013–2024 Stephen Early <steve@assorted.org.uk>
158
168
  and other contributors.
159
169
 
160
170
  It is distributed under the terms of the GNU Lesser General Public
@@ -85,6 +85,8 @@ Library structure
85
85
 
86
86
  - ``serial`` - asyncio-based driver for Lunatone LUBA RS232 interfaces
87
87
 
88
+ - ``atxled`` - Driver for ATX LED SERIAL DALI HAT
89
+
88
90
  - ``exceptions`` - DALI related exceptions
89
91
 
90
92
  - ``frame`` - Forward and backward frames; stable
@@ -127,11 +129,19 @@ Contributors
127
129
 
128
130
  - Sean Lanigan, Wallace Building Systems Pty Ltd
129
131
 
132
+ - Markus Becker
133
+
134
+ - Jan Kundrát
135
+
136
+ - Olivier Pieters
137
+
138
+ - David Russell
139
+
130
140
 
131
141
  Copyright
132
142
  ---------
133
143
 
134
- python-dali is Copyright (C) 2013–2022 Stephen Early <steve@assorted.org.uk>
144
+ python-dali is Copyright (C) 2013–2024 Stephen Early <steve@assorted.org.uk>
135
145
  and other contributors.
136
146
 
137
147
  It is distributed under the terms of the GNU Lesser General Public
@@ -7,3 +7,5 @@ part 103 ("General requirements - Control devices") and parts 3xx
7
7
  import dali.device.general
8
8
  import dali.device.sequences
9
9
  import dali.device.pushbutton # noqa: F401
10
+ import dali.device.occupancy # noqa: F401
11
+ import dali.device.light # noqa: F401
@@ -127,8 +127,7 @@ class DeviceInstanceTypeMapper:
127
127
  if isinstance(rsp, QueryDeviceStatusResponse):
128
128
  # Make sure the status is OK
129
129
  if (
130
- rsp.input_device_error
131
- or rsp.short_address_is_mask
130
+ rsp.short_address_is_mask
132
131
  or rsp.reset_state
133
132
  ):
134
133
  continue
@@ -14,6 +14,9 @@ from dali.device.general import (
14
14
  DTR2,
15
15
  EventScheme,
16
16
  InstanceEventFilter,
17
+ QueryResolution,
18
+ QueryInputValue,
19
+ QueryInputValueLatch,
17
20
  QueryEventFilterH,
18
21
  QueryEventFilterL,
19
22
  QueryEventFilterM,
@@ -23,6 +26,7 @@ from dali.device.general import (
23
26
  SetEventScheme,
24
27
  )
25
28
  from dali.device.helpers import check_bad_rsp
29
+ from dali.exceptions import DALISequenceError
26
30
 
27
31
 
28
32
  def SetEventSchemes(
@@ -225,3 +229,49 @@ def QueryEventFilters(
225
229
  hi = rsp.value
226
230
 
227
231
  return filter_type(int.from_bytes((lo, md, hi), "little"))
232
+
233
+
234
+ def query_input_value(
235
+ device: DeviceShort,
236
+ instance: InstanceNumber,
237
+ resolution: Optional[int] = None
238
+ ) -> Generator[Command, Optional[Response], Optional[int]]:
239
+ """
240
+ A generator sequence to retrieve full sensor value from a part-103 control device.
241
+ Use with an appropriate DALI driver instance, through the `run_sequence()`
242
+ method.
243
+
244
+ :param device: A DeviceShort address to target
245
+ :param instance: An InstanceNumber to target
246
+ :param resolution: Number of valid bits that the device provides
247
+ """
248
+ # Although the proper types are expected, ints are common enough for
249
+ # addresses and their meaning is unambiguous in this context
250
+ if isinstance(device, int):
251
+ device = DeviceShort(device)
252
+ if isinstance(instance, int):
253
+ instance = InstanceNumber(instance)
254
+
255
+ if resolution is None:
256
+ resolution = yield QueryResolution(device, instance)
257
+ if check_bad_rsp(resolution):
258
+ raise DALISequenceError("query_input_value: QueryResolution failed")
259
+ resolution = resolution.value
260
+
261
+ value = yield QueryInputValue(device, instance)
262
+ if check_bad_rsp(value):
263
+ raise DALISequenceError("query_input_value: QueryInputValue failed")
264
+ value = value.value
265
+ while resolution > 8:
266
+ resolution -= 8
267
+ value <<= 8
268
+ chunk = yield QueryInputValueLatch(device, instance)
269
+ if check_bad_rsp(chunk):
270
+ raise DALISequenceError("query_input_value: QueryInputValueLatch failed")
271
+ value += chunk.value
272
+
273
+ if resolution > 0:
274
+ # Strip the repeated trailing bytes as per IEC 62386-103:2014, part 9.7.2
275
+ value >>= 8 - resolution
276
+
277
+ return value
@@ -0,0 +1,154 @@
1
+ from dali.command import Command
2
+ from dali.driver.base import SyncDALIDriver, DALIDriver
3
+ from dali.frame import BackwardFrame
4
+ import logging
5
+ import sys
6
+
7
+ import serial
8
+ import threading
9
+ import time
10
+
11
+ DALI_PACKET_SIZE = {"j": 8, "h": 16, "l": 24, "m": 25}
12
+ DALI_PACKET_PREFIX = {v: k for k, v in DALI_PACKET_SIZE.items()}
13
+
14
+
15
+ class DaliHatSerialDriver(DALIDriver):
16
+ """Driver for communicating with DALI devices over a serial connection."""
17
+
18
+ def __init__(self, port="/dev/ttyS0", LOG=None):
19
+ """Initialize the serial connection to the DALI interface."""
20
+ self.port = port
21
+ self.lock = threading.RLock()
22
+ self.buffer = []
23
+ if not LOG:
24
+ self.LOG = logging.getLogger("AtxLedDaliDriver")
25
+ handler = logging.StreamHandler(sys.stdout)
26
+ self.LOG.addHandler(handler)
27
+ else:
28
+ self.LOG = LOG
29
+ try:
30
+ self.conn = serial.Serial(
31
+ port=self.port,
32
+ baudrate=19200,
33
+ parity=serial.PARITY_NONE,
34
+ stopbits=serial.STOPBITS_ONE,
35
+ bytesize=serial.EIGHTBITS,
36
+ timeout=0.2,
37
+ )
38
+ except Exception as e:
39
+ self.LOG.exception("Could not open serial connection: %s", e)
40
+ self.conn = None
41
+
42
+ def read_line(self):
43
+ """Read the next line from the buffer, refilling the buffer if necessary."""
44
+ with self.lock:
45
+ while not self.buffer:
46
+ line = self.conn.read_until(b"\n").decode("ascii")
47
+ if not line:
48
+ return ""
49
+ self.buffer.append(line)
50
+ return self.buffer.pop(0)
51
+
52
+ def construct(self, command):
53
+ """Construct a DALI command to be sent over the serial connection."""
54
+ assert isinstance(command, Command)
55
+ f = command.frame
56
+ packet_size = len(f)
57
+ prefix = DALI_PACKET_PREFIX[packet_size]
58
+ if command.sendtwice and packet_size == 16:
59
+ prefix = "t"
60
+ data = "".join(["{:02X}".format(byte) for byte in f.pack])
61
+ command_str = (f"{prefix}{data}\n").encode("ascii")
62
+ return command_str
63
+
64
+ def extract(self, data):
65
+ """Parse the response from the serial device and return the corresponding frame."""
66
+ if data.startswith("J"):
67
+ try:
68
+ data = int(data[1:], 16)
69
+ return BackwardFrame(data)
70
+ except ValueError as e:
71
+ self.LOG.error(f"Failed to parse response '{data}': {e}")
72
+ return None
73
+
74
+ def close(self):
75
+ """Close the serial connection."""
76
+ if self.conn:
77
+ self.conn.close()
78
+
79
+
80
+ class SyncDaliHatDriver(DaliHatSerialDriver, SyncDALIDriver):
81
+ """Synchronous DALI driver."""
82
+
83
+ def send(self, command: Command):
84
+ """Send a command to the DALI interface and wait for a response."""
85
+ with self.lock:
86
+ lines = []
87
+ last_resp = None
88
+ send_twice = command.sendtwice
89
+ cmd = self.construct(command)
90
+ self.LOG.debug("command string sent: %r", cmd)
91
+ self.conn.write(cmd)
92
+ REPS = 5
93
+ i = 0
94
+ already_resent = False
95
+ resent_times = 0
96
+ resp = None
97
+ while i < REPS:
98
+ i += 1
99
+ resp = self.read_line()
100
+ self.LOG.debug("raw response received: %r", resp)
101
+ resend = False
102
+ if cmd[:3] not in ["hB1", "hB3", "hB5"]:
103
+ if resp and resp[0] in {"N", "J"}:
104
+ if send_twice:
105
+ if last_resp:
106
+ if last_resp == resp:
107
+ resp = self.extract(resp)
108
+ break
109
+ resend = True
110
+ last_resp = None
111
+ else:
112
+ last_resp = resp
113
+ else:
114
+ resp = self.extract(resp)
115
+ break
116
+ elif resp and resp[0] in {"X", "Z", ""}:
117
+ time.sleep(0.1)
118
+ collision_bytes = None
119
+ while collision_bytes != "":
120
+ collision_bytes = self.read_line()
121
+ if resp[0] == "X":
122
+ break
123
+ self.LOG.info(
124
+ "got conflict (%s) sending %r, sending again", resp, cmd
125
+ )
126
+ last_resp = None
127
+ resend = True
128
+ elif resp:
129
+ lines.append(resp)
130
+
131
+ resp = None
132
+ if resend and not already_resent:
133
+ self.conn.write((cmd).encode("ascii"))
134
+ REPS += 1 + send_twice
135
+ already_resent = True
136
+ else:
137
+ if resp and resp[0] == "N":
138
+ resp = self.extract(resp)
139
+ break
140
+ elif resp and resp[0] in {"X", "Z", ""}:
141
+ time.sleep(0.1)
142
+ collision_bytes = None
143
+ while collision_bytes != "":
144
+ collision_bytes = self.read_line()
145
+ elif resp:
146
+ last_resp = None
147
+ resend = True
148
+ if resend and resent_times < 5:
149
+ self.conn.write(cmd.encode("ascii"))
150
+ REPS += 1 + send_twice
151
+ resent_times += 1
152
+ if command.is_query:
153
+ return command.response(resp)
154
+ return resp
@@ -123,18 +123,20 @@ class hid:
123
123
  path = glob.glob(self._path)
124
124
  else:
125
125
  path = [self._path]
126
+ ex = None
126
127
  if path:
127
128
  try:
128
129
  if self._glob:
129
130
  self._log.debug("trying concrete path %s", path[0])
130
131
  self._f = os.open(path[0], os.O_RDWR | os.O_NONBLOCK)
131
- except:
132
+ except Exception as e:
132
133
  self._f = None
134
+ ex = e
133
135
  else:
134
136
  self._log.debug("path %s not found", self._path)
135
137
  if not self._f:
136
138
  # It didn't work. Schedule a reconnection attempt if we can.
137
- self._log.debug("hid failed to open %s - waiting to try again", self._path)
139
+ self._log.debug("hid failed to open %s (%s) - waiting to try again", self._path, ex)
138
140
  self._reconnect_task = asyncio.create_task(self._reconnect())
139
141
  return False
140
142
  self._reconnect_count = 0
@@ -208,6 +210,29 @@ class hid:
208
210
  if not in_transaction:
209
211
  self.transaction_lock.release()
210
212
 
213
+ async def power_supply(self, supply_on, in_transaction=False, exceptions=None):
214
+ """
215
+ TODO
216
+ """
217
+ if exceptions is None:
218
+ exceptions = self.exceptions_on_send
219
+
220
+ if not in_transaction:
221
+ await self.transaction_lock.acquire()
222
+ try:
223
+ command_sent = False
224
+ while not command_sent:
225
+ try:
226
+ response = await self._power_supply(supply_on)
227
+ command_sent = True
228
+ except CommunicationError:
229
+ if exceptions:
230
+ raise
231
+ return response
232
+ finally:
233
+ if not in_transaction:
234
+ self.transaction_lock.release()
235
+
211
236
  async def run_sequence(self, seq, progress=None):
212
237
  """Run a command sequence as a transaction
213
238
  """
@@ -277,6 +302,7 @@ class tridonic(hid):
277
302
  _CMD_SET_IOPINS = 0x20
278
303
  _CMD_READ_IOPINS = 0x21
279
304
  _CMD_IDENTIFY = 0x22
305
+ _CMD_POWERSUPPLY = 0x40
280
306
 
281
307
  # For _CMD_INIT, send in byte 1:
282
308
  _CMD_INIT_READVERSION = 0x00
@@ -298,6 +324,11 @@ class tridonic(hid):
298
324
  _SEND_MODE_DALI24 = 6
299
325
  _SEND_MODE_HELVAR17 = 8
300
326
 
327
+ # For _CMD_POWERSUPPLY, send in byte 1;
328
+ _POWER_SUPPLY_OFF = 0x00
329
+ _POWER_SUPPLY_ON = 0x01
330
+ _POWER_SUPPLY_TBD = 0xFF # APPLY?
331
+
301
332
  # Responses received from the interface
302
333
  # Decodes to mode, response type, frame, interval, seq
303
334
  _resptmpl = struct.Struct(">BB4sHB55x")
@@ -358,6 +389,31 @@ class tridonic(hid):
358
389
  os.write(self._f, self._cmd(
359
390
  tridonic._CMD_INIT, tridonic._CMD_INIT_READVERSION))
360
391
 
392
+ async def _power_supply(self, supply_on):
393
+ await self.connected.wait()
394
+ async with self._command_semaphore:
395
+ data = self._cmd(self._CMD_POWERSUPPLY,
396
+ self._POWER_SUPPLY_ON if supply_on else self._POWER_SUPPLY_OFF)
397
+
398
+ try:
399
+ os.write(self._f, data)
400
+ except OSError:
401
+ # The device has failed. Disconnect, schedule a
402
+ # reconnection, and report this command as failed.
403
+ self._log.debug("fail on transmit, disconnecting")
404
+ self.disconnect(reconnect=True)
405
+ raise CommunicationError
406
+
407
+ @staticmethod
408
+ def _command_mode(frame):
409
+ if len(frame) == 8:
410
+ return tridonic._SEND_MODE_DALI8
411
+ if len(frame) == 16:
412
+ return tridonic._SEND_MODE_DALI16
413
+ if len(frame) == 24:
414
+ return tridonic._SEND_MODE_DALI24
415
+ raise UnsupportedFrameTypeError
416
+
361
417
  async def _send_raw(self, command):
362
418
  frame = command.frame
363
419
  if len(frame) not in (16, 24):
@@ -376,8 +432,7 @@ class tridonic(hid):
376
432
  data = self._cmd(
377
433
  self._CMD_SEND, seq,
378
434
  ctrl=self._SEND_CTRL_SENDTWICE if command.sendtwice else 0,
379
- mode=self._SEND_MODE_DALI16 if len(frame) == 16
380
- else self._SEND_MODE_DALI24,
435
+ mode=self._command_mode(frame),
381
436
  frame=frame.pack_len(4))
382
437
  try:
383
438
  os.write(self._f, data)
@@ -473,7 +528,7 @@ class tridonic(hid):
473
528
  frame = dali.frame.BackwardFrame(raw_frame)
474
529
  elif rtype == self._RESPONSE_NO_FRAME:
475
530
  frame = "no"
476
- elif rtype == self._RESPONSE_BUS_STATUS \
531
+ elif rtype == self._RESPONSE_INFO \
477
532
  and message[5] == self._BUS_STATUS_FRAMING_ERROR:
478
533
  frame = dali.frame.BackwardFrameError(255)
479
534
  else:
@@ -502,7 +557,7 @@ class tridonic(hid):
502
557
  current_command = None
503
558
  continue
504
559
  else:
505
- self._log.debug("Failed config command (second frame didn't match): %s", current_comment)
560
+ self._log.debug("Failed config command (second frame didn't match): %s", current_command)
506
561
  self.bus_traffic._invoke(current_command, None, True)
507
562
  current_command = None
508
563
  # Fall through to continue processing frame