From e3f682c7d3bb67d57187e50b1f22583bd5e39a75 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 21 Apr 2017 07:46:12 +0200 Subject: [PATCH 01/16] LIFX light effects (#7145) * Refactor into find_hsbk This will be useful for new methods that also have to find passed in colors. * Add AwaitAioLIFX This encapsulates the callback and Event that aiolifx needs and thus avoids an explosion of those when new calls are added. The refresh_state is now generally useful, so move it into its own method. * Initial effects support for LIFX These effects are useful as notifications. They mimic the breathe and pulse effects from the LIFX HTTP API: https://api.developer.lifx.com/docs/breathe-effect https://api.developer.lifx.com/docs/pulse-effect However, this implementation runs locally with the LIFX LAN protocol. * Saturate LIFX no color value Now the color is "full saturation, no brightness". This avoids a lot of temporary white when fading from the "no color" value and into a real color. * Organize LIFX effects in classes This is to move the setup/restore away from the actual effect, making it quite simple to add additional effects. * Stop running LIFX effects on conflicting service calls Turning the light on/off or starting a new effect will now stop the running effect. * Present default LIFX effects as light.turn_on effects This makes the effects (with default parameters) easily accessible from the UI. * Add LIFX colorloop effect This cycles the HSV colors, so that is added as an internal way to set a color. * Move lifx to its own package and split effects into a separate file * Always show LIFX light name in logs The name is actually the easiest way to identify a bulb so just using it as a fallback was a bit odd. * Compact effect getter * Always use full brightness for random flash color This is a stopgap. When a bit more infrastructure is in place, the intention is to turn the current hue some degrees. This will guarantee a flash color that is both unlike the current color and unlike white. * Clear effects concurrently We have to wait for the bulbs, so let us wait for all of them at once. * Add lifx_effect_stop The colorloop effect is most impressive if run on many lights. Testing this has revealed the need for an easy way to stop effects on all lights and return to the initial state of each bulb. This new call does just that. Calling turn_on/turn_off could also stop the effect but that would not restore the initial state. * Always calculate the initial effect color To fade nicely from power off, the breathe effect needs to keep an unchanging hue. So give up on using a static start color and just find the correct hue from the target color. The colorloop effect can start from anything but we use a random color just to keep things a little interesting during power on. * Fix lint * Update .coveragerc --- .coveragerc | 2 +- .../light/{lifx.py => lifx/__init__.py} | 190 +++++++--- .../components/light/lifx/effects.py | 338 ++++++++++++++++++ .../components/light/lifx/services.yaml | 99 +++++ 4 files changed, 571 insertions(+), 58 deletions(-) rename homeassistant/components/light/{lifx.py => lifx/__init__.py} (74%) create mode 100644 homeassistant/components/light/lifx/effects.py create mode 100644 homeassistant/components/light/lifx/services.yaml diff --git a/.coveragerc b/.coveragerc index 623e985c8f5..c8e59e55357 100644 --- a/.coveragerc +++ b/.coveragerc @@ -231,7 +231,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.py b/homeassistant/components/light/lifx/__init__.py similarity index 74% rename from homeassistant/components/light/lifx.py rename to homeassistant/components/light/lifx/__init__.py index 945c163435b..7c7bdd4eeea 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx/__init__.py @@ -10,19 +10,24 @@ import asyncio import sys from functools import partial from datetime import timedelta +import async_timeout import voluptuous as vol from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_TRANSITION, + Light, PLATFORM_SCHEMA, ATTR_BRIGHTNESS, ATTR_COLOR_NAME, ATTR_RGB_COLOR, + ATTR_COLOR_TEMP, ATTR_TRANSITION, ATTR_EFFECT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, - SUPPORT_TRANSITION, Light, PLATFORM_SCHEMA) + SUPPORT_TRANSITION, SUPPORT_EFFECT) 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 +import homeassistant.util.color as color_util + +from . import effects as lifx_effects _LOGGER = logging.getLogger(__name__) @@ -35,18 +40,19 @@ BULB_LATENCY = 500 CONF_SERVER = 'server' +ATTR_HSBK = 'hsbk' + BYTE_MAX = 255 SHORT_MAX = 65535 SUPPORT_LIFX = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR | - SUPPORT_TRANSITION) + SUPPORT_TRANSITION | SUPPORT_EFFECT) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_SERVER, default='0.0.0.0'): cv.string, }) -# pylint: disable=unused-argument @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Setup the LIFX platform.""" @@ -65,6 +71,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): local_addr=(server_addr, UDP_BROADCAST_PORT)) hass.async_add_job(coro) + + lifx_effects.setup(hass, lifx_manager) + return True @@ -104,10 +113,42 @@ class LIFXManager(object): entity = self.entities[device.mac_addr] _LOGGER.debug("%s unregister", entity.who) entity.device = None - entity.updated_event.set() self.hass.async_add_job(entity.async_update_ha_state()) +class AwaitAioLIFX: + """Wait for an aiolifx callback and return the message.""" + + def __init__(self, light): + """Initialize the wrapper.""" + self.light = light + self.device = None + self.message = None + self.event = asyncio.Event() + + @callback + def callback(self, device, message): + """Callback that aiolifx invokes when the response is received.""" + self.device = device + self.message = message + self.event.set() + + @asyncio.coroutine + def wait(self, method): + """Call an aiolifx method and wait for its response or a timeout.""" + 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 + + return self.message + + def convert_rgb_to_hsv(rgb): """Convert Home Assistant RGB values to HSV values.""" red, green, blue = [_ / BYTE_MAX for _ in rgb] @@ -125,8 +166,8 @@ class LIFXLight(Light): def __init__(self, device): """Initialize the light.""" self.device = device - self.updated_event = asyncio.Event() self.blocker = None + self.effect_data = None self.postponed_update = None self._name = device.label self.set_power(device.power_level) @@ -145,10 +186,10 @@ class LIFXLight(Light): @property def who(self): """Return a string identifying the device.""" + ip_addr = '-' if self.device: - return self.device.ip_addr[0] - else: - return "(%s)" % self.name + ip_addr = self.device.ip_addr[0] + return "%s (%s)" % (ip_addr, self.name) @property def rgb_color(self): @@ -178,11 +219,21 @@ class LIFXLight(Light): _LOGGER.debug("is_on: %d", self._power) return self._power != 0 + @property + def effect(self): + """Return the currently running effect.""" + return self.effect_data.effect.name if self.effect_data else None + @property def supported_features(self): """Flag supported features.""" return SUPPORT_LIFX + @property + def effect_list(self): + """Return the list of supported effects.""" + return lifx_effects.effect_list() + @callback def update_after_transition(self, now): """Request new status after completion of the last transition.""" @@ -213,13 +264,85 @@ class LIFXLight(Light): @asyncio.coroutine def async_turn_on(self, **kwargs): """Turn the device on.""" + yield from self.stop_effect() + + if ATTR_EFFECT in kwargs: + yield from lifx_effects.default_effect(self, **kwargs) + return + if ATTR_TRANSITION in kwargs: fade = int(kwargs[ATTR_TRANSITION] * 1000) else: fade = 0 + hsbk, changed_color = self.find_hsbk(**kwargs) + _LOGGER.debug("turn_on: %s (%d) %d %d %d %d %d", + self.who, self._power, fade, *hsbk) + + if self._power == 0: + if changed_color: + self.device.set_color(hsbk, None, 0) + self.device.set_power(True, None, fade) + else: + 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) + + @asyncio.coroutine + def async_turn_off(self, **kwargs): + """Turn the device off.""" + yield from self.stop_effect() + + if ATTR_TRANSITION in kwargs: + fade = int(kwargs[ATTR_TRANSITION] * 1000) + else: + fade = 0 + + self.device.set_power(False, None, fade) + + self.update_later(fade) + if fade < BULB_LATENCY: + self.set_power(0) + + @asyncio.coroutine + 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: + 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.""" + 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] + + color_name = kwargs.pop(ATTR_COLOR_NAME, None) + if color_name is not None: + kwargs[ATTR_RGB_COLOR] = color_util.color_name_to_rgb(color_name) + if ATTR_RGB_COLOR in kwargs: hue, saturation, brightness = \ convert_rgb_to_hsv(kwargs[ATTR_RGB_COLOR]) @@ -242,54 +365,7 @@ class LIFXLight(Light): else: kelvin = self._kel - hsbk = [hue, saturation, brightness, kelvin] - _LOGGER.debug("turn_on: %s (%d) %d %d %d %d %d", - self.who, self._power, fade, *hsbk) - - if self._power == 0: - if changed_color: - self.device.set_color(hsbk, None, 0) - self.device.set_power(True, None, fade) - else: - 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) - - @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 - - 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._name = device.label - self.updated_event.set() - - @asyncio.coroutine - 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: - self.updated_event.clear() - self.device.get_color(self.got_color) - yield from self.updated_event.wait() + return [[hue, saturation, brightness, kelvin], changed_color] def set_power(self, power): """Set power state value.""" diff --git a/homeassistant/components/light/lifx/effects.py b/homeassistant/components/light/lifx/effects.py new file mode 100644 index 00000000000..e2ba0a73534 --- /dev/null +++ b/homeassistant/components/light/lifx/effects.py @@ -0,0 +1,338 @@ +"""Support for light effects for the LIFX light platform.""" +import logging +import asyncio +import random +from os import path + +import voluptuous as vol + +from homeassistant.components.light import ( + DOMAIN, ATTR_BRIGHTNESS, ATTR_COLOR_NAME, ATTR_RGB_COLOR, ATTR_EFFECT) +from homeassistant.config import load_yaml_config_file +from homeassistant.const import (ATTR_ENTITY_ID) +from homeassistant.helpers.service import extract_entity_ids +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_SPREAD = 'spread' +ATTR_CHANGE = 'change' + +# aiolifx waveform modes +WAVEFORM_SINE = 1 +WAVEFORM_PULSE = 4 + +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: vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)), + ATTR_COLOR_NAME: cv.string, + ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)), + vol.Coerce(tuple)), + vol.Optional(ATTR_PERIOD, default=1.0): vol.All(vol.Coerce(float), + vol.Range(min=0.05)), + vol.Optional(ATTR_CYCLES, default=1.0): vol.All(vol.Coerce(float), + vol.Range(min=1)), +}) + +LIFX_EFFECT_PULSE_SCHEMA = LIFX_EFFECT_BREATHE_SCHEMA + +LIFX_EFFECT_COLORLOOP_SCHEMA = LIFX_EFFECT_SCHEMA.extend({ + ATTR_BRIGHTNESS: vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)), + vol.Optional(ATTR_PERIOD, default=60): vol.All(vol.Coerce(float), + vol.Clamp(min=1)), + 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)), +}) + +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): + """Internal func for applying a service.""" + entity_ids = extract_entity_ids(hass, service) + if entity_ids: + devices = [entity for entity in lifx_manager.entities.values() + if entity.entity_id in entity_ids] + else: + devices = list(lifx_manager.entities.values()) + + if devices: + yield from start_effect(hass, devices, + service.service, **service.data) + + descriptions = load_yaml_config_file( + path.join(path.dirname(__file__), 'services.yaml')) + + 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, + } + if service in (SERVICE_EFFECT_BREATHE, SERVICE_EFFECT_PULSE): + data[ATTR_RGB_COLOR] = [ + random.randint(1, 127), + random.randint(1, 127), + random.randint(1, 127), + ] + data[ATTR_BRIGHTNESS] = 255 + yield from light.hass.services.async_call(DOMAIN, service, data) + + +def effect_list(): + """Return the list of supported effects.""" + 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: + 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) + + # 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.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 + self.lights.remove(light) + + def from_poweroff_hsbk(self, light, **kwargs): + """The initial color when starting from a powered off state.""" + return None + + +class LIFXEffectBreathe(LIFXEffect): + """Representation of a breathe effect.""" + + def __init__(self, hass, lights): + """Initialize the breathe effect.""" + super(LIFXEffectBreathe, self).__init__(hass, lights) + self.name = SERVICE_EFFECT_BREATHE + self.waveform = WAVEFORM_SINE + + @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.""" + period = kwargs[ATTR_PERIOD] + cycles = kwargs[ATTR_CYCLES] + hsbk, _ = light.find_hsbk(**kwargs) + + # Start the effect + args = { + 'transient': 1, + 'color': hsbk, + 'period': int(period*1000), + 'cycles': cycles, + 'duty_cycle': 0, + 'waveform': self.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): + """Initial color is the target color, but no brightness.""" + hsbk, _ = light.find_hsbk(**kwargs) + return [hsbk[0], hsbk[1], 0, hsbk[2]] + + +class LIFXEffectPulse(LIFXEffectBreathe): + """Representation of a pulse effect.""" + + def __init__(self, hass, lights): + """Initialize the pulse effect.""" + super(LIFXEffectPulse, self).__init__(hass, lights) + self.name = SERVICE_EFFECT_PULSE + self.waveform = WAVEFORM_PULSE + + +class LIFXEffectColorloop(LIFXEffect): + """Representation of a colorloop effect.""" + + def __init__(self, hass, lights): + """Initialize the colorloop effect.""" + super(LIFXEffectColorloop, self).__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.randint(0, 359) + + while self.lights: + hue = (hue + direction*change) % 360 + + random.shuffle(self.lights) + lhue = hue + + transition = int(1000 * random.uniform(period/2, period)) + for light in self.lights: + if 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/359*lhue), + int(random.uniform(0.8, 1.0)*65535), + brightness, + 4000, + ] + 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) + + def from_poweroff_hsbk(self, light, **kwargs): + """Start from a random hue.""" + return [random.randint(0, 65535), 65535, 0, 4000] + + +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(LIFXEffectStop, self).__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 new file mode 100644 index 00000000000..1b34c54f253 --- /dev/null +++ b/homeassistant/components/light/lifx/services.yaml @@ -0,0 +1,99 @@ +lifx_effect_breathe: + description: Run a breathe effect by fading to a color and back. + + fields: + entity_id: + description: Name(s) of entities to run the effect on + example: 'light.kitchen' + + brightness: + description: Number between 0..255 indicating brightness when the effect peaks + example: 120 + + color_name: + description: A human readable color name + example: 'red' + + rgb_color: + description: Color for the fade 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_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' + + 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..255 indicating brightness of the effect. Leave this out to maintain the current brightness of each participating light + example: 120 + + period: + description: Duration between color changes (deafult 60) + example: 180 + + change: + description: Hue movement per period, in degrees on a color wheel (default 20) + example: 45 + + spread: + description: Maximum hue difference between participating lights, in degrees on a color wheel (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' From 5c737cfa6ee5aa7e5367da6f6b90daacd40eba72 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 21 Apr 2017 12:20:19 +0200 Subject: [PATCH 02/16] HassIO API v2 (#7201) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an optional extended description… --- homeassistant/components/hassio.py | 10 ++++++++-- homeassistant/components/services.yaml | 3 +++ tests/components/test_hassio.py | 18 ++++++++++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hassio.py b/homeassistant/components/hassio.py index 25da7a67f4b..c74918dbfa0 100644 --- a/homeassistant/components/hassio.py +++ b/homeassistant/components/hassio.py @@ -31,9 +31,11 @@ SERVICE_HOST_SHUTDOWN = 'host_shutdown' SERVICE_HOST_REBOOT = 'host_reboot' SERVICE_HOST_UPDATE = 'host_update' -SERVICE_SUPERVISOR_UPDATE = 'supervisor_update' SERVICE_HOMEASSISTANT_UPDATE = 'homeassistant_update' +SERVICE_SUPERVISOR_UPDATE = 'supervisor_update' +SERVICE_SUPERVISOR_RELOAD = 'supervisor_reload' + SERVICE_ADDON_INSTALL = 'addon_install' SERVICE_ADDON_UNINSTALL = 'addon_uninstall' SERVICE_ADDON_UPDATE = 'addon_update' @@ -61,8 +63,9 @@ SERVICE_MAP = { SERVICE_HOST_SHUTDOWN: None, SERVICE_HOST_REBOOT: None, SERVICE_HOST_UPDATE: SCHEMA_SERVICE_UPDATE, - SERVICE_SUPERVISOR_UPDATE: SCHEMA_SERVICE_UPDATE, SERVICE_HOMEASSISTANT_UPDATE: SCHEMA_SERVICE_UPDATE, + SERVICE_SUPERVISOR_UPDATE: SCHEMA_SERVICE_UPDATE, + SERVICE_SUPERVISOR_RELOAD: None, SERVICE_ADDON_INSTALL: SCHEMA_SERVICE_ADDONS_VERSION, SERVICE_ADDON_UNINSTALL: SCHEMA_SERVICE_ADDONS, SERVICE_ADDON_START: SCHEMA_SERVICE_ADDONS, @@ -117,6 +120,9 @@ def async_setup(hass, config): elif service.service == SERVICE_SUPERVISOR_UPDATE: yield from hassio.send_command( "/supervisor/update", payload=version) + elif service.service == SERVICE_SUPERVISOR_RELOAD: + yield from hassio.send_command( + "/supervisor/reload", timeout=LONG_TASK_TIMEOUT) elif service.service == SERVICE_HOMEASSISTANT_UPDATE: yield from hassio.send_command( "/homeassistant/update", payload=version, diff --git a/homeassistant/components/services.yaml b/homeassistant/components/services.yaml index cf5999200d8..6cff6d5f4f4 100644 --- a/homeassistant/components/services.yaml +++ b/homeassistant/components/services.yaml @@ -338,6 +338,9 @@ hassio: description: Optional or it will be use the latest version. example: '0.3' + supervisor_reload: + description: Reload HassIO supervisor addons/updates/configs. + homeassistant_update: description: Update HomeAssistant docker image. fields: diff --git a/tests/components/test_hassio.py b/tests/components/test_hassio.py index bde419c4104..2574e7fa9f3 100644 --- a/tests/components/test_hassio.py +++ b/tests/components/test_hassio.py @@ -53,6 +53,8 @@ class TestHassIOSetup(object): assert self.hass.services.has_service( ho.DOMAIN, ho.SERVICE_SUPERVISOR_UPDATE) + assert self.hass.services.has_service( + ho.DOMAIN, ho.SERVICE_SUPERVISOR_RELOAD) assert self.hass.services.has_service( ho.DOMAIN, ho.SERVICE_ADDON_INSTALL) @@ -216,6 +218,22 @@ class TestHassIOComponent(object): assert len(aioclient_mock.mock_calls) == 2 assert aioclient_mock.mock_calls[-1][2]['version'] == '0.4' + def test_rest_command_http_supervisor_reload(self, aioclient_mock): + """Call a hassio for supervisor reload.""" + aioclient_mock.get( + "http://127.0.0.1/supervisor/ping", json=self.ok_msg) + with assert_setup_component(0, ho.DOMAIN): + setup_component(self.hass, ho.DOMAIN, self.config) + + aioclient_mock.get( + self.url.format("supervisor/reload"), json=self.ok_msg) + + self.hass.services.call( + ho.DOMAIN, ho.SERVICE_SUPERVISOR_RELOAD, {}) + self.hass.block_till_done() + + assert len(aioclient_mock.mock_calls) == 2 + def test_rest_command_http_homeassistant_update(self, aioclient_mock): """Call a hassio for homeassistant update.""" aioclient_mock.get( From 2d57c6a1c78626d369cf0c9d3efd8aded4213020 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sat, 22 Apr 2017 05:16:36 +0200 Subject: [PATCH 03/16] Support xy_color with LIFX lights (#7208) --- homeassistant/components/light/lifx/__init__.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/light/lifx/__init__.py b/homeassistant/components/light/lifx/__init__.py index 7c7bdd4eeea..4d3cd0d4931 100644 --- a/homeassistant/components/light/lifx/__init__.py +++ b/homeassistant/components/light/lifx/__init__.py @@ -16,9 +16,9 @@ import voluptuous as vol from homeassistant.components.light import ( Light, PLATFORM_SCHEMA, ATTR_BRIGHTNESS, ATTR_COLOR_NAME, ATTR_RGB_COLOR, - ATTR_COLOR_TEMP, ATTR_TRANSITION, ATTR_EFFECT, + ATTR_XY_COLOR, ATTR_COLOR_TEMP, ATTR_TRANSITION, ATTR_EFFECT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, - SUPPORT_TRANSITION, SUPPORT_EFFECT) + SUPPORT_XY_COLOR, SUPPORT_TRANSITION, SUPPORT_EFFECT) from homeassistant.util.color import ( color_temperature_mired_to_kelvin, color_temperature_kelvin_to_mired) from homeassistant import util @@ -46,7 +46,7 @@ BYTE_MAX = 255 SHORT_MAX = 65535 SUPPORT_LIFX = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR | - SUPPORT_TRANSITION | SUPPORT_EFFECT) + SUPPORT_XY_COLOR | SUPPORT_TRANSITION | SUPPORT_EFFECT) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_SERVER, default='0.0.0.0'): cv.string, @@ -358,6 +358,14 @@ class LIFXLight(Light): else: brightness = self._bri + if ATTR_XY_COLOR in kwargs: + hue, saturation, _ = \ + color_util.color_xy_brightness_to_hsv( + *kwargs[ATTR_XY_COLOR], + ibrightness=(brightness // (BYTE_MAX + 1))) + saturation = saturation * (BYTE_MAX + 1) + changed_color = True + if ATTR_COLOR_TEMP in kwargs: kelvin = int(color_temperature_mired_to_kelvin( kwargs[ATTR_COLOR_TEMP])) From 07fcf22aebca2f579056caecf4a63dbfbb963bdb Mon Sep 17 00:00:00 2001 From: Nikolas Beutler Date: Sat, 22 Apr 2017 05:16:59 +0200 Subject: [PATCH 04/16] Update ios.py (#7160) * Update ios.py as discussed. the part: if battery_state == ios.ATTR_BATTERY_STATE_FULL: returning_icon_level = DEFAULT_ICON_LEVEL kinda screws up the charging icon. i might just miss a logical solution for that though. let me know what you think. it might not be beautiful but i think its an overall improve over the current "double battery" solution * Update ios.py chound fix and full_battery_charge fix * Update ios.py removed new line * Update ios.py * Update ios.py * Update ios.py * Update ios.py * Update ios.py * Update ios.py * merged request from robbie * Update ios.py * Update ios.py * Update ios.py --- homeassistant/components/sensor/ios.py | 42 ++++++++++++++++++-------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/sensor/ios.py b/homeassistant/components/sensor/ios.py index 35f03d7a1c1..3173eec4285 100644 --- a/homeassistant/components/sensor/ios.py +++ b/homeassistant/components/sensor/ios.py @@ -14,7 +14,8 @@ SENSOR_TYPES = { "state": ["Battery State", None] } -DEFAULT_ICON = "mdi:battery" +DEFAULT_ICON_LEVEL = "mdi:battery" +DEFAULT_ICON_STATE = "mdi:power-plug" def setup_platform(hass, config, add_devices, discovery_info=None): @@ -62,7 +63,6 @@ class IOSSensor(Entity): @property def unit_of_measurement(self): """Return the unit of measurement this sensor expresses itself in.""" - return self._unit_of_measurement @property def device_state_attributes(self): @@ -84,28 +84,44 @@ class IOSSensor(Entity): battery_state = device_battery[ios.ATTR_BATTERY_STATE] battery_level = device_battery[ios.ATTR_BATTERY_LEVEL] rounded_level = round(battery_level, -1) - returning_icon = DEFAULT_ICON + returning_icon_level = DEFAULT_ICON_LEVEL if battery_state == ios.ATTR_BATTERY_STATE_FULL: - returning_icon = DEFAULT_ICON + returning_icon_level = DEFAULT_ICON_LEVEL + if battery_state == ios.ATTR_BATTERY_STATE_CHARGING: + returning_icon_state = DEFAULT_ICON_STATE + else: + returning_icon_state = "{}-off".format(DEFAULT_ICON_STATE) elif battery_state == ios.ATTR_BATTERY_STATE_CHARGING: # Why is MDI missing 10, 50, 70? if rounded_level in (20, 30, 40, 60, 80, 90, 100): - returning_icon = "{}-charging-{}".format(DEFAULT_ICON, - str(rounded_level)) + returning_icon_level = "{}-charging-{}".format( + DEFAULT_ICON_LEVEL, str(rounded_level)) + returning_icon_state = DEFAULT_ICON_STATE else: - returning_icon = "{}-charging".format(DEFAULT_ICON) + returning_icon_level = "{}-charging".format( + DEFAULT_ICON_LEVEL) + returning_icon_state = DEFAULT_ICON_STATE elif battery_state == ios.ATTR_BATTERY_STATE_UNPLUGGED: if rounded_level < 10: - returning_icon = "{}-outline".format(DEFAULT_ICON) + returning_icon_level = "{}-outline".format( + DEFAULT_ICON_LEVEL) + returning_icon_state = "{}-off".format(DEFAULT_ICON_STATE) elif battery_level > 95: - returning_icon = DEFAULT_ICON + returning_icon_state = "{}-off".format(DEFAULT_ICON_STATE) + returning_icon_level = "{}-outline".format( + DEFAULT_ICON_LEVEL) else: - returning_icon = "{}-{}".format(DEFAULT_ICON, - str(rounded_level)) + returning_icon_level = "{}-{}".format(DEFAULT_ICON_LEVEL, + str(rounded_level)) + returning_icon_state = "{}-off".format(DEFAULT_ICON_STATE) elif battery_state == ios.ATTR_BATTERY_STATE_UNKNOWN: - returning_icon = "{}-unknown".format(DEFAULT_ICON) + returning_icon_level = "{}-unknown".format(DEFAULT_ICON_LEVEL) + returning_icon_state = "{}-unknown".format(DEFAULT_ICON_LEVEL) - return returning_icon + if self.type == "state": + return returning_icon_state + else: + return returning_icon_level def update(self): """Get the latest state of the sensor.""" From b641f6863cfc0779d598c903cda5479822cd5e82 Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Fri, 21 Apr 2017 23:22:36 -0400 Subject: [PATCH 05/16] Fix arwn platform to update hass state when events are received (#7202) The arwn platform was refactored to be asyncio friendly, however in doing so one thing was missed which was explicitly telling hass when something interesting has happened. This led to the very interesting to debug issue that the state cards were all out of date, even though the graphs were not. --- homeassistant/components/sensor/arwn.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/sensor/arwn.py b/homeassistant/components/sensor/arwn.py index 0bf68e68b0d..d2e148b8204 100644 --- a/homeassistant/components/sensor/arwn.py +++ b/homeassistant/components/sensor/arwn.py @@ -116,6 +116,7 @@ class ArwnSensor(Entity): """Update the sensor with the most recent event.""" self.event = {} self.event.update(event) + self.hass.async_add_job(self.async_update_ha_state()) @property def state(self): From dafbdbd2d028faad0ca0ada3dab24a2ef6b6c50f Mon Sep 17 00:00:00 2001 From: Henrik Nicolaisen Date: Sat, 22 Apr 2017 05:24:21 +0200 Subject: [PATCH 06/16] Issue 6749 updated pylgtv to 0.1.6 to fix thread leak in asyncio loop (#7199) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * updated pylgtv module to fix problems with timeouts * - update pylgtv to 0.1.6 - handle new TimeoutError exception from pylgtv * used full name for exception handling of concurrent.futures._base.TimeoutError * the exception handling should now follow the rules * float typecasting should not be necessary * use asyncio for TimeoutError it’s an alias for concurrent.futures.TimeoutError --- homeassistant/components/media_player/webostv.py | 12 ++++++++---- homeassistant/components/notify/webostv.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/media_player/webostv.py b/homeassistant/components/media_player/webostv.py index b8f43486db1..c1b51e2d32a 100644 --- a/homeassistant/components/media_player/webostv.py +++ b/homeassistant/components/media_player/webostv.py @@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.webostv/ """ import logging +import asyncio from datetime import timedelta from urllib.parse import urlparse @@ -24,7 +25,7 @@ from homeassistant.const import ( from homeassistant.loader import get_component import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pylgtv==0.1.5', +REQUIREMENTS = ['pylgtv==0.1.6', 'websockets==3.2', 'wakeonlan==0.2.2'] @@ -99,7 +100,8 @@ def setup_tv(host, mac, name, customize, config, hass, add_devices): _LOGGER.warning( "Connected to LG webOS TV %s but not paired", host) return - except (OSError, ConnectionClosed): + except (OSError, ConnectionClosed, TypeError, + asyncio.TimeoutError): _LOGGER.error("Unable to connect to host %s", host) return else: @@ -196,7 +198,8 @@ class LgWebOSDevice(MediaPlayerDevice): app = self._app_list[source['appId']] self._source_list[app['title']] = app - except (OSError, ConnectionClosed): + except (OSError, ConnectionClosed, TypeError, + asyncio.TimeoutError): self._state = STATE_OFF @property @@ -257,7 +260,8 @@ class LgWebOSDevice(MediaPlayerDevice): self._state = STATE_OFF try: self._client.power_off() - except (OSError, ConnectionClosed): + except (OSError, ConnectionClosed, TypeError, + asyncio.TimeoutError): pass def turn_on(self): diff --git a/homeassistant/components/notify/webostv.py b/homeassistant/components/notify/webostv.py index 50788b8cccf..0e91fc8698a 100644 --- a/homeassistant/components/notify/webostv.py +++ b/homeassistant/components/notify/webostv.py @@ -14,7 +14,7 @@ from homeassistant.components.notify import ( ATTR_DATA, BaseNotificationService, PLATFORM_SCHEMA) from homeassistant.const import (CONF_FILENAME, CONF_HOST, CONF_ICON) -REQUIREMENTS = ['pylgtv==0.1.5'] +REQUIREMENTS = ['pylgtv==0.1.6'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 7775d07fb63..e98df3bf99b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -564,7 +564,7 @@ pylast==1.8.0 # homeassistant.components.media_player.webostv # homeassistant.components.notify.webostv -pylgtv==0.1.5 +pylgtv==0.1.6 # homeassistant.components.litejet pylitejet==0.1 From 80e9e9bfda49f28d254e004e4e80af396b9752dd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 21 Apr 2017 23:32:51 -0700 Subject: [PATCH 07/16] tradfri: Improve color temp support detection (#7211) --- homeassistant/components/light/tradfri.py | 32 ++++++++++++++--------- script/dev_docker | 1 + virtualization/Docker/Dockerfile.dev | 3 ++- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/light/tradfri.py b/homeassistant/components/light/tradfri.py index 542f8dae8ef..ee4aa273224 100644 --- a/homeassistant/components/light/tradfri.py +++ b/homeassistant/components/light/tradfri.py @@ -13,13 +13,11 @@ from homeassistant.util import slugify _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['tradfri'] -SUPPORTED_FEATURES = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR) -SUPPORTED_FEATURES_IKEA = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP) PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA -IKEA = 'ikea_of_sweden' - -ALLOWED_TEMPERATURES = {IKEA: {2200: 'efd275', 2700: 'f1e0b5', 4000: 'f5faf6'}} -ALLOWED_FEATURES = {IKEA: SUPPORTED_FEATURES_IKEA} +IKEA = 'IKEA of Sweden' +ALLOWED_TEMPERATURES = { + IKEA: {2200: 'efd275', 2700: 'f1e0b5', 4000: 'f5faf6'} +} def setup_platform(hass, config, add_devices, discovery_info=None): @@ -46,8 +44,14 @@ class Tradfri(Light): self._light_data = light.light_control.lights[0] self._name = light.name self._rgb_color = None - self._features = ALLOWED_FEATURES.get( - slugify(self._light.device_info.manufacturer), SUPPORTED_FEATURES) + self._features = SUPPORT_BRIGHTNESS + + if self._light_data.hex_color is not None: + if self._light.device_info.manufacturer == IKEA: + self._features &= SUPPORT_COLOR_TEMP + else: + self._features &= SUPPORT_RGB_COLOR + self._ok_temps = ALLOWED_TEMPERATURES.get( slugify(self._light.device_info.manufacturer)) @@ -74,16 +78,18 @@ class Tradfri(Light): @property def color_temp(self): """Return the CT color value in mireds.""" - if not self.supported_features & SUPPORT_COLOR_TEMP or \ - not self._ok_temps: - return + if (self._light_data.hex_color is None or + self.supported_features & SUPPORT_COLOR_TEMP == 0 or + not self._ok_temps): + return None + kelvin = next(( kelvin for kelvin, hex_color in self._ok_temps.items() if hex_color == self._light_data.hex_color), None) if kelvin is None: _LOGGER.error( - 'unexpected color temperature found %s', - self._light_data.hex_color) + 'unexpected color temperature found for %s: %s', + self.name, self._light_data.hex_color) return return color_util.color_temperature_kelvin_to_mired(kelvin) diff --git a/script/dev_docker b/script/dev_docker index 73c4ee60d0a..514fce73477 100755 --- a/script/dev_docker +++ b/script/dev_docker @@ -27,6 +27,7 @@ else -v /etc/localtime:/etc/localtime:ro \ -v `pwd`:/usr/src/app \ -v `pwd`/config:/config \ + --rm \ -t -i home-assistant-dev fi diff --git a/virtualization/Docker/Dockerfile.dev b/virtualization/Docker/Dockerfile.dev index 62c9f9f6596..5d16e9400ef 100644 --- a/virtualization/Docker/Dockerfile.dev +++ b/virtualization/Docker/Dockerfile.dev @@ -12,6 +12,7 @@ MAINTAINER Paulus Schoutsen #ENV INSTALL_OPENZWAVE no #ENV INSTALL_LIBCEC no #ENV INSTALL_PHANTOMJS no +#ENV INSTALL_COAP_CLIENT no VOLUME /config @@ -25,7 +26,7 @@ RUN virtualization/Docker/setup_docker_prereqs # Install hass component dependencies COPY requirements_all.txt requirements_all.txt RUN pip3 install --no-cache-dir -r requirements_all.txt && \ - pip3 install --no-cache-dir mysqlclient psycopg2 uvloop + pip3 install --no-cache-dir mysqlclient psycopg2 uvloop cchardet # BEGIN: Development additions From 093c7f0e44082e2d3e4ff4abb646755af9defa45 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 22 Apr 2017 09:31:23 +0200 Subject: [PATCH 08/16] Fix tradfri lights (#7212) * Remove leftover use of slugify * The IKEA manufacturer key is now exactly as found in device info. * Fix bitwise addition of supported features --- homeassistant/components/light/tradfri.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/light/tradfri.py b/homeassistant/components/light/tradfri.py index ee4aa273224..9c0284c22f6 100644 --- a/homeassistant/components/light/tradfri.py +++ b/homeassistant/components/light/tradfri.py @@ -8,7 +8,6 @@ from homeassistant.components.light import \ PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA from homeassistant.components.tradfri import KEY_GATEWAY from homeassistant.util import color as color_util -from homeassistant.util import slugify _LOGGER = logging.getLogger(__name__) @@ -48,12 +47,12 @@ class Tradfri(Light): if self._light_data.hex_color is not None: if self._light.device_info.manufacturer == IKEA: - self._features &= SUPPORT_COLOR_TEMP + self._features |= SUPPORT_COLOR_TEMP else: - self._features &= SUPPORT_RGB_COLOR + self._features |= SUPPORT_RGB_COLOR self._ok_temps = ALLOWED_TEMPERATURES.get( - slugify(self._light.device_info.manufacturer)) + self._light.device_info.manufacturer) @property def supported_features(self): @@ -123,7 +122,7 @@ class Tradfri(Light): kelvin = color_util.color_temperature_mired_to_kelvin( kwargs[ATTR_COLOR_TEMP]) # find closest allowed kelvin temp from user input - kelvin = min(self._ok_temps.keys(), key=lambda x: abs(x-kelvin)) + kelvin = min(self._ok_temps.keys(), key=lambda x: abs(x - kelvin)) self._light_control.set_hex_color(self._ok_temps[kelvin]) def update(self): From 477ebd99b47484ddaade572298b56a4f8cc1218f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 22 Apr 2017 00:32:27 -0700 Subject: [PATCH 09/16] Version bump to 0.43 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index a65b721625c..5cc08642299 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 43 -PATCH_VERSION = '0.dev0' +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4, 2) From e01c36c906427f469797cdc7a21d82cc873a06b2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 24 Apr 2017 20:34:56 -0700 Subject: [PATCH 10/16] Version bump to 0.43.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5cc08642299..52d5f5abcde 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 43 -PATCH_VERSION = '0' +PATCH_VERSION = '1' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4, 2) From 79c6467797c6b4b462934d3d77defe9b79b1484d Mon Sep 17 00:00:00 2001 From: John Arild Berentsen Date: Sat, 22 Apr 2017 16:23:39 +0200 Subject: [PATCH 11/16] Zwave cover workaround for graber shades. (#7204) * wierd pylint complaint * Workaround for Graber csz1 shades * logging * Try direct * Try direct * Use workaround * Review changes and tests * test * reset test * Use Bright and Dim also as open and close is --- homeassistant/components/zwave/discovery_schemas.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zwave/discovery_schemas.py b/homeassistant/components/zwave/discovery_schemas.py index f6e56ce79c8..17c37556aea 100644 --- a/homeassistant/components/zwave/discovery_schemas.py +++ b/homeassistant/components/zwave/discovery_schemas.py @@ -81,12 +81,12 @@ DISCOVERY_SCHEMAS = [ }, 'open': { const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_SWITCH_MULTILEVEL], - const.DISC_LABEL: ['Open', 'Up'], + const.DISC_LABEL: ['Open', 'Up', 'Bright'], const.DISC_OPTIONAL: True, }, 'close': { const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_SWITCH_MULTILEVEL], - const.DISC_LABEL: ['Close', 'Down'], + const.DISC_LABEL: ['Close', 'Down', 'Dim'], const.DISC_OPTIONAL: True, }})}, {const.DISC_COMPONENT: 'cover', # Garage Door From c8404cb29999284cc4c1e6a49f865f956b9a38e0 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 23 Apr 2017 09:25:34 +0200 Subject: [PATCH 12/16] Upgrade paho-mqtt to 1.2.3 (#7214) --- homeassistant/components/mqtt/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 2b6774939da..458c5952a69 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -28,7 +28,7 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_PORT, CONF_PROTOCOL, CONF_PAYLOAD) from homeassistant.components.mqtt.server import HBMQTT_CONFIG_SCHEMA -REQUIREMENTS = ['paho-mqtt==1.2.2'] +REQUIREMENTS = ['paho-mqtt==1.2.3'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index e98df3bf99b..c6d14431017 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -419,7 +419,7 @@ openhomedevice==0.2.1 orvibo==1.1.1 # homeassistant.components.mqtt -paho-mqtt==1.2.2 +paho-mqtt==1.2.3 # homeassistant.components.media_player.panasonic_viera panasonic_viera==0.2 From 6c594e20f6465f151d833af96f7926a13e9222cd Mon Sep 17 00:00:00 2001 From: Greg Dowling Date: Mon, 24 Apr 2017 07:18:43 +0100 Subject: [PATCH 13/16] Workround for wemo subscription bug. (#7245) --- homeassistant/components/switch/wemo.py | 2 ++ homeassistant/components/wemo.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index c92523ad705..700d9d25b5a 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -76,6 +76,8 @@ class WemoSwitch(SwitchDevice): @property def should_poll(self): """No polling needed with subscriptions.""" + if self._model_name == 'Insight': + return True return False @property diff --git a/homeassistant/components/wemo.py b/homeassistant/components/wemo.py index a4b6674af74..98eefbc42d8 100644 --- a/homeassistant/components/wemo.py +++ b/homeassistant/components/wemo.py @@ -14,7 +14,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.const import EVENT_HOMEASSISTANT_STOP -REQUIREMENTS = ['pywemo==0.4.18'] +REQUIREMENTS = ['pywemo==0.4.19'] DOMAIN = 'wemo' diff --git a/requirements_all.txt b/requirements_all.txt index c6d14431017..461423f1ce7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -678,7 +678,7 @@ pyvera==0.2.26 pywebpush==0.6.1 # homeassistant.components.wemo -pywemo==0.4.18 +pywemo==0.4.19 # homeassistant.components.zabbix pyzabbix==0.7.4 From 1b55dbeb44d6b9c1a02cbe3238db577fb02140b4 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 24 Apr 2017 08:20:04 +0200 Subject: [PATCH 14/16] Fix telegram webhooks (#7236) * Always register the view if a webhook exists. * Return True if platform is set up succesfully, False otherwise. * Remove the webhook when home assistant stops. Webhooks and long polling are mutually excklusive. If a webhook is left after home assistant is stopped, a polling telegram bot is unable to be set up, on next start of home assistant. --- .../components/telegram_bot/__init__.py | 12 ++++----- .../components/telegram_bot/webhooks.py | 25 ++++++++++--------- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index 92a87153d99..1cd77491c51 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -50,7 +50,7 @@ def async_setup(hass, config): _LOGGER.error("Unknown notification service specified") return - _LOGGER.info("Setting up1 %s.%s", DOMAIN, p_type) + _LOGGER.info("Setting up %s.%s", DOMAIN, p_type) try: if hasattr(platform, 'async_setup_platform'): @@ -73,8 +73,6 @@ def async_setup(hass, config): _LOGGER.exception('Error setting up platform %s', p_type) return - return True - setup_tasks = [async_setup_platform(p_type, p_config) for p_type, p_config in config_per_platform(config, DOMAIN)] @@ -103,10 +101,10 @@ class BaseTelegramBotEntity: """Check for basic message rules and fire an event if message is ok.""" data = data.get('message') - if (not data - or 'from' not in data - or 'text' not in data - or data['from'].get('id') not in self.allowed_chat_ids): + if (not data or + 'from' not in data or + 'text' not in data or + data['from'].get('id') not in self.allowed_chat_ids): # Message is not correct. _LOGGER.error("Incoming message does not have required data.") return False diff --git a/homeassistant/components/telegram_bot/webhooks.py b/homeassistant/components/telegram_bot/webhooks.py index 3ffc03780bd..9fddf18d278 100644 --- a/homeassistant/components/telegram_bot/webhooks.py +++ b/homeassistant/components/telegram_bot/webhooks.py @@ -11,9 +11,8 @@ from ipaddress import ip_network import voluptuous as vol - from homeassistant.const import ( - HTTP_BAD_REQUEST, HTTP_UNAUTHORIZED) + EVENT_HOMEASSISTANT_STOP, HTTP_BAD_REQUEST, HTTP_UNAUTHORIZED) import homeassistant.helpers.config_validation as cv from homeassistant.components.http import HomeAssistantView from homeassistant.components.telegram_bot import CONF_ALLOWED_CHAT_IDS, \ @@ -27,6 +26,7 @@ REQUIREMENTS = ['python-telegram-bot==5.3.0'] _LOGGER = logging.getLogger(__name__) TELEGRAM_HANDLER_URL = '/api/telegram_webhooks' +REMOVE_HANDLER_URL = '' CONF_TRUSTED_NETWORKS = 'trusted_networks' DEFAULT_TRUSTED_NETWORKS = [ @@ -50,20 +50,21 @@ def setup_platform(hass, config, async_add_devices, discovery_info=None): bot = telegram.Bot(config[CONF_API_KEY]) current_status = bot.getWebhookInfo() - handler_url = "{0}{1}".format(hass.config.api.base_url, - TELEGRAM_HANDLER_URL) + handler_url = '{0}{1}'.format( + hass.config.api.base_url, TELEGRAM_HANDLER_URL) if current_status and current_status['url'] != handler_url: if bot.setWebhook(handler_url): _LOGGER.info("set new telegram webhook %s", handler_url) - - hass.http.register_view( - BotPushReceiver( - hass, - config[CONF_ALLOWED_CHAT_IDS], - config[CONF_TRUSTED_NETWORKS])) - else: _LOGGER.error("set telegram webhook failed %s", handler_url) + return False + + hass.bus.listen_once( + EVENT_HOMEASSISTANT_STOP, + lambda event: bot.setWebhook(REMOVE_HANDLER_URL)) + hass.http.register_view(BotPushReceiver( + hass, config[CONF_ALLOWED_CHAT_IDS], config[CONF_TRUSTED_NETWORKS])) + return True class BotPushReceiver(HomeAssistantView, BaseTelegramBotEntity): @@ -71,7 +72,7 @@ class BotPushReceiver(HomeAssistantView, BaseTelegramBotEntity): requires_auth = False url = TELEGRAM_HANDLER_URL - name = "telegram_webhooks" + name = 'telegram_webhooks' def __init__(self, hass, allowed_chat_ids, trusted_networks): """Initialize the class.""" From 41a803a7be07dea4358a641618ae91ed38c3ec32 Mon Sep 17 00:00:00 2001 From: Klaas Hoekema Date: Mon, 24 Apr 2017 02:58:17 -0400 Subject: [PATCH 15/16] Work around bad content-type in Hook api response (#7267) --- homeassistant/components/switch/hook.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switch/hook.py b/homeassistant/components/switch/hook.py index 58d3813f6fa..00fb7fdd909 100644 --- a/homeassistant/components/switch/hook.py +++ b/homeassistant/components/switch/hook.py @@ -47,7 +47,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): data={ 'username': username, 'password': password}) - data = yield from response.json() + # The Hook API returns JSON but calls it 'text/html'. Setting + # content_type=None disables aiohttp's content-type validation. + data = yield from response.json(content_type=None) except (asyncio.TimeoutError, aiohttp.ClientError) as error: _LOGGER.error("Failed authentication API call: %s", error) return False @@ -63,7 +65,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): response = yield from websession.get( '{}{}'.format(HOOK_ENDPOINT, 'device'), params={"token": token}) - data = yield from response.json() + data = yield from response.json(content_type=None) except (asyncio.TimeoutError, aiohttp.ClientError) as error: _LOGGER.error("Failed getting devices: %s", error) return False @@ -110,7 +112,7 @@ class HookSmartHome(SwitchDevice): with async_timeout.timeout(TIMEOUT, loop=self.hass.loop): response = yield from websession.get( url, params={"token": self._token}) - data = yield from response.json() + data = yield from response.json(content_type=None) except (asyncio.TimeoutError, aiohttp.ClientError) as error: _LOGGER.error("Failed setting state: %s", error) From 166bcc06875a09c6bbe9b687492776d97afa2f94 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 24 Apr 2017 20:51:03 -0700 Subject: [PATCH 16/16] Recorder: Check for ENTITY_ID key that contains None value (#7287) --- homeassistant/components/recorder/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 7e31523f7d1..04322646f80 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -246,8 +246,8 @@ class Recorder(threading.Thread): self.queue.task_done() continue - if ATTR_ENTITY_ID in event.data: - entity_id = event.data[ATTR_ENTITY_ID] + entity_id = event.data.get(ATTR_ENTITY_ID) + if entity_id is not None: domain = split_entity_id(entity_id)[0] # Exclude entities OR