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
124 changes: 94 additions & 30 deletions homeassistant/components/template/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@

from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP,
ENTITY_ID_FORMAT,
SUPPORT_BRIGHTNESS,
SUPPORT_COLOR_TEMP,
Light,
)
from homeassistant.const import (
Expand Down Expand Up @@ -38,6 +40,8 @@
CONF_OFF_ACTION = "turn_off"
CONF_LEVEL_ACTION = "set_level"
CONF_LEVEL_TEMPLATE = "level_template"
CONF_TEMPERATURE_TEMPLATE = "temperature_template"
CONF_TEMPERATURE_ACTION = "set_temperature"

LIGHT_SCHEMA = vol.Schema(
{
Expand All @@ -51,6 +55,8 @@
vol.Optional(CONF_LEVEL_TEMPLATE): cv.template,
vol.Optional(CONF_FRIENDLY_NAME): cv.string,
vol.Optional(CONF_ENTITY_ID): cv.entity_ids,
vol.Optional(CONF_TEMPERATURE_TEMPLATE): cv.template,
vol.Optional(CONF_TEMPERATURE_ACTION): cv.SCRIPT_SCHEMA,
}
)

Expand All @@ -75,13 +81,16 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
on_action = device_config[CONF_ON_ACTION]
off_action = device_config[CONF_OFF_ACTION]
level_action = device_config.get(CONF_LEVEL_ACTION)
temperature_action = device_config.get(CONF_TEMPERATURE_ACTION)
temperature_template = device_config.get(CONF_TEMPERATURE_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,
}

initialise_templates(hass, templates)
Expand All @@ -101,6 +110,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
level_action,
level_template,
entity_ids,
temperature_action,
temperature_template,
)
)

