Add temperature support to light template (#30595)

* Add temperature support

* Use guard clause
This commit is contained in:
tetienne 2020-01-23 18:18:59 +01:00 committed by Paulus Schoutsen
parent 894b841a15
commit 1a1ef7680d
2 changed files with 187 additions and 31 deletions

View File

@ -5,8 +5,10 @@ import voluptuous as vol
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP,
ENTITY_ID_FORMAT, ENTITY_ID_FORMAT,
SUPPORT_BRIGHTNESS, SUPPORT_BRIGHTNESS,
SUPPORT_COLOR_TEMP,
Light, Light,
) )
from homeassistant.const import ( from homeassistant.const import (
@ -38,6 +40,8 @@ CONF_ON_ACTION = "turn_on"
CONF_OFF_ACTION = "turn_off" CONF_OFF_ACTION = "turn_off"
CONF_LEVEL_ACTION = "set_level" CONF_LEVEL_ACTION = "set_level"
CONF_LEVEL_TEMPLATE = "level_template" CONF_LEVEL_TEMPLATE = "level_template"
CONF_TEMPERATURE_TEMPLATE = "temperature_template"
CONF_TEMPERATURE_ACTION = "set_temperature"
LIGHT_SCHEMA = vol.Schema( LIGHT_SCHEMA = vol.Schema(
{ {
@ -51,6 +55,8 @@ LIGHT_SCHEMA = vol.Schema(
vol.Optional(CONF_LEVEL_TEMPLATE): cv.template, vol.Optional(CONF_LEVEL_TEMPLATE): cv.template,
vol.Optional(CONF_FRIENDLY_NAME): cv.string, vol.Optional(CONF_FRIENDLY_NAME): cv.string,
vol.Optional(CONF_ENTITY_ID): cv.entity_ids, vol.Optional(CONF_ENTITY_ID): cv.entity_ids,
vol.Optional(CONF_TEMPERATURE_TEMPLATE): cv.template,
vol.Optional(CONF_TEMPERATURE_ACTION): cv.SCRIPT_SCHEMA,
} }
) )
@ -75,6 +81,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
on_action = device_config[CONF_ON_ACTION] on_action = device_config[CONF_ON_ACTION]
off_action = device_config[CONF_OFF_ACTION] off_action = device_config[CONF_OFF_ACTION]
level_action = device_config.get(CONF_LEVEL_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 = { templates = {
CONF_VALUE_TEMPLATE: state_template, CONF_VALUE_TEMPLATE: state_template,
@ -82,6 +90,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
CONF_ENTITY_PICTURE_TEMPLATE: entity_picture_template, CONF_ENTITY_PICTURE_TEMPLATE: entity_picture_template,
CONF_AVAILABILITY_TEMPLATE: availability_template, CONF_AVAILABILITY_TEMPLATE: availability_template,
CONF_LEVEL_TEMPLATE: level_template, CONF_LEVEL_TEMPLATE: level_template,
CONF_TEMPERATURE_TEMPLATE: temperature_template,
} }
initialise_templates(hass, templates) initialise_templates(hass, templates)
@ -101,6 +110,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
level_action, level_action,
level_template, level_template,
entity_ids, entity_ids,
temperature_action,
temperature_template,
) )
) )
@ -129,6 +140,8 @@ class LightTemplate(Light):
level_action, level_action,
level_template, level_template,
entity_ids, entity_ids,
temperature_action,
temperature_template,
): ):
"""Initialize the light.""" """Initialize the light."""
self.hass = hass self.hass = hass
@ -146,11 +159,16 @@ class LightTemplate(Light):
if level_action is not None: if level_action is not None:
self._level_script = Script(hass, level_action) self._level_script = Script(hass, level_action)
self._level_template = level_template 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._state = False
self._icon = None self._icon = None
self._entity_picture = None self._entity_picture = None
self._brightness = None self._brightness = None
self._temperature = None
self._entities = entity_ids self._entities = entity_ids
self._available = True self._available = True
@ -164,12 +182,19 @@ class LightTemplate(Light):
self._entity_picture_template.hass = self.hass self._entity_picture_template.hass = self.hass
if self._availability_template is not None: if self._availability_template is not None:
self._availability_template.hass = self.hass self._availability_template.hass = self.hass
if self._temperature_template is not None:
self._temperature_template.hass = self.hass
@property @property
def brightness(self): def brightness(self):
"""Return the brightness of the light.""" """Return the brightness of the light."""
return self._brightness return self._brightness
@property
def color_temp(self):
"""Return the CT color value in mireds."""
return self._temperature
@property @property
def name(self): def name(self):
"""Return the display name of this light.""" """Return the display name of this light."""
@ -178,10 +203,12 @@ class LightTemplate(Light):
@property @property
def supported_features(self): def supported_features(self):
"""Flag supported features.""" """Flag supported features."""
supported_features = 0
if self._level_script is not None: if self._level_script is not None:
return SUPPORT_BRIGHTNESS supported_features |= SUPPORT_BRIGHTNESS
if self._temperature_script is not None:
return 0 supported_features |= SUPPORT_COLOR_TEMP
return supported_features
@property @property
def is_on(self): def is_on(self):
@ -222,6 +249,7 @@ class LightTemplate(Light):
if ( if (
self._template is not None self._template is not None
or self._level_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 or self._availability_template is not None
): ):
async_track_state_change( async_track_state_change(
@ -249,10 +277,22 @@ class LightTemplate(Light):
self._brightness = kwargs[ATTR_BRIGHTNESS] self._brightness = kwargs[ATTR_BRIGHTNESS]
optimistic_set = True 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: if ATTR_BRIGHTNESS in kwargs and self._level_script:
await self._level_script.async_run( await self._level_script.async_run(
{"brightness": kwargs[ATTR_BRIGHTNESS]}, context=self._context {"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: else:
await self._on_script.async_run() await self._on_script.async_run()
@ -272,6 +312,8 @@ class LightTemplate(Light):
self.update_brightness() self.update_brightness()
self.update_temperature()
for property_name, template in ( for property_name, template in (
("_icon", self._icon_template), ("_icon", self._icon_template),
("_entity_picture", self._entity_picture_template), ("_entity_picture", self._entity_picture_template),
@ -311,35 +353,57 @@ class LightTemplate(Light):
@callback @callback
def update_brightness(self): def update_brightness(self):
"""Update the brightness from the template.""" """Update the brightness from the template."""
if self._level_template is not None: if self._level_template is None:
try: return
brightness = self._level_template.async_render() try:
if 0 <= int(brightness) <= 255: brightness = self._level_template.async_render()
self._brightness = int(brightness) if 0 <= int(brightness) <= 255:
else: self._brightness = int(brightness)
_LOGGER.error( else:
"Received invalid brightness : %s. Expected: 0-255", brightness _LOGGER.error(
) "Received invalid brightness : %s. Expected: 0-255", brightness
self._brightness = None )
except TemplateError as ex: self._brightness = None
_LOGGER.error(ex) except TemplateError as ex:
self._state = None _LOGGER.error(ex)
self._state = None
@callback @callback
def update_state(self): def update_state(self):
"""Update the state from the template.""" """Update the state from the template."""
if self._template is not None: if self._template is None:
try: return
state = self._template.async_render().lower() try:
if state in _VALID_STATES: state = self._template.async_render().lower()
self._state = state in ("true", STATE_ON) if state in _VALID_STATES:
else: self._state = state in ("true", STATE_ON)
_LOGGER.error( else:
"Received invalid light is_on state: %s. Expected: %s", _LOGGER.error(
state, "Received invalid light is_on state: %s. Expected: %s",
", ".join(_VALID_STATES), state,
) ", ".join(_VALID_STATES),
self._state = None )
except TemplateError as ex:
_LOGGER.error(ex)
self._state = None 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

View File

@ -4,7 +4,7 @@ import logging
import pytest import pytest
from homeassistant import setup 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.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE
from homeassistant.core import callback from homeassistant.core import callback
@ -582,6 +582,98 @@ class TestTemplateLight:
assert state is not None assert state is not None
assert state.attributes.get("brightness") == expected_level 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): def test_friendly_name(self):
"""Test the accessibility of the friendly_name attribute.""" """Test the accessibility of the friendly_name attribute."""
with assert_setup_component(1, "light"): with assert_setup_component(1, "light"):