mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 20:57:21 +00:00
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:
parent
9dcc0b5ef5
commit
ed5f94fd8a
@ -50,13 +50,15 @@ ATTR_TRANSITION = "transition"
|
|||||||
ATTR_RGB_COLOR = "rgb_color"
|
ATTR_RGB_COLOR = "rgb_color"
|
||||||
ATTR_XY_COLOR = "xy_color"
|
ATTR_XY_COLOR = "xy_color"
|
||||||
ATTR_COLOR_TEMP = "color_temp"
|
ATTR_COLOR_TEMP = "color_temp"
|
||||||
|
ATTR_KELVIN = "kelvin"
|
||||||
ATTR_MIN_MIREDS = "min_mireds"
|
ATTR_MIN_MIREDS = "min_mireds"
|
||||||
ATTR_MAX_MIREDS = "max_mireds"
|
ATTR_MAX_MIREDS = "max_mireds"
|
||||||
ATTR_COLOR_NAME = "color_name"
|
ATTR_COLOR_NAME = "color_name"
|
||||||
ATTR_WHITE_VALUE = "white_value"
|
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 = "brightness"
|
||||||
|
ATTR_BRIGHTNESS_PCT = "brightness_pct"
|
||||||
|
|
||||||
# String representing a profile (built-in ones or external defined).
|
# String representing a profile (built-in ones or external defined).
|
||||||
ATTR_PROFILE = "profile"
|
ATTR_PROFILE = "profile"
|
||||||
@ -92,18 +94,21 @@ PROP_TO_ATTR = {
|
|||||||
# Service call validation schemas
|
# Service call validation schemas
|
||||||
VALID_TRANSITION = vol.All(vol.Coerce(float), vol.Clamp(min=0, max=6553))
|
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 = 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({
|
LIGHT_TURN_ON_SCHEMA = vol.Schema({
|
||||||
ATTR_ENTITY_ID: cv.entity_ids,
|
ATTR_ENTITY_ID: cv.entity_ids,
|
||||||
ATTR_PROFILE: cv.string,
|
ATTR_PROFILE: cv.string,
|
||||||
ATTR_TRANSITION: VALID_TRANSITION,
|
ATTR_TRANSITION: VALID_TRANSITION,
|
||||||
ATTR_BRIGHTNESS: VALID_BRIGHTNESS,
|
ATTR_BRIGHTNESS: VALID_BRIGHTNESS,
|
||||||
|
ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT,
|
||||||
ATTR_COLOR_NAME: cv.string,
|
ATTR_COLOR_NAME: cv.string,
|
||||||
ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)),
|
ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)),
|
||||||
vol.Coerce(tuple)),
|
vol.Coerce(tuple)),
|
||||||
ATTR_XY_COLOR: vol.All(vol.ExactSequence((cv.small_float, cv.small_float)),
|
ATTR_XY_COLOR: vol.All(vol.ExactSequence((cv.small_float, cv.small_float)),
|
||||||
vol.Coerce(tuple)),
|
vol.Coerce(tuple)),
|
||||||
ATTR_COLOR_TEMP: vol.All(vol.Coerce(int), vol.Range(min=1)),
|
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_WHITE_VALUE: vol.All(vol.Coerce(int), vol.Range(min=0, max=255)),
|
||||||
ATTR_FLASH: vol.In([FLASH_SHORT, FLASH_LONG]),
|
ATTR_FLASH: vol.In([FLASH_SHORT, FLASH_LONG]),
|
||||||
ATTR_EFFECT: cv.string,
|
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,
|
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):
|
profile=None, flash=None, effect=None, color_name=None):
|
||||||
"""Turn all or specified light on."""
|
"""Turn all or specified light on."""
|
||||||
hass.add_job(
|
hass.add_job(
|
||||||
async_turn_on, hass, entity_id, transition, brightness,
|
async_turn_on, hass, entity_id, transition, brightness, brightness_pct,
|
||||||
rgb_color, xy_color, color_temp, white_value,
|
rgb_color, xy_color, color_temp, kelvin, white_value,
|
||||||
profile, flash, effect, color_name)
|
profile, flash, effect, color_name)
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_turn_on(hass, entity_id=None, transition=None, brightness=None,
|
def async_turn_on(hass, entity_id=None, transition=None, brightness=None,
|
||||||
rgb_color=None, xy_color=None, color_temp=None,
|
brightness_pct=None, rgb_color=None, xy_color=None,
|
||||||
white_value=None, profile=None, flash=None, effect=None,
|
color_temp=None, kelvin=None, white_value=None,
|
||||||
color_name=None):
|
profile=None, flash=None, effect=None, color_name=None):
|
||||||
"""Turn all or specified light on."""
|
"""Turn all or specified light on."""
|
||||||
data = {
|
data = {
|
||||||
key: value for key, value in [
|
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_PROFILE, profile),
|
||||||
(ATTR_TRANSITION, transition),
|
(ATTR_TRANSITION, transition),
|
||||||
(ATTR_BRIGHTNESS, brightness),
|
(ATTR_BRIGHTNESS, brightness),
|
||||||
|
(ATTR_BRIGHTNESS_PCT, brightness_pct),
|
||||||
(ATTR_RGB_COLOR, rgb_color),
|
(ATTR_RGB_COLOR, rgb_color),
|
||||||
(ATTR_XY_COLOR, xy_color),
|
(ATTR_XY_COLOR, xy_color),
|
||||||
(ATTR_COLOR_TEMP, color_temp),
|
(ATTR_COLOR_TEMP, color_temp),
|
||||||
|
(ATTR_KELVIN, kelvin),
|
||||||
(ATTR_WHITE_VALUE, white_value),
|
(ATTR_WHITE_VALUE, white_value),
|
||||||
(ATTR_FLASH, flash),
|
(ATTR_FLASH, flash),
|
||||||
(ATTR_EFFECT, effect),
|
(ATTR_EFFECT, effect),
|
||||||
@ -207,6 +215,27 @@ def toggle(hass, entity_id=None, transition=None):
|
|||||||
hass.services.call(DOMAIN, SERVICE_TOGGLE, data)
|
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
|
@asyncio.coroutine
|
||||||
def async_setup(hass, config):
|
def async_setup(hass, config):
|
||||||
"""Expose light control via statemachine and services."""
|
"""Expose light control via statemachine and services."""
|
||||||
@ -215,10 +244,8 @@ def async_setup(hass, config):
|
|||||||
yield from component.async_setup(config)
|
yield from component.async_setup(config)
|
||||||
|
|
||||||
# load profiles from files
|
# load profiles from files
|
||||||
profiles = yield from hass.loop.run_in_executor(
|
profiles_valid = yield from Profiles.load_profiles(hass)
|
||||||
None, _load_profile_data, hass)
|
if not profiles_valid:
|
||||||
|
|
||||||
if profiles is None:
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
@ -231,17 +258,7 @@ def async_setup(hass, config):
|
|||||||
target_lights = component.async_extract_from_service(service)
|
target_lights = component.async_extract_from_service(service)
|
||||||
params.pop(ATTR_ENTITY_ID, None)
|
params.pop(ATTR_ENTITY_ID, None)
|
||||||
|
|
||||||
# Processing extra data for turn light on request.
|
preprocess_turn_on_alternatives(params)
|
||||||
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)
|
|
||||||
|
|
||||||
for light in target_lights:
|
for light in target_lights:
|
||||||
if service.service == SERVICE_TURN_ON:
|
if service.service == SERVICE_TURN_ON:
|
||||||
@ -287,31 +304,51 @@ def async_setup(hass, config):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _load_profile_data(hass):
|
class Profiles:
|
||||||
"""Load built-in profiles and custom profiles."""
|
"""Representation of available color profiles."""
|
||||||
profile_paths = [os.path.join(os.path.dirname(__file__),
|
|
||||||
LIGHT_PROFILES_FILE),
|
|
||||||
hass.config.path(LIGHT_PROFILES_FILE)]
|
|
||||||
profiles = {}
|
|
||||||
|
|
||||||
for profile_path in profile_paths:
|
_all = None
|
||||||
if not os.path.isfile(profile_path):
|
|
||||||
continue
|
|
||||||
with open(profile_path) as inp:
|
|
||||||
reader = csv.reader(inp)
|
|
||||||
|
|
||||||
# Skip the header
|
@classmethod
|
||||||
next(reader, None)
|
@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 profile_path in profile_paths:
|
||||||
for rec in reader:
|
if not os.path.isfile(profile_path):
|
||||||
profile, color_x, color_y, brightness = PROFILE_SCHEMA(rec)
|
continue
|
||||||
profiles[profile] = (color_x, color_y, brightness)
|
with open(profile_path) as inp:
|
||||||
except vol.MultipleInvalid as ex:
|
reader = csv.reader(inp)
|
||||||
_LOGGER.error("Error parsing light profile from %s: %s",
|
|
||||||
profile_path, ex)
|
# Skip the header
|
||||||
return None
|
next(reader, None)
|
||||||
return 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
|
||||||
|
|
||||||
|
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):
|
class Light(ToggleEntity):
|
||||||
|
@ -18,10 +18,11 @@ import voluptuous as vol
|
|||||||
|
|
||||||
from homeassistant.components.light import (
|
from homeassistant.components.light import (
|
||||||
Light, DOMAIN, PLATFORM_SCHEMA, LIGHT_TURN_ON_SCHEMA,
|
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,
|
ATTR_XY_COLOR, ATTR_COLOR_TEMP, ATTR_TRANSITION, ATTR_EFFECT,
|
||||||
SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR,
|
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.config import load_yaml_config_file
|
||||||
from homeassistant.util.color import (
|
from homeassistant.util.color import (
|
||||||
color_temperature_mired_to_kelvin, color_temperature_kelvin_to_mired)
|
color_temperature_mired_to_kelvin, color_temperature_kelvin_to_mired)
|
||||||
@ -434,9 +435,7 @@ class LIFXLight(Light):
|
|||||||
if hsbk is not None:
|
if hsbk is not None:
|
||||||
return [hsbk, True]
|
return [hsbk, True]
|
||||||
|
|
||||||
color_name = kwargs.pop(ATTR_COLOR_NAME, None)
|
preprocess_turn_on_alternatives(kwargs)
|
||||||
if color_name is not None:
|
|
||||||
kwargs[ATTR_RGB_COLOR] = color_util.color_name_to_rgb(color_name)
|
|
||||||
|
|
||||||
if ATTR_RGB_COLOR in kwargs:
|
if ATTR_RGB_COLOR in kwargs:
|
||||||
hue, saturation, brightness = \
|
hue, saturation, brightness = \
|
||||||
|
@ -6,8 +6,9 @@ import random
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.light import (
|
from homeassistant.components.light import (
|
||||||
DOMAIN, ATTR_BRIGHTNESS, ATTR_COLOR_NAME, ATTR_RGB_COLOR, ATTR_EFFECT,
|
DOMAIN, ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_NAME,
|
||||||
ATTR_TRANSITION)
|
ATTR_RGB_COLOR, ATTR_EFFECT, ATTR_TRANSITION,
|
||||||
|
VALID_BRIGHTNESS, VALID_BRIGHTNESS_PCT)
|
||||||
from homeassistant.const import (ATTR_ENTITY_ID)
|
from homeassistant.const import (ATTR_ENTITY_ID)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
@ -36,7 +37,8 @@ LIFX_EFFECT_SCHEMA = vol.Schema({
|
|||||||
})
|
})
|
||||||
|
|
||||||
LIFX_EFFECT_BREATHE_SCHEMA = LIFX_EFFECT_SCHEMA.extend({
|
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_COLOR_NAME: cv.string,
|
||||||
ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)),
|
ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)),
|
||||||
vol.Coerce(tuple)),
|
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_PULSE_SCHEMA = LIFX_EFFECT_BREATHE_SCHEMA
|
||||||
|
|
||||||
LIFX_EFFECT_COLORLOOP_SCHEMA = LIFX_EFFECT_SCHEMA.extend({
|
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.Optional(ATTR_PERIOD, default=60):
|
||||||
vol.All(vol.Coerce(float), vol.Clamp(min=0.05)),
|
vol.All(vol.Coerce(float), vol.Clamp(min=0.05)),
|
||||||
vol.Optional(ATTR_CHANGE, default=20):
|
vol.Optional(ATTR_CHANGE, default=20):
|
||||||
|
@ -26,7 +26,11 @@ turn_on:
|
|||||||
|
|
||||||
color_temp:
|
color_temp:
|
||||||
description: Color temperature for the light in mireds
|
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:
|
white_value:
|
||||||
description: Number between 0..255 indicating level of white
|
description: Number between 0..255 indicating level of white
|
||||||
@ -36,6 +40,10 @@ turn_on:
|
|||||||
description: Number between 0..255 indicating brightness
|
description: Number between 0..255 indicating brightness
|
||||||
example: 120
|
example: 120
|
||||||
|
|
||||||
|
brightness_pct:
|
||||||
|
description: Number between 0..100 indicating percentage of full brightness
|
||||||
|
example: 47
|
||||||
|
|
||||||
profile:
|
profile:
|
||||||
description: Name of a light profile to use
|
description: Name of a light profile to use
|
||||||
example: relax
|
example: relax
|
||||||
|
@ -56,6 +56,11 @@ class TestDemoLight(unittest.TestCase):
|
|||||||
self.assertEqual(154, state.attributes.get(light.ATTR_MIN_MIREDS))
|
self.assertEqual(154, state.attributes.get(light.ATTR_MIN_MIREDS))
|
||||||
self.assertEqual(500, state.attributes.get(light.ATTR_MAX_MIREDS))
|
self.assertEqual(500, state.attributes.get(light.ATTR_MAX_MIREDS))
|
||||||
self.assertEqual('none', state.attributes.get(light.ATTR_EFFECT))
|
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):
|
def test_turn_off(self):
|
||||||
"""Test light turn off method."""
|
"""Test light turn off method."""
|
||||||
|
Loading…
x
Reference in New Issue
Block a user