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:
parent
cf57db919e
commit
f0db698f75
5 changed files with 120 additions and 24 deletions
|
@ -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."""
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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'))
|
||||
|
|
Loading…
Add table
Reference in a new issue