Add kelvin/brightness_pct alternatives to light.turn_on (#7596)

* Refactor color profiles to a class

* Refactor into preprocess_turn_on_alternatives

* LIFX: use light.preprocess_turn_on_alternatives

This avoids the color_name duplication and gains support for profile.

* Add kelvin parameter to light.turn_on

* Add brightness_pct parameter to light.turn_on

* LIFX: accept brightness_pct in effects

* Add test of kelvin/brightness_pct conversions
This commit is contained in:
Anders Melchiorsen 2017-05-17 08:00:46 +02:00 committed by Paulus Schoutsen
parent 9dcc0b5ef5
commit ed5f94fd8a
5 changed files with 106 additions and 54 deletions

View File

@ -50,13 +50,15 @@ ATTR_TRANSITION = "transition"
ATTR_RGB_COLOR = "rgb_color"
ATTR_XY_COLOR = "xy_color"
ATTR_COLOR_TEMP = "color_temp"
ATTR_KELVIN = "kelvin"
ATTR_MIN_MIREDS = "min_mireds"
ATTR_MAX_MIREDS = "max_mireds"
ATTR_COLOR_NAME = "color_name"
ATTR_WHITE_VALUE = "white_value"
# int with value 0 .. 255 representing brightness of the light.
# Brightness of the light, 0..255 or percentage
ATTR_BRIGHTNESS = "brightness"
ATTR_BRIGHTNESS_PCT = "brightness_pct"
# String representing a profile (built-in ones or external defined).
ATTR_PROFILE = "profile"
@ -92,18 +94,21 @@ PROP_TO_ATTR = {
# Service call validation schemas
VALID_TRANSITION = vol.All(vol.Coerce(float), vol.Clamp(min=0, max=6553))
VALID_BRIGHTNESS = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255))
VALID_BRIGHTNESS_PCT = vol.All(vol.Coerce(float), vol.Range(min=0, max=100))
LIGHT_TURN_ON_SCHEMA = vol.Schema({
ATTR_ENTITY_ID: cv.entity_ids,
ATTR_PROFILE: cv.string,
ATTR_TRANSITION: VALID_TRANSITION,
ATTR_BRIGHTNESS: VALID_BRIGHTNESS,
ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT,
ATTR_COLOR_NAME: cv.string,
ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)),
vol.Coerce(tuple)),
ATTR_XY_COLOR: vol.All(vol.ExactSequence((cv.small_float, cv.small_float)),
vol.Coerce(tuple)),
ATTR_COLOR_TEMP: vol.All(vol.Coerce(int), vol.Range(min=1)),
ATTR_KELVIN: vol.All(vol.Coerce(int), vol.Range(min=0)),
ATTR_WHITE_VALUE: vol.All(vol.Coerce(int), vol.Range(min=0, max=255)),
ATTR_FLASH: vol.In([FLASH_SHORT, FLASH_LONG]),
ATTR_EFFECT: cv.string,
@ -142,20 +147,21 @@ def is_on(hass, entity_id=None):
def turn_on(hass, entity_id=None, transition=None, brightness=None,
rgb_color=None, xy_color=None, color_temp=None, white_value=None,
brightness_pct=None, rgb_color=None, xy_color=None,
color_temp=None, kelvin=None, white_value=None,
profile=None, flash=None, effect=None, color_name=None):
"""Turn all or specified light on."""
hass.add_job(
async_turn_on, hass, entity_id, transition, brightness,
rgb_color, xy_color, color_temp, white_value,
async_turn_on, hass, entity_id, transition, brightness, brightness_pct,
rgb_color, xy_color, color_temp, kelvin, white_value,
profile, flash, effect, color_name)
@callback
def async_turn_on(hass, entity_id=None, transition=None, brightness=None,
rgb_color=None, xy_color=None, color_temp=None,
white_value=None, profile=None, flash=None, effect=None,
color_name=None):
brightness_pct=None, rgb_color=None, xy_color=None,
color_temp=None, kelvin=None, white_value=None,
profile=None, flash=None, effect=None, color_name=None):
"""Turn all or specified light on."""
data = {
key: value for key, value in [
@ -163,9 +169,11 @@ def async_turn_on(hass, entity_id=None, transition=None, brightness=None,
(ATTR_PROFILE, profile),
(ATTR_TRANSITION, transition),
(ATTR_BRIGHTNESS, brightness),
(ATTR_BRIGHTNESS_PCT, brightness_pct),
(ATTR_RGB_COLOR, rgb_color),
(ATTR_XY_COLOR, xy_color),
(ATTR_COLOR_TEMP, color_temp),
(ATTR_KELVIN, kelvin),
(ATTR_WHITE_VALUE, white_value),
(ATTR_FLASH, flash),
(ATTR_EFFECT, effect),
@ -207,6 +215,27 @@ def toggle(hass, entity_id=None, transition=None):
hass.services.call(DOMAIN, SERVICE_TOGGLE, data)
def preprocess_turn_on_alternatives(params):
"""Processing extra data for turn light on request."""
profile = Profiles.get(params.pop(ATTR_PROFILE, None))
if profile is not None:
params.setdefault(ATTR_XY_COLOR, profile[:2])
params.setdefault(ATTR_BRIGHTNESS, profile[2])
color_name = params.pop(ATTR_COLOR_NAME, None)
if color_name is not None:
params[ATTR_RGB_COLOR] = color_util.color_name_to_rgb(color_name)
kelvin = params.pop(ATTR_KELVIN, None)
if kelvin is not None:
mired = color_util.color_temperature_kelvin_to_mired(kelvin)
params[ATTR_COLOR_TEMP] = mired
brightness_pct = params.pop(ATTR_BRIGHTNESS_PCT, None)
if brightness_pct is not None:
params[ATTR_BRIGHTNESS] = int(255 * brightness_pct/100)
@asyncio.coroutine
def async_setup(hass, config):
"""Expose light control via statemachine and services."""
@ -215,10 +244,8 @@ def async_setup(hass, config):
yield from component.async_setup(config)
# load profiles from files
profiles = yield from hass.loop.run_in_executor(
None, _load_profile_data, hass)
if profiles is None:
profiles_valid = yield from Profiles.load_profiles(hass)
if not profiles_valid:
return False
@asyncio.coroutine
@ -231,17 +258,7 @@ def async_setup(hass, config):
target_lights = component.async_extract_from_service(service)
params.pop(ATTR_ENTITY_ID, None)
# Processing extra data for turn light on request.
profile = profiles.get(params.pop(ATTR_PROFILE, None))
if profile:
params.setdefault(ATTR_XY_COLOR, profile[:2])
params.setdefault(ATTR_BRIGHTNESS, profile[2])
color_name = params.pop(ATTR_COLOR_NAME, None)
if color_name is not None:
params[ATTR_RGB_COLOR] = color_util.color_name_to_rgb(color_name)
preprocess_turn_on_alternatives(params)
for light in target_lights:
if service.service == SERVICE_TURN_ON:
@ -287,31 +304,51 @@ def async_setup(hass, config):
return True
def _load_profile_data(hass):
"""Load built-in profiles and custom profiles."""
profile_paths = [os.path.join(os.path.dirname(__file__),
LIGHT_PROFILES_FILE),
hass.config.path(LIGHT_PROFILES_FILE)]
profiles = {}
class Profiles:
"""Representation of available color profiles."""
for profile_path in profile_paths:
if not os.path.isfile(profile_path):
continue
with open(profile_path) as inp:
reader = csv.reader(inp)
_all = None
# Skip the header
next(reader, None)
@classmethod
@asyncio.coroutine
def load_profiles(cls, hass):
"""Load and cache profiles."""
def load_profile_data(hass):
"""Load built-in profiles and custom profiles."""
profile_paths = [os.path.join(os.path.dirname(__file__),
LIGHT_PROFILES_FILE),
hass.config.path(LIGHT_PROFILES_FILE)]
profiles = {}
try:
for rec in reader:
profile, color_x, color_y, brightness = PROFILE_SCHEMA(rec)
profiles[profile] = (color_x, color_y, brightness)
except vol.MultipleInvalid as ex:
_LOGGER.error("Error parsing light profile from %s: %s",
profile_path, ex)
return None
return profiles
for profile_path in profile_paths:
if not os.path.isfile(profile_path):
continue
with open(profile_path) as inp:
reader = csv.reader(inp)
# Skip the header
next(reader, None)
try:
for rec in reader:
profile, color_x, color_y, brightness = \
PROFILE_SCHEMA(rec)
profiles[profile] = (color_x, color_y, brightness)
except vol.MultipleInvalid as ex:
_LOGGER.error(
"Error parsing light profile from %s: %s",
profile_path, ex)
return None
return profiles
cls._all = yield from hass.loop.run_in_executor(
None, load_profile_data, hass)
return cls._all is not None
@classmethod
def get(cls, name):
"""Return a named profile."""
return cls._all.get(name)
class Light(ToggleEntity):

View File

@ -18,10 +18,11 @@ import voluptuous as vol
from homeassistant.components.light import (
Light, DOMAIN, PLATFORM_SCHEMA, LIGHT_TURN_ON_SCHEMA,
ATTR_BRIGHTNESS, ATTR_COLOR_NAME, ATTR_RGB_COLOR,
ATTR_BRIGHTNESS, ATTR_RGB_COLOR,
ATTR_XY_COLOR, ATTR_COLOR_TEMP, ATTR_TRANSITION, ATTR_EFFECT,
SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR,
SUPPORT_XY_COLOR, SUPPORT_TRANSITION, SUPPORT_EFFECT)
SUPPORT_XY_COLOR, SUPPORT_TRANSITION, SUPPORT_EFFECT,
preprocess_turn_on_alternatives)
from homeassistant.config import load_yaml_config_file
from homeassistant.util.color import (
color_temperature_mired_to_kelvin, color_temperature_kelvin_to_mired)
@ -434,9 +435,7 @@ class LIFXLight(Light):
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)
preprocess_turn_on_alternatives(kwargs)
if ATTR_RGB_COLOR in kwargs:
hue, saturation, brightness = \

View File

@ -6,8 +6,9 @@ import random
import voluptuous as vol
from homeassistant.components.light import (
DOMAIN, ATTR_BRIGHTNESS, ATTR_COLOR_NAME, ATTR_RGB_COLOR, ATTR_EFFECT,
ATTR_TRANSITION)
DOMAIN, ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_NAME,
ATTR_RGB_COLOR, ATTR_EFFECT, ATTR_TRANSITION,
VALID_BRIGHTNESS, VALID_BRIGHTNESS_PCT)
from homeassistant.const import (ATTR_ENTITY_ID)
import homeassistant.helpers.config_validation as cv
@ -36,7 +37,8 @@ LIFX_EFFECT_SCHEMA = vol.Schema({
})
LIFX_EFFECT_BREATHE_SCHEMA = LIFX_EFFECT_SCHEMA.extend({
ATTR_BRIGHTNESS: vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)),
ATTR_BRIGHTNESS: VALID_BRIGHTNESS,
ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT,
ATTR_COLOR_NAME: cv.string,
ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)),
vol.Coerce(tuple)),
@ -49,7 +51,8 @@ LIFX_EFFECT_BREATHE_SCHEMA = LIFX_EFFECT_SCHEMA.extend({
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)),
ATTR_BRIGHTNESS: VALID_BRIGHTNESS,
ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT,
vol.Optional(ATTR_PERIOD, default=60):
vol.All(vol.Coerce(float), vol.Clamp(min=0.05)),
vol.Optional(ATTR_CHANGE, default=20):

