From 71d909483ca1627149dd5764af7e75ecb2f53896 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 26 Apr 2017 22:44:33 +0200 Subject: [PATCH 1/7] LIFX: refresh state after stopping an effect This clears the internal cache in case polling picked up the state as set by an effect. For example, aborting an effect by selecting a new brightness could keep a color set by the effect. --- homeassistant/components/light/lifx/effects.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/light/lifx/effects.py b/homeassistant/components/light/lifx/effects.py index 07b97d03a12..2c054d49e1a 100644 --- a/homeassistant/components/light/lifx/effects.py +++ b/homeassistant/components/light/lifx/effects.py @@ -205,6 +205,7 @@ class LIFXEffect(object): light.device.set_color(light.effect_data.color) yield from asyncio.sleep(0.5) light.effect_data = None + yield from light.refresh_state() self.lights.remove(light) def from_poweroff_hsbk(self, light, **kwargs): From 99e34539b9dd08291033228e4405b91676356065 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Tue, 2 May 2017 23:30:07 +0200 Subject: [PATCH 2/7] LIFX: fix color restore after running effects State restoration takes up to a second because bulbs can be slow to react. During this time an effect could keep running, overwriting the state that we were trying to restore. Now the effect forgets the light immediately and it thus avoids further changes while the restored state settles. --- .../components/light/lifx/effects.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/light/lifx/effects.py b/homeassistant/components/light/lifx/effects.py index 2c054d49e1a..ae9bd626fca 100644 --- a/homeassistant/components/light/lifx/effects.py +++ b/homeassistant/components/light/lifx/effects.py @@ -196,18 +196,19 @@ class LIFXEffect(object): @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 - yield from light.refresh_state() + if light in self.lights: self.lights.remove(light) + if light.effect_data and 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 + yield from light.refresh_state() + def from_poweroff_hsbk(self, light, **kwargs): """Return the color when starting from a powered off state.""" return None From 8233f086cd7904cdf11c7c229dd6f1f499837da5 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 26 Apr 2017 22:47:32 +0200 Subject: [PATCH 3/7] LIFX: Use 3500K as neutral white This does not really matter because the colorloop uses saturated colors (without much white). Anyway, just copy the 3500K that the LIFX app uses. --- homeassistant/components/light/lifx/effects.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/light/lifx/effects.py b/homeassistant/components/light/lifx/effects.py index ae9bd626fca..1259d463e0e 100644 --- a/homeassistant/components/light/lifx/effects.py +++ b/homeassistant/components/light/lifx/effects.py @@ -31,6 +31,8 @@ ATTR_CHANGE = 'change' 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, @@ -314,7 +316,7 @@ class LIFXEffectColorloop(LIFXEffect): int(65535/359*lhue), int(random.uniform(0.8, 1.0)*65535), brightness, - 4000, + NEUTRAL_WHITE, ] light.device.set_color(hsbk, None, transition) @@ -326,7 +328,7 @@ class LIFXEffectColorloop(LIFXEffect): def from_poweroff_hsbk(self, light, **kwargs): """Start from a random hue.""" - return [random.randint(0, 65535), 65535, 0, 4000] + return [random.randint(0, 65535), 65535, 0, NEUTRAL_WHITE] class LIFXEffectStop(LIFXEffect): From ec490070ca70c4be13f959a3c1f6fceee0c24132 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 26 Apr 2017 22:51:33 +0200 Subject: [PATCH 4/7] LIFX: Move random hue initial color to the LIFXEffect base class It's a reasonable default for several light effects. --- homeassistant/components/light/lifx/effects.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/components/light/lifx/effects.py b/homeassistant/components/light/lifx/effects.py index 1259d463e0e..2dc56443723 100644 --- a/homeassistant/components/light/lifx/effects.py +++ b/homeassistant/components/light/lifx/effects.py @@ -213,7 +213,7 @@ class LIFXEffect(object): def from_poweroff_hsbk(self, light, **kwargs): """Return the color when starting from a powered off state.""" - return None + return [random.randint(0, 65535), 65535, 0, NEUTRAL_WHITE] class LIFXEffectBreathe(LIFXEffect): @@ -326,10 +326,6 @@ class LIFXEffectColorloop(LIFXEffect): yield from asyncio.sleep(period) - def from_poweroff_hsbk(self, light, **kwargs): - """Start from a random hue.""" - return [random.randint(0, 65535), 65535, 0, NEUTRAL_WHITE] - class LIFXEffectStop(LIFXEffect): """A no-op effect, but starting it will stop an existing effect.""" From 193270c4fbe5baeb196cf42eeb4116f0116ecaa4 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sat, 29 Apr 2017 18:27:22 +0200 Subject: [PATCH 5/7] LIFX: Update aiolifx requirement This update silences some warnings (frawau/aiolifx#7). --- homeassistant/components/light/lifx/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/light/lifx/__init__.py b/homeassistant/components/light/lifx/__init__.py index f1b20f904d2..22e7ee04fcc 100644 --- a/homeassistant/components/light/lifx/__init__.py +++ b/homeassistant/components/light/lifx/__init__.py @@ -32,7 +32,7 @@ from . import effects as lifx_effects _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['aiolifx==0.4.5'] +REQUIREMENTS = ['aiolifx==0.4.6'] UDP_BROADCAST_PORT = 56700 diff --git a/requirements_all.txt b/requirements_all.txt index 358db1d3802..16f29438937 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -48,7 +48,7 @@ aiodns==1.1.1 aiohttp_cors==0.5.3 # homeassistant.components.light.lifx -aiolifx==0.4.5 +aiolifx==0.4.6 # homeassistant.components.alarmdecoder alarmdecoder==0.12.1.0 From 494a776959141c11eb0a0b3f36f167380cd92ee0 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sat, 29 Apr 2017 22:24:18 +0200 Subject: [PATCH 6/7] LIFX: avoid warnings about already running updates Forcing a refresh will log a warning if the periodic async_update happens to be running already. So let's do the refresh locally and remove the force_refresh. --- homeassistant/components/light/lifx/__init__.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/light/lifx/__init__.py b/homeassistant/components/light/lifx/__init__.py index 22e7ee04fcc..01038814f51 100644 --- a/homeassistant/components/light/lifx/__init__.py +++ b/homeassistant/components/light/lifx/__init__.py @@ -263,17 +263,19 @@ class LIFXLight(Light): """Return the list of supported effects.""" return lifx_effects.effect_list() - @callback + @asyncio.coroutine 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)) + yield from self.refresh_state() + yield from self.async_update_ha_state() - @callback + @asyncio.coroutine 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)) + yield from self.refresh_state() + yield from self.async_update_ha_state() def update_later(self, when): """Block immediate update requests and schedule one for later.""" From 78a3f259d6eac2e080ebab3daf59a7e78f99c77e Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 3 May 2017 21:26:04 +0200 Subject: [PATCH 7/7] LIFX: handle unavailable lights gracefully Recent aiolifx allow sending messages to unregistered devices (as a no-op). This is handy because bulbs can disappear anytime we yield and constantly testing for availability is both error-prone and annoying. So keep the aiolifx device around until a new one registers on the same mac_addr. --- .../components/light/lifx/__init__.py | 19 +++++++------ .../components/light/lifx/effects.py | 27 +++++++++---------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/light/lifx/__init__.py b/homeassistant/components/light/lifx/__init__.py index 01038814f51..f13934011e9 100644 --- a/homeassistant/components/light/lifx/__init__.py +++ b/homeassistant/components/light/lifx/__init__.py @@ -93,6 +93,7 @@ class LIFXManager(object): if device.mac_addr in self.entities: entity = self.entities[device.mac_addr] entity.device = device + entity.registered = True _LOGGER.debug("%s register AGAIN", entity.who) self.hass.async_add_job(entity.async_update_ha_state()) else: @@ -118,7 +119,7 @@ class LIFXManager(object): if device.mac_addr in self.entities: entity = self.entities[device.mac_addr] _LOGGER.debug("%s unregister", entity.who) - entity.device = None + entity.registered = False self.hass.async_add_job(entity.async_update_ha_state()) @@ -172,6 +173,7 @@ class LIFXLight(Light): def __init__(self, device): """Initialize the light.""" self.device = device + self.registered = True self.product = device.product self.blocker = None self.effect_data = None @@ -183,7 +185,7 @@ class LIFXLight(Light): @property def available(self): """Return the availability of the device.""" - return self.device is not None + return self.registered @property def name(self): @@ -345,7 +347,7 @@ class LIFXLight(Light): 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: + if self.blocker is None: yield from self.refresh_state() @asyncio.coroutine @@ -357,11 +359,12 @@ class LIFXLight(Light): @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 + 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.""" diff --git a/homeassistant/components/light/lifx/effects.py b/homeassistant/components/light/lifx/effects.py index 2dc56443723..a15360df33e 100644 --- a/homeassistant/components/light/lifx/effects.py +++ b/homeassistant/components/light/lifx/effects.py @@ -176,18 +176,16 @@ class LIFXEffect(object): 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() - if not light.device: - self.lights.remove(light) - else: - light.effect_data = LIFXEffectData( - self, light.is_on, light.device.color) + 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) + # 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 @@ -202,12 +200,13 @@ class LIFXEffect(object): self.lights.remove(light) if light.effect_data and light.effect_data.effect == self: - if light.device and not light.effect_data.power: + if 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.device.set_color(light.effect_data.color) + yield from asyncio.sleep(0.5) + light.effect_data = None yield from light.refresh_state()