From c5d2e44baf24e5f14dadcc5ce040db99a4e6e7d8 Mon Sep 17 00:00:00 2001
From: Elahd Bar-Shai <elahd@users.noreply.github.com>
Date: Mon, 20 Apr 2020 08:26:28 -0400
Subject: [PATCH] Add white value in light template platform (#32481)

* Added support for white value in Template Light integration.

* Tests now working.

* Clean up

* Make template more robust

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
---
 homeassistant/components/template/light.py |  67 ++++++++++++++
 tests/components/template/test_light.py    | 103 ++++++++++++++++++++-
 2 files changed, 168 insertions(+), 2 deletions(-)

diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py
index c6d6656f134..1c19dfb33a6 100644
--- a/homeassistant/components/template/light.py
+++ b/homeassistant/components/template/light.py
@@ -7,10 +7,12 @@ from homeassistant.components.light import (
     ATTR_BRIGHTNESS,
     ATTR_COLOR_TEMP,
     ATTR_HS_COLOR,
+    ATTR_WHITE_VALUE,
     ENTITY_ID_FORMAT,
     SUPPORT_BRIGHTNESS,
     SUPPORT_COLOR,
     SUPPORT_COLOR_TEMP,
+    SUPPORT_WHITE_VALUE,
     Light,
 )
 from homeassistant.const import (
@@ -46,6 +48,8 @@ CONF_TEMPERATURE_TEMPLATE = "temperature_template"
 CONF_TEMPERATURE_ACTION = "set_temperature"
 CONF_COLOR_TEMPLATE = "color_template"
 CONF_COLOR_ACTION = "set_color"
+CONF_WHITE_VALUE_TEMPLATE = "white_value_template"
+CONF_WHITE_VALUE_ACTION = "set_white_value"
 
 LIGHT_SCHEMA = vol.Schema(
     {
@@ -63,6 +67,8 @@ LIGHT_SCHEMA = vol.Schema(
         vol.Optional(CONF_TEMPERATURE_ACTION): cv.SCRIPT_SCHEMA,
         vol.Optional(CONF_COLOR_TEMPLATE): cv.template,
         vol.Optional(CONF_COLOR_ACTION): cv.SCRIPT_SCHEMA,
+        vol.Optional(CONF_WHITE_VALUE_TEMPLATE): cv.template,
+        vol.Optional(CONF_WHITE_VALUE_ACTION): cv.SCRIPT_SCHEMA,
     }
 )
 
@@ -95,6 +101,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
         color_action = device_config.get(CONF_COLOR_ACTION)
         color_template = device_config.get(CONF_COLOR_TEMPLATE)
 
+        white_value_action = device_config.get(CONF_WHITE_VALUE_ACTION)
+        white_value_template = device_config.get(CONF_WHITE_VALUE_TEMPLATE)
+
         templates = {
             CONF_VALUE_TEMPLATE: state_template,
             CONF_ICON_TEMPLATE: icon_template,
@@ -103,6 +112,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
             CONF_LEVEL_TEMPLATE: level_template,
             CONF_TEMPERATURE_TEMPLATE: temperature_template,
             CONF_COLOR_TEMPLATE: color_template,
+            CONF_WHITE_VALUE_TEMPLATE: white_value_template,
         }
 
         initialise_templates(hass, templates)
@@ -128,6 +138,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
                 temperature_template,
                 color_action,
                 color_template,
+                white_value_action,
+                white_value_template,
             )
         )
 
@@ -155,6 +167,8 @@ class LightTemplate(Light):
         temperature_template,
         color_action,
         color_template,
+        white_value_action,
+        white_value_template,
     ):
         """Initialize the light."""
         self.hass = hass
@@ -180,6 +194,10 @@ class LightTemplate(Light):
         if color_action is not None:
             self._color_script = Script(hass, color_action)
         self._color_template = color_template
