From e3f682c7d3bb67d57187e50b1f22583bd5e39a75 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 21 Apr 2017 07:46:12 +0200 Subject: [PATCH] LIFX light effects (#7145) * Refactor into find_hsbk This will be useful for new methods that also have to find passed in colors. * Add AwaitAioLIFX This encapsulates the callback and Event that aiolifx needs and thus avoids an explosion of those when new calls are added. The refresh_state is now generally useful, so move it into its own method. * Initial effects support for LIFX These effects are useful as notifications. They mimic the breathe and pulse effects from the LIFX HTTP API: https://api.developer.lifx.com/docs/breathe-effect https://api.developer.lifx.com/docs/pulse-effect However, this implementation runs locally with the LIFX LAN protocol. * Saturate LIFX no color value Now the color is "full saturation, no brightness". This avoids a lot of temporary white when fading from the "no color" value and into a real color. * Organize LIFX effects in classes This is to move the setup/restore away from the actual effect, making it quite simple to add additional effects. * Stop running LIFX effects on conflicting service calls Turning the light on/off or starting a new effect will now stop the running effect. * Present default LIFX effects as light.turn_on effects This makes the effects (with default parameters) easily accessible from the UI. * Add LIFX colorloop effect This cycles the HSV colors, so that is added as an internal way to set a color. * Move lifx to its own package and split effects into a separate file * Always show LIFX light name in logs The name is actually the easiest way to identify a bulb so just using it as a fallback was a bit odd. * Compact effect getter * Always use full brightness for random flash color This is a stopgap. When a bit more infrastructure is in place, the intention is to turn the current hue some degrees. This will guarantee a flash color that is both unlike the current color and unlike white. * Clear effects concurrently We have to wait for the bulbs, so let us wait for all of them at once. * Add lifx_effect_stop The colorloop effect is most impressive if run on many lights. Testing this has revealed the need for an easy way to stop effects on all lights and return to the initial state of each bulb. This new call does just that. Calling turn_on/turn_off could also stop the effect but that would not restore the initial state. * Always calculate the initial effect color To fade nicely from power off, the breathe effect needs to keep an unchanging hue. So give up on using a static start color and just find the correct hue from the target color. The colorloop effect can start from anything but we use a random color just to keep things a little interesting during power on. * Fix lint * Update .coveragerc --- .coveragerc | 2 +- .../light/{lifx.py => lifx/__init__.py} | 166 ++++++--- .../components/light/lifx/effects.py | 338 ++++++++++++++++++ .../components/light/lifx/services.yaml | 99 +++++ 4 files changed, 559 insertions(+), 46 deletions(-) rename homeassistant/components/light/{lifx.py => lifx/__init__.py} (74%) create mode 100644 homeassistant/components/light/lifx/effects.py create mode 100644 homeassistant/components/light/lifx/services.yaml diff --git a/.coveragerc b/.coveragerc index 623e985c8f5d33..c8e59e55357db8 100644 --- a/.coveragerc +++ b/.coveragerc @@ -231,7 +231,7 @@ omit = homeassistant/components/light/flux_led.py homeassistant/components/light/hue.py homeassistant/components/light/hyperion.py - homeassistant/components/light/lifx.py + homeassistant/components/light/lifx/*.py homeassistant/components/light/lifx_legacy.py homeassistant/components/light/limitlessled.py homeassistant/components/light/mystrom.py diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx/__init__.py similarity index 74% rename from homeassistant/components/light/lifx.py rename to homeassistant/components/light/lifx/__init__.py index 945c163435b36e..7c7bdd4eeea6cb 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx/__init__.py @@ -10,19 +10,24 @@ import sys from functools import partial from datetime import timedelta +import async_timeout import voluptuous as vol from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_TRANSITION, + Light, PLATFORM_SCHEMA, ATTR_BRIGHTNESS, ATTR_COLOR_NAME, ATTR_RGB_COLOR, + ATTR_COLOR_TEMP, ATTR_TRANSITION, ATTR_EFFECT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, - SUPPORT_TRANSITION, Light, PLATFORM_SCHEMA) + SUPPORT_TRANSITION, SUPPORT_EFFECT) from homeassistant.util.color import ( color_temperature_mired_to_kelvin, color_temperature_kelvin_to_mired) from homeassistant import util from homeassistant.core import callback from homeassistant.helpers.event import async_track_point_in_utc_time import homeassistant.helpers.config_validation as cv +import homeassistant.util.color as color_util + +from . import effects as lifx_effects _LOGGER = logging.getLogger(__name__) @@ -35,18 +40,19 @@ CONF_SERVER = 'server' +ATTR_HSBK = 'hsbk' + BYTE_MAX = 255 SHORT_MAX = 65535 SUPPORT_LIFX = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR | - SUPPORT_TRANSITION) + SUPPORT_TRANSITION | SUPPORT_EFFECT) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_SERVER, default='0.0.0.0'): cv.string, }) -# pylint: disable=unused-argument @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Setup the LIFX platform.""" @@ -65,6 +71,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): local_addr=(server_addr, UDP_BROADCAST_PORT)) hass.async_add_job(coro) + + lifx_effects.setup(hass, lifx_manager) + return True @@ -104,10 +113,42 @@ def unregister(self, device): entity = self.entities[device.mac_addr] _LOGGER.debug("%s unregister", entity.who) entity.device = None - entity.updated_event.set() self.hass.async_add_job(entity.async_update_ha_state()) +class AwaitAioLIFX: + """Wait for an aiolifx callback and return the message.""" + + def __init__(self, light): + """Initialize the wrapper.""" + self.light = light + self.device = None + self.message = None + self.event = asyncio.Event() + + @callback + def callback(self, device, message): + """Callback that aiolifx invokes when the response is received.""" + self.device = device + self.message = message + self.event.set() + + @asyncio.coroutine + def wait(self, method): + """Call an aiolifx method and wait for its response or a timeout.""" + self.event.clear() + method(self.callback) + + while self.light.available and not self.event.is_set(): + try: + with async_timeout.timeout(1.0, loop=self.light.hass.loop): + yield from self.event.wait() + except asyncio.TimeoutError: + pass + + return self.message + + def convert_rgb_to_hsv(rgb): """Convert Home Assistant RGB values to HSV values.""" red, green, blue = [_ / BYTE_MAX for _ in rgb] @@ -125,8 +166,8 @@ class LIFXLight(Light): def __init__(self, device): """Initialize the light.""" self.device = device - self.updated_event = asyncio.Event() self.blocker = None + self.effect_data = None self.postponed_update = None self._name = device.label self.set_power(device.power_level) @@ -145,10 +186,10 @@ def name(self): @property def who(self): """Return a string identifying the device.""" + ip_addr = '-' if self.device: - return self.device.ip_addr[0] - else: - return "(%s)" % self.name + ip_addr = self.device.ip_addr[0] + return "%s (%s)" % (ip_addr, self.name) @property def rgb_color(self): @@ -178,11 +219,21 @@ def is_on(self): _LOGGER.debug("is_on: %d", self._power) return self._power != 0 + @property + def effect(self): + """Return the currently running effect.""" + return self.effect_data.effect.name if self.effect_data else None + @property def supported_features(self): """Flag supported features.""" return SUPPORT_LIFX + @property + def effect_list(self): + """Return the list of supported effects.""" + return lifx_effects.effect_list() + @callback def update_after_transition(self, now): """Request new status after completion of the last transition.""" @@ -213,36 +264,18 @@ def update_later(self, when): @asyncio.coroutine def async_turn_on(self, **kwargs): """Turn the device on.""" + yield from self.stop_effect() + + if ATTR_EFFECT in kwargs: + yield from lifx_effects.default_effect(self, **kwargs) + return + if ATTR_TRANSITION in kwargs: fade = int(kwargs[ATTR_TRANSITION] * 1000) else: fade = 0 - changed_color = False - - if ATTR_RGB_COLOR in kwargs: - hue, saturation, brightness = \ - convert_rgb_to_hsv(kwargs[ATTR_RGB_COLOR]) - changed_color = True - else: - hue = self._hue - saturation = self._sat - brightness = self._bri - - if ATTR_BRIGHTNESS in kwargs: - brightness = kwargs[ATTR_BRIGHTNESS] * (BYTE_MAX + 1) - changed_color = True - else: - brightness = self._bri - - if ATTR_COLOR_TEMP in kwargs: - kelvin = int(color_temperature_mired_to_kelvin( - kwargs[ATTR_COLOR_TEMP])) - changed_color = True - else: - kelvin = self._kel - - hsbk = [hue, saturation, brightness, kelvin] + hsbk, changed_color = self.find_hsbk(**kwargs) _LOGGER.debug("turn_on: %s (%d) %d %d %d %d %d", self.who, self._power, fade, *hsbk) @@ -263,6 +296,8 @@ def async_turn_on(self, **kwargs): @asyncio.coroutine def async_turn_off(self, **kwargs): """Turn the device off.""" + yield from self.stop_effect() + if ATTR_TRANSITION in kwargs: fade = int(kwargs[ATTR_TRANSITION] * 1000) else: @@ -274,22 +309,63 @@ def async_turn_off(self, **kwargs): if fade < BULB_LATENCY: self.set_power(0) - @callback - def got_color(self, device, msg): - """Callback that gets current power/color status.""" - self.set_power(device.power_level) - self.set_color(*device.color) - self._name = device.label - self.updated_event.set() - @asyncio.coroutine def async_update(self): """Update bulb status (if it is available).""" _LOGGER.debug("%s async_update", self.who) if self.available and self.blocker is None: - self.updated_event.clear() - self.device.get_color(self.got_color) - yield from self.updated_event.wait() + yield from self.refresh_state() + + @asyncio.coroutine + def stop_effect(self): + """Stop the currently running effect (if any).""" + if self.effect_data: + yield from self.effect_data.effect.async_restore(self) + + @asyncio.coroutine + def refresh_state(self): + """Ask the device about its current state and update our copy.""" + msg = yield from AwaitAioLIFX(self).wait(self.device.get_color) + if msg is not None: + self.set_power(self.device.power_level) + self.set_color(*self.device.color) + self._name = self.device.label + + def find_hsbk(self, **kwargs): + """Find the desired color from a number of possible inputs.""" + changed_color = False + + hsbk = kwargs.pop(ATTR_HSBK, None) + if hsbk is not None: + return [hsbk, True] + + color_name = kwargs.pop(ATTR_COLOR_NAME, None) + if color_name is not None: + kwargs[ATTR_RGB_COLOR] = color_util.color_name_to_rgb(color_name) + + if ATTR_RGB_COLOR in kwargs: + hue, saturation, brightness = \ + convert_rgb_to_hsv(kwargs[ATTR_RGB_COLOR]) + changed_color = True + else: + hue = self._hue + saturation = self._sat + brightness = self._bri + + if ATTR_BRIGHTNESS in kwargs: + brightness = kwargs[ATTR_BRIGHTNESS] * (BYTE_MAX + 1) + changed_color = True + else: + brightness = self._bri + + if ATTR_COLOR_TEMP in kwargs: + kelvin = int(color_temperature_mired_to_kelvin( + kwargs[ATTR_COLOR_TEMP])) + changed_color = True + else: + kelvin = self._kel + + return [[hue, saturation, brightness, kelvin], changed_color] def set_power(self, power): """Set power state value.""" diff --git a/homeassistant/components/light/lifx/effects.py b/homeassistant/components/light/lifx/effects.py new file mode 100644 index 00000000000000..e2ba0a73534809 --- /dev/null +++ b/homeassistant/components/light/lifx/effects.py @@ -0,0 +1,338 @@ +"""Support for light effects for the LIFX light platform.""" +import logging +import asyncio +import random +from os import path + +import voluptuous as vol + +from homeassistant.components.light import ( + DOMAIN, ATTR_BRIGHTNESS, ATTR_COLOR_NAME, ATTR_RGB_COLOR, ATTR_EFFECT) +from homeassistant.config import load_yaml_config_file +from homeassistant.const import (ATTR_ENTITY_ID) +from homeassistant.helpers.service import extract_entity_ids +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +SERVICE_EFFECT_BREATHE = 'lifx_effect_breathe' +SERVICE_EFFECT_PULSE = 'lifx_effect_pulse' +SERVICE_EFFECT_COLORLOOP = 'lifx_effect_colorloop' +SERVICE_EFFECT_STOP = 'lifx_effect_stop' + +ATTR_POWER_ON = 'power_on' +ATTR_PERIOD = 'period' +ATTR_CYCLES = 'cycles' +ATTR_SPREAD = 'spread' +ATTR_CHANGE = 'change' + +# aiolifx waveform modes +WAVEFORM_SINE = 1 +WAVEFORM_PULSE = 4 + +LIFX_EFFECT_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(ATTR_POWER_ON, default=True): cv.boolean, +}) + +LIFX_EFFECT_BREATHE_SCHEMA = LIFX_EFFECT_SCHEMA.extend({ + ATTR_BRIGHTNESS: vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)), + ATTR_COLOR_NAME: cv.string, + ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)), + vol.Coerce(tuple)), + vol.Optional(ATTR_PERIOD, default=1.0): vol.All(vol.Coerce(float), + vol.Range(min=0.05)), + vol.Optional(ATTR_CYCLES, default=1.0): vol.All(vol.Coerce(float), + vol.Range(min=1)), +}) + +LIFX_EFFECT_PULSE_SCHEMA = LIFX_EFFECT_BREATHE_SCHEMA + +LIFX_EFFECT_COLORLOOP_SCHEMA = LIFX_EFFECT_SCHEMA.extend({ + ATTR_BRIGHTNESS: vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)), + vol.Optional(ATTR_PERIOD, default=60): vol.All(vol.Coerce(float), + vol.Clamp(min=1)), + vol.Optional(ATTR_CHANGE, default=20): vol.All(vol.Coerce(float), + vol.Clamp(min=0, max=360)), + vol.Optional(ATTR_SPREAD, default=30): vol.All(vol.Coerce(float), + vol.Clamp(min=0, max=360)), +}) + +LIFX_EFFECT_STOP_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(ATTR_POWER_ON, default=False): cv.boolean, +}) + + +def setup(hass, lifx_manager): + """Register the LIFX effects as hass service calls.""" + @asyncio.coroutine + def async_service_handle(service): + """Internal func for applying a service.""" + entity_ids = extract_entity_ids(hass, service) + if entity_ids: + devices = [entity for entity in lifx_manager.entities.values() + if entity.entity_id in entity_ids] + else: + devices = list(lifx_manager.entities.values()) + + if devices: + yield from start_effect(hass, devices, + service.service, **service.data) + + descriptions = load_yaml_config_file( + path.join(path.dirname(__file__), 'services.yaml')) + + hass.services.async_register( + DOMAIN, SERVICE_EFFECT_BREATHE, async_service_handle, + descriptions.get(SERVICE_EFFECT_BREATHE), + schema=LIFX_EFFECT_BREATHE_SCHEMA) + + hass.services.async_register( + DOMAIN, SERVICE_EFFECT_PULSE, async_service_handle, + descriptions.get(SERVICE_EFFECT_PULSE), + schema=LIFX_EFFECT_PULSE_SCHEMA) + + hass.services.async_register( + DOMAIN, SERVICE_EFFECT_COLORLOOP, async_service_handle, + descriptions.get(SERVICE_EFFECT_COLORLOOP), + schema=LIFX_EFFECT_COLORLOOP_SCHEMA) + + hass.services.async_register( + DOMAIN, SERVICE_EFFECT_STOP, async_service_handle, + descriptions.get(SERVICE_EFFECT_STOP), + schema=LIFX_EFFECT_STOP_SCHEMA) + + +@asyncio.coroutine +def start_effect(hass, devices, service, **data): + """Start a light effect.""" + tasks = [] + for light in devices: + tasks.append(hass.async_add_job(light.stop_effect())) + yield from asyncio.wait(tasks, loop=hass.loop) + + if service in SERVICE_EFFECT_BREATHE: + effect = LIFXEffectBreathe(hass, devices) + elif service in SERVICE_EFFECT_PULSE: + effect = LIFXEffectPulse(hass, devices) + elif service == SERVICE_EFFECT_COLORLOOP: + effect = LIFXEffectColorloop(hass, devices) + elif service == SERVICE_EFFECT_STOP: + effect = LIFXEffectStop(hass, devices) + + hass.async_add_job(effect.async_perform(**data)) + + +@asyncio.coroutine +def default_effect(light, **kwargs): + """Start an effect with default parameters.""" + service = kwargs[ATTR_EFFECT] + data = { + ATTR_ENTITY_ID: light.entity_id, + } + if service in (SERVICE_EFFECT_BREATHE, SERVICE_EFFECT_PULSE): + data[ATTR_RGB_COLOR] = [ + random.randint(1, 127), + random.randint(1, 127), + random.randint(1, 127), + ] + data[ATTR_BRIGHTNESS] = 255 + yield from light.hass.services.async_call(DOMAIN, service, data) + + +def effect_list(): + """Return the list of supported effects.""" + return [ + SERVICE_EFFECT_COLORLOOP, + SERVICE_EFFECT_BREATHE, + SERVICE_EFFECT_PULSE, + SERVICE_EFFECT_STOP, + ] + + +class LIFXEffectData(object): + """Structure describing a running effect.""" + + def __init__(self, effect, power, color): + """Initialize data structure.""" + self.effect = effect + self.power = power + self.color = color + + +class LIFXEffect(object): + """Representation of a light effect running on a number of lights.""" + + def __init__(self, hass, lights): + """Initialize the effect.""" + self.hass = hass + self.lights = lights + + @asyncio.coroutine + def async_perform(self, **kwargs): + """Do common setup and play the effect.""" + yield from self.async_setup(**kwargs) + yield from self.async_play(**kwargs) + + @asyncio.coroutine + def async_setup(self, **kwargs): + """Prepare all lights for the effect.""" + for light in self.lights: + yield from light.refresh_state() + if not light.device: + self.lights.remove(light) + else: + light.effect_data = LIFXEffectData( + self, light.is_on, light.device.color) + + # Temporarily turn on power for the effect to be visible + if kwargs[ATTR_POWER_ON] and not light.is_on: + hsbk = self.from_poweroff_hsbk(light, **kwargs) + light.device.set_color(hsbk) + light.device.set_power(True) + + # pylint: disable=no-self-use + @asyncio.coroutine + def async_play(self, **kwargs): + """Play the effect.""" + yield None + + @asyncio.coroutine + def async_restore(self, light): + """Restore to the original state (if we are still running).""" + if light.effect_data: + if light.effect_data.effect == self: + if light.device and not light.effect_data.power: + light.device.set_power(False) + yield from asyncio.sleep(0.5) + if light.device: + light.device.set_color(light.effect_data.color) + yield from asyncio.sleep(0.5) + light.effect_data = None + self.lights.remove(light) + + def from_poweroff_hsbk(self, light, **kwargs): + """The initial color when starting from a powered off state.""" + return None + + +class LIFXEffectBreathe(LIFXEffect): + """Representation of a breathe effect.""" + + def __init__(self, hass, lights): + """Initialize the breathe effect.""" + super(LIFXEffectBreathe, self).__init__(hass, lights) + self.name = SERVICE_EFFECT_BREATHE + self.waveform = WAVEFORM_SINE + + @asyncio.coroutine + def async_play(self, **kwargs): + """Play the effect on all lights.""" + for light in self.lights: + self.hass.async_add_job(self.async_light_play(light, **kwargs)) + + @asyncio.coroutine + def async_light_play(self, light, **kwargs): + """Play a light effect on the bulb.""" + period = kwargs[ATTR_PERIOD] + cycles = kwargs[ATTR_CYCLES] + hsbk, _ = light.find_hsbk(**kwargs) + + # Start the effect + args = { + 'transient': 1, + 'color': hsbk, + 'period': int(period*1000), + 'cycles': cycles, + 'duty_cycle': 0, + 'waveform': self.waveform, + } + light.device.set_waveform(args) + + # Wait for completion and restore the initial state + yield from asyncio.sleep(period*cycles) + yield from self.async_restore(light) + + def from_poweroff_hsbk(self, light, **kwargs): + """Initial color is the target color, but no brightness.""" + hsbk, _ = light.find_hsbk(**kwargs) + return [hsbk[0], hsbk[1], 0, hsbk[2]] + + +class LIFXEffectPulse(LIFXEffectBreathe): + """Representation of a pulse effect.""" + + def __init__(self, hass, lights): + """Initialize the pulse effect.""" + super(LIFXEffectPulse, self).__init__(hass, lights) + self.name = SERVICE_EFFECT_PULSE + self.waveform = WAVEFORM_PULSE + + +class LIFXEffectColorloop(LIFXEffect): + """Representation of a colorloop effect.""" + + def __init__(self, hass, lights): + """Initialize the colorloop effect.""" + super(LIFXEffectColorloop, self).__init__(hass, lights) + self.name = SERVICE_EFFECT_COLORLOOP + + @asyncio.coroutine + def async_play(self, **kwargs): + """Play the effect on all lights.""" + period = kwargs[ATTR_PERIOD] + spread = kwargs[ATTR_SPREAD] + change = kwargs[ATTR_CHANGE] + direction = 1 if random.randint(0, 1) else -1 + + # Random start + hue = random.randint(0, 359) + + while self.lights: + hue = (hue + direction*change) % 360 + + random.shuffle(self.lights) + lhue = hue + + transition = int(1000 * random.uniform(period/2, period)) + for light in self.lights: + if spread > 0: + transition = int(1000 * random.uniform(period/2, period)) + + if ATTR_BRIGHTNESS in kwargs: + brightness = int(65535/255*kwargs[ATTR_BRIGHTNESS]) + else: + brightness = light.effect_data.color[2] + + hsbk = [ + int(65535/359*lhue), + int(random.uniform(0.8, 1.0)*65535), + brightness, + 4000, + ] + light.device.set_color(hsbk, None, transition) + + # Adjust the next light so the full spread is used + if len(self.lights) > 1: + lhue = (lhue + spread/(len(self.lights)-1)) % 360 + + yield from asyncio.sleep(period) + + def from_poweroff_hsbk(self, light, **kwargs): + """Start from a random hue.""" + return [random.randint(0, 65535), 65535, 0, 4000] + + +class LIFXEffectStop(LIFXEffect): + """A no-op effect, but starting it will stop an existing effect.""" + + def __init__(self, hass, lights): + """Initialize the stop effect.""" + super(LIFXEffectStop, self).__init__(hass, lights) + self.name = SERVICE_EFFECT_STOP + + @asyncio.coroutine + def async_perform(self, **kwargs): + """Do nothing.""" + yield None diff --git a/homeassistant/components/light/lifx/services.yaml b/homeassistant/components/light/lifx/services.yaml new file mode 100644 index 00000000000000..1b34c54f253891 --- /dev/null +++ b/homeassistant/components/light/lifx/services.yaml @@ -0,0 +1,99 @@ +lifx_effect_breathe: + description: Run a breathe effect by fading to a color and back. + + fields: + entity_id: + description: Name(s) of entities to run the effect on + example: 'light.kitchen' + + brightness: + description: Number between 0..255 indicating brightness when the effect peaks + example: 120 + + color_name: + description: A human readable color name + example: 'red' + + rgb_color: + description: Color for the fade in RGB-format + example: '[255, 100, 100]' + + period: + description: Duration of the effect in seconds (default 1.0) + example: 3 + + cycles: + description: Number of times the effect should run (default 1.0) + example: 2 + + power_on: + description: Powered off lights are temporarily turned on during the effect (default True) + example: False + +lifx_effect_pulse: + description: Run a flash effect by changing to a color and back. + + fields: + entity_id: + description: Name(s) of entities to run the effect on + example: 'light.kitchen' + + brightness: + description: Number between 0..255 indicating brightness of the temporary color + example: 120 + + color_name: + description: A human readable color name + example: 'red' + + rgb_color: + description: The temporary color in RGB-format + example: '[255, 100, 100]' + + period: + description: Duration of the effect in seconds (default 1.0) + example: 3 + + cycles: + description: Number of times the effect should run (default 1.0) + example: 2 + + power_on: + description: Powered off lights are temporarily turned on during the effect (default True) + example: False + +lifx_effect_colorloop: + description: Run an effect with looping colors. + + fields: + entity_id: + description: Name(s) of entities to run the effect on + example: 'light.disco1, light.disco2, light.disco3' + + brightness: + description: Number between 0..255 indicating brightness of the effect. Leave this out to maintain the current brightness of each participating light + example: 120 + + period: + description: Duration between color changes (deafult 60) + example: 180 + + change: + description: Hue movement per period, in degrees on a color wheel (default 20) + example: 45 + + spread: + description: Maximum hue difference between participating lights, in degrees on a color wheel (default 30) + example: 0 + + power_on: + description: Powered off lights are temporarily turned on during the effect (default True) + example: False + +lifx_effect_stop: + description: Stop a running effect. + + fields: + entity_id: + description: Name(s) of entities to stop effects on. Leave out to stop effects everywhere. + example: 'light.bedroom'