Skip to content

Commit

Permalink
Add color to light template (home-assistant#31435)
Browse files Browse the repository at this point in the history
* Add color support to light template

* Add tests about color support
  • Loading branch information
tetienne authored Feb 3, 2020
1 parent 45c997e commit 8bc77f0
Show file tree
Hide file tree
Showing 2 changed files with 191 additions and 2 deletions.
70 changes: 69 additions & 1 deletion homeassistant/components/template/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP,
ATTR_HS_COLOR,
ENTITY_ID_FORMAT,
SUPPORT_BRIGHTNESS,
SUPPORT_COLOR,
SUPPORT_COLOR_TEMP,
Light,
)
Expand Down Expand Up @@ -42,6 +44,8 @@
CONF_LEVEL_TEMPLATE = "level_template"
CONF_TEMPERATURE_TEMPLATE = "temperature_template"
CONF_TEMPERATURE_ACTION = "set_temperature"
CONF_COLOR_TEMPLATE = "color_template"
CONF_COLOR_ACTION = "set_color"

LIGHT_SCHEMA = vol.Schema(
{
Expand All @@ -57,6 +61,8 @@
vol.Optional(CONF_ENTITY_ID): cv.entity_ids,
vol.Optional(CONF_TEMPERATURE_TEMPLATE): cv.template,
vol.Optional(CONF_TEMPERATURE_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_COLOR_TEMPLATE): cv.template,
vol.Optional(CONF_COLOR_ACTION): cv.SCRIPT_SCHEMA,
}
)

Expand All @@ -76,21 +82,27 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
icon_template = device_config.get(CONF_ICON_TEMPLATE)
entity_picture_template = device_config.get(CONF_ENTITY_PICTURE_TEMPLATE)
availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE)
level_template = device_config.get(CONF_LEVEL_TEMPLATE)

on_action = device_config[CONF_ON_ACTION]
off_action = device_config[CONF_OFF_ACTION]

level_action = device_config.get(CONF_LEVEL_ACTION)
level_template = device_config.get(CONF_LEVEL_TEMPLATE)

temperature_action = device_config.get(CONF_TEMPERATURE_ACTION)
temperature_template = device_config.get(CONF_TEMPERATURE_TEMPLATE)

color_action = device_config.get(CONF_COLOR_ACTION)
color_template = device_config.get(CONF_COLOR_TEMPLATE)

templates = {
CONF_VALUE_TEMPLATE: state_template,
CONF_ICON_TEMPLATE: icon_template,
CONF_ENTITY_PICTURE_TEMPLATE: entity_picture_template,
CONF_AVAILABILITY_TEMPLATE: availability_template,
CONF_LEVEL_TEMPLATE: level_template,
CONF_TEMPERATURE_TEMPLATE: temperature_template,
CONF_COLOR_TEMPLATE: color_template,
}

initialise_templates(hass, templates)
Expand All @@ -114,6 +126,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
entity_ids,
temperature_action,
temperature_template,
color_action,
color_template,
)
)

