Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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