From 9ef084d9034351a7aa26480d0604aad9857dbbc7 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Thu, 16 Mar 2017 06:50:33 +0100 Subject: [PATCH] Move LIFX to aiolifx for driving the bulbs (#6584) * Move LIFX to aiolifx for driving the bulbs * Fix whitespace * Fix more whitespace * Fix lint * Define _available in init * Add @callback decorators * Use hass.async_add_job * Rename class --- homeassistant/components/light/lifx.py | 247 ++++++++++++++----------- requirements_all.txt | 6 +- 2 files changed, 144 insertions(+), 109 deletions(-) diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py index 6b0c8a63f99..72621b55a0d 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -6,6 +6,9 @@ https://home-assistant.io/components/light.lifx/ """ import colorsys import logging +import asyncio +from functools import partial +from datetime import timedelta import voluptuous as vol @@ -13,117 +16,90 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_TRANSITION, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, Light, PLATFORM_SCHEMA) -from homeassistant.helpers.event import track_time_change 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 _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['liffylights==0.9.4'] +REQUIREMENTS = ['aiolifx==0.4.1.post1'] -BYTE_MAX = 255 +UDP_BROADCAST_PORT = 56700 + +# Delay (in ms) expected for changes to take effect in the physical bulb +BULB_LATENCY = 500 -CONF_BROADCAST = 'broadcast' CONF_SERVER = 'server' +BYTE_MAX = 255 SHORT_MAX = 65535 -TEMP_MAX = 9000 -TEMP_MAX_HASS = 500 -TEMP_MIN = 2500 -TEMP_MIN_HASS = 154 - SUPPORT_LIFX = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR | SUPPORT_TRANSITION) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_SERVER, default=None): cv.string, - vol.Optional(CONF_BROADCAST, default=None): cv.string, + vol.Optional(CONF_SERVER, default='0.0.0.0'): cv.string, }) # pylint: disable=unused-argument -def setup_platform(hass, config, add_devices, discovery_info=None): +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Setup the LIFX platform.""" + import aiolifx + server_addr = config.get(CONF_SERVER) - broadcast_addr = config.get(CONF_BROADCAST) - lifx_library = LIFX(add_devices, server_addr, broadcast_addr) + lifx_manager = LIFXManager(hass, async_add_devices) - # Register our poll service - track_time_change(hass, lifx_library.poll, second=[10, 40]) + coro = hass.loop.create_datagram_endpoint( + partial(aiolifx.LifxDiscovery, hass.loop, lifx_manager), + local_addr=(server_addr, UDP_BROADCAST_PORT)) - lifx_library.probe() + hass.async_add_job(coro) + return True -class LIFX(object): - """Representation of a LIFX light.""" +class LIFXManager(object): + """Representation of all known LIFX entities.""" - def __init__(self, add_devices_callback, server_addr=None, - broadcast_addr=None): + def __init__(self, hass, async_add_devices): """Initialize the light.""" - import liffylights + self.entities = {} + self.hass = hass + self.async_add_devices = async_add_devices - self._devices = [] - - self._add_devices_callback = add_devices_callback - - self._liffylights = liffylights.LiffyLights( - self.on_device, self.on_power, self.on_color, server_addr, - broadcast_addr) - - def find_bulb(self, ipaddr): - """Search for bulbs.""" - bulb = None - for device in self._devices: - if device.ipaddr == ipaddr: - bulb = device - break - return bulb - - def on_device(self, ipaddr, name, power, hue, sat, bri, kel): - """Initialize the light.""" - bulb = self.find_bulb(ipaddr) - - if bulb is None: - _LOGGER.debug("new bulb %s %s %d %d %d %d %d", - ipaddr, name, power, hue, sat, bri, kel) - bulb = LIFXLight( - self._liffylights, ipaddr, name, power, hue, sat, bri, kel) - self._devices.append(bulb) - self._add_devices_callback([bulb]) + @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()) else: - _LOGGER.debug("update bulb %s %s %d %d %d %d %d", - ipaddr, name, power, hue, sat, bri, kel) - bulb.set_power(power) - bulb.set_color(hue, sat, bri, kel) - bulb.schedule_update_ha_state() + _LOGGER.debug("%s register NEW", device.ip_addr) + device.get_color(self.ready) - def on_color(self, ipaddr, hue, sat, bri, kel): - """Initialize the light.""" - bulb = self.find_bulb(ipaddr) + @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]) - if bulb is not None: - bulb.set_color(hue, sat, bri, kel) - bulb.schedule_update_ha_state() - - def on_power(self, ipaddr, power): - """Initialize the light.""" - bulb = self.find_bulb(ipaddr) - - if bulb is not None: - bulb.set_power(power) - bulb.schedule_update_ha_state() - - # pylint: disable=unused-argument - def poll(self, now): - """Polling for the light.""" - self.probe() - - def probe(self, address=None): - """Probe the light.""" - self._liffylights.probe(address) + @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()) def convert_rgb_to_hsv(rgb): @@ -140,31 +116,35 @@ def convert_rgb_to_hsv(rgb): class LIFXLight(Light): """Representation of a LIFX light.""" - def __init__(self, liffy, ipaddr, name, power, hue, saturation, brightness, - kelvin): + def __init__(self, device): """Initialize the light.""" - _LOGGER.debug("LIFXLight: %s %s", ipaddr, name) - - self._liffylights = liffy - self._ip = ipaddr - self.set_name(name) - self.set_power(power) - self.set_color(hue, saturation, brightness, kelvin) + 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) @property - def should_poll(self): - """No polling needed for LIFX light.""" - return False + 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 @property def name(self): """Return the name of the device.""" - return self._name + return self.device.label @property def ipaddr(self): """Return the IP address of the device.""" - return self._ip + return self.device.ip_addr[0] @property def rgb_color(self): @@ -199,16 +179,47 @@ class LIFXLight(Light): """Flag supported features.""" return SUPPORT_LIFX - def turn_on(self, **kwargs): + @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): """Turn the device on.""" 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 @@ -216,40 +227,64 @@ class LIFXLight(Light): 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] _LOGGER.debug("turn_on: %s (%d) %d %d %d %d %d", - self._ip, self._power, - hue, saturation, brightness, kelvin, fade) + self.ipaddr, self._power, fade, *hsbk) if self._power == 0: - self._liffylights.set_color(self._ip, hue, saturation, - brightness, kelvin, 0) - self._liffylights.set_power(self._ip, 65535, fade) + if changed_color: + self.device.set_color(hsbk, None, 0) + self.device.set_power(True, None, fade) else: - self._liffylights.set_color(self._ip, hue, saturation, - brightness, kelvin, fade) + self.device.set_power(True, None, 0) # racing for power status + if changed_color: + self.device.set_color(hsbk, None, fade) - def turn_off(self, **kwargs): + self.update_later(0) + if fade < BULB_LATENCY: + self.set_power(1) + self.set_color(*hsbk) + + @asyncio.coroutine + def async_turn_off(self, **kwargs): """Turn the device off.""" if ATTR_TRANSITION in kwargs: fade = int(kwargs[ATTR_TRANSITION] * 1000) else: fade = 0 - _LOGGER.debug("turn_off: %s %d", self._ip, fade) - self._liffylights.set_power(self._ip, 0, fade) + self.device.set_power(False, None, fade) - def set_name(self, name): - """Set name of the light.""" - self._name = name + 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() def set_power(self, power): """Set power state value.""" diff --git a/requirements_all.txt b/requirements_all.txt index 533843c09f8..277ddb31a9f 100755 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -40,6 +40,9 @@ aiodns==1.1.1 # homeassistant.components.http aiohttp_cors==0.5.0 +# homeassistant.components.light.lifx +aiolifx==0.4.1.post1 + # homeassistant.components.camera.amcrest # homeassistant.components.sensor.amcrest amcrest==1.1.4 @@ -334,9 +337,6 @@ libnacl==1.5.0 # homeassistant.components.media_player.soundtouch libsoundtouch==0.1.0 -# homeassistant.components.light.lifx -liffylights==0.9.4 - # homeassistant.components.light.limitlessled limitlessled==1.0.5