Skip to content
Closed
Show file tree
Hide file tree
Changes from 11 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
2 changes: 1 addition & 1 deletion CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ homeassistant/components/tado/* @michaelarnauts
homeassistant/components/tahoma/* @philklei
homeassistant/components/tautulli/* @ludeeus
homeassistant/components/tellduslive/* @fredrike
homeassistant/components/template/* @PhracturedBlue
homeassistant/components/template/* @PhracturedBlue @tetienne
homeassistant/components/tesla/* @zabuldon
homeassistant/components/tfiac/* @fredrike @mellado
homeassistant/components/thethingsnetwork/* @fabaff
Expand Down
202 changes: 168 additions & 34 deletions homeassistant/components/template/light.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
"""Support for Template lights."""
import logging

from ast import literal_eval as make_tuple
Comment thread
tetienne marked this conversation as resolved.
Outdated
import voluptuous as vol

from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP,
ENTITY_ID_FORMAT,
SUPPORT_BRIGHTNESS,
SUPPORT_COLOR_TEMP,
Light,
SUPPORT_COLOR,
ATTR_HS_COLOR,
)
from homeassistant.const import (
CONF_ENTITY_ID,
Expand Down Expand Up @@ -38,6 +43,10 @@
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"
CONF_COLOR_TEMPLATE = "color_template"
CONF_COLOR_ACTION = "set_color"

LIGHT_SCHEMA = vol.Schema(
{
Expand All @@ -51,6 +60,10 @@
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,
vol.Optional(CONF_COLOR_TEMPLATE): cv.template,
vol.Optional(CONF_COLOR_ACTION): cv.SCRIPT_SCHEMA,
}
)

Expand All @@ -70,18 +83,24 @@ 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 @@ -101,6 +120,10 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
level_action,
level_template,
entity_ids,
temperature_action,
temperature_template,
color_action,
color_template,
)
)

Expand Down Expand Up @@ -129,6 +152,10 @@ def __init__(
level_action,
level_template,
entity_ids,
temperature_action,
temperature_template,
color_action,
color_template,
):
"""Initialize the light."""
self.hass = hass
Expand All @@ -146,11 +173,21 @@ 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._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 @@ -164,12 +201,26 @@ 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
if self._color_template is not None:
self._color_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 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 @@ -178,10 +229,14 @@ 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:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting that this is based off the existence of a script, not on a value template ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, I just follow the existing solution. But IMO it seems logic: you need an action to be able to change the brightness/color/temperature.

supported_features |= SUPPORT_COLOR_TEMP
if self._color_script is not None:
supported_features |= SUPPORT_COLOR
return supported_features

@property
def is_on(self):
Expand Down Expand Up @@ -222,6 +277,8 @@ 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._color_template is not None
or self._availability_template is not None
):
async_track_state_change(
Expand Down Expand Up @@ -249,10 +306,33 @@ 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 self._color_template is None and ATTR_HS_COLOR in kwargs:
_LOGGER.info("Optimistically setting color to %s", kwargs[ATTR_HS_COLOR])
self._color = kwargs[ATTR_HS_COLOR]
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
)
elif ATTR_HS_COLOR in kwargs and self._color_script:
hs_value = kwargs[ATTR_HS_COLOR]
await self._color_script.async_run(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are all these different scripts? Since it is all done inside the async_turn_on method, shouldn't there just be a generic turn_on script ?

@tetienne tetienne Dec 10, 2019

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All these scripts map the actions to call when the brightness/color/temperature is updated. See https://www.home-assistant.io/integrations/light.template

{"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 @@ -267,38 +347,14 @@ async def async_turn_off(self, **kwargs):
self.async_schedule_update_ha_state()

async def async_update(self):
"""Update the state from the template."""
if self._template is not None:
try:
state = self._template.async_render().lower()
except TemplateError as ex:
_LOGGER.error(ex)
self._state = None
"""Update from templates."""
await self.update_state()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of making these awaitable, you can mark them as @callback which means you don't need to yield.

@tetienne tetienne Dec 9, 2019

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, I can make update_state() not async also?

 @callback
  def update_state(self):
        """Update the state from the template."""
        if self._template is not None:
          ...
``

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't get the magic behind, but indeed it works well. Thx.


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
await self.update_brightness()

if self._level_template is not None:
try:
brightness = self._level_template.async_render()
except TemplateError as ex:
_LOGGER.error(ex)
self._state = None
await self.update_temperature()

if 0 <= int(brightness) <= 255:
self._brightness = int(brightness)
else:
_LOGGER.error(
"Received invalid brightness : %s. Expected: 0-255", brightness
)
self._brightness = None
await self.update_color()

for property_name, template in (
("_icon", self._icon_template),
Expand All @@ -320,7 +376,7 @@ async def async_update(self):
):
# Common during HA startup - so just a warning
_LOGGER.warning(
"Could not render %s template %s," " the state is unknown.",
"Could not render %s template %s, the state is unknown.",
friendly_property_name,
self._name,
)
Expand All @@ -335,3 +391,81 @@ async def async_update(self):
self._name,
ex,
)

async def update_temperature(self):
"""Update the temperature from the template."""
if self._temperature_template is not None:
try:
temperature = int(self._temperature_template.async_render())
except TemplateError as ex:
_LOGGER.error(ex)
Comment thread
tetienne marked this conversation as resolved.
Outdated
self._temperature = None

if 0 <= temperature <= self.max_mireds:
Comment thread
tetienne marked this conversation as resolved.
Outdated
self._temperature = temperature
else:
_LOGGER.error(
"Received invalid color temperature : %s. Expected: 0-%s",
temperature,
self.max_mireds,
)
self._temperature = None

async def update_brightness(self):
"""Update the brightness from the template."""
if self._level_template is not None:
try:
brightness = self._level_template.async_render()
except TemplateError as ex:
_LOGGER.error(ex)
self._state = None

if 0 <= int(brightness) <= 255:
self._brightness = int(brightness)
else:
_LOGGER.error(
"Received invalid brightness : %s. Expected: 0-255", brightness
)
self._brightness = None

async def update_color(self):
"""Update the hs_color from the template."""
if self._color_template is not None:
hs_color = None
self._color = None

try:
hs_color = make_tuple(self._color_template.async_render())
Comment thread
tetienne marked this conversation as resolved.
Outdated
except TemplateError as ex:
_LOGGER.error(ex)
if (
hs_color is not None
and 0 <= hs_color[0] <= 360
and 0 <= hs_color[1] <= 100
):
self._color = hs_color
elif hs_color is not None:
_LOGGER.error(
"Received invalid hs_color : (%s, %s). Expected: (0-360, 0-100)",
hs_color[0],
hs_color[1],
)

async def update_state(self):
"""Update the state from the template."""
if self._template is not None:
try:
state = self._template.async_render().lower()
except TemplateError as ex:
_LOGGER.error(ex)
self._state = None

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
2 changes: 1 addition & 1 deletion homeassistant/components/template/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
"requirements": [],
"dependencies": [],
"codeowners": [
"@PhracturedBlue"
"@PhracturedBlue", "@tetienne"
]
}
Loading