MiLight API?


(Michael Langkilde) #1

I found this and wonder if anyone can make any use of it. I am still learning and trying to get a grasp of it. I wonder if some reworking of the code could make ST communicate to it.

https://github.com/ojarva/python-ledcontroller/blob/master/ledcontroller/init.py

“”"
Library for controlling limitless/milight/easybulb RGBW/white leds bulbs.

Commands for white and RGBW bulbs are implemented. Older RGB lights are not supported yet.

Before using this library, you need to use smartphone/tablet app to configure
light groups to the gateway. Configuring remotes does nothing to configure the gateway.

See https://github.com/ojarva/python-ledcontroller for more information.

Based on the documentation available at http://www.limitlessled.com/dev/ .
"""

import socket
import struct
import time

all = [“LedController”]

class LedController(object):
"""
Main class for controlling limitless/milight/easybulb lights.

Usage:
# All keyword arguments are optional.
led = LedController(ip, port=8899, repeat_commands=3, pause_between_commands=0.1)
led.on()
led.off(1)
led.disco(4)
led.nightmode(3)
led.set_color("red", 2)
led.set_brightness(50, 2)
"""

WHITE_COMMANDS = {
 "all_on": (b"\x35",),
 "all_off": (b"\x39",),
 "all_full": (b"\xb5",),
 "all_nightmode": (b"\xb9",),
 "warmer": (b"\x3e",),
 "cooler": (b"\x3f",),
 "brightness_up": (b"\x3c",),
 "brightness_down": (b"\x34",),
}

WHITE_GROUP_X_ON = [(b"\x38",), (b"\x3d",), (b"\x37",), (b"\x32",)]
WHITE_GROUP_X_OFF = [(b"\x3b",), (b"\x33",), (b"\x3a",), (b"\x36",)]
WHITE_GROUP_X_FULL = [(b"\xb8",), (b"\xbd",), (b"\xb7",), (b"\xb2",)]
WHITE_GROUP_X_NIGHTMODE = [(b"\xbb",), (b"\xb3",), (b"\xba",), (b"\xb6",)]


RGBW_GROUP_X_ON = [(b"\x45",), (b"\x47",), (b"\x49",), (b"\x4b",)]
RGBW_GROUP_X_OFF = [(b"\x46",), (b"\x48",), (b"\x4a",), (b"\x4c",)]
RGBW_GROUP_X_TO_WHITE = [(b"\xc5",), (b"\xc7",), (b"\xc9",), (b"\xcb",)]
RGBW_GROUP_X_NIGHTMODE = [(b"\xc6",), (b"\xc8",), (b"\xca",), (b"\xcc",)]
RGBW_COMMANDS = {
 "all_on": (b"\x42",),
 "all_off": (b"\x41",),
 "all_white": (b"\xc2",),
 "disco": (b"\x4d",),
 "disco_faster": (b"\x44",),
 "disco_slower": (b"\x43",),
 "all_nightmode": (b"\xc1",),
 "color_to_violet": (b"\x40", b"\x00"),
 "color_to_royal_blue": (b"\x40", b"\x10"),
 "color_to_baby_blue": (b"\x40", b"\x20"),
 "color_to_aqua": (b"\x40", b"\x30"),
 "color_to_royal_mint": (b"\x40", b"\x40"),
 "color_to_seafoam_green": (b"\x40", b"\x50"),
 "color_to_green": (b"\x40", b"\x60"),
 "color_to_lime_green": (b"\x40", b"\x70"),
 "color_to_yellow": (b"\x40", b"\x80"),
 "color_to_yellow_orange": (b"\x40", b"\x90"),
 "color_to_orange": (b"\x40", b"\xa0"),
 "color_to_red": (b"\x40", b"\xb0"),
 "color_to_pink": (b"\x40", b"\xc0"),
 "color_to_fusia": (b"\x40", b"\xd0"),
 "color_to_lilac": (b"\x40", b"\xe0"),
 "color_to_lavendar": (b"\x40", b"\xf0"),
}

def __init__(self, gateway_ip, **kwargs):
    """ Optional keyword arguments:
        - repeat_commands (default 3): how many times safe commands are repeated to ensure successful execution.
        - port (default 8899): UDP port on wifi gateway. Port is 50000 for gw v1 and v2.
        - pause_between_commands (default 0.1 (in seconds)): how long pause there should be between sending commands to the gateway.
        - group_1, group_2, ...: set bulb type for group. Currently either rgbw (default) and "white" are supported. See also .set_group_type method.
        """
    self.group = {}
    self.has_white = False
    self.has_rgbw = False
    for group in range(1, 5):
        self.set_group_type(group, kwargs.get("group_%s" % group, "rgbw"))
    self.gateway_ip = gateway_ip
    self.gateway_port = int(kwargs.get("port", 8899))
    self.last_command_at = 0
    self.repeat_commands = int(kwargs.get("repeat_commands", 3))
    if self.repeat_commands == 0:
        self.repeat_commands = 1
    self.pause_between_commands = float(kwargs.get("pause_between_commands", 0.1))

def get_group_type(self, group):
    """ Gets bulb type for specified group.

    Group must be int between 1 and 4.
    """
    return self.group[group]

def set_group_type(self, group, bulb_type):
    """ Sets bulb type for specified group.

    Group must be int between 1 and 4.

    Type must be "rgbw" or "white".

    Alternatively, use constructor keywords group_1, group_2 etc. to set bulb types.
    """
    self.group[group] = bulb_type
    if "white" in self.group.values():
        self.has_white = True
    else:
        self.has_white = False

    if "rgbw" in self.group.values():
        self.has_rgbw = True
    else:
        self.has_rgbw = False

def _send_command(self, input_command):
    """ You shouldn't use this method directly.

        Sends a single command. If previous command was sent
        recently, sleep for 100ms (configurable with pause_between_commands
        constructor keyword). """
    if input_command is None:
        return
    time_since_last_command = time.time() - self.last_command_at
    if time_since_last_command < self.pause_between_commands:
        # Lights require 100ms pause between commands to function at least almost reliably.
        time.sleep(self.pause_between_commands - time_since_last_command)
    self.last_command_at = time.time()
    command = b""
    for item in input_command:
        command = command + item
    if len(command) == 1:
        command = command + b"\x00"
    if len(command) == 2:
        command = command + b"\x55"

    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.sendto(command, (self.gateway_ip, self.gateway_port))
    sock.close()
    return command

def _send_to_group(self, group, **kwargs):
    """ You shouldn't use this method directly.

    Sends a single command to specific group.

    Handles automatically sending command to white or rgbw group.
    """
    retries = kwargs.get("retries", self.repeat_commands)
    for _ in range(retries):
        if kwargs.get("send_on", True):
            self.on(group)
        if group is None or group == 0:
            if self.has_white:
                self._send_command(self.WHITE_COMMANDS.get(kwargs["command"]))
            if self.has_rgbw:
                self._send_command(self.RGBW_COMMANDS.get(kwargs["command"]))
        else:
            if group < 1 or group > 4:
                raise AttributeError("Group must be between 1 and 4 (was %s)" % group)
            if kwargs.get("per_group"):
                self._send_command(kwargs.get("%s_cmd" % self.get_group_type(group), [None, None, None, None])[group-1])
            else:
                if self.get_group_type(group) == "white":
                    cmd_tmp = self.WHITE_COMMANDS
                elif self.get_group_type(group) == "rgbw":
                    cmd_tmp = self.RGBW_COMMANDS
                else:
                    raise NotImplementedError("Invalid group type: %s" % self.get_group_type(group))
                self._send_command(cmd_tmp.get(kwargs["command"]))

def on(self, group=None):
    """ Switches lights on. If group (1-4) is not specified,
        all four groups will be switched on. """
    if group is None or group == 0:
        self._send_to_group(group, send_on=False, command="all_on")
        return
    self._send_to_group(group, per_group=True, white_cmd=self.WHITE_GROUP_X_ON, rgbw_cmd=self.RGBW_GROUP_X_ON, send_on=False)

def off(self, group=None):
    """ Switches lights off. If group (1-4) is not specified,
        all four groups will be switched off. """
    if group is None or group == 0:
        self._send_to_group(group, send_on=False, command="all_off")
        return
    self._send_to_group(group, per_group=True, send_on=False, rgbw_cmd=self.RGBW_GROUP_X_OFF, white_cmd=self.WHITE_GROUP_X_OFF)

def white(self, group=None):
    """ Switches lights on and changes color to white.
        If group (1-4) is not specified, all four groups
        will be switched on and to white. """
    if group is None or group == 0:
        self._send_to_group(group, command="all_white")
        return
    self._send_to_group(group, per_group=True, rgbw_cmd=self.RGBW_GROUP_X_TO_WHITE)

def set_color(self, color, group=None):
    """ Switches lights on and changes color. Available colors:

         - violet
         - royal_blue
         - baby_blue
         - aqua
         - royal_mint
         - seafoam_green
         - green
         - lime_green
         - yellow
         - yellow_orange
         - orange
         - red
         - pink
         - fusia
         - lilac
         - lavendar

        If group (1-4) is not specified, all four groups
        will be switched on and to specified color."""
    if color == "white": # hack, as commands for setting color to white differ from other colors.
        self.white(group)
    else:
        self._send_to_group(group, command="color_to_"+color)
    return color

def brightness_up(self, group=None):
    """ Adjusts white bulb brightness up.

    Calling this method for RGBW lights won't
    have any effect on the brightness."""
    self._send_to_group(group, command="brightness_down")

def brightness_down(self, group=None):
    """ Adjusts white bulb brightness down.

    Calling this method for RGBW lights won't
    have any effect on the brightness."""
    self._send_to_group(group, command="brightness_up")

def cooler(self, group=None):
    """ Adjusts white bulb to cooler color temperature.

    Calling this method for RGBW lights won't
    have any effect. """
    self._send_to_group(group, command="cooler")

def warmer(self, group=None):
    """ Adjusts white bulb to warmer color temperature.

    Calling this method for RGBW lights won't
    have any effect. """
    self._send_to_group(group, command="warmer")

def set_brightness(self, percent, group=None):
    """ Sets brightness.

        Percent is int between 0 (minimum brightness) and 100 (maximum brightness), or
        float between 0 (minimum brightness) and 1.0 (maximum brightness).

        See also .nightmode().

        If group (1-4) is not specified, brightness of all four groups will be adjusted.
        """
    # If input is float, assume it is percent value from 0 to 1.
    if isinstance(percent, float):
        if percent > 1:
            percent = int(percent)
        else:
            percent = int(percent * 100)
    # Clamp to appropriate range.
    percent = min(100, max(0, percent))

    # Map 0-100 to 2-27
    value = int(2 + ((float(percent) / 100) * 25))
    self.on(group)
    self._send_command((b"\x4e", struct.pack("B", value)))
    return percent

def disco(self, group=None):
    """ Starts disco mode. The command is executed only once, as multiple commands would cycle
        disco modes rapidly. There is no way to automatically detect whether transmitting the command
        succeeded or not.

    Consecutive calls cycle disco modes:
        1. Static white color.
        2. White color smooth change.
        3. All colors smooth change.
        4. Red / Green / Blue colors smooth change.
        5. Seven Colors
        6. Three Colors
        7. Red / Green
        8. Red / Blue
        9. Blue / Green
        10. White Blink
        11. White Strobe
        12. Red Blink
        13. Red Strobe
        14. Green Blinks
        15. Green Strobe
        16. Blue Blinks
        17. Blue Strobe
        18. Yellow Blinks
        19. Yellow Strobe
        20. All of the above in an endless cycle.

        (Above list is copied from http://www.limitlessled.com/faqs/how-is-limitlessled-better-than-greenwave-led/)."""
    self._send_to_group(group, command="disco", retries=1)

def disco_faster(self, group=None):
    """ Adjusts up the speed of disco mode (if enabled; does not start disco mode). """
    self._send_to_group(group, command="disco_faster", retries=1)

def disco_slower(self, group=None):
    """ Adjusts down the speed of disco mode (if enabled; does not start disco mode). """
    self._send_to_group(group, command="disco_slower", retries=1)

def nightmode(self, group=None):
    """ Enables nightmode (very dim white light).

        The command is sent only once, as multiple commands would blink lights rapidly.
        There is no way to automatically detect whether transmitting the command succeeded or not.

        This does not work with wifi gateway v3.

        Contrary to limitlessled documentation, this works with RGBW bulbs.
        """
    self.off(group)
    if group is None or group == 0:
        if self.has_rgbw:
            self._send_command(self.RGBW_COMMANDS["all_nightmode"])
        if self.has_white:
            self._send_command(self.WHITE_COMMANDS["all_nightmode"])
    else:
        self._send_to_group(group, per_group=True, rgbw_cmd=self.RGBW_GROUP_X_NIGHTMODE, white_cmd=self.WHITE_GROUP_X_NIGHTMODE, send_on=False, retries=1)

def batch_run(self, *commands):
    """ Run batch of commands in sequence.

        Input is positional arguments with (function pointer, *args) tuples.

        This method is useful for executing commands to multiple groups with retries,
        without having too long delays. For example,

        - Set group 1 to red and brightness to 10%
        - Set group 2 to red and brightness to 10%
        - Set group 3 to white and brightness to 100%
        - Turn off group 4

        With three repeats, running these consecutively takes approximately 100ms * 13 commands * 3 times = 3.9 seconds.

        With batch_run, execution takes same time, but first loop - each command is sent once to every group -
        is finished within 1.3 seconds. After that, each command is repeated two times. Most of the time, this ensures
        slightly faster changes for each group.

        Usage:

        led.batch_run((led.set_color, "red", 1), (led.set_brightness, 10, 1), (led.set_color, "white", 3), ...)
    """
    original_retries = self.repeat_commands
    self.repeat_commands = 1
    for _ in range(original_retries):
        for command in commands:
            cmd = command[0]
            args = command[1:]
            cmd(*args)
    self.repeat_commands = original_retries

def main():
""" Test function for development “”"
led = LedController(“192.168.1.6”)
led.batch_run((led.disco, 3), (led.set_brightness, 10, 3), (led.set_color, “red”, 3))

if name == ‘main’:
main()


(Michael Langkilde) #2

Is this duable to get integrated with ST?


(Kevin Tierney) #3

Looks like they are the same as the Limitless Led.

Based on @hamishahern 's comments, it sounds like those won’t currently work natively.


(Kevin Tierney) #4

Was digging a little further and looks like @pstuart figured out a way to make UDP work (which was the missing piece for integration). Wonder if @hamishahern could use that technique to make them work now


(Limitless Led) #5

Yeah I will give it a go. I will let you know on the LimitlessLED thread.


(Alex Goodyear) #6

How did you go with this?