Light effects (#4538)

* Add support for light effects

* Move PLATFORM_SCHEMA changes in light to mqtt_template

* Add effect validation

* Add unittests

* Add light effect to demo and unittests

* Use cv.string for config validation

* Use cv.ensure_list for config validation

* Fix typo

* Remove unused exception management for effect
This commit is contained in:
Antoine Bertin 2016-11-28 02:15:28 +01:00 committed by Paulus Schoutsen
parent cf57db919e
commit f0db698f75
5 changed files with 120 additions and 24 deletions

View file

@ -64,6 +64,9 @@ ATTR_FLASH = "flash"
FLASH_SHORT = "short"
FLASH_LONG = "long"
# List of possible effects
ATTR_EFFECT_LIST = "effect_list"
# Apply an effect to the light, can be EFFECT_COLORLOOP.
ATTR_EFFECT = "effect"
EFFECT_COLORLOOP = "colorloop"
@ -78,6 +81,8 @@ PROP_TO_ATTR = {
'rgb_color': ATTR_RGB_COLOR,
'xy_color': ATTR_XY_COLOR,
'white_value': ATTR_WHITE_VALUE,
'effect_list': ATTR_EFFECT_LIST,
'effect': ATTR_EFFECT,
'supported_features': ATTR_SUPPORTED_FEATURES,
}
@ -87,10 +92,10 @@ VALID_BRIGHTNESS = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255))
LIGHT_TURN_ON_SCHEMA = vol.Schema({
ATTR_ENTITY_ID: cv.entity_ids,
ATTR_PROFILE: str,
ATTR_PROFILE: cv.string,
ATTR_TRANSITION: VALID_TRANSITION,
ATTR_BRIGHTNESS: VALID_BRIGHTNESS,
ATTR_COLOR_NAME: str,
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)),
@ -99,7 +104,7 @@ LIGHT_TURN_ON_SCHEMA = vol.Schema({
max=color_util.HASS_COLOR_MAX)),
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]),
ATTR_EFFECT: cv.string,
})
LIGHT_TURN_OFF_SCHEMA = vol.Schema({
@ -314,6 +319,16 @@ class Light(ToggleEntity):
"""Return the white value of this light between 0..255."""
return None
@property
def effect_list(self):
"""Return the list of supported effects."""
return None
@property
def effect(self):
"""Return the current effect."""
return None
@property
def state_attributes(self):
"""Return optional state attributes."""

View file

@ -7,25 +7,29 @@ https://home-assistant.io/components/demo/
import random
from homeassistant.components.light import (
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)
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT,
ATTR_RGB_COLOR, ATTR_WHITE_VALUE, ATTR_XY_COLOR, SUPPORT_BRIGHTNESS,
SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_RGB_COLOR, SUPPORT_WHITE_VALUE,
Light)
LIGHT_COLORS = [
[237, 224, 33],
[255, 63, 111],
]
LIGHT_EFFECT_LIST = ['rainbow', 'none']
LIGHT_TEMPS = [240, 380]
SUPPORT_DEMO = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR |
SUPPORT_WHITE_VALUE)
SUPPORT_DEMO = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_EFFECT |
SUPPORT_RGB_COLOR | SUPPORT_WHITE_VALUE)
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Setup the demo light platform."""
add_devices_callback([
DemoLight("Bed Light", False),
DemoLight("Bed Light", False, effect_list=LIGHT_EFFECT_LIST,
effect=LIGHT_EFFECT_LIST[0]),
DemoLight("Ceiling Lights", True, LIGHT_COLORS[0], LIGHT_TEMPS[1]),
DemoLight("Kitchen Lights", True, LIGHT_COLORS[1], LIGHT_TEMPS[0])
])
@ -36,7 +40,7 @@ class DemoLight(Light):
def __init__(
self, name, state, rgb=None, ct=None, brightness=180,
xy_color=(.5, .5), white=200):
xy_color=(.5, .5), white=200, effect_list=None, effect=None):
"""Initialize the light."""
self._name = name
self._state = state
@ -45,6 +49,8 @@ class DemoLight(Light):
self._brightness = brightness
self._xy_color = xy_color
self._white = white
self._effect_list = effect_list
self._effect = effect
@property
def should_poll(self):
@ -81,6 +87,16 @@ class DemoLight(Light):
"""Return the white value of this light between 0..255."""
return self._white
@property
def effect_list(self):
"""Return the list of supported effects."""
return self._effect_list
@property
def effect(self):
"""Return the current effect."""
return self._effect
@property
def is_on(self):
"""Return true if light is on."""
@ -110,6 +126,9 @@ class DemoLight(Light):
if ATTR_WHITE_VALUE in kwargs:
self._white = kwargs[ATTR_WHITE_VALUE]
if ATTR_EFFECT in kwargs:
self._effect = kwargs[ATTR_EFFECT]
self.update_ha_state()
def turn_off(self, **kwargs):

View file

@ -10,8 +10,8 @@ import voluptuous as vol
import homeassistant.components.mqtt as mqtt
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_TRANSITION, PLATFORM_SCHEMA,
ATTR_FLASH, SUPPORT_BRIGHTNESS, SUPPORT_FLASH,
ATTR_BRIGHTNESS, ATTR_EFFECT, ATTR_FLASH, ATTR_RGB_COLOR, ATTR_TRANSITION,
PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_EFFECT, SUPPORT_FLASH,
SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, Light)
from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, STATE_ON, STATE_OFF
from homeassistant.components.mqtt import (
@ -27,6 +27,7 @@ DEPENDENCIES = ['mqtt']
DEFAULT_NAME = 'MQTT Template Light'
DEFAULT_OPTIMISTIC = False
CONF_EFFECT_LIST = "effect_list"
CONF_COMMAND_ON_TEMPLATE = 'command_on_template'
CONF_COMMAND_OFF_TEMPLATE = 'command_off_template'
CONF_STATE_TEMPLATE = 'state_template'
@ -34,12 +35,14 @@ CONF_BRIGHTNESS_TEMPLATE = 'brightness_template'
CONF_RED_TEMPLATE = 'red_template'
CONF_GREEN_TEMPLATE = 'green_template'
CONF_BLUE_TEMPLATE = 'blue_template'
CONF_EFFECT_TEMPLATE = 'effect_template'
SUPPORT_MQTT_TEMPLATE = (SUPPORT_BRIGHTNESS | SUPPORT_FLASH |
SUPPORT_MQTT_TEMPLATE = (SUPPORT_BRIGHTNESS | SUPPORT_EFFECT | SUPPORT_FLASH |
SUPPORT_RGB_COLOR | SUPPORT_TRANSITION)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_EFFECT_LIST): vol.All(cv.ensure_list, [cv.string]),
vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Required(CONF_COMMAND_ON_TEMPLATE): cv.template,
@ -49,6 +52,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_RED_TEMPLATE): cv.template,
vol.Optional(CONF_GREEN_TEMPLATE): cv.template,
vol.Optional(CONF_BLUE_TEMPLATE): cv.template,
vol.Optional(CONF_EFFECT_TEMPLATE): cv.template,
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
vol.Optional(CONF_QOS, default=mqtt.DEFAULT_QOS):
vol.All(vol.Coerce(int), vol.In([0, 1, 2])),
@ -61,6 +65,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices([MqttTemplate(
hass,
config.get(CONF_NAME),
config.get(CONF_EFFECT_LIST),
{
key: config.get(key) for key in (
CONF_STATE_TOPIC,
@ -75,7 +80,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
CONF_BRIGHTNESS_TEMPLATE,
CONF_RED_TEMPLATE,
CONF_GREEN_TEMPLATE,
CONF_BLUE_TEMPLATE
CONF_BLUE_TEMPLATE,
CONF_EFFECT_TEMPLATE
)
},
config.get(CONF_OPTIMISTIC),
@ -87,10 +93,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class MqttTemplate(Light):
"""Representation of a MQTT Template light."""
def __init__(self, hass, name, topics, templates, optimistic, qos, retain):
def __init__(self, hass, name, effect_list, topics, templates, optimistic,
qos, retain):
"""Initialize MQTT Template light."""
self._hass = hass
self._name = name
self._effect_list = effect_list
self._topics = topics
self._templates = templates
for tpl in self._templates.values():
@ -114,6 +122,7 @@ class MqttTemplate(Light):
self._rgb = [0, 0, 0]
else:
self._rgb = None
self._effect = None
def state_received(topic, payload, qos):
"""A new MQTT message has been received."""
@ -152,6 +161,17 @@ class MqttTemplate(Light):
except ValueError:
_LOGGER.warning('Invalid color value received')
# read effect
if self._templates[CONF_EFFECT_TEMPLATE] is not None:
effect = self._templates[CONF_EFFECT_TEMPLATE].\
render_with_possible_json_value(payload)
# validate effect value
if effect in self._effect_list:
self._effect = effect
else:
_LOGGER.warning('Unsupported effect value received')
self.update_ha_state()
if self._topics[CONF_STATE_TOPIC] is not None:
@ -191,6 +211,16 @@ class MqttTemplate(Light):
"""Return True if unable to access real state of the entity."""
return self._optimistic
@property
def effect_list(self):
"""Return the list of supported effects."""
return self._effect_list
@property
def effect(self):
"""Return the current effect."""
return self._effect
def turn_on(self, **kwargs):
"""Turn the entity on."""
# state
@ -214,6 +244,10 @@ class MqttTemplate(Light):
if self._optimistic:
self._rgb = kwargs[ATTR_RGB_COLOR]
# effect
if ATTR_EFFECT in kwargs:
values['effect'] = kwargs.get(ATTR_EFFECT)
# flash
if ATTR_FLASH in kwargs:
values['flash'] = kwargs.get(ATTR_FLASH)

View file

@ -37,6 +37,7 @@ class TestDemoClimate(unittest.TestCase):
self.assertEqual(25, state.attributes.get(light.ATTR_BRIGHTNESS))
self.assertEqual(
(82, 91, 0), state.attributes.get(light.ATTR_RGB_COLOR))
self.assertEqual('rainbow', state.attributes.get(light.ATTR_EFFECT))
light.turn_on(
self.hass, ENTITY_LIGHT, rgb_color=(251, 252, 253),
white_value=254)
@ -45,10 +46,11 @@ class TestDemoClimate(unittest.TestCase):
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)
light.turn_on(self.hass, ENTITY_LIGHT, color_temp=400, effect='none')
self.hass.block_till_done()
state = self.hass.states.get(ENTITY_LIGHT)
self.assertEqual(400, state.attributes.get(light.ATTR_COLOR_TEMP))
self.assertEqual('none', state.attributes.get(light.ATTR_EFFECT))
def test_turn_off(self):
"""Test light turn off method."""

View file

@ -90,22 +90,24 @@ class TestLightMQTTTemplate(unittest.TestCase):
self.assertIsNone(state.attributes.get('rgb_color'))
self.assertIsNone(state.attributes.get('brightness'))
def test_state_brightness_color_change_via_topic(self): \
def test_state_brightness_color_effect_change_via_topic(self): \
# pylint: disable=invalid-name
"""Test state, brightness and color change via topic."""
"""Test state, brightness, color and effect change via topic."""
self.hass.config.components = ['mqtt']
with assert_setup_component(1):
assert setup_component(self.hass, light.DOMAIN, {
light.DOMAIN: {
'platform': 'mqtt_template',
'name': 'test',
'effect_list': ['rainbow', 'colorloop'],
'state_topic': 'test_light_rgb',
'command_topic': 'test_light_rgb/set',
'command_on_template': 'on,'
'{{ brightness|d }},'
'{{ red|d }}-'
'{{ green|d }}-'
'{{ blue|d }}',
'{{ blue|d }},'
'{{ effect|d }}',
'command_off_template': 'off',
'state_template': '{{ value.split(",")[0] }}',
'brightness_template': '{{ value.split(",")[1] }}',
@ -114,7 +116,8 @@ class TestLightMQTTTemplate(unittest.TestCase):
'green_template': '{{ value.split(",")[2].'
'split("-")[1] }}',
'blue_template': '{{ value.split(",")[2].'
'split("-")[2] }}'
'split("-")[2] }}',
'effect_template': '{{ value.split(",")[3] }}'
}
})
@ -122,16 +125,18 @@ class TestLightMQTTTemplate(unittest.TestCase):
self.assertEqual(STATE_OFF, state.state)
self.assertIsNone(state.attributes.get('rgb_color'))
self.assertIsNone(state.attributes.get('brightness'))
self.assertIsNone(state.attributes.get('effect'))
self.assertIsNone(state.attributes.get(ATTR_ASSUMED_STATE))
# turn on the light, full white
fire_mqtt_message(self.hass, 'test_light_rgb', 'on,255,255-255-255')
fire_mqtt_message(self.hass, 'test_light_rgb', 'on,255,255-255-255,')
self.hass.block_till_done()
state = self.hass.states.get('light.test')
self.assertEqual(STATE_ON, state.state)
self.assertEqual([255, 255, 255], state.attributes.get('rgb_color'))
self.assertEqual(255, state.attributes.get('brightness'))
self.assertIsNone(state.attributes.get('effect'))
# turn the light off
fire_mqtt_message(self.hass, 'test_light_rgb', 'off')
@ -155,6 +160,13 @@ class TestLightMQTTTemplate(unittest.TestCase):
light_state = self.hass.states.get('light.test')
self.assertEqual([41, 42, 43], light_state.attributes.get('rgb_color'))
# change the effect
fire_mqtt_message(self.hass, 'test_light_rgb', 'on,,41-42-43,rainbow')
self.hass.block_till_done()
light_state = self.hass.states.get('light.test')
self.assertEqual('rainbow', light_state.attributes.get('effect'))
def test_optimistic(self): \
# pylint: disable=invalid-name
"""Test optimistic mode."""
@ -314,13 +326,15 @@ class TestLightMQTTTemplate(unittest.TestCase):
light.DOMAIN: {
'platform': 'mqtt_template',
'name': 'test',
'effect_list': ['rainbow', 'colorloop'],
'state_topic': 'test_light_rgb',
'command_topic': 'test_light_rgb/set',
'command_on_template': 'on,'
'{{ brightness|d }},'
'{{ red|d }}-'
'{{ green|d }}-'
'{{ blue|d }}',
'{{ blue|d }},'
'{{ effect|d }}',
'command_off_template': 'off',
'state_template': '{{ value.split(",")[0] }}',
'brightness_template': '{{ value.split(",")[1] }}',
@ -329,7 +343,8 @@ class TestLightMQTTTemplate(unittest.TestCase):
'green_template': '{{ value.split(",")[2].'
'split("-")[1] }}',
'blue_template': '{{ value.split(",")[2].'
'split("-")[2] }}'
'split("-")[2] }}',
'effect_template': '{{ value.split(",")[3] }}',
}
})
@ -337,16 +352,19 @@ class TestLightMQTTTemplate(unittest.TestCase):
self.assertEqual(STATE_OFF, state.state)
self.assertIsNone(state.attributes.get('rgb_color'))
self.assertIsNone(state.attributes.get('brightness'))
self.assertIsNone(state.attributes.get('effect'))
self.assertIsNone(state.attributes.get(ATTR_ASSUMED_STATE))
# turn on the light, full white
fire_mqtt_message(self.hass, 'test_light_rgb', 'on,255,255-255-255')
fire_mqtt_message(self.hass, 'test_light_rgb',
'on,255,255-255-255,rainbow')
self.hass.block_till_done()
state = self.hass.states.get('light.test')
self.assertEqual(STATE_ON, state.state)
self.assertEqual(255, state.attributes.get('brightness'))
self.assertEqual([255, 255, 255], state.attributes.get('rgb_color'))
self.assertEqual('rainbow', state.attributes.get('effect'))
# bad state value
fire_mqtt_message(self.hass, 'test_light_rgb', 'offf')
@ -371,3 +389,11 @@ class TestLightMQTTTemplate(unittest.TestCase):
# color should not have changed
state = self.hass.states.get('light.test')
self.assertEqual([255, 255, 255], state.attributes.get('rgb_color'))
# bad effect value
fire_mqtt_message(self.hass, 'test_light_rgb', 'on,255,a-b-c,white')
self.hass.block_till_done()
# effect should not have changed
state = self.hass.states.get('light.test')
self.assertEqual('rainbow', state.attributes.get('effect'))