python-dali 0.13__tar.gz → 0.14__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.13/python_dali.egg-info → python_dali-0.14}/PKG-INFO +1 -1
- {python_dali-0.13 → python_dali-0.14}/dali/device/helpers.py +1 -1
- {python_dali-0.13 → python_dali-0.14}/dali/device/sequences.py +3 -3
- {python_dali-0.13 → python_dali-0.14}/dali/driver/hid.py +68 -47
- {python_dali-0.13 → python_dali-0.14}/dali/driver/serial.py +163 -130
- {python_dali-0.13 → python_dali-0.14}/dali/tests/fakes_serial.py +44 -53
- {python_dali-0.13 → python_dali-0.14}/dali/tests/test_dummy.py +95 -3
- {python_dali-0.13 → python_dali-0.14/python_dali.egg-info}/PKG-INFO +1 -1
- {python_dali-0.13 → python_dali-0.14}/setup.cfg +1 -1
- {python_dali-0.13 → python_dali-0.14}/COPYING +0 -0
- {python_dali-0.13 → python_dali-0.14}/COPYING.LESSER +0 -0
- {python_dali-0.13 → python_dali-0.14}/LICENSE +0 -0
- {python_dali-0.13 → python_dali-0.14}/README.rst +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/__init__.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/address.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/command.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/device/__init__.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/device/general.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/device/general_sensor.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/device/light.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/device/occupancy.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/device/pushbutton.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/driver/__init__.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/driver/atxled.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/driver/base.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/driver/daliserver.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/driver/hasseb.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/driver/trace_logging.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/driver/tridonic.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/driver/unipi.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/driver/usb.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/exceptions.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/frame.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/gear/__init__.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/gear/colour.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/gear/converter.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/gear/emergency.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/gear/general.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/gear/incandescent.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/gear/led.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/gear/sequences.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/memory/__init__.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/memory/diagnostics.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/memory/emergency.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/memory/energy.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/memory/info.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/memory/location.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/memory/maintenance.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/memory/oem.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/sequences.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/tests/__init__.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/tests/fakes.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/tests/test_colour.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/tests/test_command.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/tests/test_device_sequences.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/tests/test_emergency.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/tests/test_events.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/tests/test_exceptions.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/tests/test_fakes.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/tests/test_frame.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/tests/test_general_sensor.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/tests/test_memory.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/tests/test_sequences.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/dali/tests/test_serial_driver.py +0 -0
- {python_dali-0.13 → python_dali-0.14}/pyproject.toml +0 -0
- {python_dali-0.13 → python_dali-0.14}/python_dali.egg-info/SOURCES.txt +0 -0
- {python_dali-0.13 → python_dali-0.14}/python_dali.egg-info/dependency_links.txt +0 -0
- {python_dali-0.13 → python_dali-0.14}/python_dali.egg-info/requires.txt +0 -0
- {python_dali-0.13 → python_dali-0.14}/python_dali.egg-info/top_level.txt +0 -0
- {python_dali-0.13 → python_dali-0.14}/setup.py +0 -0
|
@@ -62,7 +62,7 @@ def SetEventSchemes(
|
|
|
62
62
|
|
|
63
63
|
Example:
|
|
64
64
|
```
|
|
65
|
-
await driver.
|
|
65
|
+
await driver.send(
|
|
66
66
|
SetEventSchemes(
|
|
67
67
|
device=address.DeviceShort(1),
|
|
68
68
|
instance=address.InstanceNumber(2),
|
|
@@ -114,7 +114,7 @@ def SetEventFilters(
|
|
|
114
114
|
from dali import address
|
|
115
115
|
from dali.device.pushbutton import InstanceEventFilter as filter_pb
|
|
116
116
|
|
|
117
|
-
await driver.
|
|
117
|
+
await driver.send(
|
|
118
118
|
SetEventFilters(
|
|
119
119
|
device=address.DeviceShort(1),
|
|
120
120
|
instance=address.InstanceNumber(2),
|
|
@@ -204,7 +204,7 @@ def QueryEventFilters(
|
|
|
204
204
|
from dali import address
|
|
205
205
|
from dali.device import pushbutton
|
|
206
206
|
|
|
207
|
-
await driver.
|
|
207
|
+
await driver.send(
|
|
208
208
|
QueryEventFilters(
|
|
209
209
|
device=address.DeviceShort(1),
|
|
210
210
|
instance=address.InstanceNumber(2),
|
|
@@ -10,6 +10,7 @@ import struct
|
|
|
10
10
|
import logging
|
|
11
11
|
import random
|
|
12
12
|
import glob
|
|
13
|
+
import warnings
|
|
13
14
|
from dali.exceptions import UnsupportedFrameTypeError, CommunicationError
|
|
14
15
|
from dali.sequences import sleep as seq_sleep
|
|
15
16
|
from dali.sequences import progress as seq_progress
|
|
@@ -23,6 +24,11 @@ from dali.gear.general import EnableDeviceType
|
|
|
23
24
|
def _hex(b):
|
|
24
25
|
return ''.join("%02X" % x for x in b)
|
|
25
26
|
|
|
27
|
+
|
|
28
|
+
def _single_command_sequence(cmd):
|
|
29
|
+
"""Wrap a single command as a trivial sequence that returns its response."""
|
|
30
|
+
return (yield cmd)
|
|
31
|
+
|
|
26
32
|
class _callback:
|
|
27
33
|
"""Helper class for callback registration
|
|
28
34
|
"""
|
|
@@ -174,41 +180,71 @@ class hid:
|
|
|
174
180
|
if reconnect:
|
|
175
181
|
self._reconnect_task = asyncio.ensure_future(self._reconnect())
|
|
176
182
|
|
|
177
|
-
async def send(self,
|
|
178
|
-
"""Send a
|
|
183
|
+
async def send(self, msg, *, priority=5, progress=None, exceptions=None):
|
|
184
|
+
"""Send a command or run a sequence as a single transaction
|
|
185
|
+
|
|
186
|
+
A bare Command is wrapped in a trivial sequence, so a single command
|
|
187
|
+
and a multi-command sequence share the same execution path. The whole
|
|
188
|
+
transaction is sent while holding the transaction_lock, so it cannot be
|
|
189
|
+
interrupted by another sender. Returns the command's response for a
|
|
190
|
+
single command, otherwise the sequence's return value.
|
|
179
191
|
|
|
180
|
-
|
|
181
|
-
|
|
192
|
+
When a command has a non-zero device type, an EnableDeviceType frame is
|
|
193
|
+
transmitted immediately before it.
|
|
182
194
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
195
|
+
priority is accepted for a uniform driver API and validated against the
|
|
196
|
+
IEC 62386-101 Table 22 range 2..5 (a value outside it raises
|
|
197
|
+
ValueError), but HID dongles transmit at a fixed priority, so it has no
|
|
198
|
+
effect on the wire.
|
|
186
199
|
|
|
187
|
-
This call can raise dali.exceptions.CommunicationError if
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
200
|
+
This call can raise dali.exceptions.CommunicationError if there is a
|
|
201
|
+
problem sending a command to the device. If you prefer to wait for the
|
|
202
|
+
device to become available again, pass exceptions=False or set the
|
|
203
|
+
exceptions_on_send attribute to False.
|
|
191
204
|
"""
|
|
205
|
+
if priority not in (2, 3, 4, 5):
|
|
206
|
+
raise ValueError(
|
|
207
|
+
f"priority must be in the range 2..5, not {priority!r}"
|
|
208
|
+
)
|
|
192
209
|
if exceptions is None:
|
|
193
210
|
exceptions = self.exceptions_on_send
|
|
194
211
|
|
|
195
|
-
|
|
196
|
-
|
|
212
|
+
seq = (
|
|
213
|
+
_single_command_sequence(msg)
|
|
214
|
+
if isinstance(msg, dali.command.Command)
|
|
215
|
+
else msg
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
await self.transaction_lock.acquire()
|
|
219
|
+
response = None
|
|
197
220
|
try:
|
|
198
|
-
|
|
199
|
-
while not command_sent:
|
|
221
|
+
while True:
|
|
200
222
|
try:
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
223
|
+
cmd = seq.send(response)
|
|
224
|
+
except StopIteration as r:
|
|
225
|
+
return r.value
|
|
226
|
+
response = None
|
|
227
|
+
if isinstance(cmd, seq_sleep):
|
|
228
|
+
await asyncio.sleep(cmd.delay)
|
|
229
|
+
elif isinstance(cmd, seq_progress):
|
|
230
|
+
if progress:
|
|
231
|
+
progress(cmd)
|
|
232
|
+
else:
|
|
233
|
+
command_sent = False
|
|
234
|
+
while not command_sent:
|
|
235
|
+
try:
|
|
236
|
+
if cmd.devicetype != 0:
|
|
237
|
+
await self._send_raw(
|
|
238
|
+
EnableDeviceType(cmd.devicetype)
|
|
239
|
+
)
|
|
240
|
+
response = await self._send_raw(cmd)
|
|
241
|
+
command_sent = True
|
|
242
|
+
except CommunicationError:
|
|
243
|
+
if exceptions:
|
|
244
|
+
raise
|
|
209
245
|
finally:
|
|
210
|
-
|
|
211
|
-
|
|
246
|
+
self.transaction_lock.release()
|
|
247
|
+
seq.close()
|
|
212
248
|
|
|
213
249
|
async def power_supply(self, supply_on, in_transaction=False, exceptions=None):
|
|
214
250
|
"""
|
|
@@ -234,29 +270,14 @@ class hid:
|
|
|
234
270
|
self.transaction_lock.release()
|
|
235
271
|
|
|
236
272
|
async def run_sequence(self, seq, progress=None):
|
|
237
|
-
"""
|
|
273
|
+
"""Deprecated alias for send(), which now accepts commands and sequences
|
|
238
274
|
"""
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
except StopIteration as r:
|
|
246
|
-
return r.value
|
|
247
|
-
response = None
|
|
248
|
-
if isinstance(cmd, seq_sleep):
|
|
249
|
-
await asyncio.sleep(cmd.delay)
|
|
250
|
-
elif isinstance(cmd, seq_progress):
|
|
251
|
-
if progress:
|
|
252
|
-
progress(cmd)
|
|
253
|
-
else:
|
|
254
|
-
if cmd.devicetype != 0:
|
|
255
|
-
await self._send_raw(EnableDeviceType(cmd.devicetype))
|
|
256
|
-
response = await self._send_raw(cmd)
|
|
257
|
-
finally:
|
|
258
|
-
self.transaction_lock.release()
|
|
259
|
-
seq.close()
|
|
275
|
+
warnings.warn(
|
|
276
|
+
"run_sequence() is deprecated; use send() instead",
|
|
277
|
+
DeprecationWarning,
|
|
278
|
+
stacklevel=2,
|
|
279
|
+
)
|
|
280
|
+
return await self.send(seq, progress=progress)
|
|
260
281
|
|
|
261
282
|
def _initialise_device(self):
|
|
262
283
|
"""Send any device-specific initialisation commands
|
|
@@ -23,6 +23,7 @@ from __future__ import annotations
|
|
|
23
23
|
|
|
24
24
|
import asyncio
|
|
25
25
|
import logging
|
|
26
|
+
import warnings
|
|
26
27
|
from enum import Enum
|
|
27
28
|
from functools import reduce
|
|
28
29
|
from operator import xor
|
|
@@ -57,6 +58,13 @@ def _drain_queue(queue: asyncio.Queue) -> list:
|
|
|
57
58
|
return items
|
|
58
59
|
|
|
59
60
|
|
|
61
|
+
def _single_command_sequence(
|
|
62
|
+
cmd: command.Command,
|
|
63
|
+
) -> Generator[command.Command, Optional[command.Response], Optional[command.Response]]:
|
|
64
|
+
"""Wrap a single command as a trivial sequence that returns its response."""
|
|
65
|
+
return (yield cmd)
|
|
66
|
+
|
|
67
|
+
|
|
60
68
|
class DistributorQueue(asyncio.Queue):
|
|
61
69
|
def __init__(self, parent: Optional[DistributorQueue] = None, *, maxsize=0):
|
|
62
70
|
"""
|
|
@@ -215,22 +223,24 @@ class DriverSerialBase:
|
|
|
215
223
|
"""
|
|
216
224
|
await self._connected.wait()
|
|
217
225
|
|
|
218
|
-
async def
|
|
219
|
-
self,
|
|
226
|
+
async def _send_frame(
|
|
227
|
+
self, cmd: command.Command, *, priority: int = 1
|
|
220
228
|
) -> Optional[command.Response]:
|
|
221
229
|
"""
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
+
Transmit a single DALI frame over the bus. If the command expects a
|
|
231
|
+
response it is returned.
|
|
232
|
+
|
|
233
|
+
This is the low-level, per-frame transmit used by `send()`. Callers
|
|
234
|
+
should use `send()` instead, which runs commands and sequences as
|
|
235
|
+
transactions and takes care of EnableDeviceType and priority.
|
|
236
|
+
|
|
237
|
+
:param cmd: A Command object to transmit
|
|
238
|
+
:param priority: IEC 62386-101 Table 22 transmit priority (1..5) for
|
|
239
|
+
this frame. Drivers that cannot set a transmit priority ignore it.
|
|
230
240
|
:return: Either None if no response is expected, or a Response object
|
|
231
241
|
"""
|
|
232
242
|
raise NotImplementedError(
|
|
233
|
-
"'
|
|
243
|
+
"'_send_frame()' needs to be implemented in a subclass"
|
|
234
244
|
)
|
|
235
245
|
|
|
236
246
|
def new_dali_rx_queue(self) -> DistributorQueue:
|
|
@@ -259,29 +269,52 @@ class DriverSerialBase:
|
|
|
259
269
|
"'new_dali_rx_queue()' needs to be implemented in a subclass"
|
|
260
270
|
)
|
|
261
271
|
|
|
262
|
-
async def
|
|
272
|
+
async def send(
|
|
263
273
|
self,
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
Any, # The return type depends specifically on the sequence
|
|
268
|
-
],
|
|
274
|
+
msg: command.Command | Generator,
|
|
275
|
+
*,
|
|
276
|
+
priority: int = 5,
|
|
269
277
|
progress: Optional[Callable[[str | sequences.progress], None]] = None,
|
|
270
278
|
) -> Any:
|
|
271
279
|
"""
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
280
|
+
Send a command or run a sequence as a single transaction.
|
|
281
|
+
|
|
282
|
+
A bare Command is wrapped in a trivial sequence, so a single command
|
|
283
|
+
and a multi-command sequence share the same execution path. The whole
|
|
284
|
+
transaction is sent while holding the transaction lock, so it cannot be
|
|
285
|
+
interrupted by another sender.
|
|
286
|
+
|
|
287
|
+
The first forward frame of the transaction is sent at `priority`; all
|
|
288
|
+
subsequent frames are sent at priority 1, forming an IEC 62386-101 §9.3
|
|
289
|
+
transaction. When a command has a non-zero device type, an
|
|
290
|
+
EnableDeviceType frame is transmitted immediately before it (taking the
|
|
291
|
+
caller's priority, so the command itself follows at priority 1).
|
|
292
|
+
|
|
293
|
+
:param msg: A Command to send, or a sequence generator to run. Sequences
|
|
294
|
+
are available in various places in the python-dali library.
|
|
295
|
+
:param priority: IEC 62386-101 Table 22 priority for the first frame, in
|
|
296
|
+
the range 2..5 (see IEC 62386-103 §9.14 for how to choose it). A value
|
|
297
|
+
outside this range raises ValueError.
|
|
298
|
+
:param progress: A function to call with progress updates, used by some
|
|
299
|
+
sequences to provide status information. The function must accept a
|
|
300
|
+
single argument. A suitable example is `progress=print`.
|
|
301
|
+
:return: The command response for a single command, otherwise the
|
|
302
|
+
sequence's return value.
|
|
282
303
|
"""
|
|
304
|
+
if priority not in (2, 3, 4, 5):
|
|
305
|
+
raise ValueError(
|
|
306
|
+
f"priority must be in the range 2..5, not {priority!r}"
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
seq = (
|
|
310
|
+
_single_command_sequence(msg)
|
|
311
|
+
if isinstance(msg, command.Command)
|
|
312
|
+
else msg
|
|
313
|
+
)
|
|
314
|
+
|
|
283
315
|
async with self.transaction_lock:
|
|
284
316
|
response = None
|
|
317
|
+
next_priority = priority
|
|
285
318
|
try:
|
|
286
319
|
while True:
|
|
287
320
|
try:
|
|
@@ -298,16 +331,38 @@ class DriverSerialBase:
|
|
|
298
331
|
progress(cmd)
|
|
299
332
|
else:
|
|
300
333
|
if cmd.devicetype != 0:
|
|
301
|
-
|
|
302
|
-
# transmit method
|
|
303
|
-
await self.send(
|
|
334
|
+
await self._send_frame(
|
|
304
335
|
gear.general.EnableDeviceType(cmd.devicetype),
|
|
305
|
-
|
|
336
|
+
priority=next_priority,
|
|
306
337
|
)
|
|
307
|
-
|
|
338
|
+
next_priority = 1
|
|
339
|
+
response = await self._send_frame(
|
|
340
|
+
cmd, priority=next_priority
|
|
341
|
+
)
|
|
342
|
+
next_priority = 1
|
|
308
343
|
finally:
|
|
309
344
|
seq.close()
|
|
310
345
|
|
|
346
|
+
async def run_sequence(
|
|
347
|
+
self,
|
|
348
|
+
seq: Generator[
|
|
349
|
+
command.Command, # Sequences yield commands to send
|
|
350
|
+
command.Response, # Sequences get sent the response from the previous command
|
|
351
|
+
Any, # The return type depends specifically on the sequence
|
|
352
|
+
],
|
|
353
|
+
progress: Optional[Callable[[str | sequences.progress], None]] = None,
|
|
354
|
+
) -> Any:
|
|
355
|
+
"""
|
|
356
|
+
Deprecated alias for `send()`, which now accepts both commands and
|
|
357
|
+
sequences. Kept for backwards compatibility.
|
|
358
|
+
"""
|
|
359
|
+
warnings.warn(
|
|
360
|
+
"run_sequence() is deprecated; use send() instead",
|
|
361
|
+
DeprecationWarning,
|
|
362
|
+
stacklevel=2,
|
|
363
|
+
)
|
|
364
|
+
return await self.send(seq, progress=progress)
|
|
365
|
+
|
|
311
366
|
|
|
312
367
|
def drivers_map() -> dict[str, type[DriverSerialBase]]:
|
|
313
368
|
"""
|
|
@@ -548,13 +603,16 @@ class DriverLubaRs232(DriverSerialBase):
|
|
|
548
603
|
def _insert_checksum(in_ints: list[int]) -> None:
|
|
549
604
|
in_ints[-1] = reduce(xor, in_ints[1:-1])
|
|
550
605
|
|
|
551
|
-
async def send_dali_command(
|
|
606
|
+
async def send_dali_command(
|
|
607
|
+
self, tx: command.Command, priority: int = 1
|
|
608
|
+
) -> None:
|
|
552
609
|
"""
|
|
553
610
|
Sends a variable length DALI command (16 or 24 bits), waiting
|
|
554
611
|
until the LUBA device confirms it has sent the message before
|
|
555
612
|
returning the frame ID.
|
|
556
613
|
|
|
557
614
|
:param tx: A single DALI command to send
|
|
615
|
+
:param priority: IEC 62386-101 Table 22 transmit priority (1..5)
|
|
558
616
|
"""
|
|
559
617
|
# Make sure the serial interface is not in the process of reading
|
|
560
618
|
# data before we send
|
|
@@ -565,18 +623,7 @@ class DriverLubaRs232(DriverSerialBase):
|
|
|
565
623
|
raise ValueError(
|
|
566
624
|
f"Only works with 16 or 24 bit messages, not {8*len(dali_ints)}"
|
|
567
625
|
)
|
|
568
|
-
|
|
569
|
-
# high priority, others are low
|
|
570
|
-
if (
|
|
571
|
-
isinstance(tx, gear.general._StandardCommand)
|
|
572
|
-
and not tx.response
|
|
573
|
-
and not tx.sendtwice
|
|
574
|
-
) or isinstance(tx, gear.general.DAPC):
|
|
575
|
-
priority = 0b00000010 # Priority 2 (second-highest)
|
|
576
|
-
else:
|
|
577
|
-
priority = 0b00000101 # Priority 5 (lowest)
|
|
578
|
-
luba_mode = 0
|
|
579
|
-
luba_mode |= priority
|
|
626
|
+
luba_mode = priority # IEC 62386-101 Table 22 priority (1..5)
|
|
580
627
|
luba_mode |= 0b10000000 if tx.sendtwice else 0 # "Send Twice" flag
|
|
581
628
|
tx_ints = [
|
|
582
629
|
0x59, # ASCII 'Y'
|
|
@@ -1327,7 +1374,7 @@ class DriverLubaRs232(DriverSerialBase):
|
|
|
1327
1374
|
# to instance types
|
|
1328
1375
|
if scan_dev_inst:
|
|
1329
1376
|
_LOG.info("Scanning DALI bus for control devices")
|
|
1330
|
-
await self.
|
|
1377
|
+
await self.send(self.dev_inst_map.autodiscover())
|
|
1331
1378
|
_LOG.info(
|
|
1332
1379
|
f"Found {len(self.dev_inst_map.mapping)} enabled control "
|
|
1333
1380
|
"device instances"
|
|
@@ -1358,8 +1405,8 @@ class DriverLubaRs232(DriverSerialBase):
|
|
|
1358
1405
|
raise IOError("DALI driver cannot query, not connected")
|
|
1359
1406
|
return await self._protocol.send_read_status()
|
|
1360
1407
|
|
|
1361
|
-
async def
|
|
1362
|
-
self,
|
|
1408
|
+
async def _send_frame(
|
|
1409
|
+
self, cmd: command.Command, *, priority: int = 1
|
|
1363
1410
|
) -> Optional[command.Response]:
|
|
1364
1411
|
# Only send if the driver is connected
|
|
1365
1412
|
if not self.is_connected:
|
|
@@ -1368,47 +1415,39 @@ class DriverLubaRs232(DriverSerialBase):
|
|
|
1368
1415
|
|
|
1369
1416
|
response = None
|
|
1370
1417
|
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
response
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
"DALI response expected to be 'int' but got type "
|
|
1405
|
-
f"'{type(raw_rsp)}': {raw_rsp}"
|
|
1406
|
-
)
|
|
1407
|
-
raw_rsp = None
|
|
1408
|
-
continue
|
|
1409
|
-
finally:
|
|
1410
|
-
if not in_transaction:
|
|
1411
|
-
self.transaction_lock.release()
|
|
1418
|
+
# Make sure the received command buffer is empty, so that an
|
|
1419
|
+
# unexpected response can't accidentally be used
|
|
1420
|
+
self._protocol.reset_dali_response()
|
|
1421
|
+
await self._protocol.send_dali_command(cmd, priority=priority)
|
|
1422
|
+
if cmd.is_query:
|
|
1423
|
+
# Default to the command's own response type so that a query
|
|
1424
|
+
# which times out is reported as a missing value; a bare
|
|
1425
|
+
# Response(None) passes check_bad_rsp and leaks None to callers.
|
|
1426
|
+
response = cmd.response(None)
|
|
1427
|
+
while True:
|
|
1428
|
+
try:
|
|
1429
|
+
raw_rsp = await asyncio.wait_for(
|
|
1430
|
+
self._protocol.wait_dali_raw_response(),
|
|
1431
|
+
timeout=DriverLubaRs232.timeout_rx,
|
|
1432
|
+
)
|
|
1433
|
+
except asyncio.exceptions.TimeoutError:
|
|
1434
|
+
_LOG.debug(f"DALI response timeout, from message: {cmd}")
|
|
1435
|
+
break
|
|
1436
|
+
if raw_rsp is _DALI_FRAMING_ERROR:
|
|
1437
|
+
response = cmd.response(frame.BackwardFrameError(255))
|
|
1438
|
+
_LOG.debug(f"DALI framing error, from message: {cmd}")
|
|
1439
|
+
break
|
|
1440
|
+
if isinstance(raw_rsp, int):
|
|
1441
|
+
response = cmd.response(frame.BackwardFrame(raw_rsp))
|
|
1442
|
+
_LOG.debug(f"DALI response received: {raw_rsp}")
|
|
1443
|
+
break
|
|
1444
|
+
else:
|
|
1445
|
+
_LOG.warning(
|
|
1446
|
+
"DALI response expected to be 'int' but got type "
|
|
1447
|
+
f"'{type(raw_rsp)}': {raw_rsp}"
|
|
1448
|
+
)
|
|
1449
|
+
raw_rsp = None
|
|
1450
|
+
continue
|
|
1412
1451
|
|
|
1413
1452
|
return response
|
|
1414
1453
|
|
|
@@ -1944,15 +1983,17 @@ class DriverSCIRS232(DriverSerialBase):
|
|
|
1944
1983
|
# to instance types
|
|
1945
1984
|
if scan_dev_inst:
|
|
1946
1985
|
_LOG.info("Scanning DALI bus for control devices")
|
|
1947
|
-
await self.
|
|
1986
|
+
await self.send(self.dev_inst_map.autodiscover())
|
|
1948
1987
|
_LOG.info(
|
|
1949
1988
|
f"Found {len(self.dev_inst_map.mapping)} enabled control "
|
|
1950
1989
|
"device instances"
|
|
1951
1990
|
)
|
|
1952
1991
|
|
|
1953
|
-
async def
|
|
1954
|
-
self,
|
|
1992
|
+
async def _send_frame(
|
|
1993
|
+
self, cmd: command.Command, *, priority: int = 1
|
|
1955
1994
|
) -> Optional[command.Response]:
|
|
1995
|
+
# The SCI RS232 protocol has no transmit priority field, so `priority`
|
|
1996
|
+
# is accepted for a uniform driver API but not used on the wire.
|
|
1956
1997
|
# Only send if the driver is connected
|
|
1957
1998
|
if not self.is_connected:
|
|
1958
1999
|
_LOG.critical(f"DALI driver cannot send, not connected: {self}")
|
|
@@ -1960,44 +2001,36 @@ class DriverSCIRS232(DriverSerialBase):
|
|
|
1960
2001
|
|
|
1961
2002
|
response = None
|
|
1962
2003
|
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
response
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
"DALI response expected to be 'int' but got type "
|
|
1994
|
-
f"'{type(raw_rsp)}': {raw_rsp}"
|
|
1995
|
-
)
|
|
1996
|
-
raw_rsp = None
|
|
1997
|
-
continue
|
|
1998
|
-
finally:
|
|
1999
|
-
if not in_transaction:
|
|
2000
|
-
self.transaction_lock.release()
|
|
2004
|
+
# Make sure the received command buffer is empty, so that an
|
|
2005
|
+
# unexpected response can't accidentally be used
|
|
2006
|
+
self._protocol.reset_dali_response()
|
|
2007
|
+
await self._protocol.send_dali_command(cmd)
|
|
2008
|
+
if cmd.is_query:
|
|
2009
|
+
# Default to the command's own response type so that a query
|
|
2010
|
+
# which times out is reported as a missing value; a bare
|
|
2011
|
+
# Response(None) passes check_bad_rsp and leaks None to callers.
|
|
2012
|
+
response = cmd.response(None)
|
|
2013
|
+
while True:
|
|
2014
|
+
try:
|
|
2015
|
+
raw_rsp = await asyncio.wait_for(
|
|
2016
|
+
self._protocol._queue_rx_raw_dali.get(),
|
|
2017
|
+
#self._protocol.wait_dali_raw_response(),
|
|
2018
|
+
timeout=DriverSCIRS232.timeout_rx,
|
|
2019
|
+
)
|
|
2020
|
+
except asyncio.exceptions.TimeoutError:
|
|
2021
|
+
_LOG.debug(f"DALI response timeout, from message: {cmd}")
|
|
2022
|
+
break
|
|
2023
|
+
if isinstance(raw_rsp, int):
|
|
2024
|
+
response = cmd.response(frame.BackwardFrame(raw_rsp))
|
|
2025
|
+
_LOG.debug(f"DALI response received: {raw_rsp}")
|
|
2026
|
+
break
|
|
2027
|
+
else:
|
|
2028
|
+
_LOG.warning(
|
|
2029
|
+
"DALI response expected to be 'int' but got type "
|
|
2030
|
+
f"'{type(raw_rsp)}': {raw_rsp}"
|
|
2031
|
+
)
|
|
2032
|
+
raw_rsp = None
|
|
2033
|
+
continue
|
|
2001
2034
|
|
|
2002
2035
|
return response
|
|
2003
2036
|
|
|
@@ -183,6 +183,10 @@ class DriverSerialDummy(DriverSerialBase):
|
|
|
183
183
|
super().__init__(uri=uri, dev_inst_map=dev_inst_map)
|
|
184
184
|
uri = self.uri
|
|
185
185
|
|
|
186
|
+
# Record of every frame sent, as (command, priority) tuples, so tests
|
|
187
|
+
# can assert what was transmitted and at which priority.
|
|
188
|
+
self.sent_frames: list[tuple[command.Command, int]] = []
|
|
189
|
+
|
|
186
190
|
self._log_path = uri.path
|
|
187
191
|
query = parse_qs(uri.query, strict_parsing=True)
|
|
188
192
|
self._num_leds = 0
|
|
@@ -289,75 +293,62 @@ class DriverSerialDummy(DriverSerialBase):
|
|
|
289
293
|
# to instance types
|
|
290
294
|
if scan_dev_inst:
|
|
291
295
|
_LOG.info("Scanning DALI bus for control devices")
|
|
292
|
-
await self.
|
|
296
|
+
await self.send(self.dev_inst_map.autodiscover())
|
|
293
297
|
_LOG.info(f"Found {len(self.dev_inst_map.mapping)} instances")
|
|
294
298
|
|
|
295
|
-
async def
|
|
296
|
-
self,
|
|
299
|
+
async def _send_frame(
|
|
300
|
+
self, cmd: command.Command, *, priority: int = 1
|
|
297
301
|
) -> Optional[command.Response]:
|
|
298
302
|
"""
|
|
299
|
-
The '
|
|
300
|
-
communicate with anything external - it just tries to imitate
|
|
301
|
-
at a very basic level. For now, all that is implemented is:
|
|
303
|
+
The '_send_frame()' method for the DriverSerialDummy class doesn't
|
|
304
|
+
actually communicate with anything external - it just tries to imitate
|
|
305
|
+
hardware, at a very basic level. For now, all that is implemented is:
|
|
302
306
|
* Short addressing (i.e. referring to a dummy directly by its address)
|
|
303
307
|
* Broadcast addressing
|
|
304
308
|
* Brightness/Power Level (DAPC, RecallMaxLevel, RecallMinLevel, Off)
|
|
305
309
|
* Device Type
|
|
306
310
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
311
|
+
Every transmitted frame is appended to `self.sent_frames` as a
|
|
312
|
+
`(command, priority)` tuple, so tests can assert what was sent and at
|
|
313
|
+
which IEC 62386-101 Table 22 priority.
|
|
314
|
+
|
|
315
|
+
:param cmd: The DALI message to send
|
|
316
|
+
:param priority: The transmit priority (1..5) chosen by `send()`
|
|
312
317
|
:return: If a command being sent expects a response, and the type is
|
|
313
|
-
supported by DriverSerialDummy, then it will be returned
|
|
314
|
-
|
|
315
|
-
response will be returned. Otherwise, None.
|
|
318
|
+
supported by DriverSerialDummy, then it will be returned. Otherwise,
|
|
319
|
+
None.
|
|
316
320
|
"""
|
|
317
321
|
# Only send if the driver is connected
|
|
318
322
|
if not self.is_connected:
|
|
319
323
|
_LOG.critical(f"Cannot send, driver is not connected: {self}")
|
|
320
324
|
raise IOError("Cannot send, driver is not connected")
|
|
321
325
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
_LOG.info(
|
|
349
|
-
f"Scheduling dummy call to turn off A{dst}"
|
|
350
|
-
)
|
|
351
|
-
asyncio.get_running_loop().call_later(
|
|
352
|
-
delay=10.0,
|
|
353
|
-
callback=lambda: self._set_level_off(
|
|
354
|
-
addr=dst
|
|
355
|
-
),
|
|
356
|
-
)
|
|
357
|
-
|
|
358
|
-
finally:
|
|
359
|
-
if not in_transaction:
|
|
360
|
-
self.transaction_lock.release()
|
|
326
|
+
self.sent_frames.append((cmd, priority))
|
|
327
|
+
|
|
328
|
+
with open(self._log_path, mode="at", encoding="utf-8") as log_file:
|
|
329
|
+
# Write the message to the log file, emulating sending it over
|
|
330
|
+
# a DALI bus
|
|
331
|
+
log_file.write(f"{self._get_date()} {cmd}\n")
|
|
332
|
+
# Because writing to a log file is much faster than writing
|
|
333
|
+
# over RS232, there is a short delay here to make the dummy act
|
|
334
|
+
# a bit more like a real DALI device
|
|
335
|
+
await asyncio.sleep(DriverSerialDummy.hardware_delay)
|
|
336
|
+
# Get the response from the dummy bus
|
|
337
|
+
response = self._dummy_bus.send(cmd)
|
|
338
|
+
|
|
339
|
+
# Special handling for a Lunatone Jalousie dummy device
|
|
340
|
+
if hasattr(cmd, "destination"):
|
|
341
|
+
if hasattr(cmd.destination, "address"):
|
|
342
|
+
if isinstance(cmd, gear.general.GoToScene):
|
|
343
|
+
dst = cmd.destination.address
|
|
344
|
+
if dst in self._cover_addrs:
|
|
345
|
+
_LOG.info(
|
|
346
|
+
f"Scheduling dummy call to turn off A{dst}"
|
|
347
|
+
)
|
|
348
|
+
asyncio.get_running_loop().call_later(
|
|
349
|
+
delay=10.0,
|
|
350
|
+
callback=lambda: self._set_level_off(addr=dst),
|
|
351
|
+
)
|
|
361
352
|
|
|
362
353
|
return response
|
|
363
354
|
|
|
@@ -26,6 +26,7 @@ import pytest
|
|
|
26
26
|
from dali.driver.serial import DriverSerialBase, DriverLubaRs232, DriverSCIRS232, drivers_map
|
|
27
27
|
from dali.tests.fakes_serial import DriverSerialDummy
|
|
28
28
|
from dali import address, gear
|
|
29
|
+
from dali.gear.colour import QueryColourStatus
|
|
29
30
|
from dali.sequences import QueryDeviceTypes
|
|
30
31
|
|
|
31
32
|
|
|
@@ -171,15 +172,106 @@ async def test_dummy_write_3(dummy_driver):
|
|
|
171
172
|
assert "GoToScene(<address (control gear) 1>,11)" in dummy_driver.log.read()
|
|
172
173
|
|
|
173
174
|
|
|
175
|
+
@pytest.mark.asyncio
|
|
176
|
+
async def test_dummy_send_auto_prefixes_enable_device_type(dummy_driver):
|
|
177
|
+
"""
|
|
178
|
+
A bare command with a non-zero device type is sent with an
|
|
179
|
+
EnableDeviceType frame immediately before it, so callers no longer need a
|
|
180
|
+
trivial wrapper sequence to do the prefixing.
|
|
181
|
+
"""
|
|
182
|
+
await dummy_driver.driver.connect()
|
|
183
|
+
dummy_driver.driver.sent_frames.clear()
|
|
184
|
+
await dummy_driver.driver.send(QueryColourStatus(address.GearShort(0)))
|
|
185
|
+
|
|
186
|
+
sent = dummy_driver.driver.sent_frames
|
|
187
|
+
assert len(sent) == 2
|
|
188
|
+
assert isinstance(sent[0][0], gear.general.EnableDeviceType)
|
|
189
|
+
assert sent[0][0].param == 8
|
|
190
|
+
assert isinstance(sent[1][0], QueryColourStatus)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@pytest.mark.asyncio
|
|
194
|
+
async def test_dummy_send_no_prefix_for_general_command(dummy_driver):
|
|
195
|
+
"""
|
|
196
|
+
A device-type-0 command is sent on its own, without any EnableDeviceType.
|
|
197
|
+
"""
|
|
198
|
+
await dummy_driver.driver.connect()
|
|
199
|
+
dummy_driver.driver.sent_frames.clear()
|
|
200
|
+
await dummy_driver.driver.send(gear.general.DAPC(address.GearShort(1), 254))
|
|
201
|
+
|
|
202
|
+
sent = dummy_driver.driver.sent_frames
|
|
203
|
+
assert len(sent) == 1
|
|
204
|
+
assert isinstance(sent[0][0], gear.general.DAPC)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@pytest.mark.parametrize("bad_priority", [0, 1, 6, -1, "5"])
|
|
208
|
+
@pytest.mark.asyncio
|
|
209
|
+
async def test_dummy_send_rejects_invalid_priority(dummy_driver, bad_priority):
|
|
210
|
+
"""
|
|
211
|
+
Only IEC 62386-101 Table 22 priorities 2..5 are accepted.
|
|
212
|
+
"""
|
|
213
|
+
await dummy_driver.driver.connect()
|
|
214
|
+
with pytest.raises(ValueError):
|
|
215
|
+
await dummy_driver.driver.send(
|
|
216
|
+
gear.general.DAPC(address.GearShort(1), 254),
|
|
217
|
+
priority=bad_priority,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@pytest.mark.parametrize("priority", [2, 3, 4, 5])
|
|
222
|
+
@pytest.mark.asyncio
|
|
223
|
+
async def test_dummy_send_valid_priority_accepted(dummy_driver, priority):
|
|
224
|
+
"""
|
|
225
|
+
A general command is transmitted at exactly the requested priority.
|
|
226
|
+
"""
|
|
227
|
+
await dummy_driver.driver.connect()
|
|
228
|
+
dummy_driver.driver.sent_frames.clear()
|
|
229
|
+
await dummy_driver.driver.send(
|
|
230
|
+
gear.general.DAPC(address.GearShort(1), 254), priority=priority
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
sent = dummy_driver.driver.sent_frames
|
|
234
|
+
assert len(sent) == 1
|
|
235
|
+
assert sent[0][1] == priority
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@pytest.mark.asyncio
|
|
239
|
+
async def test_dummy_send_transaction_priority(dummy_driver):
|
|
240
|
+
"""
|
|
241
|
+
The first frame of a transaction carries the caller's priority and every
|
|
242
|
+
subsequent frame drops to priority 1 (IEC 62386-101 §9.3). The
|
|
243
|
+
auto-prefixed EnableDeviceType is the first frame, so it takes the caller's
|
|
244
|
+
priority and the command itself follows at priority 1.
|
|
245
|
+
"""
|
|
246
|
+
await dummy_driver.driver.connect()
|
|
247
|
+
dummy_driver.driver.sent_frames.clear()
|
|
248
|
+
await dummy_driver.driver.send(
|
|
249
|
+
QueryColourStatus(address.GearShort(0)), priority=3
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
priorities = [priority for _, priority in dummy_driver.driver.sent_frames]
|
|
253
|
+
assert priorities == [3, 1]
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@pytest.mark.asyncio
|
|
257
|
+
async def test_dummy_run_sequence_alias_warns(dummy_driver):
|
|
258
|
+
"""
|
|
259
|
+
The deprecated run_sequence alias still works but warns.
|
|
260
|
+
"""
|
|
261
|
+
await dummy_driver.driver.connect()
|
|
262
|
+
with pytest.warns(DeprecationWarning):
|
|
263
|
+
await dummy_driver.driver.run_sequence(
|
|
264
|
+
QueryDeviceTypes(address.GearShort(0))
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
|
|
174
268
|
@pytest.mark.asyncio
|
|
175
269
|
async def test_dummy_sequence_device_types(dummy_driver):
|
|
176
270
|
await dummy_driver.driver.connect()
|
|
177
271
|
# The default of dummy_driver has 12 addressed control gears
|
|
178
272
|
for ad in range(11):
|
|
179
273
|
short = address.GearShort(ad)
|
|
180
|
-
dev_types = await dummy_driver.driver.
|
|
181
|
-
QueryDeviceTypes(short)
|
|
182
|
-
)
|
|
274
|
+
dev_types = await dummy_driver.driver.send(QueryDeviceTypes(short))
|
|
183
275
|
# Colour Temperature LEDs, DT8, are created first by the dummy driver
|
|
184
276
|
if ad in range(0, 5):
|
|
185
277
|
assert len(dev_types) == 1
|
|
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
|
|
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
|