+        self._white_value_script = None
+        if white_value_action is not None:
+            self._white_value_script = Script(hass, white_value_action)
+        self._white_value_template = white_value_template
 
         self._state = False
         self._icon = None
@@ -187,6 +205,7 @@ class LightTemplate(Light):
         self._brightness = None
         self._temperature = None
         self._color = None
+        self._white_value = None
         self._entities = entity_ids
         self._available = True
 
@@ -200,6 +219,11 @@ class LightTemplate(Light):
         """Return the CT color value in mireds."""
         return self._temperature
 
+    @property
+    def white_value(self):
+        """Return the white value."""
+        return self._white_value
+
     @property
     def hs_color(self):
         """Return the hue and saturation color value [float, float]."""
@@ -220,6 +244,8 @@ class LightTemplate(Light):
             supported_features |= SUPPORT_COLOR_TEMP
         if self._color_script is not None:
             supported_features |= SUPPORT_COLOR
+        if self._white_value_script is not None:
+            supported_features |= SUPPORT_WHITE_VALUE
         return supported_features
 
     @property
@@ -263,6 +289,7 @@ class LightTemplate(Light):
                 or self._level_template is not None
                 or self._temperature_template is not None
                 or self._color_template is not None
+                or self._white_value_template is not None
                 or self._availability_template is not None
             ):
                 async_track_state_change(
@@ -290,6 +317,13 @@ class LightTemplate(Light):
             self._brightness = kwargs[ATTR_BRIGHTNESS]
             optimistic_set = True
 
+        if self._white_value_template is None and ATTR_WHITE_VALUE in kwargs:
+            _LOGGER.info(
+                "Optimistically setting white value to %s", kwargs[ATTR_WHITE_VALUE]
+            )
+            self._white_value = kwargs[ATTR_WHITE_VALUE]
+            optimistic_set = True
+
         if self._temperature_template is None and ATTR_COLOR_TEMP in kwargs:
             _LOGGER.info(
                 "Optimistically setting color temperature to %s",
@@ -306,6 +340,10 @@ class LightTemplate(Light):
             await self._temperature_script.async_run(
                 {"color_temp": kwargs[ATTR_COLOR_TEMP]}, context=self._context
             )
+        elif ATTR_WHITE_VALUE in kwargs and self._white_value_script:
+            await self._white_value_script.async_run(
+                {"white_value": kwargs[ATTR_WHITE_VALUE]}, context=self._context
+            )
         elif ATTR_HS_COLOR in kwargs and self._color_script:
             hs_value = kwargs[ATTR_HS_COLOR]
             await self._color_script.async_run(
@@ -335,6 +373,8 @@ class LightTemplate(Light):
 
         self.update_color()
 
+        self.update_white_value()
+
         for property_name, template in (
             ("_icon", self._icon_template),
             ("_entity_picture", self._entity_picture_template),
@@ -398,6 +438,33 @@ class LightTemplate(Light):
             _LOGGER.error("Invalid template", exc_info=True)
             self._brightness = None
 
+    @callback
+    def update_white_value(self):
+        """Update the white value from the template."""
+        if self._white_value_template is None:
+            return
+        try:
+            white_value = self._white_value_template.async_render()
+            if white_value in ("None", ""):
+                self._white_value = None
+                return
+            if 0 <= int(white_value) <= 255:
+                self._white_value = int(white_value)
+            else:
+                _LOGGER.error(
+                    "Received invalid white value: %s. Expected: 0-255", white_value
+                )
+                self._white_value = None
+        except ValueError:
+            _LOGGER.error(
+                "Template must supply an integer white_value from 0-255, or 'None'",
+                exc_info=True,
+            )
+            self._white_value = None
+        except TemplateError as ex:
+            _LOGGER.error(ex)
+            self._state = None
+
     @callback
     def update_state(self):
         """Update the state from the template."""
diff --git a/tests/components/template/test_light.py b/tests/components/template/test_light.py
index d9adf6015c9..9982d72326b 100644
--- a/tests/components/template/test_light.py
+++ b/tests/components/template/test_light.py
@@ -8,6 +8,7 @@ from homeassistant.components.light import (
     ATTR_BRIGHTNESS,
     ATTR_COLOR_TEMP,
     ATTR_HS_COLOR,
+    ATTR_WHITE_VALUE,
 )
 from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE
 from homeassistant.core import callback
@@ -35,7 +36,7 @@ class TestTemplateLight:
 
         @callback
         def record_call(service):
-            """Track function calls.."""
+            """Track function calls."""
             self.calls.append(service)
 
         self.hass.services.register("test", "automation", record_call)
@@ -494,6 +495,105 @@ class TestTemplateLight:
         state = self.hass.states.get("light.test_template_light")
         assert state.state == STATE_OFF
 
+    def test_white_value_action_no_template(self):
+        """Test setting white value with optimistic template."""
+        assert setup.setup_component(
+            self.hass,
+            "light",
+            {
+                "light": {
+                    "platform": "template",
+                    "lights": {
+                        "test_template_light": {
+                            "value_template": "{{1 == 1}}",
+                            "turn_on": {
+                                "service": "light.turn_on",
+                                "entity_id": "light.test_state",
+                            },
+                            "turn_off": {
+                                "service": "light.turn_off",
+                                "entity_id": "light.test_state",
+                            },
+                            "set_white_value": {
+                                "service": "test.automation",
+                                "data_template": {
+                                    "entity_id": "test.test_state",
+                                    "white_value": "{{white_value}}",
+                                },
+                            },
+                        }
+                    },
+                }
+            },
+        )
+        self.hass.start()
+        self.hass.block_till_done()
+
+        state = self.hass.states.get("light.test_template_light")
+        assert state.attributes.get("white_value") is None
+
+        common.turn_on(
+            self.hass, "light.test_template_light", **{ATTR_WHITE_VALUE: 124}
+        )
+        self.hass.block_till_done()
+        assert len(self.calls) == 1
+        assert self.calls[0].data["white_value"] == "124"
+
+        state = self.hass.states.get("light.test_template_light")
+        assert state is not None
+        assert state.attributes.get("white_value") == 124
+
+    @pytest.mark.parametrize(
+        "expected_white_value,template",
+        [
+            (255, "{{255}}"),
+            (None, "{{256}}"),
+            (None, "{{x - 12}}"),
+            (None, "{{ none }}"),
+            (None, ""),
+        ],
+    )
+    def test_white_value_template(self, expected_white_value, template):
+        """Test the template for the white value."""
+        with assert_setup_component(1, "light"):
+            assert setup.setup_component(
+                self.hass,
+                "light",
+                {
+                    "light": {
+                        "platform": "template",
+                        "lights": {
+                            "test_template_light": {
+                                "value_template": "{{ 1 == 1 }}",
+                                "turn_on": {
+                                    "service": "light.turn_on",
+                                    "entity_id": "light.test_state",
+                                },
+                                "turn_off": {
+                                    "service": "light.turn_off",
+                                    "entity_id": "light.test_state",
+                                },
+                                "set_white_value": {
+                                    "service": "light.turn_on",
+                                    "data_template": {
+                                        "entity_id": "light.test_state",
+                                        "white_value": "{{white_value}}",
+                                    },
+                                },
+                                "white_value_template": template,
+                            }
+                        },
+                    }
+                },
+            )
+
+        self.hass.start()
+        self.hass.block_till_done()
+
+        state = self.hass.states.get("light.test_template_light")
+        assert state is not None
+        assert state.attributes.get("white_value") == expected_white_value
+
     def test_level_action_no_template(self):
         """Test setting brightness with optimistic template."""
         assert setup.setup_component(
@@ -955,7 +1055,6 @@ class TestTemplateLight:
 
 async def test_available_template_with_entities(hass):
     """Test availability templates with values from other entities."""
-
     await setup.async_setup_component(
         hass,
         "light",