diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fa3dd98 --- /dev/null +++ b/.gitignore @@ -0,0 +1,94 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# dotenv +.env + +# virtualenv +.venv/ +venv/ +ENV/ + +# Spyder project settings +.spyderproject + +# Rope project settings +.ropeproject + +# IntelliJ project settings +.idea \ No newline at end of file diff --git a/README.md b/README.md index 7de9bbd..705d157 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # python-limitlessled -`python-limitlessled` controls LimitlessLED bridges. It supports `white` and `rgbw` bulb groups. +`python-limitlessled` controls LimitlessLED bridges. It supports `white`, `rgbw` and `rgbww` bulb groups as well as the `bridge-led` of newer wifi bridges. ## Install `pip install limitlessled` @@ -12,6 +12,7 @@ Group names can be any string, but must be unique amongst all bridges. ```python from limitlessled.bridge import Bridge from limitlessled.group.rgbw import RGBW +from limitlessled.group.rgbww import RGBWW from limitlessled.group.white import WHITE bridge = Bridge('') @@ -19,6 +20,7 @@ bridge.add_group(1, 'bedroom', RGBW) # A group number can support two groups as long as the types differ bridge.add_group(2, 'bathroom', WHITE) bridge.add_group(2, 'living_room', RGBW) +bridge.add_group(2, 'kitchen', RGBWW) ``` Get access to groups either via the return value of `add_group`, or with the `LimitlessLED` object. @@ -29,6 +31,9 @@ bedroom = bridge.add_group(1, 'bedroom', RGBW) limitlessled = LimitlessLED() limitlessled.add_bridge(bridge) bedroom = limitlessled.group('bedroom') + +# The bridge led can be controlled and acts as a RGBW group +bridge_led = bridge.bridge_led ``` ### Control diff --git a/limitlessled/bridge.py b/limitlessled/bridge.py index e7e7262..b2ad51c 100644 --- a/limitlessled/bridge.py +++ b/limitlessled/bridge.py @@ -4,17 +4,29 @@ import socket import time import threading +from datetime import datetime, timedelta from limitlessled import MIN_WAIT, REPS -from limitlessled.group.rgbw import RgbwGroup, RGBW +from limitlessled.group.rgbw import RgbwGroup, RGBW, BRIDGE_LED +from limitlessled.group.rgbww import RgbwwGroup, RGBWW from limitlessled.group.white import WhiteGroup, WHITE -BRIDGE_PORT = 8899 -BRIDGE_VERSION = 5 -BRIDGE_SHORT_VERSION_MIN = 3 -BRIDGE_LONG_BYTE = 0x55 +BRIDGE_PORT = 5987 +BRIDGE_VERSION = 6 +BRIDGE_LED_GROUP = 1 +BRIDGE_LED_NAME = 'bridge' SELECT_WAIT = 0.025 +BRIDGE_INITIALIZATION_COMMAND = [0x20, 0x00, 0x00, 0x00, 0x16, 0x02, 0x62, + 0x3a, 0xd5, 0xed, 0xa3, 0x01, 0xae, 0x08, + 0x2d, 0x46, 0x61, 0x41, 0xa7, 0xf6, 0xdc, + 0xaf, 0xfe, 0xf7, 0x00, 0x00, 0x1e] +KEEP_ALIVE_COMMAND_PREAMBLE = [0xD0, 0x00, 0x00, 0x00, 0x02] +KEEP_ALIVE_RESPONSE_PREAMBLE = [0xd8, 0x0, 0x0, 0x0, 0x07] +KEEP_ALIVE_TIME = 1 +RECONNECT_TIME = 5 +SOCKET_TIMEOUT = 5 +STARTING_SEQUENTIAL_BYTE = 0x02 def group_factory(bridge, number, name, led_type): @@ -23,11 +35,13 @@ def group_factory(bridge, number, name, led_type): :param bridge: Member of this bridge. :param number: Group number (1-4). :param name: Name of group. - :param led_type: Either `RGBW` or `WHITE`. + :param led_type: Either `RGBW`, `RGBWW`, `WHITE` or `BRIDGE_LED`. :returns: New group. """ - if led_type == RGBW: - return RgbwGroup(bridge, number, name) + if led_type in [RGBW, BRIDGE_LED]: + return RgbwGroup(bridge, number, name, led_type) + elif led_type == RGBWW: + return RgbwwGroup(bridge, number, name) elif led_type == WHITE: return WhiteGroup(bridge, number, name) else: @@ -37,34 +51,78 @@ def group_factory(bridge, number, name, led_type): class Bridge(object): """ Represents a LimitlessLED bridge. """ - def __init__(self, ip, port=BRIDGE_PORT, version=BRIDGE_VERSION): + def __init__(self, ip, port=BRIDGE_PORT, version=BRIDGE_VERSION, + bridge_led_name=BRIDGE_LED_NAME): """ Initialize bridge. - Bridge version 3 through 5 (latest as of this release) + Bridge version 6 (latest as of this release) can use the default parameters. For lower versions, - use port 50000. Lower versions also require sending a - larger payload to the bridge (slower). + use port 8899 (3 to 5) or 50000 (lower then 3). + Lower versions also require sending a larger payload + to the bridge (slower). :param ip: IP address of bridge. :param port: Bridge port. :param version: Bridge version. + :param bridge_led_name: Name of the bridge led group. """ + self.is_closed = False self.wait = MIN_WAIT self.reps = REPS self.groups = [] self.ip = ip self.version = version + self._sn = STARTING_SEQUENTIAL_BYTE self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self._socket.settimeout(SOCKET_TIMEOUT) self._socket.connect((ip, port)) self._command_queue = queue.Queue() self._lock = threading.Lock() self.active = 0 self._selected_number = None + # Start queue consumer thread. consumer = threading.Thread(target=self._consume) consumer.daemon = True consumer.start() + # Version specific stuff + self._wb1 = None + self._wb2 = None + self._bridge_led = None + if self.version >= 6: + # Create bridge led group + self._bridge_led = group_factory(self, BRIDGE_LED_GROUP, + bridge_led_name, BRIDGE_LED) + + # Initialize connection to retrieve bridge session ids (wb1, wb2) + self._init_connection() + + # Start keep alive thread. + keep_alive_thread = threading.Thread(target=self._keep_alive) + keep_alive_thread.daemon = True + keep_alive_thread.start() + + @property + def sn(self): + """ Gets the current sequential byte. """ + return self._sn + + @property + def wb1(self): + """ Gets the bridge session id 1. """ + return self._wb1 + + @property + def wb2(self): + """ Gets the bridge session id 2. """ + return self._wb2 + + @property + def bridge_led(self): + """ Get the group to control the bridge led. """ + return self._bridge_led + def incr_active(self): """ Increment number of active groups. """ with self._lock: @@ -87,21 +145,19 @@ def add_group(self, number, name, led_type): self.groups.append(group) return group - def send(self, group, command, reps=REPS, wait=MIN_WAIT, select=False): + def send(self, command, reps=REPS, wait=MIN_WAIT): """ Send a command to the physical bridge. - :param group: Run on this group. - :param command: A bytearray. + :param command: A Command instance. :param reps: Number of repetitions. :param wait: Wait time in seconds. - :param select: Select group if necessary. """ # Enqueue the command. - self._command_queue.put((group, command, reps, wait, select)) + self._command_queue.put((command, reps, wait)) # Wait before accepting another command. - # This keeps indvidual groups relatively synchronized. + # This keeps individual groups relatively synchronized. sleep = reps * wait * self.active - if select and self._selected_number != group.number: + if command.select and self._selected_number != command.group_number: sleep += SELECT_WAIT time.sleep(sleep) @@ -118,17 +174,83 @@ def _consume(self): TODO: Only wait when another command comes in. """ - while True: + while not self.is_closed: # Get command from queue. - (group, command, reps, wait, select) = self._command_queue.get() + (command, reps, wait) = self._command_queue.get() # Select group if a different group is currently selected. - if select and self._selected_number != group.number: - self._socket.send(bytearray(group.get_select_cmd())) + if command.select and self._selected_number != command.group_number: + self._send_raw(command.select_command.bytes) time.sleep(SELECT_WAIT) # Repeat command as necessary. for _ in range(reps): - if self.version < BRIDGE_SHORT_VERSION_MIN: - command.append(BRIDGE_LONG_BYTE) - self._socket.send(bytearray(command)) + self._send_raw(command.bytes) time.sleep(wait) - self._selected_number = group.number + self._selected_number = command.group_number + + def _send_raw(self, command): + """ + Sends an raw command directly to the physical bridge. + :param command: A bytearray. + """ + self._socket.send(bytearray(command)) + self._sn = (self._sn + 1) % 256 + + def _init_connection(self): + """ + Requests the session ids of the bridge. + :returns: True, if initialization was successful. False, otherwise. + """ + try: + response = bytearray(22) + self._send_raw(BRIDGE_INITIALIZATION_COMMAND) + self._socket.recv_into(response) + self._wb1 = response[19] + self._wb2 = response[20] + except socket.timeout: + return False + + return True + + def _reconnect(self): + """ + Try continuously to reconnect to the bridge. + """ + while not self.is_closed: + if self._init_connection(): + return + + time.sleep(RECONNECT_TIME) + + def _keep_alive(self): + """ + Send keep alive messages continuously to bridge. + """ + while not self.is_closed: + command = KEEP_ALIVE_COMMAND_PREAMBLE + [self.wb1, self.wb2] + self._send_raw(command) + + start = datetime.now() + connection_alive = False + while datetime.now() - start < timedelta(seconds=SOCKET_TIMEOUT): + response = bytearray(12) + try: + self._socket.recv_into(response) + except socket.timeout: + break + + if response[:5] == bytearray(KEEP_ALIVE_RESPONSE_PREAMBLE): + connection_alive = True + break + + if not connection_alive: + self._reconnect() + continue + + time.sleep(KEEP_ALIVE_TIME) + + def close(self): + """ + Closes the connection to the bridge. + """ + self.is_closed = True + diff --git a/limitlessled/group/__init__.py b/limitlessled/group/__init__.py index e450944..287b06c 100644 --- a/limitlessled/group/__init__.py +++ b/limitlessled/group/__init__.py @@ -7,6 +7,7 @@ from limitlessled import MIN_WAIT, REPS from limitlessled.pipeline import Pipeline, PipelineQueue +from limitlessled.group.commands import command_set_factory def rate(wait=MIN_WAIT, reps=REPS): @@ -41,17 +42,19 @@ def wrapper(self, *args, **kwargs): class Group(object): """ LimitlessLED group. """ - def __init__(self, bridge, number, name): + def __init__(self, bridge, number, name, led_type): """ Initialize group. :param bridge: Member of this bridge. :param number: Group number (1-4). :param name: Group name. + :param led_type: The type of the led. """ self.name = name self.number = number self._bridge = bridge self._index = number - 1 + self._command_set = command_set_factory(bridge, number, led_type) self._on = False self._brightness = 0.5 self._queue = queue.Queue() @@ -70,11 +73,28 @@ def on(self): """ return self._on + @on.setter + def on(self, state): + """ Turn on or off. + + :param state: True (on) or False (off). + """ + self._on = state + cmd = self.command_set.off() + if state: + cmd = self.command_set.on() + self.send(cmd) + @property def bridge(self): """ Bridge property. """ return self._bridge + @property + def command_set(self): + """Command set property. """ + return self._command_set + def flash(self, duration=0.0): """ Flash a group. @@ -84,14 +104,12 @@ def flash(self, duration=0.0): self.on = not self.on time.sleep(duration) - def send(self, cmd, select=False): + def send(self, cmd): """ Send a command to the bridge. :param cmd: List of command bytes. - :param select: If command requires selection. """ - self._bridge.send(self, cmd, wait=self.wait, - reps=self.reps, select=select) + self._bridge.send(cmd, wait=self.wait, reps=self.reps) def enqueue(self, pipeline): """ Start a pipeline. @@ -106,30 +124,33 @@ def stop(self): """ Stop a running pipeline. """ self._event.set() - def _wait(self, duration, commands): + def _wait(self, duration, steps, commands): """ Compute wait time. :param duration: Total time (in seconds). + :param steps: Number of steps. :param commands: Number of commands. :returns: Wait in seconds. """ - wait = (duration / commands) - \ + wait = ((duration - self.wait * self.reps * commands) / steps) - \ (self.wait * self.reps * self._bridge.active) - if wait < 0: - wait = 0 - return wait + return max(0, wait) - def _scaled_steps(self, duration, steps, total): - """ Scale steps. + def _scale_steps(self, duration, commands, *steps): + """ Scale steps - :param duration: Total time (in seconds). - :param steps: Ideal step amount. - :param total: Total steps to take. - :returns: Steps scaled to time and total. + :param duration: Total time (in seconds) + :param commands: Number of commands to be executed. + :param steps: Steps for one or many properties to take. + :return: Steps scaled to time and total. """ - return math.ceil(duration / - (self.wait * self.reps * self._bridge.active) * - (steps / total)) + factor = duration / ((self.wait * self.reps * commands) - \ + (self.wait * self.reps * self._bridge.active)) + steps = [math.ceil(factor * step) for step in steps] + if len(steps) == 1: + return steps[0] + else: + return steps def __str__(self): """ String representation. diff --git a/limitlessled/group/commands/__init__.py b/limitlessled/group/commands/__init__.py new file mode 100644 index 0000000..996b7ce --- /dev/null +++ b/limitlessled/group/commands/__init__.py @@ -0,0 +1,121 @@ +""" LimtlessLED command sets. """ + + +def command_set_factory(bridge, group_number, led_type): + """ + Create command set for controlling a specific led group. + :param bridge: The bridge the leds are connected to. + :param group_number: The group number. + :param led_type: The type of the leds. + :return: The created command set. + """ + from limitlessled.group.commands.legacy import ( + CommandSetWhiteLegacy, CommandSetRgbwLegacy) + from limitlessled.group.commands.v6 import ( + CommandSetBridgeLightV6, CommandSetWhiteV6, + CommandSetRgbwV6, CommandSetRgbwwV6) + + command_sets = [CommandSetWhiteLegacy, CommandSetRgbwLegacy, + CommandSetBridgeLightV6, CommandSetWhiteV6, + CommandSetRgbwV6, CommandSetRgbwwV6] + try: + cls = next(cs for cs in command_sets if + bridge.version in cs.SUPPORTED_VERSIONS and + led_type in cs.SUPPORTED_LED_TYPES) + return cls(bridge, group_number) + except StopIteration: + raise ValueError('There is no command set for ' + 'specified bridge version and led type.') + + +class Command: + """ Represents a single command to be sent to the bridge. """ + + def __init__(self, bytes, group_number, + select=False, select_command=None): + """ + Initialize command. + :param bytes: A bytearray. + :param group_number: Group number (1-4). + :param select: If command requires selection. + :param select_command: Selection command bytes. + """ + self._bytes = bytearray(bytes) + self._group_number = group_number + self._select = select + self._select_command = select_command + + @property + def bytes(self): + """ The command as bytearray. """ + return self._bytes + + @property + def group_number(self): + """ The group number (1-4). """ + return self._group_number + + @property + def select(self): + """ If command requires selection. """ + return self._select + + @property + def select_command(self): + """ Selection command bytes. """ + return self._select_command + + +class CommandSet: + """ Base class for command sets.""" + + def __init__(self, bridge, group_number, + brightness_steps, hue_steps=1, + saturation_steps=1, temperature_steps=1): + """ + Initializes the command set. + :param bridge: The bridge the leds are connected to. + :param group_number: The group number. + :param brightness_steps: The number of brightness steps. + :param hue_steps: The number of hue steps. + :param saturation_steps: The number of saturation steps + :param temperature_steps: The number of temperature steps. + """ + self._bridge = bridge + self._group_number = group_number + self._brightness_steps = brightness_steps + self._hue_steps = hue_steps + self._saturation_steps = saturation_steps + self._temperature_steps = temperature_steps + + @property + def brightness_steps(self): + """ + Brightness steps property. + :return: The number of brightness steps. + """ + return self._brightness_steps + + @property + def hue_steps(self): + """ + Color steps property. + :return: The number of color steps. + """ + return self._hue_steps + + @property + def saturation_steps(self): + """ + Saturation steps property. + :return: The number of saturation steps. + """ + return self._saturation_steps + + @property + def temperature_steps(self): + """ + Temperature steps property. + :return: The number of temperature steps. + """ + return self._temperature_steps diff --git a/limitlessled/group/commands/legacy.py b/limitlessled/group/commands/legacy.py new file mode 100644 index 0000000..a98b6c2 --- /dev/null +++ b/limitlessled/group/commands/legacy.py @@ -0,0 +1,185 @@ +""" Command sets for wifi bridge version 5 and lower. """ + + +import math + +from limitlessled.group.rgbw import RGBW +from limitlessled.group.white import WHITE +from limitlessled.group.commands import CommandSet, Command + + +class CommandSetLegacy(CommandSet): + """ Base command set for legacy wifi bridges. """ + + SUPPORTED_VERSIONS = [1, 2, 3, 4, 5] + SUFFIX_BYTE = 0x00 + BRIGHTNESS_OFFSET = 2 + BRIDGE_SHORT_VERSION_MIN = 3 + BRIDGE_LONG_BYTE = 0x55 + + def convert_brightness(self, brightness): + """ + Convert the brightness from decimal percent (0.0-1.0) + to byte representation for use in commands. + :param brightness: The brightness from in decimal percent (0.0-1.0). + :return: The brightness in byte representation. + """ + brightness = math.ceil(brightness * self.brightness_steps) + return brightness + self.BRIGHTNESS_OFFSET + + @staticmethod + def convert_hue(hue): + """ + Converts the hue from HSV color circle to the LimitlessLED color wheel. + :param hue: The hue in decimal percent (0.0-1.0). + :return: The hue regarding the LimitlessLED color wheel. + """ + hue = hue * -1 + 1 + (2.0/3.0) # RGB -> BGR + + return int(math.floor((hue % 1) * 256)) + + def _build_command(self, cmd_1, cmd_2=None, + select=False, select_command=None): + """ + Constructs the complete command. + :param cmd_1: Light command 1. + :param cmd_2: Light command 2. + :param select: If command requires selection. + :param select_command: Selection command bytes. + :return: The complete command. + """ + if cmd_2 is None: + cmd_2 = self.SUFFIX_BYTE + cmd = [cmd_1, cmd_2] + + if self._bridge.version < self.BRIDGE_SHORT_VERSION_MIN: + cmd.append(self.BRIDGE_LONG_BYTE) + + return Command(cmd, self._group_number, select, select_command) + + +class CommandSetWhiteLegacy(CommandSetLegacy): + """ Command set for white led light connected to legacy wifi bridge. """ + + SUPPORTED_LED_TYPES = [WHITE] + ON_BYTES = [0x38, 0x3D, 0x37, 0x32] + OFF_BYTES = [0x3B, 0x33, 0x3A, 0x36] + BRIGHTNESS_STEPS = 10 + TEMPERATURE_STEPS = 10 + + def __init__(self, bridge, group_number): + """ + Initializes the command set. + :param bridge: The bridge the leds are connected to. + :param group_number: The group number. + """ + super().__init__(bridge, group_number, self.BRIGHTNESS_STEPS, + temperature_steps=self.TEMPERATURE_STEPS) + + def on(self): + """ + Build command for turning the led on. + :return: The command. + """ + return self._build_command(self.ON_BYTES[self._group_number - 1]) + + def off(self): + """ + Build command for turning the led off. + :return: The command. + """ + return self._build_command(self.OFF_BYTES[self._group_number - 1]) + + def dimmer(self): + """ + Build command for setting the brightness one step dimmer. + :return: The command. + """ + return self._build_command(0x34, select=True, select_command=self.on()) + + def brighter(self): + """ + Build command for setting the brightness one step brighter. + :return: The command. + """ + return self._build_command(0x3C, select=True, select_command=self.on()) + + def cooler(self): + """ + Build command for setting the temperature one step cooler. + :return: The command. + """ + return self._build_command(0x3F, select=True, select_command=self.on()) + + def warmer(self): + """ + Build command for setting the temperature one step warmer. + :return: The command. + """ + return self._build_command(0x3E, select=True, select_command=self.on()) + + +class CommandSetRgbwLegacy(CommandSetLegacy): + """ Command set for RGBW led light connected to legacy wifi bridge. """ + + SUPPORTED_LED_TYPES = [RGBW] + HUE_STEPS = 255 + BRIGHTNESS_STEPS = 25 + + def __init__(self, bridge, group_number): + """ + Initializes the command set. + :param bridge: The bridge the leds are connected to. + :param group_number: The group number. + """ + super().__init__(bridge, group_number, self.BRIGHTNESS_STEPS, + hue_steps=self.HUE_STEPS) + + def on(self): + """ + Build command for turning the led on. + :return: The command. + """ + return self._build_command(self._offset(0x45)) + + def off(self): + """ + Build command for turning the led off. + :return: The command. + """ + return self._build_command(self._offset(0x46)) + + def white(self): + """ + Build command for turning the led into white mode. + :return: The command. + """ + return self._build_command(self._offset(0xC5), + select=True, select_command=self.on()) + + def hue(self, hue): + """ + Build command for setting the hue of the led. + :param hue: Value to set (0.0-1.0). + :return: The command. + """ + return self._build_command(0x40, self.convert_hue(hue), + select=True, select_command=self.on()) + + def brightness(self, brightness): + """ + Build command for setting the brightness of the led. + :param brightness: Value to set (0.0-1.0). + :return: The command. + """ + return self._build_command(0x4E, self.convert_brightness(brightness), + select=True, select_command=self.on()) + + def _offset(self, byte): + """ Calcuate group command offset. + + :param byte: Base byte. + :returns: Appropriate byte for group. + """ + index = self._group_number - 1 + return byte + (index * 2) diff --git a/limitlessled/group/commands/v6.py b/limitlessled/group/commands/v6.py new file mode 100644 index 0000000..6f0a225 --- /dev/null +++ b/limitlessled/group/commands/v6.py @@ -0,0 +1,350 @@ +""" Command sets for wifi bridge version 6. """ + + +import math + +from limitlessled.group.rgbw import RGBW, BRIDGE_LED +from limitlessled.group.rgbww import RGBWW +from limitlessled.group.white import WHITE +from limitlessled.group.commands import CommandSet, Command + + +class CommandSetV6(CommandSet): + """ Base command set for wifi bridge v6. """ + + SUPPORTED_VERSIONS = [6] + PASSWORD_BYTE1 = 0x00 + PASSWORD_BYTE2 = 0x00 + MAX_HUE = 0xFF + MAX_SATURATION = 0x64 + MAX_BRIGHTNESS = 0x64 + MAX_TEMPERATURE = 0x64 + + def __init__(self, bridge, group_number, remote_style, + brightness_steps=None, hue_steps=None, + saturation_steps=None, temperature_steps=None): + """ + Initialize the command set. + :param bridge: The bridge the leds are connected to. + :param group_number: The group number. + :param remote_style: The remote style of the device to control. + :param brightness_steps: The number of brightness steps. + :param hue_steps: The number of color steps. + :param saturation_steps: The number of saturation steps + :param temperature_steps: The number of temperature steps. + """ + brightness_steps = brightness_steps or self.MAX_BRIGHTNESS + 1 + hue_steps = hue_steps or self.MAX_HUE + 1 + saturation_steps = saturation_steps or self.MAX_SATURATION + 1 + temperature_steps = temperature_steps or self.MAX_TEMPERATURE + 1 + super().__init__(bridge, group_number, brightness_steps, + hue_steps=hue_steps, + saturation_steps=saturation_steps, + temperature_steps=temperature_steps) + self._remote_style = remote_style + + def convert_brightness(self, brightness): + """ + Convert the brightness from decimal percent (0.0-1.0) + to byte representation for use in commands. + :param brightness: The brightness from in decimal percent (0.0-1.0). + :return: The brightness in byte representation. + """ + return math.ceil(brightness * self.MAX_BRIGHTNESS) + + def convert_saturation(self, saturation): + """ + Convert the saturation from decimal percent (0.0-1.0) + to byte representation for use in commands. + :param saturation: The saturation from in decimal percent (0.0-1.0). + 1.0 is the maximum saturation where no white leds will be on. 0.0 is no + saturation. + :return: The saturation in byte representation. + """ + + saturation_inverted = 1 - saturation + return math.ceil(saturation_inverted * self.MAX_SATURATION) + + def convert_temperature(self, temperature): + """ + Convert the temperature from decimal percent (0.0-1.0) + to byte representation for use in commands. + :param temperature: The temperature from in decimal percent (0.0-1.0). + :return: The temperature in byte representation. + """ + return math.ceil(temperature * self.MAX_TEMPERATURE) + + def convert_hue(self, hue, legacy_color_wheel=False): + """ + Converts the hue from HSV color circle to the LimitlessLED color wheel. + :param hue: The hue in decimal percent (0.0-1.0). + :param legacy_color_wheel: Whether or not use the old color wheel. + :return: The hue regarding the LimitlessLED color wheel. + """ + hue = math.ceil(hue * self.MAX_HUE) + if legacy_color_wheel: + hue = (176 - hue) % (self.MAX_HUE + 1) + hue = (self.MAX_HUE - hue - 0x37) % (self.MAX_HUE + 1) + else: + hue += 10 # The color wheel for RGBWW bulbs seems to be shifted + + return hue % (self.MAX_HUE + 1) + + def _build_command(self, cmd_1, cmd_2): + """ + Constructs the complete command. + :param cmd_1: Light command 1. + :param cmd_2: Light command 2. + :return: The complete command. + """ + wb1 = self._bridge.wb1 + wb2 = self._bridge.wb2 + sn = self._bridge.sn + + preamble = [0x80, 0x00, 0x00, 0x00, 0x11, wb1, wb2, 0x00, sn, 0x00] + cmd = [0x31, self.PASSWORD_BYTE1, self.PASSWORD_BYTE2, + self._remote_style, cmd_1, cmd_2, cmd_2, cmd_2, cmd_2] + zone_selector = [self._group_number, 0x00] + checksum = sum(cmd + zone_selector) & 0xFF + + return Command(preamble + cmd + zone_selector + [checksum], + self._group_number) + + +class CommandSetBridgeLightV6(CommandSetV6): + """ Command set for bridge light of wifi bridge v6. """ + + SUPPORTED_LED_TYPES = [BRIDGE_LED] + REMOTE_STYLE = 0x00 + + def __init__(self, bridge, group_number): + """ + Initializes the command set. + :param bridge: The bridge the leds are connected to. + :param group_number: The group number. + """ + super().__init__(bridge, group_number, self.REMOTE_STYLE) + + def on(self): + """ + Build command for turning the led on. + :return: The command. + """ + return self._build_command(0x03, 0x03) + + def off(self): + """ + Build command for turning the led off. + :return: The command. + """ + return self._build_command(0x03, 0x04) + + def white(self): + """ + Build command for turning the led into white mode. + :return: The command. + """ + return self._build_command(0x03, 0x05) + + def hue(self, hue): + """ + Build command for setting the hue of the led. + :param hue: Value to set (0.0-1.0). + :return: The command. + """ + return self._build_command(0x01, self.convert_hue(hue)) + + def brightness(self, brightness): + """ + Build command for setting the brightness of the led. + :param brightness: Value to set (0.0-1.0). + :return: The command. + """ + return self._build_command(0x02, self.convert_brightness(brightness)) + + +class CommandSetWhiteV6(CommandSetV6): + """ Command set for white led light connected to wifi bridge v6. """ + + SUPPORTED_LED_TYPES = [WHITE] + REMOTE_STYLE = 0x08 + + def __init__(self, bridge, group_number): + """ + Initializes the command set. + :param bridge: The bridge the leds are connected to. + :param group_number: The group number. + """ + super().__init__(bridge, group_number, self.REMOTE_STYLE) + + def on(self): + """ + Build command for turning the led on. + :return: The command. + """ + return self._build_command(0x04, 0x01) + + def off(self): + """ + Build command for turning the led off. + :return: The command. + """ + return self._build_command(0x04, 0x02) + + def night_light(self): + """ + Build command for turning the led into night light mode. + :return: The command. + """ + return self._build_command(0x04, 0x05) + + def brightness(self, brightness): + """ + Build command for setting the brightness of the led. + :param brightness: Value to set (0.0-1.0). + :return: The command. + """ + return self._build_command(0x03, self.convert_brightness(brightness)) + + def temperature(self, temperature): + """ + Build command for setting the temperature of the led. + :param temperature: Value to set (0.0-1.0). + :return: The command. + """ + return self._build_command(0x05, temperature) + + +class CommandSetRgbwV6(CommandSetV6): + """ Command set for RGBW led light connected to wifi bridge v6. """ + + SUPPORTED_LED_TYPES = [RGBW] + REMOTE_STYLE = 0x07 + + def __init__(self, bridge, group_number): + """ + Initializes the command set. + :param bridge: The bridge the leds are connected to. + :param group_number: The group number. + """ + super().__init__(bridge, group_number, self.REMOTE_STYLE) + + def on(self): + """ + Build command for turning the led on. + :return: The command. + """ + return self._build_command(0x03, 0x01) + + def off(self): + """ + Build command for turning the led off. + :return: The command. + """ + return self._build_command(0x03, 0x02) + + def night_light(self): + """ + Build command for turning the led into night light mode. + :return: The command. + """ + return self._build_command(0x03, 0x06) + + def white(self): + """ + Build command for turning the led into white mode. + :return: The command. + """ + return self._build_command(0x03, 0x05) + + def hue(self, hue): + """ + Build command for setting the hue of the led. + :param hue: Value to set (0.0-1.0). + :return: The command. + """ + return self._build_command(0x01, self.convert_hue(hue, True)) + + def brightness(self, brightness): + """ + Build command for setting the brightness of the led. + :param brightness: Value to set (0.0-1.0). + :return: The command. + """ + return self._build_command(0x02, self.convert_brightness(brightness)) + + +class CommandSetRgbwwV6(CommandSetV6): + """ Command set for RGBWW led light connected to wifi bridge v6. """ + + SUPPORTED_LED_TYPES = [RGBWW] + REMOTE_STYLE = 0x08 + + def __init__(self, bridge, group_number): + """ + Initializes the command set. + :param bridge: The bridge the leds are connected to. + :param group_number: The group number. + """ + super().__init__(bridge, group_number, self.REMOTE_STYLE) + + def on(self): + """ + Build command for turning the led on. + :return: The command. + """ + return self._build_command(0x04, 0x01) + + def off(self): + """ + Build command for turning the led off. + :return: The command. + """ + return self._build_command(0x04, 0x02) + + def night_light(self): + """ + Build command for turning the led into night light mode. + :return: The command. + """ + return self._build_command(0x04, 0x05) + + def white(self, temperature=1): + """ + Build command for turning the led into white mode. + :param: The temperature to set. + :return: The command. + """ + return self.temperature(temperature) + + def hue(self, hue): + """ + Build command for setting the hue of the led. + :param hue: Value to set (0.0-1.0). + :return: The command. + """ + return self._build_command(0x01, self.convert_hue(hue)) + + def saturation(self, saturation): + """ + Build command for setting the saturation of the led. + :param saturation: Value to set (0.0-1.0). + :return: The command. + """ + return self._build_command(0x02, self.convert_saturation(saturation)) + + def brightness(self, brightness): + """ + Build command for setting the brightness of the led. + :param brightness: Value to set (0.0-1.0). + :return: The command. + """ + return self._build_command(0x03, self.convert_brightness(brightness)) + + def temperature(self, temperature): + """ + Build command for setting the temperature of the led. + :param temperature: Value to set (0.0-1.0). + :return: The command. + """ + return self._build_command(0x05, self.convert_temperature(temperature)) diff --git a/limitlessled/group/rgbw.py b/limitlessled/group/rgbw.py index b6de520..7b26111 100644 --- a/limitlessled/group/rgbw.py +++ b/limitlessled/group/rgbw.py @@ -3,65 +3,31 @@ import math import time -from colorsys import rgb_to_hsv, hsv_to_rgb - from limitlessled import Color, util from limitlessled.group import Group, rate -from limitlessled.util import steps +from limitlessled.util import steps, hue_of_color RGBW = 'rgbw' -HUE_STEPS = 255 -BRIGHTNESS_STEPS = 25 -ON_BYTE = 0x45 -OFF_BYTE = 0x46 +BRIDGE_LED = 'bridge-led' RGB_WHITE = Color(255, 255, 255) -def offset(byte, index): - """ Calcuate group command offset. - - :param byte: Base byte. - :param index: Group index. - :returns: Appropriate byte for group. - """ - return byte + (index * 2) - - class RgbwGroup(Group): """ RGBW LimitlessLED group. """ - def __init__(self, bridge, number, name): + def __init__(self, bridge, number, name, led_type=RGBW): """ Initialize RGBW group. :param bridge: Associated bridge. :param number: Group number (1-4). :param name: Group name. + :param led_type: The type of the led. (RGBW or BRIDGE_LED) """ - super().__init__(bridge, number, name) + super().__init__(bridge, number, name, led_type) + self._hue = 0 self._color = RGB_WHITE - @property - def on(self): - return super(RgbwGroup, self).on - - @on.setter - def on(self, state): - """ Turn on or off. - - :param state: True (on) or False (off). - """ - self._on = state - byte = OFF_BYTE - if state: - byte = ON_BYTE - cmd = [offset(byte, self._index), 0x00] - self.send(cmd) - - def get_select_cmd(self): - """ Get selection command bytes. """ - return [offset(ON_BYTE, self._index), 0x00] - @property def color(self): """ Color property. @@ -82,15 +48,13 @@ def color(self, color): self.white() return self._color = color - hue = util.rgb_to_hue(*color) - cmd = [0x40, hue] - self.send(cmd, select=True) + self.hue = hue_of_color(color) def white(self): """ Set color to white. """ self._color = RGB_WHITE - cmd = [offset(0xC5, self._index), 0x00] - self.send(cmd, select=True) + cmd = self.command_set.white() + self.send(cmd) @property def brightness(self): @@ -110,16 +74,36 @@ def brightness(self, brightness): raise ValueError("Brightness must be a percentage " "represented as decimal 0-1.0") self._brightness = brightness - actual = math.ceil(brightness * BRIGHTNESS_STEPS) + 2 - cmd = [0x4E, actual] - self.send(cmd, select=True) + cmd = self.command_set.brightness(brightness) + self.send(cmd) + + @property + def hue(self): + """ Hue property. + + :returns: Hue. + """ + return self._hue + + @hue.setter + def hue(self, hue): + """ Set the group hue. + + :param hue: Hue in decimal percent (0.0-1.0). + """ + if hue < 0 or hue > 1: + raise ValueError("Hue must be a percentage " + "represented as decimal 0-1.0") + self._hue = hue + cmd = self.command_set.hue(hue) + self.send(cmd) def transition(self, duration, color=None, brightness=None): """ Transition wrapper. Short-circuit transition as necessary. - :param duation: Time to transition. + :param duration: Time to transition. :param color: Transition to this color. :param brightness: Transition to this brightness. """ @@ -142,50 +126,47 @@ def transition(self, duration, color=None, brightness=None): if color != self.color or brightness != self.brightness: if color is None and brightness == self.brightness: return - self._transition(duration, color, brightness) + self._transition(duration, hue_of_color(color), brightness) @rate(wait=0.025, reps=1) - def _transition(self, duration, color, brightness): + def _transition(self, duration, hue, brightness): """ Transition. :param duration: Time to transition. - :param color: Transition to this color. + :param hue: Transition to this hue. :param brightness: Transition to this brightness. """ # Calculate brightness steps. b_steps = 0 if brightness is not None: b_steps = steps(self.brightness, - brightness, BRIGHTNESS_STEPS) + brightness, self.command_set.brightness_steps) b_start = self.brightness - # Calculate color steps. - c_steps = 0 - if color is not None: - c_steps = abs(util.rgb_to_hue(*self.color) - - util.rgb_to_hue(*color)) - c_start = rgb_to_hsv(*self._color) - c_end = rgb_to_hsv(*color) + # Calculate hue steps. + h_steps = 0 + if hue is not None: + h_steps = steps(self.hue, + hue, self.command_set.hue_steps) + h_start = self.hue # Compute ideal step amount (at least one). - total = max(c_steps + b_steps, 1) + total_steps = max(b_steps, h_steps, 1) + total_commands = b_steps + h_steps # Calculate wait. - wait = self._wait(duration, total) + wait = self._wait(duration, total_steps, total_commands) # Scale down steps if no wait time. if wait == 0: - total = self._scaled_steps(duration, total, total) + b_steps, h_steps = self._scale_steps(duration, total_commands, + b_steps, h_steps) + total_steps = max(b_steps, h_steps, 1) # Perform transition. - j = 0 - for i in range(total): + for i in range(total_steps): # Brightness. - if (b_steps > 0 - and i % math.ceil(total/b_steps) == 0): - j += 1 - self.brightness = util.transition(j, b_steps, + if b_steps > 0 and i % math.ceil(total_steps/b_steps) == 0: + self.brightness = util.transition(i, total_steps, b_start, brightness) - # Color. - elif c_steps > 0: - rgb = hsv_to_rgb(*util.transition3(i - j + 1, - total - b_steps, - c_start, c_end)) - self.color = Color(*rgb) + # Hue. + if h_steps > 0 and i % math.ceil(total_steps/h_steps) == 0: + self.hue = util.transition(i, total_steps, + h_start, hue) # Wait. time.sleep(wait) diff --git a/limitlessled/group/rgbww.py b/limitlessled/group/rgbww.py new file mode 100644 index 0000000..fe79cc8 --- /dev/null +++ b/limitlessled/group/rgbww.py @@ -0,0 +1,260 @@ +""" RGBW LimitlessLED group. """ + +import math +import time + +from limitlessled import Color, util +from limitlessled.group import Group, rate +from limitlessled.util import steps, hue_of_color, saturation_of_color, to_rgb + + +RGBWW = 'rgbww' +RGB_WHITE = Color(255, 255, 255) + + +class RgbwwGroup(Group): + """ RGBW LimitlessLED group. """ + + def __init__(self, bridge, number, name): + """ Initialize RGBW group. + + :param bridge: Associated bridge. + :param number: Group number (1-4). + :param name: Group name. + """ + super().__init__(bridge, number, name, RGBWW) + self._saturation = 0 + self._hue = 0 + self._temperature = 0.5 + self._color = RGB_WHITE + + @property + def color(self): + """ Color property. + + :returns: Color. + """ + return self._color + + @color.setter + def color(self, color): + """ Set group color. + + Color is set on a best-effort basis. + + :param color: RGB color tuple. + """ + if color == RGB_WHITE: + self.white() + return + self._color = color + self.hue = hue_of_color(color) + self.saturation = saturation_of_color(color) + + def white(self): + """ Set color to white. """ + self._color = RGB_WHITE + cmd = self.command_set.white(self.temperature) + self.send(cmd) + + @property + def brightness(self): + """ Brightness property. + + :returns: Brightness. + """ + return self._brightness + + @brightness.setter + def brightness(self, brightness): + """ Set the group brightness. + + :param brightness: Brightness in decimal percent (0.0-1.0). + """ + if brightness < 0 or brightness > 1: + raise ValueError("Brightness must be a percentage " + "represented as decimal 0-1.0") + self._brightness = brightness + cmd = self.command_set.brightness(brightness) + self.send(cmd) + + @property + def hue(self): + """ Hue property. + + :returns: Hue. + """ + return self._hue + + @hue.setter + def hue(self, hue): + """ Set the group hue. + + :param hue: Hue in decimal percent (0.0-1.0). + """ + if hue < 0 or hue > 1: + raise ValueError("Hue must be a percentage " + "represented as decimal 0-1.0") + self._hue = hue + self._update_color() + cmd = self.command_set.hue(hue) + self.send(cmd) + + @property + def saturation(self): + """ Saturation property. + + :returns: Saturation. + """ + return self._saturation + + @saturation.setter + def saturation(self, saturation): + """ Set the group saturation. + + :param saturation: Saturation in decimal percent (0.0-1.0). + """ + if saturation < 0 or saturation > 1: + raise ValueError("Saturation must be a percentage " + "represented as decimal 0-1.0") + self._saturation = saturation + self._update_color() + if saturation == 0: + self.white() + else: + cmd = self.command_set.saturation(saturation) + self.send(cmd) + + def _update_color(self): + """ Update the color property from hue and saturation values. + """ + self._color = to_rgb(self.hue, self.saturation) + + @property + def temperature(self): + """ Temperature property. + + :returns: Temperature (0.0-1.0) + """ + return self._temperature + + @temperature.setter + def temperature(self, temperature): + """ Set the temperature. + + :param temperature: Value to set (0.0-1.0). + """ + if temperature < 0 or temperature > 1: + raise ValueError("Temperature must be a percentage " + "represented as decimal 0-1.0") + self._temperature = temperature + cmd = self.command_set.temperature(temperature) + self.send(cmd) + + def transition(self, duration, + color=None, brightness=None, temperature=None): + """ Transition wrapper. + + Short-circuit transition as necessary. + + :param duration: Time to transition. + :param color: Transition to this color. + :param brightness: Transition to this brightness. + :param temperature: Transition to this temperature. + """ + if color and temperature is not None: + raise ValueError("Cannot transition to color and temperature " + "simultaneously.") + + # Transition to white immediately. + if color == RGB_WHITE: + self.white() + # Transition away from white immediately. + elif self.color == RGB_WHITE and color is not None: + self.color = color + # Transition immediately if duration is zero. + if duration == 0: + if brightness is not None: + self.brightness = brightness + if color: + self.color = color + if temperature is not None: + self.temperature = temperature + return + # Perform transition + if color and color != self.color: + self._transition(duration, brightness, + hue=hue_of_color(color), + saturation=saturation_of_color(color)) + elif temperature != self.temperature: + self._transition(duration, brightness, temperature=temperature) + elif brightness != self.brightness: + self._transition(duration, brightness) + + @rate(wait=0.025, reps=1) + def _transition(self, duration, brightness, + hue=None, saturation=None, temperature=None): + """ Transition. + + :param duration: Time to transition. + :param brightness: Transition to this brightness. + :param hue: Transition to this hue. + :param saturation: Transition to this saturation. + :param temperature: Transition to this temperature. + """ + # Calculate brightness steps. + b_steps = 0 + if brightness is not None: + b_steps = steps(self.brightness, + brightness, self.command_set.brightness_steps) + b_start = self.brightness + # Calculate hue steps. + h_steps = 0 + if hue is not None: + h_steps = steps(self.hue, + hue, self.command_set.hue_steps) + h_start = self.hue + # Calculate saturation steps. + s_steps = 0 + if saturation is not None: + s_steps = steps(self.saturation, + saturation, self.command_set.saturation_steps) + s_start = self.saturation + # Calculate temperature steps. + t_steps = 0 + if temperature is not None: + t_steps = steps(self.temperature, + temperature, self.command_set.temperature_steps) + t_start = self.temperature + # Compute ideal step amount (at least one). + total_steps = max(b_steps, h_steps, s_steps, t_steps, 1) + total_commands = b_steps + h_steps + s_steps + t_steps + # Calculate wait. + wait = self._wait(duration, total_steps, total_commands) + # Scale down steps if no wait time. + if wait == 0: + scaled_steps = self._scale_steps(duration, total_commands, b_steps, + h_steps, s_steps, t_steps) + b_steps, h_steps, s_steps, t_steps = scaled_steps + total_steps = max(b_steps, h_steps, s_steps, t_steps, 1) + # Perform transition. + for i in range(total_steps): + # Brightness. + if b_steps > 0 and i % math.ceil(total_steps/b_steps) == 0: + self.brightness = util.transition(i, total_steps, + b_start, brightness) + # Hue. + if h_steps > 0 and i % math.ceil(total_steps/h_steps) == 0: + self.hue = util.transition(i, total_steps, + h_start, hue) + # Saturation. + if s_steps > 0 and i % math.ceil(total_steps/s_steps) == 0: + self.saturation = util.transition(i, total_steps, + s_start, saturation) + # Temperature. + if t_steps > 0 and i % math.ceil(total_steps/t_steps) == 0: + self.temperature = util.transition(i, total_steps, + t_start, temperature) + + # Wait. + time.sleep(wait) diff --git a/limitlessled/group/white.py b/limitlessled/group/white.py index d5f59e8..870edab 100644 --- a/limitlessled/group/white.py +++ b/limitlessled/group/white.py @@ -7,9 +7,6 @@ from limitlessled.util import steps WHITE = 'white' -STEPS = 10 -ON_BYTES = [0x38, 0x3D, 0x37, 0x32] -OFF_BYTES = [0x3B, 0x33, 0x3A, 0x36] class WhiteGroup(Group): @@ -25,34 +22,9 @@ def __init__(self, bridge, number, name): :param number: Group number (1-4). :param name: Group name. """ - super().__init__(bridge, number, name) + super().__init__(bridge, number, name, WHITE) self._temperature = 0.5 - @property - def on(self): - """ On/off property. - - :returns: On/off state. - """ - return super(WhiteGroup, self).on - - @on.setter - def on(self, state): - """ Turn on or off. - - :param state: True (on) or False (off). - """ - self._on = state - byte = OFF_BYTES - if state: - byte = ON_BYTES - cmd = [byte[self._index], 0x00] - self.send(cmd) - - def get_select_cmd(self): - """ Get selection command bytes. """ - return [ON_BYTES[self._index], 0x00] - @property def brightness(self): """ Brightness property. @@ -67,9 +39,14 @@ def brightness(self, brightness): :param brightness: Value to set (0.0-1.0). """ - self._setter('_brightness', brightness, - self._dimmest, self._brightest, - self._to_brightness) + try: + cmd = self.command_set.brightness(brightness) + self.send(cmd) + self._brightness = brightness + except AttributeError: + self._setter('_brightness', brightness, + self._dimmest, self._brightest, + self._to_brightness) @property def temperature(self): @@ -85,9 +62,14 @@ def temperature(self, temperature): :param temperature: Value to set (0.0-1.0). """ - self._setter('_temperature', temperature, - self._coolest, self._warmest, - self._to_temperature) + try: + cmd = self.command_set.temperature(temperature) + self.send(cmd) + self._temperature = temperature + except AttributeError: + self._setter('_temperature', temperature, + self._coolest, self._warmest, + self._to_temperature) def transition(self, duration, brightness=None, temperature=None): """ Transition wrapper. @@ -122,29 +104,31 @@ def _transition(self, duration, brightness, temperature): # Compute ideal step amount. b_steps = 0 if brightness is not None: - b_steps = steps(self.brightness, brightness, STEPS) + b_steps = steps(self.brightness, brightness, + self.command_set.brightness_steps) t_steps = 0 if temperature is not None: - t_steps = steps(self.temperature, temperature, STEPS) - total = b_steps + t_steps - # Compute wait. - wait = self._wait(duration, total) + t_steps = steps(self.temperature, temperature, + self.command_set.temperature_steps) + # Compute ideal step amount (at least one). + total_steps = max(b_steps, t_steps, 1) + total_commands = b_steps + t_steps + # Calculate wait. + wait = self._wait(duration, total_steps, total_commands) # Scale down steps if no wait time. if wait == 0: - b_steps = self._scaled_steps(duration, b_steps, total) - t_steps = self._scaled_steps(duration, t_steps, total) - total = b_steps + t_steps + b_steps, t_steps = self._scale_steps(duration, total_commands, + b_steps, t_steps) + total_steps = max(b_steps, t_steps, 1) # Perform transition. - j = 0 - for i in range(total): + for i in range(total_steps): # Brightness. - if b_steps > 0 and i % (total / b_steps) == 0: - j += 1 - self.brightness = util.transition(j, b_steps, + if b_steps > 0 and i % (total_steps / b_steps) == 0: + self.brightness = util.transition(i, total_steps, b_start, brightness) # Temperature. elif t_steps > 0: - self.temperature = util.transition(i - j + 1, t_steps, + self.temperature = util.transition(i, total_steps, t_start, temperature) # Wait. time.sleep(wait) @@ -174,6 +158,7 @@ def _to_brightness(self, brightness): :param brightness: Get to this brightness. """ self._to_value(self._brightness, brightness, + self.command_set.brightness_steps, self._dimmer, self._brighter) def _to_temperature(self, temperature): @@ -182,18 +167,20 @@ def _to_temperature(self, temperature): :param temperature: Get to this temperature. """ self._to_value(self._temperature, temperature, + self.command_set.temperature_steps, self._cooler, self._warmer) @rate(reps=1) - def _to_value(self, current, target, step_down, step_up): + def _to_value(self, current, target, max_steps, step_down, step_up): """ Step to a value :param current: Current value. :param target: Target value. + :param max_steps: Maximum number of steps. :param step_down: Down function. :param step_up: Up function. """ - for _ in range(steps(current, target, STEPS)): + for _ in range(steps(current, target, max_steps)): if (current - target) > 0: step_down() else: @@ -202,39 +189,43 @@ def _to_value(self, current, target, step_down, step_up): @rate(wait=0.025, reps=2) def _brightest(self): """ Group as bright as possible. """ - for _ in range(steps(self.brightness, 1.0, STEPS)): + for _ in range(steps(self.brightness, 1.0, + self.command_set.brightness_steps)): self._brighter() @rate(wait=0.025, reps=2) def _dimmest(self): """ Group brightness as dim as possible. """ - for _ in range(steps(self.brightness, 0.0, STEPS)): + for _ in range(steps(self.brightness, 0.0, + self.command_set.brightness_steps)): self._dimmer() @rate(wait=0.025, reps=2) def _warmest(self): """ Group temperature as warm as possible. """ - for _ in range(steps(self.temperature, 1.0, STEPS)): + for _ in range(steps(self.temperature, 1.0, + self.command_set.temperature_steps)): self._warmer() @rate(wait=0.025, reps=2) def _coolest(self): """ Group temperature as cool as possible. """ - for _ in range(steps(self.temperature, 0.0, STEPS)): + for _ in range(steps(self.temperature, 0.0, + self.command_set.temperature_steps)): self._cooler() def _brighter(self): """ One step brighter. """ - self.send([0x3C, 0x00], select=True) + self.send(self.command_set.brighter()) def _dimmer(self): """ One step dimmer. """ - self.send([0x34, 0x00], select=True) + self.send(self.command_set.dimmer()) def _warmer(self): """ One step warmer. """ - self.send([0x3E, 0x00], select=True) + self.send(self.command_set.warmer()) def _cooler(self): """ One step cooler. """ - self.send([0x3F, 0x00], select=True) + self.send(self.command_set.cooler()) diff --git a/limitlessled/pipeline.py b/limitlessled/pipeline.py index 100b449..0a66e64 100644 --- a/limitlessled/pipeline.py +++ b/limitlessled/pipeline.py @@ -153,6 +153,10 @@ def _execute_stage(self, index, stage, stop): self._group.on = True elif stage.name == 'off': self._group.on = False + elif stage.name == 'hue': + self._group.hue = stage.args[0] + elif stage.name == 'saturation': + self._group.saturation = stage.args[0] elif stage.name == 'color': self._group.color = Color(*stage.args) elif stage.name == 'brightness': diff --git a/limitlessled/util.py b/limitlessled/util.py index a2d94ff..f4b78f0 100644 --- a/limitlessled/util.py +++ b/limitlessled/util.py @@ -1,28 +1,36 @@ """ Utility functions. """ -import math -from colorsys import rgb_to_hls +from colorsys import rgb_to_hsv, hsv_to_rgb +from limitlessled import Color -def rgb_to_hue(red, green, blue): - """ Convert RGB color to hue value. - 0% lightness or 100% lightness (white & black), - (255, 255, 255) & (0,0,0) aren't representable. +def hue_of_color(color): + """ + Gets the hue of a color. + :param color: The RGB color tuple. + :return: The hue of the color (0.0-1.0). + """ + return rgb_to_hsv(*[x / 255 for x in color])[0] + + +def saturation_of_color(color): + """ + Gets the saturation of a color. + :param color: The RGB color tuple. + :return: The saturation of the color (0.0-1.0). + """ + return rgb_to_hsv(*[x / 255 for x in color])[1] - Also note that the LimitlessLED color spectrum - starts at blue. - :param red: Red value (0-255). - :param green: Green value (0-255). - :param blue: Blue value (0-255). - :returns: Hue value (0-255). +def to_rgb(hue, saturation): """ - hue = rgb_to_hls(red / 255, green / 255, blue / 255)[0] \ - * -1 \ - + 1 \ - + (2.0/3.0) # RGB -> BGR - return int(math.floor((hue % 1) * 256)) + Converts hue and saturation to RGB color. + :param hue: The hue of the color. + :param saturation: The saturation of the color. + :return: The RGB color tuple. + """ + return Color(*hsv_to_rgb(hue, saturation, 1)) def transition(value, maximum, start, end): @@ -37,22 +45,6 @@ def transition(value, maximum, start, end): return round(start + (end - start) * value / maximum, 2) -def transition3(value, maximum, start, end): - """ Transition three values. - - :param value: Current iteration. - :param maximum: Maximum number of iterations. - :param start: Start tuple. - :param end: End tuple. - :returns: Transitional tuple. - """ - return ( - transition(value, maximum, start[0], end[0]), - transition(value, maximum, start[1], end[1]), - transition(value, maximum, start[2], end[2]) - ) - - def steps(current, target, max_steps): """ Steps between two values. diff --git a/setup.py b/setup.py index 6627974..763290b 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ license='MIT', author='happyleaves', author_email='happyleaves.tfr@gmail.com', - packages=['limitlessled', 'limitlessled.group'], + packages=['limitlessled', 'limitlessled.group', 'limitlessled.group.commands'], install_requires=[], classifiers=[ 'License :: OSI Approved :: MIT License',