diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index f1bc83dfd17..85f611a679b 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -39,6 +39,7 @@ SUPPORT_FLASH = 8 SUPPORT_RGB_COLOR = 16 SUPPORT_TRANSITION = 32 SUPPORT_XY_COLOR = 64 +SUPPORT_WHITE_VALUE = 128 # Integer that represents transition time in seconds to make change. ATTR_TRANSITION = "transition" @@ -48,6 +49,7 @@ ATTR_RGB_COLOR = "rgb_color" ATTR_XY_COLOR = "xy_color" ATTR_COLOR_TEMP = "color_temp" ATTR_COLOR_NAME = "color_name" +ATTR_WHITE_VALUE = "white_value" # int with value 0 .. 255 representing brightness of the light. ATTR_BRIGHTNESS = "brightness" @@ -73,6 +75,7 @@ PROP_TO_ATTR = { 'color_temp': ATTR_COLOR_TEMP, 'rgb_color': ATTR_RGB_COLOR, 'xy_color': ATTR_XY_COLOR, + 'white_value': ATTR_WHITE_VALUE, 'supported_features': ATTR_SUPPORTED_FEATURES, } @@ -91,6 +94,7 @@ LIGHT_TURN_ON_SCHEMA = vol.Schema({ ATTR_XY_COLOR: vol.All(vol.ExactSequence((cv.small_float, cv.small_float)), vol.Coerce(tuple)), ATTR_COLOR_TEMP: vol.All(int, vol.Range(min=154, max=500)), + ATTR_WHITE_VALUE: vol.All(int, vol.Range(min=0, max=255)), ATTR_FLASH: vol.In([FLASH_SHORT, FLASH_LONG]), ATTR_EFFECT: vol.In([EFFECT_COLORLOOP, EFFECT_RANDOM, EFFECT_WHITE]), }) @@ -121,8 +125,8 @@ def is_on(hass, entity_id=None): # pylint: disable=too-many-arguments def turn_on(hass, entity_id=None, transition=None, brightness=None, - rgb_color=None, xy_color=None, color_temp=None, profile=None, - flash=None, effect=None, color_name=None): + rgb_color=None, xy_color=None, color_temp=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 [ @@ -133,6 +137,7 @@ def turn_on(hass, entity_id=None, transition=None, brightness=None, (ATTR_RGB_COLOR, rgb_color), (ATTR_XY_COLOR, xy_color), (ATTR_COLOR_TEMP, color_temp), + (ATTR_WHITE_VALUE, white_value), (ATTR_FLASH, flash), (ATTR_EFFECT, effect), (ATTR_COLOR_NAME, color_name), @@ -283,6 +288,11 @@ class Light(ToggleEntity): """Return the CT color value in mireds.""" return None + @property + def white_value(self): + """Return the white value of this light between 0..255.""" + return None + @property def state_attributes(self): """Return optional state attributes.""" diff --git a/homeassistant/components/light/demo.py b/homeassistant/components/light/demo.py index 4eb0a61d983..ac8e017cb7d 100644 --- a/homeassistant/components/light/demo.py +++ b/homeassistant/components/light/demo.py @@ -7,8 +7,9 @@ https://home-assistant.io/components/demo/ import random from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, Light) + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_WHITE_VALUE, + ATTR_XY_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, + SUPPORT_WHITE_VALUE, Light) LIGHT_COLORS = [ [237, 224, 33], @@ -17,7 +18,8 @@ LIGHT_COLORS = [ LIGHT_TEMPS = [240, 380] -SUPPORT_DEMO = SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR +SUPPORT_DEMO = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR | + SUPPORT_WHITE_VALUE) def setup_platform(hass, config, add_devices_callback, discovery_info=None): @@ -33,13 +35,17 @@ class DemoLight(Light): """Represenation of a demo light.""" # pylint: disable=too-many-arguments - def __init__(self, name, state, rgb=None, ct=None, brightness=180): + def __init__( + self, name, state, rgb=None, ct=None, brightness=180, + xy_color=(.5, .5), white=200): """Initialize the light.""" self._name = name self._state = state - self._rgb = rgb or random.choice(LIGHT_COLORS) + self._rgb = rgb self._ct = ct or random.choice(LIGHT_TEMPS) self._brightness = brightness + self._xy_color = xy_color + self._white = white @property def should_poll(self): @@ -56,6 +62,11 @@ class DemoLight(Light): """Return the brightness of this light between 0..255.""" return self._brightness + @property + def xy_color(self): + """Return the XY color value [float, float].""" + return self._xy_color + @property def rgb_color(self): """Return the RBG color value.""" @@ -66,6 +77,11 @@ class DemoLight(Light): """Return the CT color temperature.""" return self._ct + @property + def white_value(self): + """Return the white value of this light between 0..255.""" + return self._white + @property def is_on(self): """Return true if light is on.""" @@ -89,6 +105,12 @@ class DemoLight(Light): if ATTR_BRIGHTNESS in kwargs: self._brightness = kwargs[ATTR_BRIGHTNESS] + if ATTR_XY_COLOR in kwargs: + self._xy_color = kwargs[ATTR_XY_COLOR] + + if ATTR_WHITE_VALUE in kwargs: + self._white = kwargs[ATTR_WHITE_VALUE] + self.update_ha_state() def turn_off(self, **kwargs): diff --git a/homeassistant/components/light/mysensors.py b/homeassistant/components/light/mysensors.py index c33793127a2..6f8e984b799 100644 --- a/homeassistant/components/light/mysensors.py +++ b/homeassistant/components/light/mysensors.py @@ -9,17 +9,19 @@ import logging from homeassistant.components import mysensors from homeassistant.components.light import (ATTR_BRIGHTNESS, ATTR_RGB_COLOR, + ATTR_WHITE_VALUE, SUPPORT_BRIGHTNESS, - SUPPORT_RGB_COLOR, Light) + SUPPORT_RGB_COLOR, + SUPPORT_WHITE_VALUE, Light) from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.util.color import rgb_hex_to_rgb_list _LOGGER = logging.getLogger(__name__) -ATTR_RGB_WHITE = 'rgb_white' ATTR_VALUE = 'value' ATTR_VALUE_TYPE = 'value_type' -SUPPORT_MYSENSORS = SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR +SUPPORT_MYSENSORS = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR | + SUPPORT_WHITE_VALUE) def setup_platform(hass, config, add_devices, discovery_info=None): @@ -41,13 +43,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None): pres.S_DIMMER: MySensorsLightDimmer, } if float(gateway.protocol_version) >= 1.5: - # Add V_RGBW when rgb_white is implemented in the frontend map_sv_types.update({ pres.S_RGB_LIGHT: [set_req.V_RGB], + pres.S_RGBW_LIGHT: [set_req.V_RGBW], }) map_sv_types[pres.S_DIMMER].append(set_req.V_PERCENTAGE) device_class_map.update({ pres.S_RGB_LIGHT: MySensorsLightRGB, + pres.S_RGBW_LIGHT: MySensorsLightRGBW, }) devices = {} gateway.platform_callbacks.append(mysensors.pf_callback_factory( @@ -76,8 +79,8 @@ class MySensorsLight(mysensors.MySensorsDeviceEntity, Light): return self._rgb @property - def rgb_white(self): # not implemented in the frontend yet - """Return the white value in RGBW, value between 0..255.""" + def white_value(self): + """Return the white value of this light between 0..255.""" return self._white @property @@ -99,13 +102,15 @@ class MySensorsLight(mysensors.MySensorsDeviceEntity, Light): """Turn on light child device.""" set_req = self.gateway.const.SetReq - if not self._state and set_req.V_LIGHT in self._values: - self.gateway.set_child_value( - self.node_id, self.child_id, set_req.V_LIGHT, 1) + if self._state or set_req.V_LIGHT not in self._values: + return + self.gateway.set_child_value( + self.node_id, self.child_id, set_req.V_LIGHT, 1) if self.gateway.optimistic: # optimistically assume that light has changed state self._state = True + self._values[set_req.V_LIGHT] = STATE_ON self.update_ha_state() def _turn_on_dimmer(self, **kwargs): @@ -113,30 +118,34 @@ class MySensorsLight(mysensors.MySensorsDeviceEntity, Light): set_req = self.gateway.const.SetReq brightness = self._brightness - if ATTR_BRIGHTNESS in kwargs and \ - kwargs[ATTR_BRIGHTNESS] != self._brightness: - brightness = kwargs[ATTR_BRIGHTNESS] - percent = round(100 * brightness / 255) - self.gateway.set_child_value( - self.node_id, self.child_id, set_req.V_DIMMER, percent) + if ATTR_BRIGHTNESS not in kwargs or \ + kwargs[ATTR_BRIGHTNESS] == self._brightness or \ + set_req.V_DIMMER not in self._values: + return + brightness = kwargs[ATTR_BRIGHTNESS] + percent = round(100 * brightness / 255) + self.gateway.set_child_value( + self.node_id, self.child_id, set_req.V_DIMMER, percent) if self.gateway.optimistic: # optimistically assume that light has changed state self._brightness = brightness + self._values[set_req.V_DIMMER] = percent self.update_ha_state() def _turn_on_rgb_and_w(self, hex_template, **kwargs): """Turn on RGB or RGBW child device.""" rgb = self._rgb white = self._white + hex_color = self._values.get(self.value_type) - if ATTR_RGB_WHITE in kwargs and \ - kwargs[ATTR_RGB_WHITE] != self._white: - white = kwargs[ATTR_RGB_WHITE] + if ATTR_WHITE_VALUE in kwargs and \ + kwargs[ATTR_WHITE_VALUE] != self._white: + white = kwargs[ATTR_WHITE_VALUE] if ATTR_RGB_COLOR in kwargs and \ kwargs[ATTR_RGB_COLOR] != self._rgb: - rgb = kwargs[ATTR_RGB_COLOR] + rgb = list(kwargs[ATTR_RGB_COLOR]) if white is not None and hex_template == '%02x%02x%02x%02x': rgb.append(white) hex_color = hex_template % tuple(rgb) @@ -147,6 +156,8 @@ class MySensorsLight(mysensors.MySensorsDeviceEntity, Light): # optimistically assume that light has changed state self._rgb = rgb self._white = white + if hex_color: + self._values[self.value_type] = hex_color self.update_ha_state() def _turn_off_light(self, value_type=None, value=None): @@ -179,6 +190,7 @@ class MySensorsLight(mysensors.MySensorsDeviceEntity, Light): def _turn_off_main(self, value_type=None, value=None): """Turn the device off.""" + set_req = self.gateway.const.SetReq if value_type is None or value is None: _LOGGER.warning( '%s: value_type %s, value = %s, ' @@ -190,6 +202,8 @@ class MySensorsLight(mysensors.MySensorsDeviceEntity, Light): if self.gateway.optimistic: # optimistically assume that light has changed state self._state = False + self._values[value_type] = ( + STATE_OFF if set_req.V_LIGHT in self._values else value) self.update_ha_state() def _update_light(self): diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index d6a6931652b..afcc54d717f 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -28,6 +28,10 @@ turn_on: description: Color temperature for the light in mireds (154-500) example: '250' + white_value: + description: Number between 0..255 indicating level of white + example: '250' + brightness: description: Number between 0..255 indicating brightness example: 120 diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index d1bf12187e8..ce2ffe8b472 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -1,17 +1,18 @@ """Script to ensure a configuration file exists.""" import argparse +import logging import os from glob import glob -import logging -from typing import List, Dict, Sequence -from unittest.mock import patch from platform import system +from unittest.mock import patch + +from typing import Dict, List, Sequence -from homeassistant.exceptions import HomeAssistantError import homeassistant.bootstrap as bootstrap import homeassistant.config as config_util import homeassistant.loader as loader import homeassistant.util.yaml as yaml +from homeassistant.exceptions import HomeAssistantError REQUIREMENTS = ('colorlog>2.1,<3',) if system() == 'Windows': # Ensure colorama installed for colorlog on Windows @@ -96,7 +97,7 @@ def run(script_args: List) -> int: if args.files: print(color(C_HEAD, 'yaml files'), '(used /', - color('red', 'not used')+')') + color('red', 'not used') + ')') # Python 3.5 gets a recursive, but not in 3.4 for yfn in sorted(glob(os.path.join(config_dir, '*.yaml')) + glob(os.path.join(config_dir, '*/*.yaml'))): @@ -250,12 +251,12 @@ def dump_dict(layer, indent_count=1, listi=False, **kwargs): indent_str = indent_count * ' ' if listi or isinstance(layer, list): - indent_str = indent_str[:-1]+'-' + indent_str = indent_str[:-1] + '-' if isinstance(layer, Dict): for key, value in layer.items(): if isinstance(value, dict) or isinstance(value, list): print(indent_str, key + ':', line_src(value)) - dump_dict(value, indent_count+2) + dump_dict(value, indent_count + 2) else: print(indent_str, key + ':', value) indent_str = indent_count * ' ' diff --git a/tests/components/light/test_demo.py b/tests/components/light/test_demo.py new file mode 100644 index 00000000000..72fb80500e4 --- /dev/null +++ b/tests/components/light/test_demo.py @@ -0,0 +1,57 @@ +"""The tests for the demo light component.""" +# pylint: disable=too-many-public-methods,protected-access +import unittest + +import homeassistant.components.light as light + +from tests.common import get_test_home_assistant + +ENTITY_LIGHT = 'light.bed_light' + + +class TestDemoClimate(unittest.TestCase): + """Test the demo climate hvac.""" + + def setUp(self): # pylint: disable=invalid-name + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.assertTrue(light.setup(self.hass, {'light': { + 'platform': 'demo', + }})) + + def tearDown(self): # pylint: disable=invalid-name + """Stop down everything that was started.""" + self.hass.stop() + + def test_state_attributes(self): + """Test light state attributes.""" + light.turn_on( + self.hass, ENTITY_LIGHT, xy_color=(.4, .6), brightness=25) + self.hass.pool.block_till_done() + state = self.hass.states.get(ENTITY_LIGHT) + self.assertTrue(light.is_on(self.hass, ENTITY_LIGHT)) + self.assertEqual((.4, .6), state.attributes.get(light.ATTR_XY_COLOR)) + self.assertEqual(25, state.attributes.get(light.ATTR_BRIGHTNESS)) + self.assertEqual( + (82, 91, 0), state.attributes.get(light.ATTR_RGB_COLOR)) + light.turn_on( + self.hass, ENTITY_LIGHT, rgb_color=(251, 252, 253), + white_value=254) + self.hass.pool.block_till_done() + state = self.hass.states.get(ENTITY_LIGHT) + self.assertEqual(254, state.attributes.get(light.ATTR_WHITE_VALUE)) + self.assertEqual( + (251, 252, 253), state.attributes.get(light.ATTR_RGB_COLOR)) + light.turn_on(self.hass, ENTITY_LIGHT, color_temp=400) + self.hass.pool.block_till_done() + state = self.hass.states.get(ENTITY_LIGHT) + self.assertEqual(400, state.attributes.get(light.ATTR_COLOR_TEMP)) + + def test_turn_off(self): + """Test light turn off method.""" + light.turn_on(self.hass, ENTITY_LIGHT) + self.hass.pool.block_till_done() + self.assertTrue(light.is_on(self.hass, ENTITY_LIGHT)) + light.turn_off(self.hass, ENTITY_LIGHT) + self.hass.pool.block_till_done() + self.assertFalse(light.is_on(self.hass, ENTITY_LIGHT)) diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index c0f2c532156..04139e88feb 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -55,7 +55,9 @@ class TestLight(unittest.TestCase): brightness='brightness_val', rgb_color='rgb_color_val', xy_color='xy_color_val', - profile='profile_val') + profile='profile_val', + color_name='color_name_val', + white_value='white_val') self.hass.block_till_done() @@ -72,6 +74,9 @@ class TestLight(unittest.TestCase): self.assertEqual('rgb_color_val', call.data.get(light.ATTR_RGB_COLOR)) self.assertEqual('xy_color_val', call.data.get(light.ATTR_XY_COLOR)) self.assertEqual('profile_val', call.data.get(light.ATTR_PROFILE)) + self.assertEqual( + 'color_name_val', call.data.get(light.ATTR_COLOR_NAME)) + self.assertEqual('white_val', call.data.get(light.ATTR_WHITE_VALUE)) # Test turn_off turn_off_calls = mock_service( @@ -170,23 +175,28 @@ class TestLight(unittest.TestCase): # Ensure all attributes process correctly light.turn_on(self.hass, dev1.entity_id, - transition=10, brightness=20) + transition=10, brightness=20, color_name='blue') light.turn_on( - self.hass, dev2.entity_id, rgb_color=(255, 255, 255)) + self.hass, dev2.entity_id, rgb_color=(255, 255, 255), + white_value=255) light.turn_on(self.hass, dev3.entity_id, xy_color=(.4, .6)) self.hass.block_till_done() - method, data = dev1.last_call('turn_on') + _, data = dev1.last_call('turn_on') self.assertEqual( {light.ATTR_TRANSITION: 10, - light.ATTR_BRIGHTNESS: 20}, + light.ATTR_BRIGHTNESS: 20, + light.ATTR_RGB_COLOR: (0, 0, 255)}, data) - method, data = dev2.last_call('turn_on') - self.assertEquals(data[light.ATTR_RGB_COLOR], (255, 255, 255)) + _, data = dev2.last_call('turn_on') + self.assertEqual( + {light.ATTR_RGB_COLOR: (255, 255, 255), + light.ATTR_WHITE_VALUE: 255}, + data) - method, data = dev3.last_call('turn_on') + _, data = dev3.last_call('turn_on') self.assertEqual({light.ATTR_XY_COLOR: (.4, .6)}, data) # One of the light profiles @@ -201,13 +211,13 @@ class TestLight(unittest.TestCase): self.hass.block_till_done() - method, data = dev1.last_call('turn_on') + _, data = dev1.last_call('turn_on') self.assertEqual( {light.ATTR_BRIGHTNESS: prof_bri, light.ATTR_XY_COLOR: (prof_x, prof_y)}, data) - method, data = dev2.last_call('turn_on') + _, data = dev2.last_call('turn_on') self.assertEqual( {light.ATTR_BRIGHTNESS: 100, light.ATTR_XY_COLOR: (.4, .6)}, @@ -221,23 +231,29 @@ class TestLight(unittest.TestCase): self.hass.block_till_done() - method, data = dev1.last_call('turn_on') + _, data = dev1.last_call('turn_on') self.assertEqual({}, data) - method, data = dev2.last_call('turn_on') + _, data = dev2.last_call('turn_on') self.assertEqual({}, data) - method, data = dev3.last_call('turn_on') + _, data = dev3.last_call('turn_on') self.assertEqual({}, data) # faulty attributes will not trigger a service call light.turn_on( self.hass, dev1.entity_id, profile=prof_name, brightness='bright', rgb_color='yellowish') + light.turn_on( + self.hass, dev2.entity_id, + white_value='high') self.hass.block_till_done() - method, data = dev1.last_call('turn_on') + _, data = dev1.last_call('turn_on') + self.assertEqual({}, data) + + _, data = dev2.last_call('turn_on') self.assertEqual({}, data) def test_broken_light_profiles(self): @@ -271,13 +287,13 @@ class TestLight(unittest.TestCase): self.hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: 'test'}} )) - dev1, dev2, dev3 = platform.DEVICES + dev1, _, _ = platform.DEVICES light.turn_on(self.hass, dev1.entity_id, profile='test') self.hass.block_till_done() - method, data = dev1.last_call('turn_on') + _, data = dev1.last_call('turn_on') self.assertEqual( {light.ATTR_XY_COLOR: (.4, .6), light.ATTR_BRIGHTNESS: 100},