1
mirror of https://github.com/home-assistant/core synced 2024-07-30 21:18:57 +02:00

Adding support for a white value (#3338)

* Update __init__.py

addedattribute "WHITE_VALUE" to improve support for RGBW devices

* Update services.yaml

* Update __init__.py

* Update __init__.py

* Update __init__.py

* Update __init__.py

* Update __init__.py

* Update __init__.py

shortened line

* Update __init__.py

* Update __init__.py

* Add mysensors RGBW and light tests

* Activate support for mysensors RGBW devices with support for
	white_value attribute.
* Add white_value support in light demo platform.
* Add tests for white_value and more for light component.
* Add tests for light demo platform.
* Fix import order in check_config.
This commit is contained in:
Marc Pabst 2016-09-21 06:26:40 +02:00 committed by Paulus Schoutsen
parent e891f1a260
commit 138205a019
7 changed files with 173 additions and 49 deletions

View File

@ -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."""

View File

@ -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):

View File

@ -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):

View File

@ -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

View File

@ -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 * ' '

View File

@ -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))

View File

@ -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},