2016-01-18 18:10:32 +00:00
|
|
|
"""
|
2016-03-07 21:08:21 +00:00
|
|
|
Support for the LIFX platform that implements lights.
|
2016-01-18 18:10:32 +00:00
|
|
|
|
2016-01-27 07:07:29 +00:00
|
|
|
For more details about this platform, please refer to the documentation at
|
|
|
|
https://home-assistant.io/components/light.lifx/
|
2016-01-18 18:10:32 +00:00
|
|
|
"""
|
|
|
|
import colorsys
|
2016-02-19 05:27:50 +00:00
|
|
|
import logging
|
2017-03-16 05:50:33 +00:00
|
|
|
import asyncio
|
|
|
|
from functools import partial
|
|
|
|
from datetime import timedelta
|
2016-02-19 05:27:50 +00:00
|
|
|
|
2016-09-11 08:04:07 +00:00
|
|
|
import voluptuous as vol
|
|
|
|
|
2016-02-19 05:27:50 +00:00
|
|
|
from homeassistant.components.light import (
|
2016-08-16 06:07:07 +00:00
|
|
|
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_TRANSITION,
|
|
|
|
SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR,
|
2016-09-11 08:04:07 +00:00
|
|
|
SUPPORT_TRANSITION, Light, PLATFORM_SCHEMA)
|
2017-02-18 18:42:57 +00:00
|
|
|
from homeassistant.util.color import (
|
|
|
|
color_temperature_mired_to_kelvin, color_temperature_kelvin_to_mired)
|
2017-03-16 05:50:33 +00:00
|
|
|
from homeassistant import util
|
|
|
|
from homeassistant.core import callback
|
|
|
|
from homeassistant.helpers.event import async_track_point_in_utc_time
|
2016-09-11 08:04:07 +00:00
|
|
|
import homeassistant.helpers.config_validation as cv
|
2016-01-18 18:10:32 +00:00
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2017-03-16 05:50:33 +00:00
|
|
|
REQUIREMENTS = ['aiolifx==0.4.1.post1']
|
2016-01-18 18:10:32 +00:00
|
|
|
|
2017-03-16 05:50:33 +00:00
|
|
|
UDP_BROADCAST_PORT = 56700
|
|
|
|
|
|
|
|
# Delay (in ms) expected for changes to take effect in the physical bulb
|
|
|
|
BULB_LATENCY = 500
|
2016-09-11 08:04:07 +00:00
|
|
|
|
|
|
|
CONF_SERVER = 'server'
|
|
|
|
|
2017-03-16 05:50:33 +00:00
|
|
|
BYTE_MAX = 255
|
2016-09-11 08:04:07 +00:00
|
|
|
SHORT_MAX = 65535
|
|
|
|
|
2016-08-16 06:07:07 +00:00
|
|
|
SUPPORT_LIFX = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR |
|
|
|
|
SUPPORT_TRANSITION)
|
|
|
|
|
2016-09-11 08:04:07 +00:00
|
|
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
2017-03-16 05:50:33 +00:00
|
|
|
vol.Optional(CONF_SERVER, default='0.0.0.0'): cv.string,
|
2016-09-11 08:04:07 +00:00
|
|
|
})
|
2016-01-18 18:10:32 +00:00
|
|
|
|
2016-09-11 08:04:07 +00:00
|
|
|
|
|
|
|
# pylint: disable=unused-argument
|
2017-03-16 05:50:33 +00:00
|
|
|
@asyncio.coroutine
|
|
|
|
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
2016-09-11 08:04:07 +00:00
|
|
|
"""Setup the LIFX platform."""
|
2017-03-16 05:50:33 +00:00
|
|
|
import aiolifx
|
2016-09-11 08:04:07 +00:00
|
|
|
|
2017-03-16 05:50:33 +00:00
|
|
|
server_addr = config.get(CONF_SERVER)
|
2016-03-07 21:08:21 +00:00
|
|
|
|
2017-03-16 05:50:33 +00:00
|
|
|
lifx_manager = LIFXManager(hass, async_add_devices)
|
2016-01-24 01:13:51 +00:00
|
|
|
|
2017-03-16 05:50:33 +00:00
|
|
|
coro = hass.loop.create_datagram_endpoint(
|
|
|
|
partial(aiolifx.LifxDiscovery, hass.loop, lifx_manager),
|
|
|
|
local_addr=(server_addr, UDP_BROADCAST_PORT))
|
2016-01-18 18:10:32 +00:00
|
|
|
|
2017-03-16 05:50:33 +00:00
|
|
|
hass.async_add_job(coro)
|
|
|
|
return True
|
2016-01-18 18:10:32 +00:00
|
|
|
|
|
|
|
|
2017-03-16 05:50:33 +00:00
|
|
|
class LIFXManager(object):
|
|
|
|
"""Representation of all known LIFX entities."""
|
2016-01-18 18:10:32 +00:00
|
|
|
|
2017-03-16 05:50:33 +00:00
|
|
|
def __init__(self, hass, async_add_devices):
|
2016-03-07 21:08:21 +00:00
|
|
|
"""Initialize the light."""
|
2017-03-16 05:50:33 +00:00
|
|
|
self.entities = {}
|
|
|
|
self.hass = hass
|
|
|
|
self.async_add_devices = async_add_devices
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def register(self, device):
|
|
|
|
"""Callback for newly detected bulb."""
|
|
|
|
if device.mac_addr in self.entities:
|
|
|
|
entity = self.entities[device.mac_addr]
|
|
|
|
_LOGGER.debug("%s register AGAIN", entity.ipaddr)
|
|
|
|
entity.available = True
|
|
|
|
self.hass.async_add_job(entity.async_update_ha_state())
|
2016-02-01 18:29:43 +00:00
|
|
|
else:
|
2017-03-16 05:50:33 +00:00
|
|
|
_LOGGER.debug("%s register NEW", device.ip_addr)
|
|
|
|
device.get_color(self.ready)
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def ready(self, device, msg):
|
|
|
|
"""Callback that adds the device once all data is retrieved."""
|
|
|
|
entity = LIFXLight(device)
|
|
|
|
_LOGGER.debug("%s register READY", entity.ipaddr)
|
|
|
|
self.entities[device.mac_addr] = entity
|
|
|
|
self.hass.async_add_job(self.async_add_devices, [entity])
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def unregister(self, device):
|
|
|
|
"""Callback for disappearing bulb."""
|
|
|
|
entity = self.entities[device.mac_addr]
|
|
|
|
_LOGGER.debug("%s unregister", entity.ipaddr)
|
|
|
|
entity.available = False
|
|
|
|
entity.updated_event.set()
|
|
|
|
self.hass.async_add_job(entity.async_update_ha_state())
|
2016-01-23 22:14:57 +00:00
|
|
|
|
2016-01-18 18:10:32 +00:00
|
|
|
|
|
|
|
def convert_rgb_to_hsv(rgb):
|
2016-03-07 21:08:21 +00:00
|
|
|
"""Convert Home Assistant RGB values to HSV values."""
|
2016-01-18 18:10:32 +00:00
|
|
|
red, green, blue = [_ / BYTE_MAX for _ in rgb]
|
|
|
|
|
|
|
|
hue, saturation, brightness = colorsys.rgb_to_hsv(red, green, blue)
|
|
|
|
|
2016-01-23 22:14:57 +00:00
|
|
|
return [int(hue * SHORT_MAX),
|
|
|
|
int(saturation * SHORT_MAX),
|
|
|
|
int(brightness * SHORT_MAX)]
|
2016-01-18 18:10:32 +00:00
|
|
|
|
|
|
|
|
|
|
|
class LIFXLight(Light):
|
2016-03-07 21:08:21 +00:00
|
|
|
"""Representation of a LIFX light."""
|
|
|
|
|
2017-03-16 05:50:33 +00:00
|
|
|
def __init__(self, device):
|
2016-03-07 21:08:21 +00:00
|
|
|
"""Initialize the light."""
|
2017-03-16 05:50:33 +00:00
|
|
|
self.device = device
|
|
|
|
self.updated_event = asyncio.Event()
|
|
|
|
self.blocker = None
|
|
|
|
self.postponed_update = None
|
|
|
|
self._available = True
|
|
|
|
self.set_power(device.power_level)
|
|
|
|
self.set_color(*device.color)
|
2016-01-18 18:10:32 +00:00
|
|
|
|
|
|
|
@property
|
2017-03-16 05:50:33 +00:00
|
|
|
def available(self):
|
|
|
|
"""Return the availability of the device."""
|
|
|
|
return self._available
|
|
|
|
|
|
|
|
@available.setter
|
|
|
|
def available(self, value):
|
|
|
|
"""Set the availability of the device."""
|
|
|
|
self._available = value
|
2016-01-18 18:10:32 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def name(self):
|
2016-03-07 21:08:21 +00:00
|
|
|
"""Return the name of the device."""
|
2017-03-16 05:50:33 +00:00
|
|
|
return self.device.label
|
2016-01-18 18:10:32 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def ipaddr(self):
|
2016-03-07 21:08:21 +00:00
|
|
|
"""Return the IP address of the device."""
|
2017-03-16 05:50:33 +00:00
|
|
|
return self.device.ip_addr[0]
|
2016-01-18 18:10:32 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def rgb_color(self):
|
2016-03-07 21:08:21 +00:00
|
|
|
"""Return the RGB value."""
|
2016-09-11 08:04:07 +00:00
|
|
|
_LOGGER.debug(
|
|
|
|
"rgb_color: [%d %d %d]", self._rgb[0], self._rgb[1], self._rgb[2])
|
2016-01-18 18:10:32 +00:00
|
|
|
return self._rgb
|
|
|
|
|
|
|
|
@property
|
|
|
|
def brightness(self):
|
2016-03-07 21:08:21 +00:00
|
|
|
"""Return the brightness of this light between 0..255."""
|
2016-01-25 13:30:52 +00:00
|
|
|
brightness = int(self._bri / (BYTE_MAX + 1))
|
2016-03-07 21:08:21 +00:00
|
|
|
_LOGGER.debug("brightness: %d", brightness)
|
2016-01-25 13:30:52 +00:00
|
|
|
return brightness
|
2016-01-18 18:10:32 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def color_temp(self):
|
2016-03-07 21:08:21 +00:00
|
|
|
"""Return the color temperature."""
|
2017-02-18 18:42:57 +00:00
|
|
|
temperature = color_temperature_kelvin_to_mired(self._kel)
|
2016-01-25 13:30:52 +00:00
|
|
|
|
2016-03-07 21:08:21 +00:00
|
|
|
_LOGGER.debug("color_temp: %d", temperature)
|
2016-01-25 13:30:52 +00:00
|
|
|
return temperature
|
2016-01-18 18:10:32 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def is_on(self):
|
2016-03-07 21:08:21 +00:00
|
|
|
"""Return true if device is on."""
|
|
|
|
_LOGGER.debug("is_on: %d", self._power)
|
2016-01-18 18:10:32 +00:00
|
|
|
return self._power != 0
|
|
|
|
|
2016-08-16 06:07:07 +00:00
|
|
|
@property
|
|
|
|
def supported_features(self):
|
|
|
|
"""Flag supported features."""
|
|
|
|
return SUPPORT_LIFX
|
|
|
|
|
2017-03-16 05:50:33 +00:00
|
|
|
@callback
|
|
|
|
def update_after_transition(self, now):
|
|
|
|
"""Request new status after completion of the last transition."""
|
|
|
|
self.postponed_update = None
|
|
|
|
self.hass.async_add_job(self.async_update_ha_state(force_refresh=True))
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def unblock_updates(self, now):
|
|
|
|
"""Allow async_update after the new state has settled on the bulb."""
|
|
|
|
self.blocker = None
|
|
|
|
self.hass.async_add_job(self.async_update_ha_state(force_refresh=True))
|
|
|
|
|
|
|
|
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))
|
|
|
|
|
|
|
|
if self.postponed_update:
|
|
|
|
self.postponed_update()
|
|
|
|
if when > BULB_LATENCY:
|
|
|
|
self.postponed_update = async_track_point_in_utc_time(
|
|
|
|
self.hass, self.update_after_transition,
|
|
|
|
util.dt.utcnow() + timedelta(milliseconds=when+BULB_LATENCY))
|
|
|
|
|
|
|
|
@asyncio.coroutine
|
|
|
|
def async_turn_on(self, **kwargs):
|
2016-03-07 21:08:21 +00:00
|
|
|
"""Turn the device on."""
|
2016-01-25 03:32:55 +00:00
|
|
|
if ATTR_TRANSITION in kwargs:
|
2017-02-27 05:21:12 +00:00
|
|
|
fade = int(kwargs[ATTR_TRANSITION] * 1000)
|
2016-01-25 03:32:55 +00:00
|
|
|
else:
|
|
|
|
fade = 0
|
|
|
|
|
2017-03-16 05:50:33 +00:00
|
|
|
changed_color = False
|
|
|
|
|
2016-01-18 18:10:32 +00:00
|
|
|
if ATTR_RGB_COLOR in kwargs:
|
|
|
|
hue, saturation, brightness = \
|
|
|
|
convert_rgb_to_hsv(kwargs[ATTR_RGB_COLOR])
|
2017-03-16 05:50:33 +00:00
|
|
|
changed_color = True
|
2016-01-18 18:10:32 +00:00
|
|
|
else:
|
|
|
|
hue = self._hue
|
|
|
|
saturation = self._sat
|
|
|
|
brightness = self._bri
|
|
|
|
|
2016-01-25 21:19:27 +00:00
|
|
|
if ATTR_BRIGHTNESS in kwargs:
|
|
|
|
brightness = kwargs[ATTR_BRIGHTNESS] * (BYTE_MAX + 1)
|
2017-03-16 05:50:33 +00:00
|
|
|
changed_color = True
|
2016-01-25 21:19:27 +00:00
|
|
|
else:
|
|
|
|
brightness = self._bri
|
|
|
|
|
2016-01-18 18:10:32 +00:00
|
|
|
if ATTR_COLOR_TEMP in kwargs:
|
2017-02-18 18:42:57 +00:00
|
|
|
kelvin = int(color_temperature_mired_to_kelvin(
|
|
|
|
kwargs[ATTR_COLOR_TEMP]))
|
2017-03-16 05:50:33 +00:00
|
|
|
changed_color = True
|
2016-01-18 18:10:32 +00:00
|
|
|
else:
|
|
|
|
kelvin = self._kel
|
|
|
|
|
2017-03-16 05:50:33 +00:00
|
|
|
hsbk = [hue, saturation, brightness, kelvin]
|
2016-01-25 13:30:52 +00:00
|
|
|
_LOGGER.debug("turn_on: %s (%d) %d %d %d %d %d",
|
2017-03-16 05:50:33 +00:00
|
|
|
self.ipaddr, self._power, fade, *hsbk)
|
2016-01-25 13:30:52 +00:00
|
|
|
|
2016-01-23 22:14:57 +00:00
|
|
|
if self._power == 0:
|
2017-03-16 05:50:33 +00:00
|
|
|
if changed_color:
|
|
|
|
self.device.set_color(hsbk, None, 0)
|
|
|
|
self.device.set_power(True, None, fade)
|
2017-03-05 10:11:33 +00:00
|
|
|
else:
|
2017-03-16 05:50:33 +00:00
|
|
|
self.device.set_power(True, None, 0) # racing for power status
|
|
|
|
if changed_color:
|
|
|
|
self.device.set_color(hsbk, None, fade)
|
|
|
|
|
|
|
|
self.update_later(0)
|
|
|
|
if fade < BULB_LATENCY:
|
|
|
|
self.set_power(1)
|
|
|
|
self.set_color(*hsbk)
|
2016-01-18 18:10:32 +00:00
|
|
|
|
2017-03-16 05:50:33 +00:00
|
|
|
@asyncio.coroutine
|
|
|
|
def async_turn_off(self, **kwargs):
|
2016-03-07 21:08:21 +00:00
|
|
|
"""Turn the device off."""
|
2016-01-25 03:32:55 +00:00
|
|
|
if ATTR_TRANSITION in kwargs:
|
2017-02-27 05:21:12 +00:00
|
|
|
fade = int(kwargs[ATTR_TRANSITION] * 1000)
|
2016-01-25 03:32:55 +00:00
|
|
|
else:
|
|
|
|
fade = 0
|
|
|
|
|
2017-03-16 05:50:33 +00:00
|
|
|
self.device.set_power(False, None, fade)
|
|
|
|
|
|
|
|
self.update_later(fade)
|
|
|
|
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.updated_event.set()
|
|
|
|
|
|
|
|
@asyncio.coroutine
|
|
|
|
def async_update(self):
|
|
|
|
"""Update bulb status (if it is available)."""
|
|
|
|
_LOGGER.debug("%s async_update", self.ipaddr)
|
|
|
|
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()
|
2016-01-18 18:10:32 +00:00
|
|
|
|
|
|
|
def set_power(self, power):
|
2016-03-07 21:08:21 +00:00
|
|
|
"""Set power state value."""
|
|
|
|
_LOGGER.debug("set_power: %d", power)
|
2016-01-18 18:10:32 +00:00
|
|
|
self._power = (power != 0)
|
|
|
|
|
|
|
|
def set_color(self, hue, sat, bri, kel):
|
2016-03-07 21:08:21 +00:00
|
|
|
"""Set color state values."""
|
2016-01-18 18:10:32 +00:00
|
|
|
self._hue = hue
|
|
|
|
self._sat = sat
|
|
|
|
self._bri = bri
|
|
|
|
self._kel = kel
|
|
|
|
|
2016-01-23 22:14:57 +00:00
|
|
|
red, green, blue = colorsys.hsv_to_rgb(hue / SHORT_MAX,
|
|
|
|
sat / SHORT_MAX,
|
|
|
|
bri / SHORT_MAX)
|
2016-01-18 18:10:32 +00:00
|
|
|
|
2016-01-25 13:30:52 +00:00
|
|
|
red = int(red * BYTE_MAX)
|
|
|
|
green = int(green * BYTE_MAX)
|
|
|
|
blue = int(blue * BYTE_MAX)
|
|
|
|
|
|
|
|
_LOGGER.debug("set_color: %d %d %d %d [%d %d %d]",
|
|
|
|
hue, sat, bri, kel, red, green, blue)
|
|
|
|
|
|
|
|
self._rgb = [red, green, blue]
|