Guest User

Untitled

a guest
Jan 18th, 2017
214
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 47.38 KB | None | 0 0
  1. #!/usr/bin/env python
  2.  
  3. """
  4. This is a utility for controlling stand-alone Flux WiFi LED light bulbs.
  5. The protocol was reverse-engineered by studying packet captures between a
  6. bulb and the controlling "Magic Home" mobile app.  The code here dealing
  7. with the network protocol is littered with magic numbers, and ain't so pretty.
  8. But it does seem to work!
  9.  
  10. So far most of the functionality of the apps is available here via the CLI
  11. and/or programmatically.
  12.  
  13. The classes in this project could very easily be used as an API, and incorporated into a GUI app written
  14. in PyQt, Kivy, or some other framework.
  15.  
  16. ##### Available:
  17. * Discovering bulbs on LAN
  18. * Turning on/off bulb
  19. * Get state information
  20. * Setting "warm white" mode
  21. * Setting single color mode
  22. * Setting preset pattern mode
  23. * Setting custom pattern mode
  24. * Reading timers
  25. * Setting timers
  26.  
  27. ##### Some missing pieces:
  28. * Initial administration to set up WiFi SSID and passphrase/key.
  29. * Remote access administration
  30. * Music-relating pulsing. This feature isn't so impressive on the Magic Home app,
  31. and looks like it might be a bit of work.
  32.  
  33. ##### Cool feature:
  34. * Specify colors with names or web hex values.  Requires that python "webcolors"
  35. package is installed.  (Easily done via pip, easy_install, or apt-get, etc.)
  36. See the following for valid color names: http://www.w3schools.com/html/html_colornames.asp
  37.  
  38. """
  39. from __future__ import print_function
  40. import socket
  41. import time
  42. import sys
  43. import datetime
  44. import colorsys
  45. from optparse import OptionParser,OptionGroup
  46. import ast
  47. import threading
  48.  
  49. try:
  50.     import webcolors
  51.     webcolors_available = True
  52. except:
  53.     webcolors_available = False
  54.  
  55. class utils:
  56.     @staticmethod
  57.     def color_object_to_tuple(color):
  58.         global webcolors_available
  59.  
  60.         # see if it's already a color tuple
  61.         if type(color) is tuple and len(color) == 3:
  62.             return color
  63.  
  64.         # can't convert non-string
  65.         if type(color) is not str:
  66.             return None
  67.         color = color.strip()
  68.  
  69.         if webcolors_available:
  70.             # try to convert from an english name
  71.             try:
  72.                 return webcolors.name_to_rgb(color)
  73.             except ValueError:
  74.                 pass
  75.             except:
  76.                 pass
  77.  
  78.             # try to convert an web hex code
  79.             try:
  80.                 return webcolors.hex_to_rgb(webcolors.normalize_hex(color))
  81.             except ValueError:
  82.                 pass
  83.             except:
  84.                 pass
  85.  
  86.         # try to convert a string RGB tuple
  87.         try:
  88.             val = ast.literal_eval(color)
  89.             if type(val) is not tuple or len(val) != 3:
  90.                 raise Exception
  91.             return val
  92.         except:
  93.             pass
  94.         return None
  95.  
  96.     @staticmethod
  97.     def color_tuple_to_string(rgb):
  98.         # try to convert to an english name
  99.         try:
  100.             return webcolors.rgb_to_name(rgb)
  101.         except Exception:
  102.             #print e
  103.             pass
  104.         return str(rgb)
  105.  
  106.     @staticmethod
  107.     def get_color_names_list():
  108.         names = set()
  109.         for key in webcolors.css2_hex_to_names.keys():
  110.             names.add(webcolors.css2_hex_to_names[key])
  111.         for key in webcolors.css21_hex_to_names.keys():
  112.             names.add(webcolors.css21_hex_to_names[key])
  113.         for key in webcolors.css3_hex_to_names.keys():
  114.             names.add(webcolors.css3_hex_to_names[key])
  115.         for key in webcolors.html4_hex_to_names.keys():
  116.             names.add(webcolors.html4_hex_to_names[key])
  117.         return sorted(names)
  118.  
  119.     @staticmethod
  120.     def date_has_passed(dt):
  121.         delta = dt - datetime.datetime.now()
  122.         return delta.total_seconds() < 0
  123.  
  124.     @staticmethod
  125.     def dump_bytes(bytes):
  126.         print(''.join('{:02x} '.format(x) for x in bytearray(bytes)))
  127.  
  128.     max_delay = 0x1f
  129.  
  130.     @staticmethod
  131.     def delayToSpeed(delay):
  132.         # speed is 0-100, delay is 1-31
  133.         # 1st translate delay to 0-30
  134.         delay = delay -1
  135.         if delay > utils.max_delay - 1 :
  136.             delay = utils.max_delay - 1
  137.         if delay < 0:
  138.             delay = 0
  139.         inv_speed = int((delay * 100)/(utils.max_delay - 1))
  140.         speed =  100-inv_speed
  141.         return speed
  142.  
  143.     @staticmethod
  144.     def speedToDelay(speed):
  145.         # speed is 0-100, delay is 1-31
  146.         if speed > 100:
  147.             speed = 100
  148.         if speed < 0:
  149.             speed = 0
  150.         inv_speed = 100-speed
  151.         delay = int((inv_speed * (utils.max_delay-1))/100)
  152.         # translate from 0-30 to 1-31
  153.         delay = delay + 1
  154.         return delay
  155.  
  156.     @staticmethod
  157.     def byteToPercent(byte):
  158.         if byte > 255:
  159.             byte = 255
  160.         if byte < 0:
  161.             byte = 0
  162.         return int((byte * 100)/255)
  163.  
  164.     @staticmethod
  165.     def percentToByte(percent):
  166.         if percent > 100:
  167.             percent = 100
  168.         if percent < 0:
  169.             percent = 0
  170.         return int((percent * 255)/100)
  171.  
  172. class PresetPattern:
  173.     seven_color_cross_fade =   0x25
  174.     red_gradual_change =       0x26
  175.     green_gradual_change =     0x27
  176.     blue_gradual_change =      0x28
  177.     yellow_gradual_change =    0x29
  178.     cyan_gradual_change =      0x2a
  179.     purple_gradual_change =    0x2b
  180.     white_gradual_change =     0x2c
  181.     red_green_cross_fade =     0x2d
  182.     red_blue_cross_fade =      0x2e
  183.     green_blue_cross_fade =    0x2f
  184.     seven_color_strobe_flash = 0x30
  185.     red_strobe_flash =         0x31
  186.     green_strobe_flash =       0x32
  187.     blue_stobe_flash =         0x33
  188.     yellow_strobe_flash =      0x34
  189.     cyan_strobe_flash =        0x35
  190.     purple_strobe_flash =      0x36
  191.     white_strobe_flash =       0x37
  192.     seven_color_jumping =      0x38
  193.  
  194.     @staticmethod
  195.     def valid(pattern):
  196.         if pattern < 0x25 or pattern > 0x38:
  197.             return False
  198.         return True
  199.  
  200.     @staticmethod
  201.     def valtostr(pattern):
  202.         for key, value in PresetPattern.__dict__.iteritems():
  203.             if type(value) is int and value == pattern:
  204.                 return key.replace("_", " ").title()
  205.         return None
  206.  
  207.  
  208.  
  209. class LedTimer():
  210.     Mo = 0x02
  211.     Tu = 0x04
  212.     We = 0x08
  213.     Th = 0x10
  214.     Fr = 0x20
  215.     Sa = 0x40
  216.     Su = 0x80
  217.     Everyday = Mo|Tu|We|Th|Fr|Sa|Su
  218.     Weekdays = Mo|Tu|We|Th|Fr
  219.     Weekend = Sa|Su
  220.  
  221.     @staticmethod
  222.     def dayMaskToStr(mask):
  223.         for key, value in LedTimer.__dict__.iteritems():
  224.             if type(value) is int and value == mask:
  225.                 return key
  226.         return None
  227.  
  228.     def __init__(self, bytes=None):
  229.         if bytes is not None:
  230.             self.fromBytes(bytes)
  231.             return
  232.  
  233.         the_time = datetime.datetime.now() + datetime.timedelta(hours=1)
  234.         self.setTime(the_time.hour, the_time.minute)
  235.         self.setDate(the_time.year, the_time.month, the_time.day)
  236.         self.setModeTurnOff()
  237.         self.setActive(False)
  238.  
  239.     def setActive(self, active=True):
  240.         self.active = active
  241.  
  242.     def isActive(self):
  243.         return self.active
  244.  
  245.     def isExpired(self):
  246.         # if no repeat mask and datetime is in past, return True
  247.         if self.repeat_mask != 0:
  248.             return False
  249.         elif self.year!=0 and self.month!=0 and self.day!=0:
  250.             dt = datetime.datetime(self.year, self.month, self.day, self.hour, self.minute)
  251.             if  utils.date_has_passed(dt):
  252.                 return True
  253.         return False
  254.  
  255.     def setTime(self, hour, minute):
  256.         self.hour = hour
  257.         self.minute = minute
  258.  
  259.     def setDate(self, year, month, day):
  260.         self.year = year
  261.         self.month = month
  262.         self.day = day
  263.         self.repeat_mask = 0
  264.  
  265.     def setRepeatMask(self, repeat_mask):
  266.         self.year = 0
  267.         self.month = 0
  268.         self.day = 0
  269.         self.repeat_mask = repeat_mask
  270.  
  271.     def setModeDefault(self):
  272.         self.mode = "default"
  273.         self.pattern_code = 0
  274.         self.turn_on = True
  275.         self.red = 0
  276.         self.green = 0
  277.         self.blue = 0
  278.         self.warmth_level = 0
  279.  
  280.     def setModePresetPattern(self, pattern, speed):
  281.         self.mode = "preset"
  282.         self.warmth_level = 0
  283.         self.pattern_code = pattern
  284.         self.delay = utils.speedToDelay(speed)
  285.         self.turn_on = True
  286.  
  287.     def setModeColor(self, r, g, b):
  288.         self.mode = "color"
  289.         self.warmth_level = 0
  290.         self.red = r
  291.         self.green = g
  292.         self.blue = b
  293.         self.pattern_code = 0x61
  294.         self.turn_on = True
  295.  
  296.     def setModeWarmWhite(self, level):
  297.         self.mode = "ww"
  298.         self.warmth_level = utils.percentToByte(level)
  299.         self.pattern_code = 0x61
  300.         self.red = 0
  301.         self.green = 0
  302.         self.blue = 0
  303.         self.turn_on = True
  304.  
  305.     def setModeTurnOff(self):
  306.         self.mode = "off"
  307.         self.turn_on = False
  308.         self.pattern_code = 0
  309.  
  310.     """
  311.  
  312.    timer are in six 14-byte structs
  313.        f0 0f 08 10 10 15 00 00 25 1f 00 00 00 f0 0f
  314.         0  1  2  3  4  5  6  7  8  9 10 11 12 13 14
  315.  
  316.        0: f0 when active entry/ 0f when not active
  317.        1: (0f=15) year when no repeat, else 0
  318.        2:  month when no repeat, else 0
  319.        3:  dayofmonth when no repeat, else 0
  320.        4: hour
  321.        5: min
  322.        6: 0
  323.        7: repeat mask, Mo=0x2,Tu=0x04, We 0x8, Th=0x10 Fr=0x20, Sa=0x40, Su=0x80
  324.        8:  61 for solid color or warm, or preset pattern code
  325.        9:  r (or delay for preset pattern)
  326.        10: g
  327.        11: b
  328.        12: warm white level
  329.        13: 0f = off, f0 = on ?
  330.    """
  331.     def fromBytes(self, bytes):
  332.         #utils.dump_bytes(bytes)
  333.         self.red = 0
  334.         self.green = 0
  335.         self.blue = 0
  336.         if bytes[0] == 0xf0:
  337.             self.active = True
  338.         else:
  339.             self.active = False
  340.         self.year = bytes[1]+2000
  341.         self.month = bytes[2]
  342.         self.day = bytes[3]
  343.         self.hour = bytes[4]
  344.         self.minute = bytes[5]
  345.         self.repeat_mask = bytes[7]
  346.         self.pattern_code = bytes[8]
  347.  
  348.         if self.pattern_code == 0x61:
  349.             self.mode = "color"
  350.             self.red = bytes[9]
  351.             self.green = bytes[10]
  352.             self.blue = bytes[11]
  353.         elif self.pattern_code == 0x00:
  354.             self.mode ="default"
  355.         else:
  356.             self.mode = "preset"
  357.             self.delay = bytes[9] #same byte as red
  358.  
  359.         self.warmth_level = bytes[12]
  360.         if self.warmth_level != 0:
  361.             self.mode = "ww"
  362.  
  363.         if bytes[13] == 0xf0:
  364.             self.turn_on = True
  365.         else:
  366.             self.turn_on = False
  367.             self.mode = "off"
  368.  
  369.     def toBytes(self):
  370.         bytes = bytearray(14)
  371.         if not self.active:
  372.             bytes[0] = 0x0f
  373.             # quit since all other zeros is good
  374.             return bytes
  375.  
  376.         bytes[0] = 0xf0
  377.  
  378.         if self.year >= 2000:
  379.             bytes[1] =  self.year - 2000
  380.         else:
  381.             bytes[1] = self.year
  382.         bytes[2] = self.month
  383.         bytes[3] = self.day
  384.         bytes[4] = self.hour
  385.         bytes[5] = self.minute
  386.         # what is 6?
  387.         bytes[7] = self.repeat_mask
  388.  
  389.         if not self.turn_on:
  390.             bytes[13] = 0x0f
  391.             return bytes
  392.         bytes[13] = 0xf0
  393.  
  394.         bytes[8] = self.pattern_code
  395.         if self.mode == "preset":
  396.             bytes[9] = self.delay
  397.             bytes[10] = 0
  398.             bytes[11] = 0
  399.         else:
  400.             bytes[9] = self.red
  401.             bytes[10] = self.green
  402.             bytes[11] = self.blue
  403.         bytes[12] = self.warmth_level
  404.  
  405.         return bytes
  406.  
  407.     def __str__(self):
  408.         txt = ""
  409.         if not self.active:
  410.           return "Unset"
  411.  
  412.         if self.turn_on:
  413.             txt += "[ON ]"
  414.         else:
  415.             txt += "[OFF]"
  416.  
  417.         txt += " "
  418.  
  419.         txt += "{:02}:{:02}  ".format(self.hour,self.minute)
  420.  
  421.         if self.repeat_mask == 0:
  422.             txt += "Once: {:04}-{:02}-{:02}".format(self.year,self.month,self.day)
  423.         else:
  424.             bits = [LedTimer.Su,LedTimer.Mo,LedTimer.Tu,LedTimer.We,LedTimer.Th,LedTimer.Fr,LedTimer.Sa]
  425.             for b in bits:
  426.                 if self.repeat_mask & b:
  427.                     txt += LedTimer.dayMaskToStr(b)
  428.                 else:
  429.                     txt += "--"
  430.             txt += "  "
  431.  
  432.         txt += "  "
  433.         if self.pattern_code == 0x61:
  434.             if self.warmth_level != 0:
  435.                 txt += "Warm White: {}%".format(utils.byteToPercent(self.warmth_level))
  436.             else:
  437.                 color_str = utils.color_tuple_to_string((self.red,self.green,self.blue))
  438.                 txt += "Color: {}".format(color_str)
  439.  
  440.         elif PresetPattern.valid(self.pattern_code):
  441.             pat = PresetPattern.valtostr(self.pattern_code)
  442.             speed = utils.delayToSpeed(self.delay)
  443.             txt += "{} (Speed:{}%)".format(pat, speed)
  444.  
  445.         return txt
  446.  
  447. class WifiLedBulb():
  448.     def __init__(self, ipaddr, port=5577, timeout=5):
  449.         self.ipaddr = ipaddr
  450.         self.port = port
  451.         self.timeout = timeout
  452.  
  453.         self.protocol = None
  454.  
  455.         self.raw_state = None
  456.         self._is_on = False
  457.         self._mode = None
  458.         self._rgb = [0, 0, 0]
  459.         self._warm_white = 0
  460.         self._cold_white = 0
  461.         self._brightness = 0
  462.         self._socket = None
  463.         self._lock = threading.Lock()
  464.  
  465.         self.connect(2)
  466.         self.update_state()
  467.  
  468.     @property
  469.     def is_on(self):
  470.         return self._is_on
  471.  
  472.     @property
  473.     def mode(self):
  474.         return self._mode
  475.  
  476.     @property
  477.     def rgb(self):
  478.         return self._rgb
  479.  
  480.     @property
  481.     def warm_white(self):
  482.         return self._warm_white
  483.  
  484.     @property
  485.     def cold_white(self):
  486.         return self._cold_white
  487.  
  488.     @property
  489.     def brightness(self):
  490.         return self._brightness
  491.  
  492.     def connect(self, retry=0):
  493.         self.close()
  494.         try:
  495.             self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  496.             self._socket.settimeout(self.timeout)
  497.             self._socket.connect((self.ipaddr, self.port))
  498.         except socket.error:
  499.             if retry < 1:
  500.                 return
  501.             self.connect(max(retry-1, 0))
  502.  
  503.     def close(self):
  504.         if self._socket is None:
  505.             return
  506.         try:
  507.             self._socket.close()
  508.         except socket.error:
  509.             pass
  510.  
  511.     def _determineMode(self, ww_level, pattern_code):
  512.         mode = "unknown"
  513.         if pattern_code in [ 0x61, 0x62]:
  514.             if ww_level != 0:
  515.                 mode = "ww"
  516.             else:
  517.                 mode = "color"
  518.         elif pattern_code == 0x60:
  519.             mode = "custom"
  520.         elif PresetPattern.valid(pattern_code):
  521.             mode = "preset"
  522.         return mode
  523.  
  524.     def update_state(self, retry=2):
  525.         msg = bytearray([0x81, 0x8a, 0x8b])
  526.         try:
  527.             self._send_msg(msg)
  528.             rx = self._read_msg(14)
  529.         except socket.error:
  530.             if retry < 1:
  531.                 self._is_on = False
  532.                 return
  533.             self.connect()
  534.             self.update_state(max(retry-1, 0))
  535.             return
  536.         if rx is None or len(rx) < 14:
  537.             if retry < 1:
  538.                 self._is_on = False
  539.                 return
  540.             self.update_state(max(retry-1, 0))
  541.             return
  542.  
  543.         pattern = rx[3]
  544.         ww_level = rx[9]
  545.         mode = self._determineMode(ww_level, pattern)
  546.         if mode == "unknown":
  547.             if retry < 1:
  548.                 return
  549.             self.connect()
  550.             self.update_state(max(retry-1, 0))
  551.             return
  552.         power_state = rx[2]
  553.  
  554.         if power_state == 0x23:
  555.             self._is_on = True
  556.         elif power_state == 0x24:
  557.             self._is_on = False
  558.         self.raw_state = rx
  559.         self._mode = mode
  560.  
  561.     def __str__(self):
  562.         rx = self.raw_state
  563.         mode = self.mode
  564.  
  565.         pattern = rx[3]
  566.         ww_level = rx[9]
  567.         power_state = rx[2]
  568.         power_str = "Unknown power state"
  569.  
  570.         if power_state == 0x23:
  571.             power_str = "ON "
  572.         elif power_state == 0x24:
  573.             power_str = False
  574.  
  575.         delay = rx[5]
  576.         speed = utils.delayToSpeed(delay)
  577.         if mode == "color":
  578.             red = rx[6]
  579.             green = rx[7]
  580.             blue = rx[8]
  581.             mode_str = "Color: {}".format((red, green, blue))
  582.         elif mode == "ww":
  583.             mode_str = "Warm White: {}%".format(utils.byteToPercent(ww_level))
  584.         elif mode == "preset":
  585.             pat = PresetPattern.valtostr(pattern)
  586.             mode_str = "Pattern: {} (Speed {}%)".format(pat, speed)
  587.         elif mode == "custom":
  588.             mode_str = "Custom pattern (Speed {}%)".format(speed)
  589.         else:
  590.             mode_str = "Unknown mode 0x{:x}".format(pattern)
  591.         if pattern == 0x62:
  592.             mode_str += " (tmp)"
  593.         mode_str += " raw state: "
  594.         for _r in rx:
  595.           mode_str += str(_r) + ","
  596.         return "{} [{}]".format(power_str, mode_str)
  597.  
  598.     def turnOn(self, retry=2):
  599.         self._is_on = True
  600.         msg = bytearray([0x71, 0x23, 0x0f])
  601.         try:
  602.             self._send_msg(msg)
  603.         except socket.error:
  604.             if retry:
  605.                 self.connect()
  606.                 self.turnOn(max(retry-1, 0))
  607.                 return
  608.             self._is_on = False
  609.  
  610.  
  611.     def turnOff(self, retry=2):
  612.         self._is_on = False
  613.         msg = bytearray([0x71, 0x24, 0x0f])
  614.         try:
  615.             self._send_msg(msg)
  616.         except socket.error:
  617.             if retry:
  618.                 self.connect()
  619.                 self.turnOff(max(retry-1, 0))
  620.  
  621.     def isOn(self):
  622.         return self.is_on
  623.  
  624.     def getWarmWhite255(self):
  625.         if self.mode != "ww":
  626.             return 255
  627.         return int(self.raw_state[9])
  628.  
  629.     def setWarmWhite(self, level, persist=True, retry=2):
  630.         self.setWarmWhite255(utils.percentToByte(level), persist, retry)
  631.  
  632.     def setWarmWhite255(self, level, persist=True, retry=2):
  633.         if persist:
  634.             msg = bytearray([0x31])
  635.         else:
  636.             msg = bytearray([0x41])
  637.         msg.append(0x00)
  638.         msg.append(0x00)
  639.         msg.append(0x00)
  640.         msg.append(int(level))
  641.  
  642.         if self.protocol == "LEDENET":
  643.             msg.append(int(level))
  644.  
  645.         msg.append(0x0f)
  646.         msg.append(0x0f)
  647.         try:
  648.             self._send_msg(msg)
  649.         except socket.error:
  650.             if retry:
  651.                 self.connect()
  652.                 self.setWarmWhite255(level, persist, max(retry-1, 0))
  653.  
  654.     def getRgbw(self):
  655.         if self.mode != "color":
  656.             return (255, 255, 255, 255)
  657.         red = self.raw_state[6]
  658.         green = self.raw_state[7]
  659.         blue = self.raw_state[8]
  660.         white = self.raw_state[9]
  661.         return (red, green, blue, white)
  662.  
  663.     def setRgbw(self, r,g,b,w, persist=True, brightness=None, retry=2):
  664.         if brightness != None:
  665.             (r, g, b) = self._calculateBrightness((r, g, b), brightness)
  666.         if persist:
  667.             msg = bytearray([0x31])
  668.         else:
  669.             msg = bytearray([0x41])
  670.         msg.append(int(r))
  671.         msg.append(int(g))
  672.         msg.append(int(b))
  673.         msg.append(int(w))
  674.  
  675.         if self.protocol == "LEDENET":
  676.             msg.append(0x00)
  677.  
  678.         msg.append(0x00)
  679.         msg.append(0x0f)
  680.         try:
  681.             self._send_msg(msg)
  682.         except socket.error:
  683.             if retry:
  684.                 self.connect()
  685.                 self.setRgbw(r,g,b,w, persist, max(retry-1, 0))
  686.  
  687.     def getRgb(self):
  688.         if self.mode != "color":
  689.             return (255, 255, 255)
  690.         red = self.raw_state[6]
  691.         green = self.raw_state[7]
  692.         blue = self.raw_state[8]
  693.         return (red, green, blue)
  694.  
  695.     def setRgb(self, r,g,b, persist=True, brightness=None, retry=2):
  696.         if brightness != None:
  697.             (r, g, b) = self._calculateBrightness((r, g, b), brightness)
  698.         if persist:
  699.             msg = bytearray([0x31])
  700.         else:
  701.             msg = bytearray([0x41])
  702.         msg.append(int(r))
  703.         msg.append(int(g))
  704.         msg.append(int(b))
  705.  
  706.         if self.protocol == "LEDENET":
  707.             msg.append(0x00)
  708.  
  709. #        msg.append(0x00)
  710.         msg.append(0x00)
  711.         msg.append(0x0f)
  712.         try:
  713.             self._send_msg(msg)
  714.         except socket.error:
  715.             if retry:
  716.                 self.connect()
  717.                 self.setRgb(r,g,b, persist, max(retry-1, 0))
  718.  
  719.     def _calculateBrightness(self, rgb, level):
  720.         r = rgb[0]
  721.         g = rgb[1]
  722.         b = rgb[2]
  723.         hsv = colorsys.rgb_to_hsv(r, g, b)
  724.         return colorsys.hsv_to_rgb(hsv[0], hsv[1], level)
  725.  
  726.     def _send_msg(self, bytes):
  727.         # calculate checksum of byte array and add to end
  728.         csum = sum(bytes) & 0xFF
  729.         bytes.append(csum)
  730.         with self._lock:
  731.             self._socket.send(bytes)
  732.  
  733.     def _read_msg(self, expected):
  734.         remaining = expected
  735.         rx = bytearray()
  736.         begin = time.time()
  737.         while remaining > 0:
  738.             if time.time() - begin > self.timeout:
  739.                 break
  740.             try:
  741.                 with self._lock:
  742.                     self._socket.setblocking(0)
  743.                     chunk = self._socket.recv(remaining)
  744.                     if chunk:
  745.                         begin = time.time()
  746.                     remaining -= len(chunk)
  747.                     rx.extend(chunk)
  748.             except socket.error:
  749.                 pass
  750.             finally:
  751.                 self._socket.setblocking(1)
  752.         return rx
  753.  
  754.     def getClock(self):
  755.         msg = bytearray([0x11, 0x1a, 0x1b, 0x0f])
  756.         self._send_msg(msg)
  757.         rx = self._read_msg(12)
  758.         if len(rx) != 12:
  759.             return
  760.         year =  rx[3] + 2000
  761.         month = rx[4]
  762.         date = rx[5]
  763.         hour = rx[6]
  764.         minute = rx[7]
  765.         second = rx[8]
  766.         #dayofweek = rx[9]
  767.         try:
  768.             dt = datetime.datetime(year,month,date,hour,minute,second)
  769.         except:
  770.             dt = None
  771.         return dt
  772.  
  773.     def setClock(self):
  774.         msg = bytearray([0x10, 0x14])
  775.         now = datetime.datetime.now()
  776.         msg.append(now.year-2000)
  777.         msg.append(now.month)
  778.         msg.append(now.day)
  779.         msg.append(now.hour)
  780.         msg.append(now.minute)
  781.         msg.append(now.second)
  782.         msg.append(now.isoweekday()) # day of week
  783.         msg.append(0x00)
  784.         msg.append(0x0f)
  785.         self._send_msg(msg)
  786.  
  787.     def setProtocol(self, protocol):
  788.         self.protocol = protocol.upper()
  789.  
  790.     def setPresetPattern(self, pattern, speed):
  791.  
  792.         PresetPattern.valtostr(pattern)
  793.         if not PresetPattern.valid(pattern):
  794.             #print "Pattern must be between 0x25 and 0x38"
  795.             raise Exception
  796.  
  797.         delay = utils.speedToDelay(speed)
  798.         #print "speed {}, delay 0x{:02x}".format(speed,delay)
  799.         pattern_set_msg = bytearray([0x61])
  800.         pattern_set_msg.append(pattern)
  801.         pattern_set_msg.append(delay)
  802.         pattern_set_msg.append(0x0f)
  803.  
  804.         self._send_msg(pattern_set_msg)
  805.  
  806.     def getTimers(self):
  807.         msg = bytearray([0x22, 0x2a, 0x2b, 0x0f])
  808.         self._send_msg(msg)
  809.         resp_len = 88
  810.         rx = self._read_msg(resp_len)
  811.         if len(rx) != resp_len:
  812.             print("response too short!")
  813.             raise Exception
  814.  
  815.         #utils.dump_data(rx)
  816.         start = 2
  817.         timer_list = []
  818.         #pass in the 14-byte timer structs
  819.         for i in range(6):
  820.           timer_bytes = rx[start:][:14]
  821.           timer = LedTimer(timer_bytes)
  822.           timer_list.append(timer)
  823.           start += 14
  824.  
  825.         return timer_list
  826.  
  827.     def sendTimers(self, timer_list):
  828.         # remove inactive or expired timers from list
  829.         for t in timer_list:
  830.             if not t.isActive() or t.isExpired():
  831.                 timer_list.remove(t)
  832.  
  833.         # truncate if more than 6
  834.         if len(timer_list) > 6:
  835.             print("too many timers, truncating list")
  836.             del timer_list[6:]
  837.  
  838.         # pad list to 6 with inactive timers
  839.         if len(timer_list) != 6:
  840.             for i in range(6-len(timer_list)):
  841.                 timer_list.append(LedTimer())
  842.  
  843.         msg_start = bytearray([0x21])
  844.         msg_end = bytearray([0x00, 0xf0])
  845.         msg = bytearray()
  846.  
  847.         # build message
  848.         msg.extend(msg_start)
  849.         for t in timer_list:
  850.             msg.extend(t.toBytes())
  851.         msg.extend(msg_end)
  852.         self._send_msg(msg)
  853.  
  854.         # not sure what the resp is, prob some sort of ack?
  855.         rx = self._read_msg(1)
  856.         rx = self._read_msg(3)
  857.  
  858.     def setCustomPattern(self, rgb_list, speed, transition_type):
  859.         # truncate if more than 16
  860.         if len(rgb_list) > 16:
  861.             print("too many colors, truncating list")
  862.             del rgb_list[16:]
  863.  
  864.         # quit if too few
  865.         if len(rgb_list) == 0:
  866.             print("no colors, aborting")
  867.             return
  868.  
  869.         msg = bytearray()
  870.  
  871.         first_color = True
  872.         for rgb in rgb_list:
  873.             if first_color:
  874.                 lead_byte = 0x51
  875.                 first_color = False
  876.             else:
  877.                 lead_byte = 0
  878.             r,g,b = rgb
  879.             msg.extend(bytearray([lead_byte, r,g,b]))
  880.  
  881.         # pad out empty slots
  882.         if len(rgb_list) != 16:
  883.             for i in range(16-len(rgb_list)):
  884.                 msg.extend(bytearray([0, 1, 2, 3]))
  885.  
  886.         msg.append(0x00)
  887.         msg.append(utils.speedToDelay(speed))
  888.  
  889.         if transition_type =="gradual":
  890.             msg.append(0x3a)
  891.         elif transition_type =="jump":
  892.             msg.append(0x3b)
  893.         elif transition_type =="strobe":
  894.             msg.append(0x3c)
  895.         else:
  896.             #unknown transition string: using 'gradual'
  897.             msg.append(0x3a)
  898.         msg.append(0xff)
  899.         msg.append(0x0f)
  900.  
  901.         self._send_msg(msg)
  902.  
  903.     def refreshState(self):
  904.         return self.update_state()
  905.  
  906.  
  907. class  BulbScanner():
  908.     def __init__(self):
  909.         self.found_bulbs = []
  910.  
  911.     def getBulbInfoByID(self, id):
  912.         for b in self.found_bulbs:
  913.             if b['id'] == id:
  914.                 return b
  915.         return b
  916.  
  917.     def getBulbInfo(self):
  918.         return self.found_bulbs
  919.  
  920.     def scan(self, timeout=10):
  921.  
  922.         DISCOVERY_PORT = 48899
  923.  
  924.         sock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
  925.         sock.bind(('', DISCOVERY_PORT))
  926.         sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
  927.  
  928.         msg = "HF-A11ASSISTHREAD".encode('ascii')
  929.  
  930.         # set the time at which we will quit the search
  931.         quit_time = time.time() + timeout
  932.  
  933.         response_list = []
  934.         # outer loop for query send
  935.         while True:
  936.             if time.time() > quit_time:
  937.                 break
  938.             # send out a broadcast query
  939.             sock.sendto(msg, ('<broadcast>', DISCOVERY_PORT))
  940.  
  941.             # inner loop waiting for responses
  942.             while True:
  943.  
  944.                 sock.settimeout(1)
  945.                 try:
  946.                     data, addr = sock.recvfrom(64)
  947.                 except socket.timeout:
  948.                     data = None
  949.                     if time.time() > quit_time:
  950.                         break
  951.  
  952.                 if data is None:
  953.                     continue
  954.                 if  data == msg:
  955.                     continue
  956.  
  957.                 data = data.decode('ascii')
  958.                 data_split = data.split(',')
  959.                 if len(data_split) < 3:
  960.                     continue
  961.                 item = dict()
  962.                 item['ipaddr'] = data_split[0]
  963.                 item['id'] = data_split[1]
  964.                 item['model'] = data_split[2]
  965.                 response_list.append(item)
  966.  
  967.         self.found_bulbs = response_list
  968.         return response_list
  969. #=========================================================================
  970. def showUsageExamples():
  971.     example_text = """
  972. Examples:
  973.  
  974. Scan network:
  975.    %prog% -s
  976.  
  977. Scan network and show info about all:
  978.    %prog% -sSti
  979.  
  980. Turn on:
  981.    %prog% 192.168.1.100 --on
  982.    %prog% 192.168.1.100 -192.168.1.101 -1
  983.  
  984. Turn on all bulbs on LAN:
  985.    %prog% -sS --on
  986.  
  987. Turn off:
  988.    %prog% 192.168.1.100 --off
  989.    %prog% 192.168.1.100 --0
  990.    %prog% -sS --off
  991.  
  992. Set warm white, 75%
  993.    %prog% 192.168.1.100 -w 75 -0
  994.  
  995. Set fixed color red :
  996.    %prog% 192.168.1.100 -c Red
  997.    %prog% 192.168.1.100 -c 255,0,0
  998.    %prog% 192.168.1.100 -c "#FF0000"
  999.  
  1000. Set preset pattern #35 with 40% speed:
  1001.    %prog% 192.168.1.100 -p 35 40
  1002.  
  1003. Set custom pattern 25% speed, red/green/blue, gradual change:
  1004.    %prog% 192.168.1.100 -C gradual 25 "red green (0,0,255)"
  1005.  
  1006. Sync all bulb's clocks with this computer's:
  1007.    %prog% -sS --setclock
  1008.  
  1009. Set timer #1 to turn on red at 5:30pm on weekdays:
  1010.    %prog% 192.168.1.100 -T 1 color "time:1730;repeat:12345;color:red"
  1011.  
  1012. Deactivate timer #4:
  1013.    %prog% 192.168.1.100 -T 4 inactive ""
  1014.  
  1015. Use --timerhelp for more details on setting timers
  1016.    """
  1017.  
  1018.     print(example_text.replace("%prog%",sys.argv[0]))
  1019.  
  1020. def showTimerHelp():
  1021.     timerhelp_text = """
  1022. There are 6 timers available for each bulb.
  1023.  
  1024. Mode Details:
  1025.    inactive:   timer is inactive and unused
  1026.    poweroff:   turns off the light
  1027.    default:    turns on the light in default mode
  1028.    color:      turns on the light with specified color
  1029.    preset:     turns on the light with specified preset and speed
  1030.    warmwhite:  turns on the light with warm white at specified brightness
  1031.  
  1032. Settings available for each mode:
  1033.    Timer Mode | Settings
  1034.    --------------------------------------------
  1035.    inactive:   [none]
  1036.    poweroff:   time, (repeat | date)
  1037.    default:    time, (repeat | date)
  1038.    color:      time, (repeat | date), color
  1039.    preset:     time, (repeat | date), code, speed
  1040.    warmwhite:  time, (repeat | date), level
  1041.  
  1042. Setting Details:
  1043.  
  1044.    time: 4 digit string with zeros, no colons
  1045.        e.g:
  1046.        "1000"  - for 10:00am
  1047.        "2312"  - for 11:23pm
  1048.        "0315"  - for 3:15am
  1049.  
  1050.    repeat: Days of the week that the timer should repeat
  1051.            (Mutually exclusive with date)
  1052.            0=Sun, 1=Mon, 2=Tue, 3=Wed, 4=Thu, 5=Fri, 6=Sat
  1053.        e.g:
  1054.        "0123456"  - everyday
  1055.        "06"       - weekends
  1056.        "12345"    - weekdays
  1057.        "2"        - only Tuesday
  1058.  
  1059.    date: Date that the one-time timer should fire
  1060.            (Mutually exclusive with repeat)
  1061.        e.g:
  1062.        "2015-09-13"
  1063.        "2016-12-03"
  1064.  
  1065.    color: Color name, hex code, or rgb triple
  1066.  
  1067.    level: Level of the warm while light (0-100)
  1068.  
  1069.    code:  Code of the preset pattern (use -l to list them)
  1070.  
  1071.    speed: Speed of the preset pattern transions (0-100)
  1072.  
  1073. Example setting strings:
  1074.    "time:2130;repeat:0123456"
  1075.    "time:2130;date:2015-08-11"
  1076.    "time:1245;repeat:12345;color:123,345,23"
  1077.    "time:1245;repeat:12345;color:green"
  1078.    "time:1245;repeat:06;code:50;speed:30"
  1079.    "time:0345;date:2015-08-11;level:100"
  1080.    """
  1081.  
  1082.     print(timerhelp_text)
  1083.  
  1084. def processSetTimerArgs(parser, args):
  1085.     mode = args[1]
  1086.     num = args[0]
  1087.     settings = args[2]
  1088.  
  1089.     if not num.isdigit() or int(num) > 6 or int(num) < 1:
  1090.         parser.error("Timer number must be between 1 and 6")
  1091.  
  1092.     # create a dict from the settings string
  1093.     settings_list=settings.split(";")
  1094.     settings_dict = {}
  1095.     for s in settings_list:
  1096.         pair = s.split(":")
  1097.         key = pair[0].strip().lower()
  1098.         val = ""
  1099.         if len(pair) > 1:
  1100.             val = pair[1].strip().lower()
  1101.         settings_dict[key] = val
  1102.  
  1103.     keys = settings_dict.keys()
  1104.     timer = LedTimer()
  1105.  
  1106.     if mode == "inactive":
  1107.         #no setting needed
  1108.         timer.setActive(False)
  1109.  
  1110.     elif mode in ["poweroff", "default","color","preset","warmwhite"]:
  1111.         timer.setActive(True)
  1112.  
  1113.         if "time" not in keys:
  1114.             parser.error("This mode needs a time: {}".format(mode))
  1115.         if  "repeat" in keys and "date" in keys:
  1116.             parser.error("This mode only a repeat or a date, not both: {}".format(mode))
  1117.  
  1118.         # validate time format
  1119.         if len(settings_dict["time"]) != 4 or not settings_dict["time"].isdigit() :
  1120.             parser.error("time must be a 4 digits")
  1121.         hour = int(settings_dict["time"][0:2:])
  1122.         minute = int(settings_dict["time"][2:4:])
  1123.         if hour > 23:
  1124.             parser.error("timer hour can't be greater than 23")
  1125.         if minute > 59:
  1126.             parser.error("timer minute can't be greater than 59")
  1127.  
  1128.         timer.setTime(hour, minute)
  1129.  
  1130.         # validate date format
  1131.         if  "repeat" not in keys and "date" not in keys:
  1132.             # Generate date for next occurance of time
  1133.             print("No time or repeat given. Defaulting to next occurance of time")
  1134.             now = datetime.datetime.now()
  1135.             dt = now.replace(hour=hour, minute=minute)
  1136.             if utils.date_has_passed(dt):
  1137.                 dt = dt + datetime.timedelta(days=1)
  1138.             #settings_dict["date"] = date
  1139.             timer.setDate(dt.year, dt.month, dt.day)
  1140.         elif "date" in keys:
  1141.             try:
  1142.                 dt = datetime.datetime.strptime(settings_dict["date"], '%Y-%m-%d')
  1143.                 timer.setDate(dt.year, dt.month, dt.day)
  1144.             except ValueError:
  1145.                 parser.error("date is not properly formatted: YYYY-MM-DD")
  1146.  
  1147.         # validate repeat format
  1148.         if "repeat" in keys:
  1149.             if len(settings_dict["repeat"]) == 0:
  1150.                 parser.error("Must specify days to repeat")
  1151.             days = set()
  1152.             for c in list(settings_dict["repeat"]):
  1153.                 if c not in ['0', '1', '2', '3', '4', '5', '6']:
  1154.                     parser.error("repeat can only contain digits 0-6")
  1155.                 days.add(int(c))
  1156.  
  1157.             repeat = 0
  1158.             if 0 in days: repeat |= LedTimer.Su
  1159.             if 1 in days: repeat |= LedTimer.Mo
  1160.             if 2 in days: repeat |= LedTimer.Tu
  1161.             if 3 in days: repeat |= LedTimer.We
  1162.             if 4 in days: repeat |= LedTimer.Th
  1163.             if 5 in days: repeat |= LedTimer.Fr
  1164.             if 6 in days: repeat |= LedTimer.Sa
  1165.             timer.setRepeatMask(repeat)
  1166.  
  1167.         if  mode == "default":
  1168.             timer.setModeDefault()
  1169.  
  1170.         if  mode == "poweroff":
  1171.             timer.setModeTurnOff()
  1172.  
  1173.         if  mode == "color":
  1174.             if  "color" not in keys:
  1175.                 parser.error("color mode needs a color setting")
  1176.             #validate color val
  1177.             c = utils.color_object_to_tuple(settings_dict["color"])
  1178.             if c is None:
  1179.                 parser.error("Invalid color value: {}".format(settings_dict["color"]))
  1180.             timer.setModeColor(c[0],c[1],c[2])
  1181.  
  1182.         if  mode == "preset":
  1183.             if  "code" not in keys:
  1184.                 parser.error("preset mode needs a code: {}".format(mode))
  1185.             if  "speed" not in keys:
  1186.                 parser.error("preset mode needs a speed: {}".format(mode))
  1187.             code = settings_dict["code"]
  1188.             speed = settings_dict["speed"]
  1189.             if not speed.isdigit() or int(speed) > 100:
  1190.                 parser.error("preset speed must be a percentage (0-100)")
  1191.             if not code.isdigit() or not PresetPattern.valid(int(code)):
  1192.                 parser.error("preset code must be in valid range")
  1193.             timer.setModePresetPattern(int(code),int(speed))
  1194.  
  1195.         if  mode == "warmwhite":
  1196.             if  "level" not in keys:
  1197.                 parser.error("warmwhite mode needs a level: {}".format(mode))
  1198.             level = settings_dict["level"]
  1199.             if not level.isdigit() or int(level) > 100:
  1200.                 parser.error("warmwhite level must be a percentage (0-100)")
  1201.             timer.setModeWarmWhite(int(level))
  1202.     else:
  1203.         parser.error("Not a valid timer mode: {}".format(mode))
  1204.  
  1205.     return timer
  1206.  
  1207. def processCustomArgs(parser, args):
  1208.     if args[0] not in ["gradual", "jump", "strobe"]:
  1209.         parser.error("bad pattern type: {}".format(args[0]))
  1210.         return None
  1211.  
  1212.     speed = int(args[1])
  1213.  
  1214.     # convert the string to a list of RGB tuples
  1215.     # it should have space separated items of either
  1216.     # color names, hex values, or byte triples
  1217.     try:
  1218.         color_list_str = args[2].strip()
  1219.         str_list = color_list_str.split(' ')
  1220.         color_list = []
  1221.         for s in str_list:
  1222.             c = utils.color_object_to_tuple(s)
  1223.             if c is not None:
  1224.                 color_list.append(c)
  1225.             else:
  1226.                 raise Exception
  1227.  
  1228.     except:
  1229.         parser.error("COLORLIST isn't formatted right.  It should be a space separated list of RGB tuples, color names or web hex values")
  1230.  
  1231.     return args[0], speed, color_list
  1232.  
  1233. def parseArgs():
  1234.  
  1235.     parser = OptionParser()
  1236.  
  1237.     parser.description = "A utility to control Flux WiFi LED Bulbs. "
  1238.     #parser.description += ""
  1239.     #parser.description += "."
  1240.     power_group = OptionGroup(parser, 'Power options (mutually exclusive)')
  1241.     mode_group = OptionGroup(parser, 'Mode options (mutually exclusive)')
  1242.     info_group = OptionGroup(parser, 'Program help and information option')
  1243.     other_group = OptionGroup(parser, 'Other options')
  1244.  
  1245.     parser.add_option_group(info_group)
  1246.     info_group.add_option("-e", "--examples",
  1247.                       action="store_true", dest="showexamples", default=False,
  1248.                       help="Show usage examples")
  1249.     info_group.add_option("", "--timerhelp",
  1250.                       action="store_true", dest="timerhelp", default=False,
  1251.                       help="Show detailed help for setting timers")
  1252.     info_group.add_option("-l", "--listpresets",
  1253.                       action="store_true", dest="listpresets", default=False,
  1254.                       help="List preset codes")
  1255.     info_group.add_option("--listcolors",
  1256.                       action="store_true", dest="listcolors", default=False,
  1257.                       help="List color names")
  1258.  
  1259.     parser.add_option("-s", "--scan",
  1260.                       action="store_true", dest="scan", default=False,
  1261.                       help="Search for bulbs on local network")
  1262.     parser.add_option("-S", "--scanresults",
  1263.                       action="store_true", dest="scanresults", default=False,
  1264.                       help="Operate on scan results instead of arg list")
  1265.     power_group.add_option("-1", "--on",
  1266.                       action="store_true", dest="on", default=False,
  1267.                       help="Turn on specified bulb(s)")
  1268.     power_group.add_option("-0", "--off",
  1269.                       action="store_true", dest="off", default=False,
  1270.                       help="Turn off specified bulb(s)")
  1271.     parser.add_option_group(power_group)
  1272.  
  1273.     mode_group.add_option("-c", "--color", dest="color", default=None,
  1274.                   help="Set single color mode.  Can be either color name, web hex, or comma-separated RGB triple",
  1275.                   metavar='COLOR')
  1276.     mode_group.add_option("-w", "--warmwhite", dest="ww", default=None,
  1277.                   help="Set warm white mode (LEVEL is percent)",
  1278.                   metavar='LEVEL', type="int")
  1279.     mode_group.add_option("-p", "--preset", dest="preset", default=None,
  1280.                   help="Set preset pattern mode (SPEED is percent)",
  1281.                   metavar='CODE SPEED', type="int", nargs=2)
  1282.     mode_group.add_option("-C", "--custom", dest="custom", metavar='TYPE SPEED COLORLIST',
  1283.                             default=None, nargs=3,
  1284.                             help="Set custom pattern mode. " +
  1285.                               "TYPE should be jump, gradual, or strobe. SPEED is percent. " +
  1286.                               "COLORLIST is a should be a space-separated list of color names, web hex values, or comma-separated RGB triples")
  1287.     parser.add_option_group(mode_group)
  1288.  
  1289.     parser.add_option("-i", "--info",
  1290.                       action="store_true", dest="info", default=False,
  1291.                       help="Info about bulb(s) state")
  1292.     parser.add_option("", "--getclock",
  1293.                       action="store_true", dest="getclock", default=False,
  1294.                       help="Get clock")
  1295.     parser.add_option("", "--setclock",
  1296.                       action="store_true", dest="setclock", default=False,
  1297.                       help="Set clock to same as current time on this computer")
  1298.     parser.add_option("-t", "--timers",
  1299.                       action="store_true", dest="showtimers", default=False,
  1300.                       help="Show timers")
  1301.     parser.add_option("-T", "--settimer", dest="settimer", metavar='NUM MODE SETTINGS',
  1302.                             default=None, nargs=3,
  1303.                             help="Set timer. " +
  1304.                               "NUM: number of the timer (1-6). " +
  1305.                               "MODE: inactive, poweroff, default, color, preset, or warmwhite. " +
  1306.                               "SETTINGS: a string of settings including time, repeatdays or date, " +
  1307.                               "and other mode specific settings.   Use --timerhelp for more details.")
  1308.  
  1309.  
  1310.     parser.add_option("--protocol", dest="protocol", default=None, metavar="PROTOCOL",
  1311.                       help="Set the device protocol. Currently only supports LEDENET")
  1312.  
  1313.     other_group.add_option("-v", "--volatile",
  1314.                       action="store_true", dest="volatile", default=False,
  1315.                       help="Don't persist mode setting with hard power cycle (RGB and WW modes only).")
  1316.     parser.add_option_group(other_group)
  1317.  
  1318.     parser.usage = "usage: %prog [-sS10cwpCiltThe] [addr1 [addr2 [addr3] ...]."
  1319.     (options, args) = parser.parse_args()
  1320.  
  1321.     if options.showexamples:
  1322.         showUsageExamples()
  1323.         sys.exit(0)
  1324.  
  1325.     if options.timerhelp:
  1326.         showTimerHelp()
  1327.         sys.exit(0)
  1328.  
  1329.     if options.listpresets:
  1330.         for c in range(PresetPattern.seven_color_cross_fade, PresetPattern.seven_color_jumping+1):
  1331.             print("{:2} {}".format(c, PresetPattern.valtostr(c)))
  1332.         sys.exit(0)
  1333.  
  1334.     global webcolors_available
  1335.     if options.listcolors:
  1336.         if webcolors_available:
  1337.             for c in utils.get_color_names_list():
  1338.                 print("{}, ".format(c))
  1339.             print("")
  1340.         else:
  1341.             print("webcolors package doesn't seem to be installed. No color names available")
  1342.         sys.exit(0)
  1343.  
  1344.     if options.settimer:
  1345.         new_timer = processSetTimerArgs(parser, options.settimer)
  1346.         options.new_timer = new_timer
  1347.     else:
  1348.         options.new_timer = None
  1349.  
  1350.     mode_count = 0
  1351.     if options.color:  mode_count += 1
  1352.     if options.ww:     mode_count += 1
  1353.     if options.preset: mode_count += 1
  1354.     if options.custom: mode_count += 1
  1355.     if mode_count > 1:
  1356.         parser.error("options --color, --warmwhite, --preset, and --custom are mutually exclusive")
  1357.  
  1358.     if options.on and options.off:
  1359.         parser.error("options --on and --off are mutually exclusive")
  1360.  
  1361.     if options.custom:
  1362.         options.custom = processCustomArgs(parser, options.custom)
  1363.  
  1364.     if options.color:
  1365.         options.color = utils.color_object_to_tuple(options.color)
  1366.         if options.color is None:
  1367.             parser.error("bad color specification")
  1368.  
  1369.     if options.preset:
  1370.         if not PresetPattern.valid(options.preset[0]):
  1371.             parser.error("Preset code is not in range")
  1372.  
  1373.     # asking for timer info, implicitly gets the state
  1374.     if options.showtimers:
  1375.         options.info = True
  1376.  
  1377.     op_count = mode_count
  1378.     if options.on:   op_count += 1
  1379.     if options.off:  op_count += 1
  1380.     if options.info: op_count += 1
  1381.     if options.getclock: op_count += 1
  1382.     if options.setclock: op_count += 1
  1383.     if options.listpresets: op_count += 1
  1384.     if options.settimer: op_count += 1
  1385.  
  1386.     if (not options.scan or options.scanresults) and (op_count == 0):
  1387.         parser.error("An operation must be specified")
  1388.  
  1389.     # if we're not scanning, IP addresses must be specified as positional args
  1390.     if  not options.scan and not options.scanresults and not options.listpresets:
  1391.         if len(args) == 0:
  1392.             parser.error("You must specify at least one IP address as an argument, or use scan results")
  1393.  
  1394.  
  1395.     return (options, args)
  1396. #-------------------------------------------
  1397. def main():
  1398.  
  1399.     (options, args) = parseArgs()
  1400.  
  1401.     if options.scan:
  1402.         scanner = BulbScanner()
  1403.         scanner.scan(timeout=2)
  1404.         bulb_info_list = scanner.getBulbInfo()
  1405.         # we have a list of buld info dicts
  1406.         addrs = []
  1407.         if options.scanresults and len(bulb_info_list) > 0 :
  1408.             for b in bulb_info_list:
  1409.                 addrs.append(b['ipaddr'])
  1410.         else:
  1411.             print("{} bulbs found".format(len(bulb_info_list)))
  1412.             for b in bulb_info_list:
  1413.                 print("  {} {}".format(b['id'], b['ipaddr']))
  1414.             sys.exit(0)
  1415.  
  1416.     else:
  1417.         addrs = args
  1418.         bulb_info_list = []
  1419.         for addr in args:
  1420.             info = dict()
  1421.             info['ipaddr'] = addr
  1422.             info['id'] = 'Unknown ID'
  1423.  
  1424.             bulb_info_list.append(info)
  1425.  
  1426.  
  1427.     # now we have our bulb list, perform same operation on all of them
  1428.     for info in bulb_info_list:
  1429.         try:
  1430.             bulb = WifiLedBulb(info['ipaddr'])
  1431.         except Exception as e:
  1432.             print("Unable to connect to bulb at [{}]: {}".format(info['ipaddr'],e))
  1433.             continue
  1434.  
  1435.         if options.getclock:
  1436.             print("{} [{}] {}".format(info['id'], info['ipaddr'],bulb.getClock()))
  1437.  
  1438.         if options.setclock:
  1439.             bulb.setClock()
  1440.  
  1441.         if options.protocol:
  1442.             bulb.setProtocol(options.protocol)
  1443.  
  1444.         if options.ww is not None:
  1445.             print("Setting warm white mode, level: {}%".format(options.ww))
  1446.             bulb.setWarmWhite(options.ww, not options.volatile)
  1447.  
  1448.         elif options.color is not None:
  1449.             print("Setting color RGB:{}".format(options.color),)
  1450.             name = utils.color_tuple_to_string(options.color)
  1451.             if name is None:
  1452.                 print()
  1453.             else:
  1454.                 print("[{}]".format(name))
  1455.             bulb.setRgb(options.color[0],options.color[1],options.color[2], not options.volatile)
  1456.  
  1457.         elif options.custom is not None:
  1458.             bulb.setCustomPattern(options.custom[2], options.custom[1], options.custom[0])
  1459.             print("Setting custom pattern: {}, Speed={}%, {}".format(
  1460.                 options.custom[0], options.custom[1], options.custom[2]))
  1461.  
  1462.         elif options.preset is not None:
  1463.             print("Setting preset pattern: {}, Speed={}%".format(PresetPattern.valtostr(options.preset[0]), options.preset[1]))
  1464.             bulb.setPresetPattern(options.preset[0], options.preset[1])
  1465.  
  1466.         if options.on:
  1467.             print("Turning on bulb at {}".format(bulb.ipaddr))
  1468.             bulb.turnOn()
  1469.         elif options.off:
  1470.             print("Turning off bulb at {}".format(bulb.ipaddr))
  1471.             bulb.turnOff()
  1472.  
  1473.         if options.info:
  1474.             bulb.update_state()
  1475.             print("{} [{}] {}".format(info['id'], info['ipaddr'],bulb))
  1476.  
  1477.         if options.settimer:
  1478.             timers = bulb.getTimers()
  1479.             num = int(options.settimer[0])
  1480.             print("New Timer ---- #{}: {}".format(num,options.new_timer))
  1481.             if options.new_timer.isExpired():
  1482.                 print("[timer is already expired, will be deactivated]")
  1483.             timers[num-1] = options.new_timer
  1484.             bulb.sendTimers(timers)
  1485.  
  1486.         if options.showtimers:
  1487.             timers = bulb.getTimers()
  1488.             num = 0
  1489.             for t in timers:
  1490.                 num += 1
  1491.                 print("  Timer #{}: {}".format(num,t))
  1492.             print("")
  1493.  
  1494.  
  1495.     sys.exit(0)
  1496.  
  1497.  
  1498. if __name__ == '__main__':
  1499.     main()
Add Comment
Please, Sign In to add comment