diff --git a/.coveragerc b/.coveragerc index 390db0f0157..cbc8f5c78a9 100644 --- a/.coveragerc +++ b/.coveragerc @@ -280,7 +280,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/__init__.py b/homeassistant/components/light/lifx.py similarity index 50% rename from homeassistant/components/light/lifx/__init__.py rename to homeassistant/components/light/lifx.py index 68d36058f8f..fc65399c80b 100644 --- a/homeassistant/components/light/lifx/__init__.py +++ b/homeassistant/components/light/lifx.py @@ -11,18 +11,19 @@ import math from os import path from functools import partial from datetime import timedelta -import async_timeout import voluptuous as vol from homeassistant.components.light import ( Light, DOMAIN, PLATFORM_SCHEMA, LIGHT_TURN_ON_SCHEMA, - ATTR_BRIGHTNESS, ATTR_RGB_COLOR, - ATTR_XY_COLOR, ATTR_COLOR_TEMP, ATTR_TRANSITION, ATTR_EFFECT, + ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_NAME, ATTR_RGB_COLOR, + ATTR_XY_COLOR, ATTR_COLOR_TEMP, ATTR_KELVIN, ATTR_TRANSITION, ATTR_EFFECT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, SUPPORT_XY_COLOR, SUPPORT_TRANSITION, SUPPORT_EFFECT, + VALID_BRIGHTNESS, VALID_BRIGHTNESS_PCT, preprocess_turn_on_alternatives) from homeassistant.config import load_yaml_config_file +from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_STOP from homeassistant import util from homeassistant.core import callback from homeassistant.helpers.event import async_track_point_in_utc_time @@ -30,34 +31,79 @@ from homeassistant.helpers.service import extract_entity_ids import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util -from . import effects as lifx_effects - _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['aiolifx==0.4.8'] +REQUIREMENTS = ['aiolifx==0.5.0', 'aiolifx_effects==0.1.0'] UDP_BROADCAST_PORT = 56700 -# Delay (in ms) expected for changes to take effect in the physical bulb -BULB_LATENCY = 500 - CONF_SERVER = 'server' -SERVICE_LIFX_SET_STATE = 'lifx_set_state' - -ATTR_HSBK = 'hsbk' -ATTR_INFRARED = 'infrared' -ATTR_POWER = 'power' - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_SERVER, default='0.0.0.0'): cv.string, }) +SERVICE_LIFX_SET_STATE = 'lifx_set_state' + +ATTR_INFRARED = 'infrared' +ATTR_POWER = 'power' + LIFX_SET_STATE_SCHEMA = LIGHT_TURN_ON_SCHEMA.extend({ ATTR_INFRARED: vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)), ATTR_POWER: cv.boolean, }) +SERVICE_EFFECT_PULSE = 'lifx_effect_pulse' +SERVICE_EFFECT_COLORLOOP = 'lifx_effect_colorloop' +SERVICE_EFFECT_STOP = 'lifx_effect_stop' + +ATTR_POWER_ON = 'power_on' +ATTR_MODE = 'mode' +ATTR_PERIOD = 'period' +ATTR_CYCLES = 'cycles' +ATTR_SPREAD = 'spread' +ATTR_CHANGE = 'change' + +PULSE_MODE_BLINK = 'blink' +PULSE_MODE_BREATHE = 'breathe' +PULSE_MODE_PING = 'ping' +PULSE_MODE_STROBE = 'strobe' +PULSE_MODE_SOLID = 'solid' + +PULSE_MODES = [PULSE_MODE_BLINK, PULSE_MODE_BREATHE, PULSE_MODE_PING, + PULSE_MODE_STROBE, PULSE_MODE_SOLID] + +LIFX_EFFECT_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(ATTR_POWER_ON, default=True): cv.boolean, +}) + +LIFX_EFFECT_PULSE_SCHEMA = LIFX_EFFECT_SCHEMA.extend({ + ATTR_BRIGHTNESS: VALID_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT, + ATTR_COLOR_NAME: cv.string, + ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)), + vol.Coerce(tuple)), + ATTR_COLOR_TEMP: vol.All(vol.Coerce(int), vol.Range(min=1)), + ATTR_KELVIN: vol.All(vol.Coerce(int), vol.Range(min=0)), + ATTR_PERIOD: vol.All(vol.Coerce(float), vol.Range(min=0.05)), + ATTR_CYCLES: vol.All(vol.Coerce(float), vol.Range(min=1)), + ATTR_MODE: vol.In(PULSE_MODES), +}) + +LIFX_EFFECT_COLORLOOP_SCHEMA = LIFX_EFFECT_SCHEMA.extend({ + ATTR_BRIGHTNESS: VALID_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT, + ATTR_PERIOD: vol.All(vol.Coerce(float), vol.Clamp(min=0.05)), + ATTR_CHANGE: vol.All(vol.Coerce(float), vol.Clamp(min=0, max=360)), + ATTR_SPREAD: vol.All(vol.Coerce(float), vol.Clamp(min=0, max=360)), + ATTR_TRANSITION: vol.All(vol.Coerce(float), vol.Range(min=0)), +}) + +LIFX_EFFECT_STOP_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, +}) + @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): @@ -71,27 +117,79 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): server_addr = config.get(CONF_SERVER) lifx_manager = LIFXManager(hass, async_add_devices) + lifx_discovery = aiolifx.LifxDiscovery(hass.loop, lifx_manager) coro = hass.loop.create_datagram_endpoint( - partial(aiolifx.LifxDiscovery, hass.loop, lifx_manager), - local_addr=(server_addr, UDP_BROADCAST_PORT)) + lambda: lifx_discovery, local_addr=(server_addr, UDP_BROADCAST_PORT)) hass.async_add_job(coro) - lifx_effects.setup(hass, lifx_manager) + @callback + def cleanup(event): + """Clean up resources.""" + lifx_discovery.cleanup() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup) return True +def find_hsbk(**kwargs): + """Find the desired color from a number of possible inputs.""" + hue, saturation, brightness, kelvin = [None]*4 + + preprocess_turn_on_alternatives(kwargs) + + if ATTR_RGB_COLOR in kwargs: + hue, saturation, brightness = \ + color_util.color_RGB_to_hsv(*kwargs[ATTR_RGB_COLOR]) + saturation = convert_8_to_16(saturation) + brightness = convert_8_to_16(brightness) + kelvin = 3500 + + if ATTR_XY_COLOR in kwargs: + hue, saturation = color_util.color_xy_to_hs(*kwargs[ATTR_XY_COLOR]) + saturation = convert_8_to_16(saturation) + kelvin = 3500 + + if ATTR_COLOR_TEMP in kwargs: + kelvin = int(color_util.color_temperature_mired_to_kelvin( + kwargs[ATTR_COLOR_TEMP])) + saturation = 0 + + if ATTR_BRIGHTNESS in kwargs: + brightness = convert_8_to_16(kwargs[ATTR_BRIGHTNESS]) + + hsbk = [hue, saturation, brightness, kelvin] + return None if hsbk == [None]*4 else hsbk + + +def merge_hsbk(base, change): + """Copy change on top of base, except when None.""" + if change is None: + return None + return list(map(lambda x, y: y if y is not None else x, base, change)) + + class LIFXManager(object): """Representation of all known LIFX entities.""" def __init__(self, hass, async_add_devices): """Initialize the light.""" + import aiolifx_effects self.entities = {} self.hass = hass self.async_add_devices = async_add_devices + self.effects_conductor = aiolifx_effects.Conductor(loop=hass.loop) + descriptions = load_yaml_config_file( + path.join(path.dirname(__file__), 'services.yaml')) + + self.register_set_state(descriptions) + self.register_effects(descriptions) + + def register_set_state(self, descriptions): + """Register the LIFX set_state service call.""" @asyncio.coroutine def async_service_handle(service): """Apply a service.""" @@ -99,22 +197,73 @@ class LIFXManager(object): for light in self.service_to_entities(service): if service.service == SERVICE_LIFX_SET_STATE: task = light.async_set_state(**service.data) - tasks.append(hass.async_add_job(task)) + tasks.append(self.hass.async_add_job(task)) if tasks: - yield from asyncio.wait(tasks, loop=hass.loop) + yield from asyncio.wait(tasks, loop=self.hass.loop) - descriptions = self.get_descriptions() - - hass.services.async_register( + self.hass.services.async_register( DOMAIN, SERVICE_LIFX_SET_STATE, async_service_handle, descriptions.get(SERVICE_LIFX_SET_STATE), schema=LIFX_SET_STATE_SCHEMA) - @staticmethod - def get_descriptions(): - """Load and return descriptions for our own service calls.""" - return load_yaml_config_file( - path.join(path.dirname(__file__), 'services.yaml')) + def register_effects(self, descriptions): + """Register the LIFX effects as hass service calls.""" + @asyncio.coroutine + def async_service_handle(service): + """Apply a service, i.e. start an effect.""" + entities = self.service_to_entities(service) + if entities: + yield from self.start_effect( + entities, service.service, **service.data) + + self.hass.services.async_register( + DOMAIN, SERVICE_EFFECT_PULSE, async_service_handle, + descriptions.get(SERVICE_EFFECT_PULSE), + schema=LIFX_EFFECT_PULSE_SCHEMA) + + self.hass.services.async_register( + DOMAIN, SERVICE_EFFECT_COLORLOOP, async_service_handle, + descriptions.get(SERVICE_EFFECT_COLORLOOP), + schema=LIFX_EFFECT_COLORLOOP_SCHEMA) + + self.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(self, entities, service, **kwargs): + """Start a light effect on entities.""" + import aiolifx_effects + devices = list(map(lambda l: l.device, entities)) + + if service == SERVICE_EFFECT_PULSE: + effect = aiolifx_effects.EffectPulse( + power_on=kwargs.get(ATTR_POWER_ON, None), + period=kwargs.get(ATTR_PERIOD, None), + cycles=kwargs.get(ATTR_CYCLES, None), + mode=kwargs.get(ATTR_MODE, None), + hsbk=find_hsbk(**kwargs), + ) + yield from self.effects_conductor.start(effect, devices) + elif service == SERVICE_EFFECT_COLORLOOP: + preprocess_turn_on_alternatives(kwargs) + + brightness = None + if ATTR_BRIGHTNESS in kwargs: + brightness = convert_8_to_16(kwargs[ATTR_BRIGHTNESS]) + + effect = aiolifx_effects.EffectColorloop( + power_on=kwargs.get(ATTR_POWER_ON, None), + period=kwargs.get(ATTR_PERIOD, None), + change=kwargs.get(ATTR_CHANGE, None), + spread=kwargs.get(ATTR_SPREAD, None), + transition=kwargs.get(ATTR_TRANSITION, None), + brightness=brightness, + ) + yield from self.effects_conductor.start(effect, devices) + elif service == SERVICE_EFFECT_STOP: + yield from self.effects_conductor.stop(devices) def service_to_entities(self, service): """Return the known devices that a service call mentions.""" @@ -148,7 +297,7 @@ class LIFXManager(object): @callback def ready(self, device, msg): """Handle the device once all data is retrieved.""" - entity = LIFXLight(device) + entity = LIFXLight(device, self.effects_conductor) _LOGGER.debug("%s register READY", entity.who) self.entities[device.mac_addr] = entity self.async_add_devices([entity]) @@ -182,16 +331,13 @@ class AwaitAioLIFX: @asyncio.coroutine def wait(self, method): - """Call an aiolifx method and wait for its response or a timeout.""" + """Call an aiolifx method and wait for its response.""" + self.device = None + self.message = None 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 + yield from self.event.wait() return self.message @@ -209,17 +355,13 @@ def convert_16_to_8(value): class LIFXLight(Light): """Representation of a LIFX light.""" - def __init__(self, device): + def __init__(self, device, effects_conductor): """Initialize the light.""" self.device = device + self.effects_conductor = effects_conductor self.registered = True self.product = device.product - self.blocker = None - self.effect_data = None self.postponed_update = None - self._name = device.label - self.set_power(device.power_level) - self.set_color(*device.color) @property def lifxwhite(self): @@ -235,7 +377,7 @@ class LIFXLight(Light): @property def name(self): """Return the name of the device.""" - return self._name + return self.device.label @property def who(self): @@ -248,21 +390,23 @@ class LIFXLight(Light): @property def rgb_color(self): """Return the RGB value.""" - _LOGGER.debug( - "rgb_color: [%d %d %d]", self._rgb[0], self._rgb[1], self._rgb[2]) - return self._rgb + hue, sat, bri, _ = self.device.color + + return color_util.color_hsv_to_RGB( + hue, convert_16_to_8(sat), convert_16_to_8(bri)) @property def brightness(self): """Return the brightness of this light between 0..255.""" - brightness = convert_16_to_8(self._bri) + brightness = convert_16_to_8(self.device.color[2]) _LOGGER.debug("brightness: %d", brightness) return brightness @property def color_temp(self): """Return the color temperature.""" - temperature = color_util.color_temperature_kelvin_to_mired(self._kel) + kelvin = self.device.color[3] + temperature = color_util.color_temperature_kelvin_to_mired(kelvin) _LOGGER.debug("color_temp: %d", temperature) return temperature @@ -290,13 +434,15 @@ class LIFXLight(Light): @property def is_on(self): """Return true if device is on.""" - _LOGGER.debug("is_on: %d", self._power) - return self._power != 0 + return self.device.power_level != 0 @property def effect(self): - """Return the currently running effect.""" - return self.effect_data.effect.name if self.effect_data else None + """Return the name of the currently running effect.""" + effect = self.effects_conductor.effect(self.device) + if effect: + return 'lifx_effect_' + effect.name + return None @property def supported_features(self): @@ -311,38 +457,35 @@ class LIFXLight(Light): @property def effect_list(self): - """Return the list of supported effects.""" - return lifx_effects.effect_list(self) + """Return the list of supported effects for this light.""" + if self.lifxwhite: + return [ + SERVICE_EFFECT_PULSE, + SERVICE_EFFECT_STOP, + ] + + return [ + SERVICE_EFFECT_COLORLOOP, + SERVICE_EFFECT_PULSE, + SERVICE_EFFECT_STOP, + ] @asyncio.coroutine def update_after_transition(self, now): """Request new status after completion of the last transition.""" self.postponed_update = None - yield from self.refresh_state() - yield from self.async_update_ha_state() - - @asyncio.coroutine - def unblock_updates(self, now): - """Allow async_update after the new state has settled on the bulb.""" - self.blocker = None - yield from self.refresh_state() + yield from self.async_update() yield from self.async_update_ha_state() def update_later(self, when): - """Block immediate update requests and schedule one for later.""" - if self.blocker: - self.blocker() - self.blocker = async_track_point_in_utc_time( - self.hass, self.unblock_updates, - util.dt.utcnow() + timedelta(milliseconds=BULB_LATENCY)) - + """Schedule an update requests when a transition is over.""" if self.postponed_update: self.postponed_update() self.postponed_update = None - if when > BULB_LATENCY: + if when > 0: self.postponed_update = async_track_point_in_utc_time( self.hass, self.update_after_transition, - util.dt.utcnow() + timedelta(milliseconds=when+BULB_LATENCY)) + util.dt.utcnow() + timedelta(milliseconds=when)) @asyncio.coroutine def async_turn_on(self, **kwargs): @@ -359,10 +502,10 @@ class LIFXLight(Light): @asyncio.coroutine def async_set_state(self, **kwargs): """Set a color on the light and turn it on/off.""" - yield from self.stop_effect() + yield from self.effects_conductor.stop([self.device]) if ATTR_EFFECT in kwargs: - yield from lifx_effects.default_effect(self, **kwargs) + yield from self.default_effect(**kwargs) return if ATTR_INFRARED in kwargs: @@ -377,124 +520,45 @@ class LIFXLight(Light): power_on = kwargs.get(ATTR_POWER, False) power_off = not kwargs.get(ATTR_POWER, True) - hsbk, changed_color = self.find_hsbk(**kwargs) - _LOGGER.debug("turn_on: %s (%d) %d %d %d %d %d", - self.who, self._power, fade, *hsbk) + hsbk = merge_hsbk(self.device.color, find_hsbk(**kwargs)) - if self._power == 0: + # Send messages, waiting for ACK each time + ack = AwaitAioLIFX(self).wait + bulb = self.device + + if not self.is_on: if power_off: - self.device.set_power(False, None, 0) - if changed_color: - self.device.set_color(hsbk, None, 0) + yield from ack(partial(bulb.set_power, False)) + if hsbk: + yield from ack(partial(bulb.set_color, hsbk)) if power_on: - self.device.set_power(True, None, fade) + yield from ack(partial(bulb.set_power, True, duration=fade)) else: if power_on: - self.device.set_power(True, None, 0) - if changed_color: - self.device.set_color(hsbk, None, fade) + yield from ack(partial(bulb.set_power, True)) + if hsbk: + yield from ack(partial(bulb.set_color, hsbk, duration=fade)) if power_off: - self.device.set_power(False, None, fade) + yield from ack(partial(bulb.set_power, False, duration=fade)) - if power_on: - self.update_later(0) - else: - self.update_later(fade) + # Avoid state ping-pong by holding off updates while the state settles + yield from asyncio.sleep(0.25) - if fade <= BULB_LATENCY: - if power_on: - self.set_power(1) - if power_off: - self.set_power(0) - if changed_color: - self.set_color(*hsbk) + # Schedule an update when the transition is complete + self.update_later(fade) + + @asyncio.coroutine + def default_effect(self, **kwargs): + """Start an effect with default parameters.""" + service = kwargs[ATTR_EFFECT] + data = { + ATTR_ENTITY_ID: self.entity_id, + } + yield from self.hass.services.async_call(DOMAIN, service, data) @asyncio.coroutine def async_update(self): - """Update bulb status (if it is available).""" + """Update bulb status.""" _LOGGER.debug("%s async_update", self.who) - if self.blocker is None: - 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.""" if self.available: - 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] - - preprocess_turn_on_alternatives(kwargs) - - if ATTR_RGB_COLOR in kwargs: - hue, saturation, brightness = \ - color_util.color_RGB_to_hsv(*kwargs[ATTR_RGB_COLOR]) - saturation = convert_8_to_16(saturation) - brightness = convert_8_to_16(brightness) - changed_color = True - else: - hue = self._hue - saturation = self._sat - brightness = self._bri - - if ATTR_XY_COLOR in kwargs: - hue, saturation = color_util.color_xy_to_hs(*kwargs[ATTR_XY_COLOR]) - saturation = convert_8_to_16(saturation) - changed_color = True - - # When color or temperature is set, use a default value for the other - if ATTR_COLOR_TEMP in kwargs: - kelvin = int(color_util.color_temperature_mired_to_kelvin( - kwargs[ATTR_COLOR_TEMP])) - if not changed_color: - saturation = 0 - changed_color = True - else: - if changed_color: - kelvin = 3500 - else: - kelvin = self._kel - - if ATTR_BRIGHTNESS in kwargs: - brightness = convert_8_to_16(kwargs[ATTR_BRIGHTNESS]) - changed_color = True - else: - brightness = self._bri - - return [[hue, saturation, brightness, kelvin], changed_color] - - def set_power(self, power): - """Set power state value.""" - _LOGGER.debug("set_power: %d", power) - self._power = (power != 0) - - def set_color(self, hue, sat, bri, kel): - """Set color state values.""" - self._hue = hue - self._sat = sat - self._bri = bri - self._kel = kel - - red, green, blue = color_util.color_hsv_to_RGB( - hue, convert_16_to_8(sat), convert_16_to_8(bri)) - - _LOGGER.debug("set_color: %d %d %d %d [%d %d %d]", - hue, sat, bri, kel, red, green, blue) - - self._rgb = [red, green, blue] + yield from AwaitAioLIFX(self).wait(self.device.get_color) diff --git a/homeassistant/components/light/lifx/effects.py b/homeassistant/components/light/lifx/effects.py deleted file mode 100644 index cdd5e44147a..00000000000 --- a/homeassistant/components/light/lifx/effects.py +++ /dev/null @@ -1,388 +0,0 @@ -"""Support for light effects for the LIFX light platform.""" -import logging -import asyncio -import random - -import voluptuous as vol - -from homeassistant.components.light import ( - DOMAIN, ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_NAME, - ATTR_RGB_COLOR, ATTR_COLOR_TEMP, ATTR_KELVIN, ATTR_EFFECT, ATTR_TRANSITION, - VALID_BRIGHTNESS, VALID_BRIGHTNESS_PCT) -from homeassistant.const import (ATTR_ENTITY_ID) -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_MODE = 'mode' -ATTR_SPREAD = 'spread' -ATTR_CHANGE = 'change' - -MODE_BLINK = 'blink' -MODE_BREATHE = 'breathe' -MODE_PING = 'ping' -MODE_STROBE = 'strobe' -MODE_SOLID = 'solid' - -MODES = [MODE_BLINK, MODE_BREATHE, MODE_PING, MODE_STROBE, MODE_SOLID] - -# aiolifx waveform modes -WAVEFORM_SINE = 1 -WAVEFORM_PULSE = 4 - -NEUTRAL_WHITE = 3500 - -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: VALID_BRIGHTNESS, - ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT, - ATTR_COLOR_NAME: cv.string, - ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)), - vol.Coerce(tuple)), - ATTR_COLOR_TEMP: vol.All(vol.Coerce(int), vol.Range(min=1)), - ATTR_KELVIN: vol.All(vol.Coerce(int), vol.Range(min=0)), - ATTR_PERIOD: vol.All(vol.Coerce(float), vol.Range(min=0.05)), - ATTR_CYCLES: vol.All(vol.Coerce(float), vol.Range(min=1)), -}) - -LIFX_EFFECT_PULSE_SCHEMA = LIFX_EFFECT_BREATHE_SCHEMA.extend({ - vol.Optional(ATTR_MODE, default=MODE_BLINK): vol.In(MODES), -}) - -LIFX_EFFECT_COLORLOOP_SCHEMA = LIFX_EFFECT_SCHEMA.extend({ - ATTR_BRIGHTNESS: VALID_BRIGHTNESS, - ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT, - vol.Optional(ATTR_PERIOD, default=60): - vol.All(vol.Coerce(float), vol.Clamp(min=0.05)), - 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)), - ATTR_TRANSITION: vol.All(vol.Coerce(float), vol.Range(min=0)), -}) - -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): - """Apply a service.""" - entities = lifx_manager.service_to_entities(service) - if entities: - yield from start_effect(hass, entities, - service.service, **service.data) - - descriptions = lifx_manager.get_descriptions() - - 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, - } - yield from light.hass.services.async_call(DOMAIN, service, data) - - -def effect_list(light): - """Return the list of supported effects for this light.""" - if light.lifxwhite: - return [ - SERVICE_EFFECT_BREATHE, - SERVICE_EFFECT_PULSE, - SERVICE_EFFECT_STOP, - ] - else: - 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: - # Remember the current state (as far as we know it) - yield from light.refresh_state() - 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 in self.lights: - self.lights.remove(light) - - if light.effect_data and light.effect_data.effect == self: - if not light.effect_data.power: - light.device.set_power(False) - yield from asyncio.sleep(0.5) - - light.device.set_color(light.effect_data.color) - yield from asyncio.sleep(0.5) - - light.effect_data = None - yield from light.refresh_state() - - def from_poweroff_hsbk(self, light, **kwargs): - """Return the color when starting from a powered off state.""" - return [random.randint(0, 65535), 65535, 0, NEUTRAL_WHITE] - - -class LIFXEffectPulse(LIFXEffect): - """Representation of a pulse effect.""" - - def __init__(self, hass, lights): - """Initialize the pulse effect.""" - super().__init__(hass, lights) - self.name = SERVICE_EFFECT_PULSE - - @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.""" - hsbk, color_changed = light.find_hsbk(**kwargs) - - if kwargs[ATTR_MODE] == MODE_STROBE: - # Strobe must flash from a dark color - light.device.set_color([0, 0, 0, NEUTRAL_WHITE]) - yield from asyncio.sleep(0.1) - default_period = 0.1 - default_cycles = 10 - else: - default_period = 1.0 - default_cycles = 1 - - period = kwargs.get(ATTR_PERIOD, default_period) - cycles = kwargs.get(ATTR_CYCLES, default_cycles) - - # Breathe has a special waveform - if kwargs[ATTR_MODE] == MODE_BREATHE: - waveform = WAVEFORM_SINE - else: - waveform = WAVEFORM_PULSE - - # Ping and solid have special duty cycles - if kwargs[ATTR_MODE] == MODE_PING: - ping_duration = int(5000 - min(2500, 300*period)) - duty_cycle = 2**15 - ping_duration - elif kwargs[ATTR_MODE] == MODE_SOLID: - duty_cycle = -2**15 - else: - duty_cycle = 0 - - # Set default effect color based on current setting - if not color_changed: - if kwargs[ATTR_MODE] == MODE_STROBE: - # Strobe: cold white - hsbk = [hsbk[0], 0, 65535, 5600] - elif light.lifxwhite or hsbk[1] < 65536/2: - # White: toggle brightness - hsbk[2] = 65535 if hsbk[2] < 65536/2 else 0 - else: - # Color: fully desaturate with full brightness - hsbk = [hsbk[0], 0, 65535, 4000] - - # Start the effect - args = { - 'transient': 1, - 'color': hsbk, - 'period': int(period*1000), - 'cycles': cycles, - 'duty_cycle': duty_cycle, - 'waveform': 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): - """Return the color is the target color, but no brightness.""" - hsbk, _ = light.find_hsbk(**kwargs) - return [hsbk[0], hsbk[1], 0, hsbk[2]] - - -class LIFXEffectBreathe(LIFXEffectPulse): - """Representation of a breathe effect.""" - - def __init__(self, hass, lights): - """Initialize the breathe effect.""" - super().__init__(hass, lights) - self.name = SERVICE_EFFECT_BREATHE - _LOGGER.warning("'lifx_effect_breathe' is deprecated. Please use " - "'lifx_effect_pulse' with 'mode: breathe'") - - @asyncio.coroutine - def async_perform(self, **kwargs): - """Prepare all lights for the effect.""" - kwargs[ATTR_MODE] = MODE_BREATHE - yield from super().async_perform(**kwargs) - - -class LIFXEffectColorloop(LIFXEffect): - """Representation of a colorloop effect.""" - - def __init__(self, hass, lights): - """Initialize the colorloop effect.""" - super().__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.uniform(0, 360) % 360 - - while self.lights: - hue = (hue + direction*change) % 360 - - random.shuffle(self.lights) - lhue = hue - - for light in self.lights: - if ATTR_TRANSITION in kwargs: - transition = int(1000*kwargs[ATTR_TRANSITION]) - elif light == self.lights[0] or 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/360*lhue), - int(random.uniform(0.8, 1.0)*65535), - brightness, - NEUTRAL_WHITE, - ] - 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) - - -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().__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 deleted file mode 100644 index 6028b0c4f9c..00000000000 --- a/homeassistant/components/light/lifx/services.yaml +++ /dev/null @@ -1,98 +0,0 @@ -lifx_set_state: - description: Set a color/brightness and possibliy turn the light on/off - - fields: - entity_id: - description: Name(s) of entities to set a state on - example: 'light.garage' - - '...': - description: All turn_on parameters can be used to specify a color - - infrared: - description: Automatic infrared level (0..255) when light brightness is low - example: 255 - - transition: - description: Duration in seconds it takes to get to the final state - example: 10 - - power: - description: Turn the light on (True) or off (False). Leave out to keep the power as it is. - example: True - - -lifx_effect_breathe: - description: Deprecated, use lifx_effect_pulse - -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' - - mode: - description: 'Decides how colors are changed. Possible values: blink, breathe, ping, strobe, solid' - example: strobe - - 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 and 255 indicating brightness of the effect. Leave this out to maintain the current brightness of each participating light - example: 120 - - period: - description: Duration (in seconds) between color changes (default 60) - example: 180 - - change: - description: Hue movement per period, in degrees on a color wheel (ranges from 0 to 360, default 20) - example: 45 - - spread: - description: Maximum hue difference between participating lights, in degrees on a color wheel (ranges from 0 to 360, 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' diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index 6ccd45dda66..ef99f18fb42 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -101,3 +101,98 @@ hue_activate_scene: scene_name: description: Name of hue scene from the hue app example: "Energize" + +lifx_set_state: + description: Set a color/brightness and possibliy turn the light on/off + + fields: + entity_id: + description: Name(s) of entities to set a state on + example: 'light.garage' + + '...': + description: All turn_on parameters can be used to specify a color + + infrared: + description: Automatic infrared level (0..255) when light brightness is low + example: 255 + + transition: + description: Duration in seconds it takes to get to the final state + example: 10 + + power: + description: Turn the light on (True) or off (False). Leave out to keep the power as it is. + example: True + +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' + + mode: + description: 'Decides how colors are changed. Possible values: blink, breathe, ping, strobe, solid' + example: strobe + + 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 and 255 indicating brightness of the effect. Leave this out to maintain the current brightness of each participating light + example: 120 + + period: + description: Duration (in seconds) between color changes (default 60) + example: 180 + + change: + description: Hue movement per period, in degrees on a color wheel (ranges from 0 to 360, default 20) + example: 45 + + spread: + description: Maximum hue difference between participating lights, in degrees on a color wheel (ranges from 0 to 360, 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' diff --git a/requirements_all.txt b/requirements_all.txt index ee2de706846..98f5078f9bc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -49,7 +49,10 @@ aiodns==1.1.1 aiohttp_cors==0.5.3 # homeassistant.components.light.lifx -aiolifx==0.4.8 +aiolifx==0.5.0 + +# homeassistant.components.light.lifx +aiolifx_effects==0.1.0 # homeassistant.components.scene.hunterdouglas_powerview aiopvapi==1.4