View File

@ -26,7 +26,11 @@ turn_on:
color_temp:
description: Color temperature for the light in mireds
example: '250'
example: 250
kelvin:
description: Color temperature for the light in Kelvin
example: 4000
white_value:
description: Number between 0..255 indicating level of white
@ -36,6 +40,10 @@ turn_on:
description: Number between 0..255 indicating brightness
example: 120
brightness_pct:
description: Number between 0..100 indicating percentage of full brightness
example: 47
profile:
description: Name of a light profile to use
example: relax

View File

@ -56,6 +56,11 @@ class TestDemoLight(unittest.TestCase):
self.assertEqual(154, state.attributes.get(light.ATTR_MIN_MIREDS))
self.assertEqual(500, state.attributes.get(light.ATTR_MAX_MIREDS))
self.assertEqual('none', state.attributes.get(light.ATTR_EFFECT))
light.turn_on(self.hass, ENTITY_LIGHT, kelvin=4000, brightness_pct=50)
self.hass.block_till_done()
state = self.hass.states.get(ENTITY_LIGHT)
self.assertEqual(250, state.attributes.get(light.ATTR_COLOR_TEMP))
self.assertEqual(127, state.attributes.get(light.ATTR_BRIGHTNESS))
def test_turn_off(self):
"""Test light turn off method."""