Control Lutron GRAFIK Eye QS with DMX
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

172 lines
7.0 KiB

5 years ago
  1. # lutron-dmx-control
  2. #
  3. # Copyright (c) 2019, Mr. Gecko's Media (James Coleman)
  4. # All rights reserved.
  5. #
  6. # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
  7. #
  8. # 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
  9. #
  10. # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
  11. # disclaimer in the documentation and/or other materials provided with the distribution.
  12. #
  13. # 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products
  14. # derived from this software without specific prior written permission.
  15. #
  16. # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
  17. # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  18. # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  19. # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  20. # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
  21. # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
  22. # ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  23. #
  24. from ola.ClientWrapper import ClientWrapper
  25. import serial
  26. import io
  27. import _thread
  28. import threading
  29. import time
  30. # Documentation
  31. # This program is designed to use the Open Lighting Arcretechture (OLA) to receive a DMX signal
  32. # and translate to commands to control the 6 dimiable zones on the Lutron GRAFIK Eye QS Control panel
  33. # through the use of a QSE-CI-NWK-E. This program uses the serial port for reliability.
  34. # Configuration
  35. # Serial port device to use to communicate with Lutron's QSE NWK.
  36. QSE_NWK_DEVICE = "/dev/ttyUSB0"
  37. # Set baud rate on Lutron's QSE NWK.
  38. QSE_NWK_BAUD = 115200
  39. # DMX Universe in OLA that is used.
  40. DMX_UNIVERSE = 3
  41. # The starting address.
  42. DMX_START_ADDRESS = 0
  43. #Verbosity
  44. VERBOSE=1
  45. # Variables used at run time, do not adjust.
  46. serialSession = None
  47. currentValues = [0,0,0,0,0,0]
  48. sendAllDataThisTime = True
  49. controlDisabled = False
  50. # To prevent data from overlapping which is known to crash the QSE NWK, we implement a thread lock which must be released before being obtained.
  51. dataLock = threading.Lock()
  52. # This fucnction translates the 0-255 signal from DMX to 0.00 to 100.00 signal used by Lutron,
  53. # and it sends the appropiate command to the QSE NWK to change the brightness level of a zone.
  54. def SetZone(zone, value):
  55. global serialSession, currentValues, sendAllDataThisTime, controlDisabled
  56. # We only want to translate a level of it has not already been sent to the zone,
  57. # or if we want to send all data this time. However we do not want to send the level
  58. # if the controls has been disabled by the designated button on the control panel.
  59. if (currentValues[zone-1]==value and not sendAllDataThisTime) or controlDisabled:
  60. return
  61. # Update the array of current values.
  62. currentValues[zone-1] = value
  63. # Translate to the command.
  64. command = "#DEVICE,1,%d,14,%.2f,00:00" % (zone,round((value/255.00)*100,2))
  65. if VERBOSE>=1:
  66. print(command)
  67. # Send to the QSE NWK.
  68. serialSession.write(bytes(command+"\n\r", 'utf-8'))
  69. def NewData(data):
  70. global sendAllDataThisTime, dataLock
  71. # Acquire the lock for the thread to prevent data from overlapping.
  72. dataLock.acquire()
  73. if VERBOSE>=2:
  74. print(data)
  75. # Send the new levels to each zone via the QSE NWK.
  76. SetZone(1,data[DMX_START_ADDRESS+0])
  77. SetZone(2,data[DMX_START_ADDRESS+1])
  78. SetZone(3,data[DMX_START_ADDRESS+2])
  79. SetZone(4,data[DMX_START_ADDRESS+3])
  80. SetZone(5,data[DMX_START_ADDRESS+4])
  81. SetZone(6,data[DMX_START_ADDRESS+5])
  82. # Reset the flag of send all data to false as we would have sent all data this time.
  83. sendAllDataThisTime = False
  84. # Allow the next command call to follow through by releasing the lock.
  85. dataLock.release()
  86. # This function reads the serial data from the QSE NWK line by line and performs a few functions based on response.
  87. def QSE_Read():
  88. global serialSession, controlDisabled
  89. # Creates a bufferred reader for the serial input.
  90. sio = io.TextIOWrapper(io.BufferedReader(serialSession))
  91. # We want to run this forever as we are in a thread.
  92. while True:
  93. # Gets the next available line from the QSE NWK and filter out the QSE prompt and any new line characters.
  94. line = sio.readline().replace("QSE>","").rstrip()
  95. if line=="":
  96. continue
  97. # If the command not found error is returned, this is either due to
  98. # the Lutron GRAFIK Eye QS Control panel not being assigned an integration ID of 1,
  99. # or due to a bug which needs the QSE NWK rebooted to fix. We attempt to reboot
  100. # to attempt to automatically fix the bug.
  101. if line=="~ERROR,6":
  102. if VERBOSE>=2:
  103. print("Error occurred, rebooting QSE NWK.")
  104. # Send the reboot command.
  105. serialSession.write(bytes("#RESET,0\n\r", 'utf-8'))
  106. # If the all zone up button is pressed, we disable control from the program to allow someone to manually control zones.
  107. if line=="~DEVICE,1,74,3":
  108. if VERBOSE>=2:
  109. print("Received disable signal.")
  110. controlDisabled = True
  111. # If the all zone down button is pressed, we re-enable the programs control of the zones.
  112. if line=="~DEVICE,1,75,3":
  113. if VERBOSE>=2:
  114. print("Received enable signal.")
  115. controlDisabled = False
  116. sendAllDataThisTime = True
  117. if VERBOSE>=1:
  118. print(line)
  119. # Reset the send all data flag every 10 seconds to ensure all zones have the correct value set.
  120. def sendAllDataReset():
  121. global sendAllDataThisTime
  122. while True:
  123. time.sleep(10)
  124. if VERBOSE>=3:
  125. print("Resetting flag to send all data")
  126. sendAllDataThisTime = True
  127. # Connect to the QSE NWK by using the serial port.
  128. print("Connecting to QSE NWK at: "+QSE_NWK_DEVICE)
  129. with serial.Serial(QSE_NWK_DEVICE, QSE_NWK_BAUD, timeout=2) as ser:
  130. serialSession = ser
  131. # If the serial session is still set to None, we did not correctly connect.
  132. if serialSession == None:
  133. print("Failed to connect.")
  134. exit(1)
  135. # We connected, so we can open the device.
  136. serialSession.open()
  137. # Now that we are ready to roll, we start the read thread.
  138. _thread.start_new_thread(QSE_Read, ())
  139. # Start the reset send all data thread.
  140. _thread.start_new_thread(sendAllDataReset, ())
  141. # Connect to the DMX universe with the OLA wrapper.
  142. wrapper = ClientWrapper()
  143. client = wrapper.Client()
  144. client.RegisterUniverse(DMX_UNIVERSE, client.REGISTER, NewData)
  145. wrapper.Run()