diff --git a/limitlessled/bridge.py b/limitlessled/bridge.py index fd0ac68..c40572a 100755 --- a/limitlessled/bridge.py +++ b/limitlessled/bridge.py @@ -11,6 +11,7 @@ from limitlessled.group.rgbw import RgbwGroup, RGBW, BRIDGE_LED from limitlessled.group.wrgb import WrgbGroup, WRGB from limitlessled.group.rgbww import RgbwwGroup, RGBWW +from limitlessled.group.eightzone import EightzoneGroup, EIGHTZONE from limitlessled.group.white import WhiteGroup, WHITE from limitlessled.group.dimmer import DimmerGroup, DIMMER @@ -38,13 +39,15 @@ 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`, `WRGB`, `RGBWW`, `WHITE`, `DIMMER` or `BRIDGE_LED`. + :param led_type: Either `RGBW`, `WRGB`, `RGBWW`, `WHITE`, `EIGHTZONE`, `DIMMER` or `BRIDGE_LED`. :returns: New group. """ 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 == EIGHTZONE: + return EightzoneGroup(bridge, number, name) elif led_type == WHITE: return WhiteGroup(bridge, number, name) elif led_type == DIMMER: diff --git a/limitlessled/group/commands/__init__.py b/limitlessled/group/commands/__init__.py index d6f27be..1adb9e1 100644 --- a/limitlessled/group/commands/__init__.py +++ b/limitlessled/group/commands/__init__.py @@ -15,12 +15,14 @@ def command_set_factory(bridge, group_number, led_type): from limitlessled.group.commands.v6 import ( CommandSetBridgeLightV6, CommandSetWhiteV6, CommandSetDimmerV6, CommandSetRgbwV6, - CommandSetRgbwwV6, CommandSetWrgbV6) + CommandSetRgbwwV6, CommandSetWrgbV6, + CommandSetEightzoneV6) command_sets = [CommandSetWhiteLegacy, CommandSetRgbwLegacy, CommandSetBridgeLightV6, CommandSetWhiteV6, CommandSetDimmerV6, CommandSetRgbwV6, - CommandSetRgbwwV6, CommandSetWrgbV6] + CommandSetRgbwwV6, CommandSetWrgbV6, + CommandSetEightzoneV6] try: cls = next(cs for cs in command_sets if bridge.version in cs.SUPPORTED_VERSIONS and diff --git a/limitlessled/group/commands/v6.py b/limitlessled/group/commands/v6.py index f512ebe..c77af57 100644 --- a/limitlessled/group/commands/v6.py +++ b/limitlessled/group/commands/v6.py @@ -5,6 +5,7 @@ from limitlessled.group.rgbw import RGBW, BRIDGE_LED from limitlessled.group.rgbww import RGBWW +from limitlessled.group.eightzone import EIGHTZONE from limitlessled.group.wrgb import WRGB from limitlessled.group.white import WHITE from limitlessled.group.dimmer import DIMMER @@ -542,3 +543,78 @@ def temperature(self, temperature): :return: The command. """ return self._build_command(0x05, self.convert_temperature(temperature)) + +class CommandSetEightzoneV6(CommandSetV6): + """ Command set for Eight Zones led light connected to wifi bridge v6. """ + + SUPPORTED_LED_TYPES = [EIGHTZONE] + REMOTE_STYLE = 0x0A + + def __init__(self, group_number): + """ + Initializes the command set. + :param group_number: The group number. + """ + super().__init__(group_number, self.REMOTE_STYLE) + + def on(self): + """ + Build command for turning the led on. + :return: The command. + """ + return self._build_command(0x06, 0x01) + + def off(self): + """ + Build command for turning the led off. + :return: The command. + """ + return self._build_command(0x06, 0x02) + + def night_light(self): + """ + Build command for turning the led into night light mode. + :return: The command. + """ + return self._build_command(0x06, 0x64) + + 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(0x03, 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(0x04, 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(0x02, self.convert_temperature(temperature)) + diff --git a/limitlessled/group/eightzone.py b/limitlessled/group/eightzone.py new file mode 100644 index 0000000..cd3b662 --- /dev/null +++ b/limitlessled/group/eightzone.py @@ -0,0 +1,263 @@ +""" 8 Zone 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 + + +EIGHTZONE = 'eightzone' +RGB_WHITE = Color(255, 255, 255) + + +class EightzoneGroup(Group): + """ Eight Zone LimitlessLED group. """ + + def __init__(self, bridge, number, name): + """ Initialize Eight Zone group. + + :param bridge: Associated bridge. + :param number: Group number (1-8). + :param name: Group name. + """ + super().__init__(bridge, number, name, EIGHTZONE) + 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. + """ + self._color = color + self.saturation = saturation_of_color(color) + if self.saturation != 0: + self.hue = hue_of_color(color) + + def white(self): + """ Set color to white. """ + self._color = RGB_WHITE + cmd = self.command_set.white(self.temperature) + self.send(cmd) + + def night_light(self): + """ Set night light mode. """ + cmd = self.command_set.night_light() + 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)