diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 853fe4d9fb1..d73d21d7b3a 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -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): diff --git a/homeassistant/components/light/lifx/__init__.py b/homeassistant/components/light/lifx/__init__.py index 17512f5dd3b..c264fec35c5 100644 --- a/homeassistant/components/light/lifx/__init__.py +++ b/homeassistant/components/light/lifx/__init__.py @@ -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 = \ diff --git a/homeassistant/components/light/lifx/effects.py b/homeassistant/components/light/lifx/effects.py index 315759738f9..0a8c9cbf80f 100644 --- a/homeassistant/components/light/lifx/effects.py +++ b/homeassistant/components/light/lifx/effects.py @@ -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): diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index 495ef9c8b39..6ccd45dda66 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -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 diff --git a/tests/components/light/test_demo.py b/tests/components/light/test_demo.py index 9e318ea9192..8d717b1d89a 100644 --- a/tests/components/light/test_demo.py +++ b/tests/components/light/test_demo.py @@ -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."""