From f0db698f75e4d0026176a92cd41bd53a4413c214 Mon Sep 17 00:00:00 2001 From: Antoine Bertin Date: Mon, 28 Nov 2016 02:15:28 +0100 Subject: [PATCH] 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 --- homeassistant/components/light/__init__.py | 21 +++++++-- homeassistant/components/light/demo.py | 33 +++++++++++--- .../components/light/mqtt_template.py | 44 ++++++++++++++++--- tests/components/light/test_demo.py | 4 +- tests/components/light/test_mqtt_template.py | 42 ++++++++++++++---- 5 files changed, 120 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index b4708164fe2..c4ed91af0af 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -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.""" diff --git a/homeassistant/components/light/demo.py b/homeassistant/components/light/demo.py index e68bde8f379..b6048da243d 100644 --- a/homeassistant/components/light/demo.py +++ b/homeassistant/components/light/demo.py @@ -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): diff --git a/homeassistant/components/light/mqtt_template.py b/homeassistant/components/light/mqtt_template.py index 4566a383645..f632ba37236 100755 --- a/homeassistant/components/light/mqtt_template.py +++ b/homeassistant/components/light/mqtt_template.py @@ -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) diff --git a/tests/components/light/test_demo.py b/tests/components/light/test_demo.py index 759127c75f9..aa8c8d9f1e8 100644 --- a/tests/components/light/test_demo.py +++ b/tests/components/light/test_demo.py @@ -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.""" diff --git a/tests/components/light/test_mqtt_template.py b/tests/components/light/test_mqtt_template.py index 94cd2a0a19f..954f3087646 100755 --- a/tests/components/light/test_mqtt_template.py +++ b/tests/components/light/test_mqtt_template.py @@ -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'))