Expand Down Expand Up @@ -144,6 +158,8 @@ def __init__(
entity_ids,
temperature_action,
temperature_template,
color_action,
color_template,
):
"""Initialize the light."""
self.hass = hass
Expand All @@ -165,12 +181,17 @@ def __init__(
if temperature_action is not None:
self._temperature_script = Script(hass, temperature_action)
self._temperature_template = temperature_template
self._color_script = None
if color_action is not None:
self._color_script = Script(hass, color_action)
self._color_template = color_template

self._state = False
self._icon = None
self._entity_picture = None
self._brightness = None
self._temperature = None
self._color = None
self._entities = entity_ids
self._available = True

Expand All @@ -184,6 +205,11 @@ def color_temp(self):
"""Return the CT color value in mireds."""
return self._temperature

@property
def hs_color(self):
"""Return the hue and saturation color value [float, float]."""
return self._color

@property
def name(self):
"""Return the display name of this light."""
Expand All @@ -197,6 +223,8 @@ def supported_features(self):
supported_features |= SUPPORT_BRIGHTNESS
if self._temperature_script is not None:
supported_features |= SUPPORT_COLOR_TEMP
if self._color_script is not None:
supported_features |= SUPPORT_COLOR
return supported_features

@property
Expand Down Expand Up @@ -239,6 +267,7 @@ def template_light_startup(event):
self._template is not None
or self._level_template is not None
or self._temperature_template is not None
or self._color_template is not None
or self._availability_template is not None
):
async_track_state_change(
Expand Down Expand Up @@ -282,6 +311,12 @@ async def async_turn_on(self, **kwargs):
await self._temperature_script.async_run(
{"color_temp": kwargs[ATTR_COLOR_TEMP]}, 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(
{"hs": hs_value, "h": int(hs_value[0]), "s": int(hs_value[1])},
context=self._context,
)
else:
await self._on_script.async_run()

Expand All @@ -303,6 +338,8 @@ async def async_update(self):

self.update_temperature()

self.update_color()

for property_name, template in (
("_icon", self._icon_template),
("_entity_picture", self._entity_picture_template),
Expand Down Expand Up @@ -396,3 +433,34 @@ def update_temperature(self):
except TemplateError:
_LOGGER.error("Cannot evaluate temperature template", exc_info=True)
self._temperature = None

@callback
def update_color(self):
"""Update the hs_color from the template."""
if self._color_template is None:
return

self._color = None

try:
render = self._color_template.async_render()
h_str, s_str = map(
float, render.replace("(", "").replace(")", "").split(",", 1)
)
if (
h_str is not None
and s_str is not None
and 0 <= h_str <= 360
and 0 <= s_str <= 100
):
self._color = (h_str, s_str)
elif h_str is not None and s_str is not None:
_LOGGER.error(
"Received invalid hs_color : (%s, %s). Expected: (0-360, 0-100)",
h_str,
s_str,
)
else:
_LOGGER.error("Received invalid hs_color : (%s)", render)
except TemplateError as ex:
_LOGGER.error(ex)
123 changes: 122 additions & 1 deletion tests/components/template/test_light.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
import pytest

from homeassistant import setup
from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_COLOR_TEMP
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP,
ATTR_HS_COLOR,
)
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE
from homeassistant.core import callback

Expand Down Expand Up @@ -816,6 +820,123 @@ def test_entity_picture_template(self):

assert state.attributes["entity_picture"] == "/local/light.png"

def test_color_action_no_template(self):
"""Test setting color 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_color": [
{
"service": "test.automation",
"data_template": {
"entity_id": "test.test_state",
"h": "{{h}}",
"s": "{{s}}",
},
},
{
"service": "test.automation",
"data_template": {
"entity_id": "test.test_state",
"s": "{{s}}",
"h": "{{h}}",
},
},
],
}
},
}
},
)
self.hass.start()
self.hass.block_till_done()

state = self.hass.states.get("light.test_template_light")
assert state.attributes.get("hs_color") is None

common.turn_on(
self.hass, "light.test_template_light", **{ATTR_HS_COLOR: (40, 50)}
)
self.hass.block_till_done()
assert len(self.calls) == 2
assert self.calls[0].data["h"] == "40"
assert self.calls[0].data["s"] == "50"
assert self.calls[1].data["h"] == "40"
assert self.calls[1].data["s"] == "50"

state = self.hass.states.get("light.test_template_light")
_LOGGER.info(str(state.attributes))
assert state is not None
assert self.calls[0].data["h"] == "40"
assert self.calls[0].data["s"] == "50"
assert self.calls[1].data["h"] == "40"
assert self.calls[1].data["s"] == "50"

@pytest.mark.parametrize(
"expected_hs,template",
[
((360, 100), "{{(360, 100)}}"),
((359.9, 99.9), "{{(359.9, 99.9)}}"),
(None, "{{(361, 100)}}"),
(None, "{{(360, 101)}}"),
(None, "{{x - 12}}"),
],
)
def test_color_template(self, expected_hs, template):
"""Test the template for the color."""
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_color": [
{
"service": "input_number.set_value",
"data_template": {
"entity_id": "input_number.h",
"color_temp": "{{h}}",
},
}
],
"color_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("hs_color") == expected_hs


async def test_available_template_with_entities(hass):
"""Test availability templates with values from other entities."""
Expand Down

0 comments on commit 8bc77f0

Please sign in to comment.