* Get full multizone state during registration We used to rely on the periodic update to get the state of each zone, only establishing the number of zones during registration. This resulted in errors if the current state was needed for a partial color change before the first async_update happened. Now we do a full update before adding the light. Thus async_update can no longer assume device.color_zones to be defined and must instead use the response message to decide the total number of zones. * Insist on getting the initial state If a response to the initial state query is lost we used to just carry on. This resulted in type errors when we next tried to access the undefined state. After this commit the light is not added before we have the full state. This scenario mostly happens when something is misbehaving and the type errors were actually useful in figuring out what happend. So an error message is logged in their place. * Remove lint
691 lines
23 KiB
Python
691 lines
23 KiB
Python
"""
|
|
Support for the LIFX platform that implements lights.
|
|
|
|
For more details about this platform, please refer to the documentation at
|
|
https://home-assistant.io/components/light.lifx/
|
|
"""
|
|
import logging
|
|
import asyncio
|
|
import sys
|
|
import math
|
|
from os import path
|
|
from functools import partial
|
|
from datetime import timedelta
|
|
|
|
import voluptuous as vol
|
|
|
|
from homeassistant.components.light import (
|
|
Light, DOMAIN, PLATFORM_SCHEMA, LIGHT_TURN_ON_SCHEMA,
|
|
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
|
|
from homeassistant.helpers.service import extract_entity_ids
|
|
import homeassistant.helpers.config_validation as cv
|
|
import homeassistant.util.color as color_util
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
REQUIREMENTS = ['aiolifx==0.5.4', 'aiolifx_effects==0.1.1']
|
|
|
|
UDP_BROADCAST_PORT = 56700
|
|
|
|
DISCOVERY_INTERVAL = 60
|
|
MESSAGE_TIMEOUT = 1.0
|
|
MESSAGE_RETRIES = 8
|
|
UNAVAILABLE_GRACE = 90
|
|
|
|
CONF_SERVER = 'server'
|
|
CONF_BROADCAST = 'broadcast'
|
|
|
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|
vol.Optional(CONF_SERVER, default='0.0.0.0'): cv.string,
|
|
vol.Optional(CONF_BROADCAST, default='255.255.255.255'): cv.string,
|
|
})
|
|
|
|
SERVICE_LIFX_SET_STATE = 'lifx_set_state'
|
|
|
|
ATTR_INFRARED = 'infrared'
|
|
ATTR_ZONES = 'zones'
|
|
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_ZONES: vol.All(cv.ensure_list, [cv.positive_int]),
|
|
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,
|
|
})
|
|
|
|
|
|
def aiolifx():
|
|
"""Return the aiolifx module."""
|
|
import aiolifx as aiolifx_module
|
|
return aiolifx_module
|
|
|
|
|
|
def aiolifx_effects():
|
|
"""Return the aiolifx_effects module."""
|
|
import aiolifx_effects as aiolifx_effects_module
|
|
return aiolifx_effects_module
|
|
|
|
|
|
@asyncio.coroutine
|
|
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|
"""Set up the LIFX platform."""
|
|
if sys.platform == 'win32':
|
|
_LOGGER.warning("The lifx platform is known to not work on Windows. "
|
|
"Consider using the lifx_legacy platform instead")
|
|
|
|
server_addr = config.get(CONF_SERVER)
|
|
|
|
lifx_manager = LIFXManager(hass, async_add_devices)
|
|
lifx_discovery = aiolifx().LifxDiscovery(
|
|
hass.loop,
|
|
lifx_manager,
|
|
discovery_interval=DISCOVERY_INTERVAL,
|
|
broadcast_ip=config.get(CONF_BROADCAST))
|
|
|
|
coro = hass.loop.create_datagram_endpoint(
|
|
lambda: lifx_discovery, local_addr=(server_addr, UDP_BROADCAST_PORT))
|
|
|
|
hass.async_add_job(coro)
|
|
|
|
@callback
|
|
def cleanup(event):
|
|
"""Clean up resources."""
|
|
lifx_discovery.cleanup()
|
|
|
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup)
|
|
|
|
return True
|
|
|
|
|
|
def lifxwhite(device):
|
|
"""Return whether this is a white-only bulb."""
|
|
features = aiolifx().products.features_map.get(device.product, None)
|
|
if features:
|
|
return not features["color"]
|
|
return False
|
|
|
|
|
|
def lifxmultizone(device):
|
|
"""Return whether this is a multizone bulb/strip."""
|
|
features = aiolifx().products.features_map.get(device.product, None)
|
|
if features:
|
|
return features["multizone"]
|
|
return False
|
|
|
|
|
|
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."""
|
|
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."""
|
|
tasks = []
|
|
for light in self.service_to_entities(service):
|
|
if service.service == SERVICE_LIFX_SET_STATE:
|
|
task = light.async_set_state(**service.data)
|
|
tasks.append(self.hass.async_add_job(task))
|
|
if tasks:
|
|
yield from asyncio.wait(tasks, loop=self.hass.loop)
|
|
|
|
self.hass.services.async_register(
|
|
DOMAIN, SERVICE_LIFX_SET_STATE, async_service_handle,
|
|
descriptions.get(SERVICE_LIFX_SET_STATE),
|
|
schema=LIFX_SET_STATE_SCHEMA)
|
|
|
|
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."""
|
|
devices = list(map(lambda l: l.device, entities))
|
|
|
|
if service == SERVICE_EFFECT_PULSE:
|
|
effect = aiolifx_effects().EffectPulse(
|
|
power_on=kwargs.get(ATTR_POWER_ON),
|
|
period=kwargs.get(ATTR_PERIOD),
|
|
cycles=kwargs.get(ATTR_CYCLES),
|
|
mode=kwargs.get(ATTR_MODE),
|
|
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),
|
|
period=kwargs.get(ATTR_PERIOD),
|
|
change=kwargs.get(ATTR_CHANGE),
|
|
spread=kwargs.get(ATTR_SPREAD),
|
|
transition=kwargs.get(ATTR_TRANSITION),
|
|
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."""
|
|
entity_ids = extract_entity_ids(self.hass, service)
|
|
if entity_ids:
|
|
entities = [entity for entity in self.entities.values()
|
|
if entity.entity_id in entity_ids]
|
|
else:
|
|
entities = list(self.entities.values())
|
|
|
|
return entities
|
|
|
|
@callback
|
|
def register(self, device):
|
|
"""Handler for newly detected bulb."""
|
|
self.hass.async_add_job(self.async_register(device))
|
|
|
|
@asyncio.coroutine
|
|
def async_register(self, device):
|
|
"""Handler for newly detected bulb."""
|
|
if device.mac_addr in self.entities:
|
|
entity = self.entities[device.mac_addr]
|
|
entity.registered = True
|
|
_LOGGER.debug("%s register AGAIN", entity.who)
|
|
yield from entity.update_hass()
|
|
else:
|
|
_LOGGER.debug("%s register NEW", device.ip_addr)
|
|
|
|
# Read initial state
|
|
ack = AwaitAioLIFX().wait
|
|
version_resp = yield from ack(device.get_version)
|
|
if version_resp:
|
|
color_resp = yield from ack(device.get_color)
|
|
|
|
if version_resp is None or color_resp is None:
|
|
_LOGGER.error("Failed to initialize %s", device.ip_addr)
|
|
else:
|
|
device.timeout = MESSAGE_TIMEOUT
|
|
device.retry_count = MESSAGE_RETRIES
|
|
device.unregister_timeout = UNAVAILABLE_GRACE
|
|
|
|
if lifxwhite(device):
|
|
entity = LIFXWhite(device, self.effects_conductor)
|
|
elif lifxmultizone(device):
|
|
entity = LIFXStrip(device, self.effects_conductor)
|
|
else:
|
|
entity = LIFXColor(device, self.effects_conductor)
|
|
|
|
_LOGGER.debug("%s register READY", entity.who)
|
|
self.entities[device.mac_addr] = entity
|
|
self.async_add_devices([entity], True)
|
|
|
|
@callback
|
|
def unregister(self, device):
|
|
"""Handle disappearing bulbs."""
|
|
if device.mac_addr in self.entities:
|
|
entity = self.entities[device.mac_addr]
|
|
_LOGGER.debug("%s unregister", entity.who)
|
|
entity.registered = False
|
|
self.hass.async_add_job(entity.async_update_ha_state())
|
|
|
|
|
|
class AwaitAioLIFX:
|
|
"""Wait for an aiolifx callback and return the message."""
|
|
|
|
def __init__(self):
|
|
"""Initialize the wrapper."""
|
|
self.device = None
|
|
self.message = None
|
|
self.event = asyncio.Event()
|
|
|
|
@callback
|
|
def callback(self, device, message):
|
|
"""Handle responses."""
|
|
self.device = device
|
|
self.message = message
|
|
self.event.set()
|
|
|
|
@asyncio.coroutine
|
|
def wait(self, method):
|
|
"""Call an aiolifx method and wait for its response."""
|
|
self.device = None
|
|
self.message = None
|
|
self.event.clear()
|
|
method(callb=self.callback)
|
|
|
|
yield from self.event.wait()
|
|
return self.message
|
|
|
|
|
|
def convert_8_to_16(value):
|
|
"""Scale an 8 bit level into 16 bits."""
|
|
return (value << 8) | value
|
|
|
|
|
|
def convert_16_to_8(value):
|
|
"""Scale a 16 bit level into 8 bits."""
|
|
return value >> 8
|
|
|
|
|
|
class LIFXLight(Light):
|
|
"""Representation of a LIFX light."""
|
|
|
|
def __init__(self, device, effects_conductor):
|
|
"""Initialize the light."""
|
|
self.device = device
|
|
self.effects_conductor = effects_conductor
|
|
self.registered = True
|
|
self.postponed_update = None
|
|
self.lock = asyncio.Lock()
|
|
|
|
@property
|
|
def available(self):
|
|
"""Return the availability of the device."""
|
|
return self.registered
|
|
|
|
@property
|
|
def name(self):
|
|
"""Return the name of the device."""
|
|
return self.device.label
|
|
|
|
@property
|
|
def who(self):
|
|
"""Return a string identifying the device."""
|
|
return "%s (%s)" % (self.device.ip_addr, self.name)
|
|
|
|
@property
|
|
def brightness(self):
|
|
"""Return the brightness of this light between 0..255."""
|
|
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."""
|
|
kelvin = self.device.color[3]
|
|
temperature = color_util.color_temperature_kelvin_to_mired(kelvin)
|
|
|
|
_LOGGER.debug("color_temp: %d", temperature)
|
|
return temperature
|
|
|
|
@property
|
|
def is_on(self):
|
|
"""Return true if device is on."""
|
|
return self.device.power_level != 0
|
|
|
|
@property
|
|
def effect(self):
|
|
"""Return the name of the currently running effect."""
|
|
effect = self.effects_conductor.effect(self.device)
|
|
if effect:
|
|
return 'lifx_effect_' + effect.name
|
|
return None
|
|
|
|
@asyncio.coroutine
|
|
def update_hass(self, now=None):
|
|
"""Request new status and push it to hass."""
|
|
self.postponed_update = None
|
|
yield from self.async_update()
|
|
yield from self.async_update_ha_state()
|
|
|
|
@asyncio.coroutine
|
|
def update_during_transition(self, when):
|
|
"""Update state at the start and end of a transition."""
|
|
if self.postponed_update:
|
|
self.postponed_update()
|
|
|
|
# Transition has started
|
|
yield from self.update_hass()
|
|
|
|
# Transition has ended
|
|
if when > 0:
|
|
self.postponed_update = async_track_point_in_utc_time(
|
|
self.hass, self.update_hass,
|
|
util.dt.utcnow() + timedelta(milliseconds=when))
|
|
|
|
@asyncio.coroutine
|
|
def async_turn_on(self, **kwargs):
|
|
"""Turn the device on."""
|
|
kwargs[ATTR_POWER] = True
|
|
self.hass.async_add_job(self.async_set_state(**kwargs))
|
|
|
|
@asyncio.coroutine
|
|
def async_turn_off(self, **kwargs):
|
|
"""Turn the device off."""
|
|
kwargs[ATTR_POWER] = False
|
|
self.hass.async_add_job(self.async_set_state(**kwargs))
|
|
|
|
@asyncio.coroutine
|
|
def async_set_state(self, **kwargs):
|
|
"""Set a color on the light and turn it on/off."""
|
|
with (yield from self.lock):
|
|
bulb = self.device
|
|
|
|
yield from self.effects_conductor.stop([bulb])
|
|
|
|
if ATTR_EFFECT in kwargs:
|
|
yield from self.default_effect(**kwargs)
|
|
return
|
|
|
|
if ATTR_INFRARED in kwargs:
|
|
bulb.set_infrared(convert_8_to_16(kwargs[ATTR_INFRARED]))
|
|
|
|
if ATTR_TRANSITION in kwargs:
|
|
fade = int(kwargs[ATTR_TRANSITION] * 1000)
|
|
else:
|
|
fade = 0
|
|
|
|
# These are both False if ATTR_POWER is not set
|
|
power_on = kwargs.get(ATTR_POWER, False)
|
|
power_off = not kwargs.get(ATTR_POWER, True)
|
|
|
|
hsbk = find_hsbk(**kwargs)
|
|
|
|
# Send messages, waiting for ACK each time
|
|
ack = AwaitAioLIFX().wait
|
|
|
|
if not self.is_on:
|
|
if power_off:
|
|
yield from self.set_power(ack, False)
|
|
if hsbk:
|
|
yield from self.set_color(ack, hsbk, kwargs)
|
|
if power_on:
|
|
yield from self.set_power(ack, True, duration=fade)
|
|
else:
|
|
if power_on:
|
|
yield from self.set_power(ack, True)
|
|
if hsbk:
|
|
yield from self.set_color(ack, hsbk, kwargs, duration=fade)
|
|
if power_off:
|
|
yield from self.set_power(ack, False, duration=fade)
|
|
|
|
# Avoid state ping-pong by holding off updates as the state settles
|
|
yield from asyncio.sleep(0.3)
|
|
|
|
# Update when the transition starts and ends
|
|
yield from self.update_during_transition(fade)
|
|
|
|
@asyncio.coroutine
|
|
def set_power(self, ack, pwr, duration=0):
|
|
"""Send a power change to the device."""
|
|
yield from ack(partial(self.device.set_power, pwr, duration=duration))
|
|
|
|
@asyncio.coroutine
|
|
def set_color(self, ack, hsbk, kwargs, duration=0):
|
|
"""Send a color change to the device."""
|
|
hsbk = merge_hsbk(self.device.color, hsbk)
|
|
yield from ack(partial(self.device.set_color, hsbk, duration=duration))
|
|
|
|
@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."""
|
|
_LOGGER.debug("%s async_update", self.who)
|
|
if self.available and not self.lock.locked():
|
|
yield from AwaitAioLIFX().wait(self.device.get_color)
|
|
|
|
|
|
class LIFXWhite(LIFXLight):
|
|
"""Representation of a white-only LIFX light."""
|
|
|
|
@property
|
|
def min_mireds(self):
|
|
"""Return the coldest color_temp that this light supports."""
|
|
return math.floor(color_util.color_temperature_kelvin_to_mired(6500))
|
|
|
|
@property
|
|
def max_mireds(self):
|
|
"""Return the warmest color_temp that this light supports."""
|
|
return math.ceil(color_util.color_temperature_kelvin_to_mired(2700))
|
|
|
|
@property
|
|
def supported_features(self):
|
|
"""Flag supported features."""
|
|
return (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_TRANSITION |
|
|
SUPPORT_EFFECT)
|
|
|
|
@property
|
|
def effect_list(self):
|
|
"""Return the list of supported effects for this light."""
|
|
return [
|
|
SERVICE_EFFECT_PULSE,
|
|
SERVICE_EFFECT_STOP,
|
|
]
|
|
|
|
|
|
class LIFXColor(LIFXLight):
|
|
"""Representation of a color LIFX light."""
|
|
|
|
@property
|
|
def min_mireds(self):
|
|
"""Return the coldest color_temp that this light supports."""
|
|
return math.floor(color_util.color_temperature_kelvin_to_mired(9000))
|
|
|
|
@property
|
|
def max_mireds(self):
|
|
"""Return the warmest color_temp that this light supports."""
|
|
return math.ceil(color_util.color_temperature_kelvin_to_mired(2500))
|
|
|
|
@property
|
|
def supported_features(self):
|
|
"""Flag supported features."""
|
|
return (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_TRANSITION |
|
|
SUPPORT_EFFECT | SUPPORT_RGB_COLOR | SUPPORT_XY_COLOR)
|
|
|
|
@property
|
|
def effect_list(self):
|
|
"""Return the list of supported effects for this light."""
|
|
return [
|
|
SERVICE_EFFECT_COLORLOOP,
|
|
SERVICE_EFFECT_PULSE,
|
|
SERVICE_EFFECT_STOP,
|
|
]
|
|
|
|
@property
|
|
def rgb_color(self):
|
|
"""Return the RGB value."""
|
|
hue, sat, bri, _ = self.device.color
|
|
|
|
return color_util.color_hsv_to_RGB(
|
|
hue, convert_16_to_8(sat), convert_16_to_8(bri))
|
|
|
|
|
|
class LIFXStrip(LIFXColor):
|
|
"""Representation of a LIFX light strip with multiple zones."""
|
|
|
|
@asyncio.coroutine
|
|
def set_color(self, ack, hsbk, kwargs, duration=0):
|
|
"""Send a color change to the device."""
|
|
bulb = self.device
|
|
num_zones = len(bulb.color_zones)
|
|
|
|
# Zone brightness is not reported when powered off
|
|
if not self.is_on and hsbk[2] is None:
|
|
yield from self.set_power(ack, True)
|
|
yield from asyncio.sleep(0.3)
|
|
yield from self.update_color_zones()
|
|
yield from self.set_power(ack, False)
|
|
yield from asyncio.sleep(0.3)
|
|
|
|
zones = kwargs.get(ATTR_ZONES, None)
|
|
if zones is None:
|
|
zones = list(range(0, num_zones))
|
|
else:
|
|
zones = list(filter(lambda x: x < num_zones, set(zones)))
|
|
|
|
# Send new color to each zone
|
|
for index, zone in enumerate(zones):
|
|
zone_hsbk = merge_hsbk(bulb.color_zones[zone], hsbk)
|
|
apply = 1 if (index == len(zones)-1) else 0
|
|
set_zone = partial(bulb.set_color_zones,
|
|
start_index=zone,
|
|
end_index=zone,
|
|
color=zone_hsbk,
|
|
duration=duration,
|
|
apply=apply)
|
|
yield from ack(set_zone)
|
|
|
|
@asyncio.coroutine
|
|
def async_update(self):
|
|
"""Update strip status."""
|
|
if self.available and not self.lock.locked():
|
|
yield from super().async_update()
|
|
yield from self.update_color_zones()
|
|
|
|
@asyncio.coroutine
|
|
def update_color_zones(self):
|
|
"""Get updated color information for each zone."""
|
|
zone = 0
|
|
top = 1
|
|
while self.available and zone < top:
|
|
# Each get_color_zones can update 8 zones at once
|
|
resp = yield from AwaitAioLIFX().wait(partial(
|
|
self.device.get_color_zones,
|
|
start_index=zone,
|
|
end_index=zone+7))
|
|
if resp:
|
|
zone += 8
|
|
top = resp.count
|