Expand Down Expand Up @@ -129,6 +140,8 @@ def __init__(
level_action,
level_template,
entity_ids,
temperature_action,
temperature_template,
):
"""Initialize the light."""
self.hass = hass
Expand All @@ -146,11 +159,16 @@ def __init__(
if level_action is not None:
self._level_script = Script(hass, level_action)
self._level_template = level_template
self._temperature_script = None
if temperature_action is not None:
self._temperature_script = Script(hass, temperature_action)
self._temperature_template = temperature_template

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

Expand All @@ -164,12 +182,19 @@ def __init__(
self._entity_picture_template.hass = self.hass
if self._availability_template is not None:
self._availability_template.hass = self.hass
if self._temperature_template is not None:
self._temperature_template.hass = self.hass

@property
def brightness(self):
"""Return the brightness of the light."""
return self._brightness

@property
def color_temp(self):
"""Return the CT color value in mireds."""
return self._temperature

@property
def name(self):
"""Return the display name of this light."""
Expand All @@ -178,10 +203,12 @@ def name(self):
@property
def supported_features(self):
"""Flag supported features."""
supported_features = 0
if self._level_script is not None:
return SUPPORT_BRIGHTNESS

return 0
supported_features |= SUPPORT_BRIGHTNESS
if self._temperature_script is not None:
supported_features |= SUPPORT_COLOR_TEMP
return supported_features

@property
def is_on(self):
Expand Down Expand Up @@ -222,6 +249,7 @@ def template_light_startup(event):
if (
self._template is not None
or self._level_template is not None
or self._temperature_template is not None
or self._availability_template is not None
):
async_track_state_change(
Expand Down Expand Up @@ -249,10 +277,22 @@ async def async_turn_on(self, **kwargs):
self._brightness = kwargs[ATTR_BRIGHTNESS]
optimistic_set = True

if self._temperature_template is None and ATTR_COLOR_TEMP in kwargs:
_LOGGER.info(
"Optimistically setting color temperature to %s",
kwargs[ATTR_COLOR_TEMP],
)
self._temperature = kwargs[ATTR_COLOR_TEMP]
optimistic_set = True

if ATTR_BRIGHTNESS in kwargs and self._level_script:
await self._level_script.async_run(
{"brightness": kwargs[ATTR_BRIGHTNESS]}, context=self._context
)
elif ATTR_COLOR_TEMP in kwargs and self._temperature_script:
await self._temperature_script.async_run(
{"color_temp": kwargs[ATTR_COLOR_TEMP]}, context=self._context
)
else:
await self._on_script.async_run()

Expand All @@ -272,6 +312,8 @@ async def async_update(self):

self.update_brightness()

self.update_temperature()

for property_name, template in (
("_icon", self._icon_template),
("_entity_picture", self._entity_picture_template),
Expand Down Expand Up @@ -311,35 +353,57 @@ async def async_update(self):
@callback
def update_brightness(self):
"""Update the brightness from the template."""
if self._level_template is not None:
try:
brightness = self._level_template.async_render()
if 0 <= int(brightness) <= 255:
self._brightness = int(brightness)
else:
_LOGGER.error(
"Received invalid brightness : %s. Expected: 0-255", brightness
)
self._brightness = None
except TemplateError as ex:
_LOGGER.error(ex)
self._state = None
if self._level_template is None:
return
try:
brightness = self._level_template.async_render()
if 0 <= int(brightness) <= 255:
self._brightness = int(brightness)
else:
_LOGGER.error(
"Received invalid brightness : %s. Expected: 0-255", brightness
)
self._brightness = None
except TemplateError as ex:
_LOGGER.error(ex)
self._state = None

@callback
def update_state(self):
"""Update the state from the template."""
if self._template is not None:
try:
state = self._template.async_render().lower()
if state in _VALID_STATES:
self._state = state in ("true", STATE_ON)
else:
_LOGGER.error(
"Received invalid light is_on state: %s. Expected: %s",
state,
", ".join(_VALID_STATES),
)
self._state = None
except TemplateError as ex:
_LOGGER.error(ex)
if self._template is None:
return
try:
state = self._template.async_render().lower()
if state in _VALID_STATES:
self._state = state in ("true", STATE_ON)
else:
_LOGGER.error(
"Received invalid light is_on state: %s. Expected: %s",
state,
", ".join(_VALID_STATES),
)
self._state = None
except TemplateError as ex:
_LOGGER.error(ex)
self._state = None

@callback
def update_temperature(self):
"""Update the temperature from the template."""
if self._temperature_template is None:
return
try:
temperature = int(self._temperature_template.async_render())
if self.min_mireds <= temperature <= self.max_mireds:
self._temperature = temperature
else:
_LOGGER.error(
"Received invalid color temperature : %s. Expected: 0-%s",
temperature,
self.max_mireds,
)
self._temperature = None
except TemplateError:
_LOGGER.error("Cannot evaluate temperature template", exc_info=True)
self._temperature = None
94 changes: 93 additions & 1 deletion tests/components/template/test_light.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import pytest

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

Expand Down Expand Up @@ -582,6 +582,98 @@ def test_level_template(self, expected_level, template):
assert state is not None
assert state.attributes.get("brightness") == expected_level

@pytest.mark.parametrize(
"expected_temp,template",
[(500, "{{500}}"), (None, "{{501}}"), (None, "{{x - 12}}")],
)
def test_temperature_template(self, expected_temp, template):
"""Test the template for the temperature."""
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_temperature": {
"service": "light.turn_on",
"data_template": {
"entity_id": "light.test_state",
"color_temp": "{{color_temp}}",
},
},
"temperature_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("color_temp") == expected_temp

def test_temperature_action_no_template(self):
"""Test setting temperature 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_temperature": {
"service": "test.automation",
"data_template": {
"entity_id": "test.test_state",
"color_temp": "{{color_temp}}",
},
},
}
},
}
},
)
self.hass.start()
self.hass.block_till_done()

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

common.turn_on(self.hass, "light.test_template_light", **{ATTR_COLOR_TEMP: 345})
self.hass.block_till_done()
assert len(self.calls) == 1
assert self.calls[0].data["color_temp"] == "345"

state = self.hass.states.get("light.test_template_light")
_LOGGER.info(str(state.attributes))
assert state is not None
assert state.attributes.get("color_temp") == 345

def test_friendly_name(self):
"""Test the accessibility of the friendly_name attribute."""
with assert_setup_component(1, "light"):
Expand Down