Add trigger based template lights (#140631)

* Add abstract template light class in preparation for trigger based template lights

* add base for trigger entity

* Update more tests

* revert trigger template entity changes and light trigger tests.

* fix merge conflicts

* address comments

* change function name

* nitpick

* fix merge conflict issue

---------

Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
Petro31 2025-04-29 12:02:44 -04:00 committed by GitHub
parent 5da57271b2
commit 95552e9a5b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 786 additions and 264 deletions

View File

@ -134,9 +134,7 @@ CONFIG_SECTION_SCHEMA = vol.All(
),
},
),
ensure_domains_do_not_have_trigger_or_action(
BUTTON_DOMAIN, COVER_DOMAIN, LIGHT_DOMAIN
),
ensure_domains_do_not_have_trigger_or_action(BUTTON_DOMAIN, COVER_DOMAIN),
)
TEMPLATE_BLUEPRINT_SCHEMA = vol.All(

View File

@ -2,6 +2,7 @@
from __future__ import annotations
from collections.abc import Generator, Sequence
import logging
from typing import TYPE_CHECKING, Any
@ -18,6 +19,7 @@ from homeassistant.components.light import (
ATTR_TRANSITION,
DEFAULT_MAX_KELVIN,
DEFAULT_MIN_KELVIN,
DOMAIN as LIGHT_DOMAIN,
ENTITY_ID_FORMAT,
PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA,
ColorMode,
@ -46,6 +48,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import color as color_util
from . import TriggerUpdateCoordinator
from .const import CONF_OBJECT_ID, CONF_PICTURE, DOMAIN
from .template_entity import (
LEGACY_FIELDS as TEMPLATE_ENTITY_LEGACY_FIELDS,
@ -55,6 +58,7 @@ from .template_entity import (
TemplateEntity,
rewrite_common_legacy_to_modern_conf,
)
from .trigger_entity import TriggerEntity
_LOGGER = logging.getLogger(__name__)
_VALID_STATES = [STATE_ON, STATE_OFF, "true", "false"]
@ -253,6 +257,13 @@ async def async_setup_platform(
)
return
if "coordinator" in discovery_info:
async_add_entities(
TriggerLightEntity(hass, discovery_info["coordinator"], config)
for config in discovery_info["entities"]
)
return
_async_create_template_tracking_entities(
async_add_entities,
hass,
@ -261,27 +272,17 @@ async def async_setup_platform(
)
class LightTemplate(TemplateEntity, LightEntity):
"""Representation of a templated Light, including dimmable."""
_attr_should_poll = False
class AbstractTemplateLight(LightEntity):
"""Representation of a template lights features."""
def __init__(
self,
hass: HomeAssistant,
config: dict[str, Any],
unique_id: str | None,
self, config: dict[str, Any], initial_state: bool | None = False
) -> None:
"""Initialize the light."""
super().__init__(hass, config=config, fallback_name=None, unique_id=unique_id)
if (object_id := config.get(CONF_OBJECT_ID)) is not None:
self.entity_id = async_generate_entity_id(
ENTITY_ID_FORMAT, object_id, hass=hass
)
name = self._attr_name
if TYPE_CHECKING:
assert name is not None
"""Initialize the features."""
self._registered_scripts: list[str] = []
# Template attributes
self._template = config.get(CONF_STATE)
self._level_template = config.get(CONF_LEVEL)
self._temperature_template = config.get(CONF_TEMPERATURE)
@ -295,12 +296,8 @@ class LightTemplate(TemplateEntity, LightEntity):
self._min_mireds_template = config.get(CONF_MIN_MIREDS)
self._supports_transition_template = config.get(CONF_SUPPORTS_TRANSITION)
for action_id in (CONF_ON_ACTION, CONF_OFF_ACTION, CONF_EFFECT_ACTION):
# Scripts can be an empty list, therefore we need to check for None
if (action_config := config.get(action_id)) is not None:
self.add_script(action_id, action_config, name, DOMAIN)
self._state = False
# Stored values for template attributes
self._state = initial_state
self._brightness = None
self._temperature: int | None = None
self._hs_color = None
@ -309,14 +306,19 @@ class LightTemplate(TemplateEntity, LightEntity):
self._rgbww_color = None
self._effect = None
self._effect_list = None
self._color_mode = None
self._max_mireds = None
self._min_mireds = None
self._supports_transition = False
self._supported_color_modes = None
self._color_mode: ColorMode | None = None
self._supported_color_modes: set[ColorMode] | None = None
color_modes = {ColorMode.ONOFF}
def _register_scripts(
self, config: dict[str, Any]
) -> Generator[tuple[str, Sequence[dict[str, Any]], ColorMode | None]]:
for action_id, color_mode in (
(CONF_ON_ACTION, None),
(CONF_OFF_ACTION, None),
(CONF_EFFECT_ACTION, None),
(CONF_TEMPERATURE_ACTION, ColorMode.COLOR_TEMP),
(CONF_LEVEL_ACTION, ColorMode.BRIGHTNESS),
(CONF_HS_ACTION, ColorMode.HS),
@ -324,21 +326,9 @@ class LightTemplate(TemplateEntity, LightEntity):
(CONF_RGBW_ACTION, ColorMode.RGBW),
(CONF_RGBWW_ACTION, ColorMode.RGBWW),
):
# Scripts can be an empty list, therefore we need to check for None
if (action_config := config.get(action_id)) is not None:
self.add_script(action_id, action_config, name, DOMAIN)
color_modes.add(color_mode)
self._supported_color_modes = filter_supported_color_modes(color_modes)
if len(self._supported_color_modes) > 1:
self._color_mode = ColorMode.UNKNOWN
if len(self._supported_color_modes) == 1:
self._color_mode = next(iter(self._supported_color_modes))
self._attr_supported_features = LightEntityFeature(0)
if (self._action_scripts.get(CONF_EFFECT_ACTION)) is not None:
self._attr_supported_features |= LightEntityFeature.EFFECT
if self._supports_transition is True:
self._attr_supported_features |= LightEntityFeature.TRANSITION
self._registered_scripts.append(action_id)
yield (action_id, action_config, color_mode)
@property
def brightness(self) -> int | None:
@ -413,107 +403,12 @@ class LightTemplate(TemplateEntity, LightEntity):
"""Return true if device is on."""
return self._state
@callback
def _async_setup_templates(self) -> None:
"""Set up templates."""
if self._template:
self.add_template_attribute(
"_state", self._template, None, self._update_state
)
if self._level_template:
self.add_template_attribute(
"_brightness",
self._level_template,
None,
self._update_brightness,
none_on_template_error=True,
)
if self._max_mireds_template:
self.add_template_attribute(
"_max_mireds_template",
self._max_mireds_template,
None,
self._update_max_mireds,
none_on_template_error=True,
)
if self._min_mireds_template:
self.add_template_attribute(
"_min_mireds_template",
self._min_mireds_template,
None,
self._update_min_mireds,
none_on_template_error=True,
)
if self._temperature_template:
self.add_template_attribute(
"_temperature",
self._temperature_template,
None,
self._update_temperature,
none_on_template_error=True,
)
if self._hs_template:
self.add_template_attribute(
"_hs_color",
self._hs_template,
None,
self._update_hs,
none_on_template_error=True,
)
if self._rgb_template:
self.add_template_attribute(
"_rgb_color",
self._rgb_template,
None,
self._update_rgb,
none_on_template_error=True,
)
if self._rgbw_template:
self.add_template_attribute(
"_rgbw_color",
self._rgbw_template,
None,
self._update_rgbw,
none_on_template_error=True,
)
if self._rgbww_template:
self.add_template_attribute(
"_rgbww_color",
self._rgbww_template,
None,
self._update_rgbww,
none_on_template_error=True,
)
if self._effect_list_template:
self.add_template_attribute(
"_effect_list",
self._effect_list_template,
None,
self._update_effect_list,
none_on_template_error=True,
)
if self._effect_template:
self.add_template_attribute(
"_effect",
self._effect_template,
None,
self._update_effect,
none_on_template_error=True,
)
if self._supports_transition_template:
self.add_template_attribute(
"_supports_transition_template",
self._supports_transition_template,
None,
self._update_supports_transition,
none_on_template_error=True,
)
super()._async_setup_templates()
def set_optimistic_attributes(self, **kwargs) -> bool: # noqa: C901
"""Update attributes which should be set optimistically.
async def async_turn_on(self, **kwargs: Any) -> None: # noqa: C901
"""Turn the light on."""
Returns True if any attribute was updated.
"""
optimistic_set = False
# set optimistic states
if self._template is None:
self._state = True
optimistic_set = True
@ -613,6 +508,10 @@ class LightTemplate(TemplateEntity, LightEntity):
self._rgbw_color = None
optimistic_set = True
return optimistic_set
def get_registered_script(self, **kwargs) -> tuple[str, dict]:
"""Get registered script for turn_on."""
common_params = {}
if ATTR_BRIGHTNESS in kwargs:
@ -621,24 +520,23 @@ class LightTemplate(TemplateEntity, LightEntity):
if ATTR_TRANSITION in kwargs and self._supports_transition is True:
common_params["transition"] = kwargs[ATTR_TRANSITION]
if ATTR_COLOR_TEMP_KELVIN in kwargs and (
temperature_script := self._action_scripts.get(CONF_TEMPERATURE_ACTION)
if (
ATTR_COLOR_TEMP_KELVIN in kwargs
and (script := CONF_TEMPERATURE_ACTION) in self._registered_scripts
):
common_params["color_temp"] = color_util.color_temperature_kelvin_to_mired(
kwargs[ATTR_COLOR_TEMP_KELVIN]
)
await self.async_run_script(
temperature_script,
run_variables=common_params,
context=self._context,
)
elif ATTR_EFFECT in kwargs and (
effect_script := self._action_scripts.get(CONF_EFFECT_ACTION)
return (script, common_params)
if (
ATTR_EFFECT in kwargs
and (script := CONF_EFFECT_ACTION) in self._registered_scripts
):
assert self._effect_list is not None
effect = kwargs[ATTR_EFFECT]
if effect not in self._effect_list:
if self._effect_list is not None and effect not in self._effect_list:
_LOGGER.error(
"Received invalid effect: %s for entity %s. Expected one of: %s",
effect,
@ -649,22 +547,22 @@ class LightTemplate(TemplateEntity, LightEntity):
common_params["effect"] = effect
await self.async_run_script(
effect_script, run_variables=common_params, context=self._context
)
elif ATTR_HS_COLOR in kwargs and (
hs_script := self._action_scripts.get(CONF_HS_ACTION)
return (script, common_params)
if (
ATTR_HS_COLOR in kwargs
and (script := CONF_HS_ACTION) in self._registered_scripts
):
hs_value = kwargs[ATTR_HS_COLOR]
common_params["hs"] = hs_value
common_params["h"] = int(hs_value[0])
common_params["s"] = int(hs_value[1])
await self.async_run_script(
hs_script, run_variables=common_params, context=self._context
)
elif ATTR_RGBWW_COLOR in kwargs and (
rgbww_script := self._action_scripts.get(CONF_RGBWW_ACTION)
return (script, common_params)
if (
ATTR_RGBWW_COLOR in kwargs
and (script := CONF_RGBWW_ACTION) in self._registered_scripts
):
rgbww_value = kwargs[ATTR_RGBWW_COLOR]
common_params["rgbww"] = rgbww_value
@ -679,11 +577,11 @@ class LightTemplate(TemplateEntity, LightEntity):
common_params["cw"] = int(rgbww_value[3])
common_params["ww"] = int(rgbww_value[4])
await self.async_run_script(
rgbww_script, run_variables=common_params, context=self._context
)
elif ATTR_RGBW_COLOR in kwargs and (
rgbw_script := self._action_scripts.get(CONF_RGBW_ACTION)
return (script, common_params)
if (
ATTR_RGBW_COLOR in kwargs
and (script := CONF_RGBW_ACTION) in self._registered_scripts
):
rgbw_value = kwargs[ATTR_RGBW_COLOR]
common_params["rgbw"] = rgbw_value
@ -697,11 +595,11 @@ class LightTemplate(TemplateEntity, LightEntity):
common_params["b"] = int(rgbw_value[2])
common_params["w"] = int(rgbw_value[3])
await self.async_run_script(
rgbw_script, run_variables=common_params, context=self._context
)
elif ATTR_RGB_COLOR in kwargs and (
rgb_script := self._action_scripts.get(CONF_RGB_ACTION)
return (script, common_params)
if (
ATTR_RGB_COLOR in kwargs
and (script := CONF_RGB_ACTION) in self._registered_scripts
):
rgb_value = kwargs[ATTR_RGB_COLOR]
common_params["rgb"] = rgb_value
@ -709,39 +607,15 @@ class LightTemplate(TemplateEntity, LightEntity):
common_params["g"] = int(rgb_value[1])
common_params["b"] = int(rgb_value[2])
await self.async_run_script(
rgb_script, run_variables=common_params, context=self._context
)
elif ATTR_BRIGHTNESS in kwargs and (
level_script := self._action_scripts.get(CONF_LEVEL_ACTION)
return (script, common_params)
if (
ATTR_BRIGHTNESS in kwargs
and (script := CONF_LEVEL_ACTION) in self._registered_scripts
):
await self.async_run_script(
level_script, run_variables=common_params, context=self._context
)
else:
await self.async_run_script(
self._action_scripts[CONF_ON_ACTION],
run_variables=common_params,
context=self._context,
)
return (script, common_params)
if optimistic_set:
self.async_write_ha_state()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the light off."""
off_script = self._action_scripts[CONF_OFF_ACTION]
if ATTR_TRANSITION in kwargs and self._supports_transition is True:
await self.async_run_script(
off_script,
run_variables={"transition": kwargs[ATTR_TRANSITION]},
context=self._context,
)
else:
await self.async_run_script(off_script, context=self._context)
if self._template is None:
self._state = False
self.async_write_ha_state()
return (CONF_ON_ACTION, common_params)
@callback
def _update_brightness(self, brightness):
@ -809,33 +683,6 @@ class LightTemplate(TemplateEntity, LightEntity):
self._effect = effect
@callback
def _update_state(self, result):
"""Update the state from the template."""
if isinstance(result, TemplateError):
# This behavior is legacy
self._state = False
if not self._availability_template:
self._attr_available = True
return
if isinstance(result, bool):
self._state = result
return
state = str(result).lower()
if state in _VALID_STATES:
self._state = state in ("true", STATE_ON)
return
_LOGGER.error(
"Received invalid light is_on state: %s for entity %s. Expected: %s",
state,
self.entity_id,
", ".join(_VALID_STATES),
)
self._state = None
@callback
def _update_temperature(self, render):
"""Update the temperature from the template."""
@ -1092,3 +939,338 @@ class LightTemplate(TemplateEntity, LightEntity):
self._supports_transition = bool(render)
if self._supports_transition:
self._attr_supported_features |= LightEntityFeature.TRANSITION
class LightTemplate(TemplateEntity, AbstractTemplateLight):
"""Representation of a templated Light, including dimmable."""
_attr_should_poll = False
def __init__(
self,
hass: HomeAssistant,
config: dict[str, Any],
unique_id: str | None,
) -> None:
"""Initialize the light."""
TemplateEntity.__init__(
self, hass, config=config, fallback_name=None, unique_id=unique_id
)
AbstractTemplateLight.__init__(self, config)
if (object_id := config.get(CONF_OBJECT_ID)) is not None:
self.entity_id = async_generate_entity_id(
ENTITY_ID_FORMAT, object_id, hass=hass
)
name = self._attr_name
if TYPE_CHECKING:
assert name is not None
color_modes = {ColorMode.ONOFF}
for action_id, action_config, color_mode in self._register_scripts(config):
self.add_script(action_id, action_config, name, DOMAIN)
if color_mode:
color_modes.add(color_mode)
self._supported_color_modes = filter_supported_color_modes(color_modes)
if len(self._supported_color_modes) > 1:
self._color_mode = ColorMode.UNKNOWN
if len(self._supported_color_modes) == 1:
self._color_mode = next(iter(self._supported_color_modes))
self._attr_supported_features = LightEntityFeature(0)
if self._action_scripts.get(CONF_EFFECT_ACTION):
self._attr_supported_features |= LightEntityFeature.EFFECT
if self._supports_transition is True:
self._attr_supported_features |= LightEntityFeature.TRANSITION
@callback
def _async_setup_templates(self) -> None:
"""Set up templates."""
if self._template:
self.add_template_attribute(
"_state", self._template, None, self._update_state
)
if self._level_template:
self.add_template_attribute(
"_brightness",
self._level_template,
None,
self._update_brightness,
none_on_template_error=True,
)
if self._max_mireds_template:
self.add_template_attribute(
"_max_mireds_template",
self._max_mireds_template,
None,
self._update_max_mireds,
none_on_template_error=True,
)
if self._min_mireds_template:
self.add_template_attribute(
"_min_mireds_template",
self._min_mireds_template,
None,
self._update_min_mireds,
none_on_template_error=True,
)
if self._temperature_template:
self.add_template_attribute(
"_temperature",
self._temperature_template,
None,
self._update_temperature,
none_on_template_error=True,
)
if self._hs_template:
self.add_template_attribute(
"_hs_color",
self._hs_template,
None,
self._update_hs,
none_on_template_error=True,
)
if self._rgb_template:
self.add_template_attribute(
"_rgb_color",
self._rgb_template,
None,
self._update_rgb,
none_on_template_error=True,
)
if self._rgbw_template:
self.add_template_attribute(
"_rgbw_color",
self._rgbw_template,
None,
self._update_rgbw,
none_on_template_error=True,
)
if self._rgbww_template:
self.add_template_attribute(
"_rgbww_color",
self._rgbww_template,
None,
self._update_rgbww,
none_on_template_error=True,
)
if self._effect_list_template:
self.add_template_attribute(
"_effect_list",
self._effect_list_template,
None,
self._update_effect_list,
none_on_template_error=True,
)
if self._effect_template:
self.add_template_attribute(
"_effect",
self._effect_template,
None,
self._update_effect,
none_on_template_error=True,
)
if self._supports_transition_template:
self.add_template_attribute(
"_supports_transition_template",
self._supports_transition_template,
None,
self._update_supports_transition,
none_on_template_error=True,
)
super()._async_setup_templates()
@callback
def _update_state(self, result):
"""Update the state from the template."""
if isinstance(result, TemplateError):
# This behavior is legacy
self._state = False
if not self._availability_template:
self._attr_available = True
return
if isinstance(result, bool):
self._state = result
return
state = str(result).lower()
if state in _VALID_STATES:
self._state = state in ("true", STATE_ON)
return
_LOGGER.error(
"Received invalid light is_on state: %s for entity %s. Expected: %s",
state,
self.entity_id,
", ".join(_VALID_STATES),
)
self._state = None
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the light on."""
optimistic_set = self.set_optimistic_attributes(**kwargs)
script_id, script_params = self.get_registered_script(**kwargs)
await self.async_run_script(
self._action_scripts[script_id],
run_variables=script_params,
context=self._context,
)
if optimistic_set:
self.async_write_ha_state()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the light off."""
off_script = self._action_scripts[CONF_OFF_ACTION]
if ATTR_TRANSITION in kwargs and self._supports_transition is True:
await self.async_run_script(
off_script,
run_variables={"transition": kwargs[ATTR_TRANSITION]},
context=self._context,
)
else:
await self.async_run_script(off_script, context=self._context)
if self._template is None:
self._state = False
self.async_write_ha_state()
class TriggerLightEntity(TriggerEntity, AbstractTemplateLight):
"""Light entity based on trigger data."""
domain = LIGHT_DOMAIN
def __init__(
self,
hass: HomeAssistant,
coordinator: TriggerUpdateCoordinator,
config: ConfigType,
) -> None:
"""Initialize the entity."""
TriggerEntity.__init__(self, hass, coordinator, config)
AbstractTemplateLight.__init__(self, config, None)
# Render the _attr_name before initializing TemplateLightEntity
self._attr_name = name = self._rendered.get(CONF_NAME, DEFAULT_NAME)
self._optimistic_attrs: dict[str, str] = {}
self._optimistic = True
for key in (
CONF_STATE,
CONF_LEVEL,
CONF_TEMPERATURE,
CONF_RGB,
CONF_RGBW,
CONF_RGBWW,
CONF_EFFECT,
CONF_MAX_MIREDS,
CONF_MIN_MIREDS,
CONF_SUPPORTS_TRANSITION,
):
if isinstance(config.get(key), template.Template):
if key == CONF_STATE:
self._optimistic = False
self._to_render_simple.append(key)
self._parse_result.add(key)
for key in (CONF_EFFECT_LIST, CONF_HS):
if isinstance(config.get(key), template.Template):
self._to_render_complex.append(key)
self._parse_result.add(key)
color_modes = {ColorMode.ONOFF}
for action_id, action_config, color_mode in self._register_scripts(config):
self.add_script(action_id, action_config, name, DOMAIN)
if color_mode:
color_modes.add(color_mode)
self._supported_color_modes = filter_supported_color_modes(color_modes)
if len(self._supported_color_modes) > 1:
self._color_mode = ColorMode.UNKNOWN
if len(self._supported_color_modes) == 1:
self._color_mode = next(iter(self._supported_color_modes))
self._attr_supported_features = LightEntityFeature(0)
if self._action_scripts.get(CONF_EFFECT_ACTION):
self._attr_supported_features |= LightEntityFeature.EFFECT
if self._supports_transition is True:
self._attr_supported_features |= LightEntityFeature.TRANSITION
@callback
def _handle_coordinator_update(self) -> None:
"""Handle update of the data."""
self._process_data()
if not self.available:
self.async_write_ha_state()
return
write_ha_state = False
for key, updater in (
(CONF_LEVEL, self._update_brightness),
(CONF_EFFECT_LIST, self._update_effect_list),
(CONF_EFFECT, self._update_effect),
(CONF_TEMPERATURE, self._update_temperature),
(CONF_HS, self._update_hs),
(CONF_RGB, self._update_rgb),
(CONF_RGBW, self._update_rgbw),
(CONF_RGBWW, self._update_rgbww),
(CONF_MAX_MIREDS, self._update_max_mireds),
(CONF_MIN_MIREDS, self._update_min_mireds),
):
if (rendered := self._rendered.get(key)) is not None:
updater(rendered)
write_ha_state = True
if (rendered := self._rendered.get(CONF_SUPPORTS_TRANSITION)) is not None:
self._update_supports_transition(rendered)
write_ha_state = True
if not self._optimistic:
raw = self._rendered.get(CONF_STATE)
self._state = template.result_as_boolean(raw)
self.async_set_context(self.coordinator.data["context"])
write_ha_state = True
elif self._optimistic and len(self._rendered) > 0:
# In case any non optimistic template
write_ha_state = True
if write_ha_state:
self.async_write_ha_state()
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the light on."""
optimistic_set = self.set_optimistic_attributes(**kwargs)
script_id, script_params = self.get_registered_script(**kwargs)
if self._template and self._state is None:
# Ensure an optimistic state is set on the entity when turn_on
# is called and the main state hasn't rendered. This will only
# occur when the state is unknown, the template hasn't triggered,
# and turn_on is called.
self._state = True
await self.async_run_script(
self._action_scripts[script_id],
run_variables=script_params,
context=self._context,
)
if optimistic_set:
self.async_write_ha_state()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the light off."""
off_script = self._action_scripts[CONF_OFF_ACTION]
if ATTR_TRANSITION in kwargs and self._supports_transition is True:
await self.async_run_script(
off_script,
run_variables={"transition": kwargs[ATTR_TRANSITION]},
context=self._context,
)
else:
await self.async_run_script(off_script, context=self._context)
if self._template is None:
self._state = False
self.async_write_ha_state()

View File

@ -25,6 +25,7 @@ from homeassistant.const import (
STATE_OFF,
STATE_ON,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import entity_registry as er
@ -159,6 +160,20 @@ OPTIMISTIC_RGBWW_COLOR_LIGHT_CONFIG = {
}
TEST_STATE_TRIGGER = {
"trigger": {"trigger": "state", "entity_id": "light.test_state"},
"variables": {"triggering_entity": "{{ trigger.entity_id }}"},
"action": [{"event": "action_event", "event_data": {"what": "triggering_entity"}}],
}
TEST_EVENT_TRIGGER = {
"trigger": {"platform": "event", "event_type": "test_event"},
"variables": {"type": "{{ trigger.event.data.type }}"},
"action": [{"event": "action_event", "event_data": {"type": "{{ type }}"}}],
}
TEST_MISSING_KEY_CONFIG = {
"turn_on": {
"service": "light.turn_on",
@ -434,7 +449,7 @@ async def async_setup_legacy_format_with_attribute(
)
async def async_setup_new_format(
async def async_setup_modern_format(
hass: HomeAssistant, count: int, light_config: dict[str, Any]
) -> None:
"""Do setup of light integration via new format."""
@ -461,7 +476,51 @@ async def async_setup_modern_format_with_attribute(
) -> None:
"""Do setup of a legacy light that has a single templated attribute."""
extra = {attribute: attribute_template} if attribute and attribute_template else {}
await async_setup_new_format(
await async_setup_modern_format(
hass,
count,
{
"name": "test_template_light",
**extra_config,
"state": "{{ 1 == 1 }}",
**extra,
},
)
async def async_setup_trigger_format(
hass: HomeAssistant, count: int, light_config: dict[str, Any]
) -> None:
"""Do setup of light integration via new format."""
config = {
"template": {
**TEST_STATE_TRIGGER,
"light": light_config,
}
}
with assert_setup_component(count, template.DOMAIN):
assert await async_setup_component(
hass,
template.DOMAIN,
config,
)
await hass.async_block_till_done()
await hass.async_start()
await hass.async_block_till_done()
async def async_setup_trigger_format_with_attribute(
hass: HomeAssistant,
count: int,
attribute: str,
attribute_template: str,
extra_config: dict,
) -> None:
"""Do setup of a legacy light that has a single templated attribute."""
extra = {attribute: attribute_template} if attribute and attribute_template else {}
await async_setup_trigger_format(
hass,
count,
{
@ -484,7 +543,9 @@ async def setup_light(
if style == ConfigurationStyle.LEGACY:
await async_setup_legacy_format(hass, count, light_config)
elif style == ConfigurationStyle.MODERN:
await async_setup_new_format(hass, count, light_config)
await async_setup_modern_format(hass, count, light_config)
elif style == ConfigurationStyle.TRIGGER:
await async_setup_trigger_format(hass, count, light_config)
@pytest.fixture
@ -507,7 +568,17 @@ async def setup_state_light(
},
)
elif style == ConfigurationStyle.MODERN:
await async_setup_new_format(
await async_setup_modern_format(
hass,
count,
{
**OPTIMISTIC_BRIGHTNESS_LIGHT_CONFIG,
"name": "test_template_light",
"state": state_template,
},
)
elif style == ConfigurationStyle.TRIGGER:
await async_setup_trigger_format(
hass,
count,
{
@ -536,6 +607,10 @@ async def setup_single_attribute_light(
await async_setup_modern_format_with_attribute(
hass, count, attribute, attribute_template, extra_config
)
elif style == ConfigurationStyle.TRIGGER:
await async_setup_trigger_format_with_attribute(
hass, count, attribute, attribute_template, extra_config
)
@pytest.fixture
@ -554,6 +629,10 @@ async def setup_single_action_light(
await async_setup_modern_format_with_attribute(
hass, count, "", "", extra_config
)
elif style == ConfigurationStyle.TRIGGER:
await async_setup_trigger_format_with_attribute(
hass, count, "", "", extra_config
)
@pytest.fixture
@ -579,7 +658,7 @@ async def setup_empty_action_light(
},
)
elif style == ConfigurationStyle.MODERN:
await async_setup_new_format(
await async_setup_modern_format(
hass,
count,
{
@ -627,7 +706,20 @@ async def setup_light_with_effects(
},
)
elif style == ConfigurationStyle.MODERN:
await async_setup_new_format(
await async_setup_modern_format(
hass,
count,
{
"name": "test_template_light",
**OPTIMISTIC_BRIGHTNESS_LIGHT_CONFIG,
"state": "{{true}}",
**common,
"effect_list": effect_list_template,
"effect": effect_template,
},
)
elif style == ConfigurationStyle.TRIGGER:
await async_setup_trigger_format(
hass,
count,
{
@ -674,7 +766,19 @@ async def setup_light_with_mireds(
},
)
elif style == ConfigurationStyle.MODERN:
await async_setup_new_format(
await async_setup_modern_format(
hass,
count,
{
"name": "test_template_light",
**OPTIMISTIC_ON_OFF_LIGHT_CONFIG,
"state": "{{ 1 == 1 }}",
**common,
"temperature": "{{200}}",
},
)
elif style == ConfigurationStyle.TRIGGER:
await async_setup_trigger_format(
hass,
count,
{
@ -720,7 +824,21 @@ async def setup_light_with_transition_template(
},
)
elif style == ConfigurationStyle.MODERN:
await async_setup_new_format(
await async_setup_modern_format(
hass,
count,
{
"name": "test_template_light",
**OPTIMISTIC_COLOR_TEMP_LIGHT_CONFIG,
"state": "{{ 1 == 1 }}",
**common,
"effect_list": "{{ ['Disco', 'Police'] }}",
"effect": "{{ None }}",
"supports_transition": transition_template,
},
)
elif style == ConfigurationStyle.TRIGGER:
await async_setup_trigger_format(
hass,
count,
{
@ -741,19 +859,24 @@ async def setup_light_with_transition_template(
[(0, [ColorMode.BRIGHTNESS])],
)
@pytest.mark.parametrize(
"style",
("style", "expected_state"),
[
ConfigurationStyle.LEGACY,
ConfigurationStyle.MODERN,
(ConfigurationStyle.LEGACY, STATE_OFF),
(ConfigurationStyle.MODERN, STATE_OFF),
(ConfigurationStyle.TRIGGER, STATE_UNKNOWN),
],
)
@pytest.mark.parametrize("state_template", ["{{states.test['big.fat...']}}"])
async def test_template_state_invalid(
hass: HomeAssistant, supported_features, supported_color_modes, setup_state_light
hass: HomeAssistant,
supported_features,
supported_color_modes,
expected_state,
setup_state_light,
) -> None:
"""Test template state with render error."""
state = hass.states.get("light.test_template_light")
assert state.state == STATE_OFF
assert state.state == expected_state
assert state.attributes["color_mode"] is None
assert state.attributes["supported_color_modes"] == supported_color_modes
assert state.attributes["supported_features"] == supported_features
@ -765,6 +888,7 @@ async def test_template_state_invalid(
[
ConfigurationStyle.LEGACY,
ConfigurationStyle.MODERN,
ConfigurationStyle.TRIGGER,
],
)
@pytest.mark.parametrize("state_template", ["{{ states.light.test_state.state }}"])
@ -795,6 +919,7 @@ async def test_template_state_text(hass: HomeAssistant, setup_state_light) -> No
[
ConfigurationStyle.LEGACY,
ConfigurationStyle.MODERN,
ConfigurationStyle.TRIGGER,
],
)
@pytest.mark.parametrize(
@ -812,13 +937,18 @@ async def test_template_state_text(hass: HomeAssistant, setup_state_light) -> No
),
],
)
async def test_legacy_template_state_boolean(
async def test_template_state_boolean(
hass: HomeAssistant,
expected_color_mode,
expected_state,
style,
setup_state_light,
) -> None:
"""Test the setting of the state with boolean on."""
if style == ConfigurationStyle.TRIGGER:
hass.states.async_set("light.test_state", expected_state)
await hass.async_block_till_done()
state = hass.states.get("light.test_template_light")
assert state.state == expected_state
assert state.attributes.get("color_mode") == expected_color_mode
@ -860,6 +990,14 @@ async def test_legacy_template_state_boolean(
},
ConfigurationStyle.MODERN,
),
(
{
**OPTIMISTIC_BRIGHTNESS_LIGHT_CONFIG,
"name": "test_template_light",
"state": "{%- if false -%}",
},
ConfigurationStyle.TRIGGER,
),
],
)
async def test_template_config_errors(hass: HomeAssistant, setup_light) -> None:
@ -880,6 +1018,11 @@ async def test_template_config_errors(hass: HomeAssistant, setup_light) -> None:
ConfigurationStyle.MODERN,
0,
),
(
{"name": "light_one", "state": "{{ 1== 1}}", **TEST_MISSING_KEY_CONFIG},
ConfigurationStyle.TRIGGER,
0,
),
],
)
async def test_missing_key(hass: HomeAssistant, count, setup_light) -> None:
@ -896,6 +1039,7 @@ async def test_missing_key(hass: HomeAssistant, count, setup_light) -> None:
[
ConfigurationStyle.LEGACY,
ConfigurationStyle.MODERN,
ConfigurationStyle.TRIGGER,
],
)
@pytest.mark.parametrize("state_template", ["{{ states.light.test_state.state }}"])
@ -946,11 +1090,21 @@ async def test_on_action(
(
{
"name": "test_template_light",
"state": "{{states.light.test_state.state}}",
**TEST_ON_ACTION_WITH_TRANSITION_CONFIG,
"supports_transition": "{{true}}",
},
ConfigurationStyle.MODERN,
),
(
{
"name": "test_template_light",
"state": "{{states.light.test_state.state}}",
**TEST_ON_ACTION_WITH_TRANSITION_CONFIG,
"supports_transition": "{{true}}",
},
ConfigurationStyle.TRIGGER,
),
],
)
async def test_on_action_with_transition(
@ -984,7 +1138,7 @@ async def test_on_action_with_transition(
@pytest.mark.parametrize("count", [1])
@pytest.mark.parametrize(
("light_config", "style"),
("light_config", "style", "initial_state"),
[
(
{
@ -993,6 +1147,7 @@ async def test_on_action_with_transition(
}
},
ConfigurationStyle.LEGACY,
STATE_OFF,
),
(
{
@ -1000,11 +1155,21 @@ async def test_on_action_with_transition(
**OPTIMISTIC_BRIGHTNESS_LIGHT_CONFIG,
},
ConfigurationStyle.MODERN,
STATE_OFF,
),
(
{
"name": "test_template_light",
**OPTIMISTIC_BRIGHTNESS_LIGHT_CONFIG,
},
ConfigurationStyle.TRIGGER,
STATE_UNKNOWN,
),
],
)
async def test_on_action_optimistic(
hass: HomeAssistant,
initial_state: str,
setup_light,
calls: list[ServiceCall],
) -> None:
@ -1013,7 +1178,7 @@ async def test_on_action_optimistic(
await hass.async_block_till_done()
state = hass.states.get("light.test_template_light")
assert state.state == STATE_OFF
assert state.state == initial_state
assert state.attributes["color_mode"] is None
assert state.attributes["supported_color_modes"] == [ColorMode.BRIGHTNESS]
assert state.attributes["supported_features"] == 0
@ -1058,6 +1223,7 @@ async def test_on_action_optimistic(
[
ConfigurationStyle.LEGACY,
ConfigurationStyle.MODERN,
ConfigurationStyle.TRIGGER,
],
)
@pytest.mark.parametrize("state_template", ["{{ states.light.test_state.state }}"])
@ -1113,6 +1279,15 @@ async def test_off_action(
},
ConfigurationStyle.MODERN,
),
(
{
"name": "test_template_light",
"state": "{{states.light.test_state.state}}",
**TEST_OFF_ACTION_WITH_TRANSITION_CONFIG,
"supports_transition": "{{true}}",
},
ConfigurationStyle.TRIGGER,
),
],
)
async def test_off_action_with_transition(
@ -1145,7 +1320,7 @@ async def test_off_action_with_transition(
@pytest.mark.parametrize("count", [1])
@pytest.mark.parametrize(
("light_config", "style"),
("light_config", "style", "initial_state"),
[
(
{
@ -1154,6 +1329,7 @@ async def test_off_action_with_transition(
}
},
ConfigurationStyle.LEGACY,
STATE_OFF,
),
(
{
@ -1161,15 +1337,24 @@ async def test_off_action_with_transition(
**OPTIMISTIC_BRIGHTNESS_LIGHT_CONFIG,
},
ConfigurationStyle.MODERN,
STATE_OFF,
),
(
{
"name": "test_template_light",
**OPTIMISTIC_BRIGHTNESS_LIGHT_CONFIG,
},
ConfigurationStyle.TRIGGER,
STATE_UNKNOWN,
),
],
)
async def test_off_action_optimistic(
hass: HomeAssistant, setup_light, calls: list[ServiceCall]
hass: HomeAssistant, initial_state, setup_light, calls: list[ServiceCall]
) -> None:
"""Test off action with optimistic state."""
state = hass.states.get("light.test_template_light")
assert state.state == STATE_OFF
assert state.state == initial_state
assert state.attributes["color_mode"] is None
assert state.attributes["supported_color_modes"] == [ColorMode.BRIGHTNESS]
assert state.attributes["supported_features"] == 0
@ -1195,6 +1380,7 @@ async def test_off_action_optimistic(
[
ConfigurationStyle.LEGACY,
ConfigurationStyle.MODERN,
ConfigurationStyle.TRIGGER,
],
)
@pytest.mark.parametrize("state_template", ["{{1 == 1}}"])
@ -1235,6 +1421,7 @@ async def test_level_action_no_template(
[
(ConfigurationStyle.LEGACY, "level_template"),
(ConfigurationStyle.MODERN, "level"),
(ConfigurationStyle.TRIGGER, "level"),
],
)
@pytest.mark.parametrize(
@ -1255,14 +1442,20 @@ async def test_level_action_no_template(
)
async def test_level_template(
hass: HomeAssistant,
style: ConfigurationStyle,
expected_level: Any,
expected_color_mode: ColorMode,
setup_single_attribute_light,
) -> None:
"""Test the template for the level."""
if style == ConfigurationStyle.TRIGGER:
hass.states.async_set("light.test_state", STATE_ON)
await hass.async_block_till_done()
state = hass.states.get("light.test_template_light")
assert state.attributes.get("brightness") == expected_level
assert state.state == STATE_ON
assert state.attributes["color_mode"] == expected_color_mode
assert state.attributes["supported_color_modes"] == [ColorMode.BRIGHTNESS]
assert state.attributes["supported_features"] == 0
@ -1276,6 +1469,7 @@ async def test_level_template(
[
(ConfigurationStyle.LEGACY, "temperature_template"),
(ConfigurationStyle.MODERN, "temperature"),
(ConfigurationStyle.TRIGGER, "temperature"),
],
)
@pytest.mark.parametrize(
@ -1292,15 +1486,20 @@ async def test_level_template(
)
async def test_temperature_template(
hass: HomeAssistant,
style: ConfigurationStyle,
expected_temp: Any,
expected_color_mode: ColorMode,
setup_single_attribute_light,
) -> None:
"""Test the template for the temperature."""
if style == ConfigurationStyle.TRIGGER:
hass.states.async_set("light.test_state", STATE_ON)
await hass.async_block_till_done()
state = hass.states.get("light.test_template_light")
assert state.attributes.get("color_temp") == expected_temp
assert state.state == STATE_ON
assert state.attributes["color_mode"] == expected_color_mode
assert state.attributes.get("color_mode") == expected_color_mode
assert state.attributes["supported_color_modes"] == [ColorMode.COLOR_TEMP]
assert state.attributes["supported_features"] == 0
@ -1313,6 +1512,7 @@ async def test_temperature_template(
[
ConfigurationStyle.LEGACY,
ConfigurationStyle.MODERN,
ConfigurationStyle.TRIGGER,
],
)
async def test_temperature_action_no_template(
@ -1369,6 +1569,15 @@ async def test_temperature_action_no_template(
ConfigurationStyle.MODERN,
"light.template_light",
),
(
{
**OPTIMISTIC_BRIGHTNESS_LIGHT_CONFIG,
"name": "Template light",
"state": "{{ 1 == 1 }}",
},
ConfigurationStyle.TRIGGER,
"light.template_light",
),
],
)
async def test_friendly_name(hass: HomeAssistant, entity_id: str, setup_light) -> None:
@ -1388,6 +1597,7 @@ async def test_friendly_name(hass: HomeAssistant, entity_id: str, setup_light) -
[
(ConfigurationStyle.LEGACY, "icon_template"),
(ConfigurationStyle.MODERN, "icon"),
(ConfigurationStyle.TRIGGER, "icon"),
],
)
@pytest.mark.parametrize(
@ -1396,7 +1606,7 @@ async def test_friendly_name(hass: HomeAssistant, entity_id: str, setup_light) -
async def test_icon_template(hass: HomeAssistant, setup_single_attribute_light) -> None:
"""Test icon template."""
state = hass.states.get("light.test_template_light")
assert state.attributes.get("icon") == ""
assert state.attributes.get("icon") in ("", None)
state = hass.states.async_set("light.test_state", STATE_ON)
await hass.async_block_till_done()
@ -1414,6 +1624,7 @@ async def test_icon_template(hass: HomeAssistant, setup_single_attribute_light)
[
(ConfigurationStyle.LEGACY, "entity_picture_template"),
(ConfigurationStyle.MODERN, "picture"),
(ConfigurationStyle.TRIGGER, "picture"),
],
)
@pytest.mark.parametrize(
@ -1425,7 +1636,7 @@ async def test_entity_picture_template(
) -> None:
"""Test entity_picture template."""
state = hass.states.get("light.test_template_light")
assert state.attributes.get("entity_picture") == ""
assert state.attributes.get("entity_picture") in ("", None)
state = hass.states.async_set("light.test_state", STATE_ON)
await hass.async_block_till_done()
@ -1488,6 +1699,7 @@ async def test_legacy_color_action_no_template(
[
ConfigurationStyle.LEGACY,
ConfigurationStyle.MODERN,
ConfigurationStyle.TRIGGER,
],
)
async def test_hs_color_action_no_template(
@ -1529,6 +1741,7 @@ async def test_hs_color_action_no_template(
[
ConfigurationStyle.LEGACY,
ConfigurationStyle.MODERN,
ConfigurationStyle.TRIGGER,
],
)
async def test_rgb_color_action_no_template(
@ -1571,6 +1784,7 @@ async def test_rgb_color_action_no_template(
[
ConfigurationStyle.LEGACY,
ConfigurationStyle.MODERN,
ConfigurationStyle.TRIGGER,
],
)
async def test_rgbw_color_action_no_template(
@ -1617,6 +1831,7 @@ async def test_rgbw_color_action_no_template(
[
ConfigurationStyle.LEGACY,
ConfigurationStyle.MODERN,
ConfigurationStyle.TRIGGER,
],
)
async def test_rgbww_color_action_no_template(
@ -1702,6 +1917,7 @@ async def test_legacy_color_template(
[
(ConfigurationStyle.LEGACY, "hs_template"),
(ConfigurationStyle.MODERN, "hs"),
(ConfigurationStyle.TRIGGER, "hs"),
],
)
@pytest.mark.parametrize(
@ -1723,9 +1939,14 @@ async def test_hs_template(
hass: HomeAssistant,
expected_hs,
expected_color_mode,
style: ConfigurationStyle,
setup_single_attribute_light,
) -> None:
"""Test the template for the color."""
if style == ConfigurationStyle.TRIGGER:
hass.states.async_set("light.test_state", STATE_ON)
await hass.async_block_till_done()
state = hass.states.get("light.test_template_light")
assert state.attributes.get("hs_color") == expected_hs
assert state.state == STATE_ON
@ -1742,6 +1963,7 @@ async def test_hs_template(
[
(ConfigurationStyle.LEGACY, "rgb_template"),
(ConfigurationStyle.MODERN, "rgb"),
(ConfigurationStyle.TRIGGER, "rgb"),
],
)
@pytest.mark.parametrize(
@ -1764,9 +1986,14 @@ async def test_rgb_template(
hass: HomeAssistant,
expected_rgb,
expected_color_mode,
style: ConfigurationStyle,
setup_single_attribute_light,
) -> None:
"""Test the template for the color."""
if style == ConfigurationStyle.TRIGGER:
hass.states.async_set("light.test_state", STATE_ON)
await hass.async_block_till_done()
state = hass.states.get("light.test_template_light")
assert state.attributes.get("rgb_color") == expected_rgb
assert state.state == STATE_ON
@ -1783,6 +2010,7 @@ async def test_rgb_template(
[
(ConfigurationStyle.LEGACY, "rgbw_template"),
(ConfigurationStyle.MODERN, "rgbw"),
(ConfigurationStyle.TRIGGER, "rgbw"),
],
)
@pytest.mark.parametrize(
@ -1806,9 +2034,14 @@ async def test_rgbw_template(
hass: HomeAssistant,
expected_rgbw,
expected_color_mode,
style: ConfigurationStyle,
setup_single_attribute_light,
) -> None:
"""Test the template for the color."""
if style == ConfigurationStyle.TRIGGER:
hass.states.async_set("light.test_state", STATE_ON)
await hass.async_block_till_done()
state = hass.states.get("light.test_template_light")
assert state.attributes.get("rgbw_color") == expected_rgbw
assert state.state == STATE_ON
@ -1825,6 +2058,7 @@ async def test_rgbw_template(
[
(ConfigurationStyle.LEGACY, "rgbww_template"),
(ConfigurationStyle.MODERN, "rgbww"),
(ConfigurationStyle.TRIGGER, "rgbww"),
],
)
@pytest.mark.parametrize(
@ -1853,9 +2087,14 @@ async def test_rgbww_template(
hass: HomeAssistant,
expected_rgbww,
expected_color_mode,
style: ConfigurationStyle,
setup_single_attribute_light,
) -> None:
"""Test the template for the color."""
if style == ConfigurationStyle.TRIGGER:
hass.states.async_set("light.test_state", STATE_ON)
await hass.async_block_till_done()
state = hass.states.get("light.test_template_light")
assert state.attributes.get("rgbww_color") == expected_rgbww
assert state.state == STATE_ON
@ -1887,6 +2126,15 @@ async def test_rgbww_template(
},
ConfigurationStyle.MODERN,
),
(
{
"name": "test_template_light",
**OPTIMISTIC_ON_OFF_LIGHT_CONFIG,
"state": "{{1 == 1}}",
**TEST_ALL_COLORS_NO_TEMPLATE_CONFIG,
},
ConfigurationStyle.TRIGGER,
),
],
)
async def test_all_colors_mode_no_template(
@ -2084,7 +2332,8 @@ async def test_all_colors_mode_no_template(
@pytest.mark.parametrize("count", [1])
@pytest.mark.parametrize(
"style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN]
"style",
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.parametrize(
("effect_list_template", "effect_template", "effect", "expected"),
@ -2097,10 +2346,17 @@ async def test_effect_action(
hass: HomeAssistant,
effect: str,
expected: Any,
style: ConfigurationStyle,
setup_light_with_effects,
calls: list[ServiceCall],
) -> None:
"""Test setting valid effect with template."""
if style == ConfigurationStyle.TRIGGER:
# Ensures the trigger template entity updates
hass.states.async_set("light.test_state", STATE_ON)
await hass.async_block_till_done()
state = hass.states.get("light.test_template_light")
assert state is not None
@ -2123,7 +2379,8 @@ async def test_effect_action(
@pytest.mark.parametrize(("count", "effect_template"), [(1, "{{ None }}")])
@pytest.mark.parametrize(
"style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN]
"style",
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.parametrize(
("expected_effect_list", "effect_list_template"),
@ -2145,9 +2402,16 @@ async def test_effect_action(
],
)
async def test_effect_list_template(
hass: HomeAssistant, expected_effect_list, setup_light_with_effects
hass: HomeAssistant,
expected_effect_list,
style: ConfigurationStyle,
setup_light_with_effects,
) -> None:
"""Test the template for the effect list."""
if style == ConfigurationStyle.TRIGGER:
hass.states.async_set("light.test_state", STATE_ON)
await hass.async_block_till_done()
state = hass.states.get("light.test_template_light")
assert state is not None
assert state.attributes.get("effect_list") == expected_effect_list
@ -2158,7 +2422,8 @@ async def test_effect_list_template(
[(1, "{{ ['Strobe color', 'Police', 'Christmas', 'RGB', 'Random Loop'] }}")],
)
@pytest.mark.parametrize(
"style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN]
"style",
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.parametrize(
("expected_effect", "effect_template"),
@ -2171,9 +2436,16 @@ async def test_effect_list_template(
],
)
async def test_effect_template(
hass: HomeAssistant, expected_effect, setup_light_with_effects
hass: HomeAssistant,
expected_effect,
style: ConfigurationStyle,
setup_light_with_effects,
) -> None:
"""Test the template for the effect."""
if style == ConfigurationStyle.TRIGGER:
hass.states.async_set("light.test_state", STATE_ON)
await hass.async_block_till_done()
state = hass.states.get("light.test_template_light")
assert state is not None
assert state.attributes.get("effect") == expected_effect
@ -2185,6 +2457,7 @@ async def test_effect_template(
[
(ConfigurationStyle.LEGACY, "min_mireds_template"),
(ConfigurationStyle.MODERN, "min_mireds"),
(ConfigurationStyle.TRIGGER, "min_mireds"),
],
)
@pytest.mark.parametrize(
@ -2199,9 +2472,16 @@ async def test_effect_template(
],
)
async def test_min_mireds_template(
hass: HomeAssistant, expected_min_mireds, setup_light_with_mireds
hass: HomeAssistant,
expected_min_mireds,
style: ConfigurationStyle,
setup_light_with_mireds,
) -> None:
"""Test the template for the min mireds."""
if style == ConfigurationStyle.TRIGGER:
hass.states.async_set("light.test_state", STATE_ON)
await hass.async_block_till_done()
state = hass.states.get("light.test_template_light")
assert state is not None
assert state.attributes.get("min_mireds") == expected_min_mireds
@ -2213,6 +2493,7 @@ async def test_min_mireds_template(
[
(ConfigurationStyle.LEGACY, "max_mireds_template"),
(ConfigurationStyle.MODERN, "max_mireds"),
(ConfigurationStyle.TRIGGER, "max_mireds"),
],
)
@pytest.mark.parametrize(
@ -2227,9 +2508,16 @@ async def test_min_mireds_template(
],
)
async def test_max_mireds_template(
hass: HomeAssistant, expected_max_mireds, setup_light_with_mireds
hass: HomeAssistant,
expected_max_mireds,
style: ConfigurationStyle,
setup_light_with_mireds,
) -> None:
"""Test the template for the max mireds."""
if style == ConfigurationStyle.TRIGGER:
hass.states.async_set("light.test_state", STATE_ON)
await hass.async_block_till_done()
state = hass.states.get("light.test_template_light")
assert state is not None
assert state.attributes.get("max_mireds") == expected_max_mireds
@ -2243,6 +2531,7 @@ async def test_max_mireds_template(
[
(ConfigurationStyle.LEGACY, "supports_transition_template"),
(ConfigurationStyle.MODERN, "supports_transition"),
(ConfigurationStyle.TRIGGER, "supports_transition"),
],
)
@pytest.mark.parametrize(
@ -2257,9 +2546,17 @@ async def test_max_mireds_template(
],
)
async def test_supports_transition_template(
hass: HomeAssistant, expected_supports_transition, setup_single_attribute_light
hass: HomeAssistant,
style: ConfigurationStyle,
expected_supports_transition,
setup_single_attribute_light,
) -> None:
"""Test the template for the supports transition."""
if style == ConfigurationStyle.TRIGGER:
# Ensures the trigger template entity updates
hass.states.async_set("light.test_state", STATE_ON)
await hass.async_block_till_done()
state = hass.states.get("light.test_template_light")
expected_value = 1
@ -2277,10 +2574,11 @@ async def test_supports_transition_template(
("count", "transition_template"), [(1, "{{ states('sensor.test') }}")]
)
@pytest.mark.parametrize(
"style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN]
"style",
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
async def test_supports_transition_template_updates(
hass: HomeAssistant, setup_light_with_transition_template
hass: HomeAssistant, style: ConfigurationStyle, setup_light_with_transition_template
) -> None:
"""Test the template for the supports transition dynamically."""
state = hass.states.get("light.test_template_light")
@ -2288,12 +2586,24 @@ async def test_supports_transition_template_updates(
hass.states.async_set("sensor.test", 0)
await hass.async_block_till_done()
if style == ConfigurationStyle.TRIGGER:
# Ensures the trigger template entity updates
hass.states.async_set("light.test_state", STATE_ON)
await hass.async_block_till_done()
state = hass.states.get("light.test_template_light")
supported_features = state.attributes.get("supported_features")
assert supported_features == LightEntityFeature.EFFECT
hass.states.async_set("sensor.test", 1)
await hass.async_block_till_done()
if style == ConfigurationStyle.TRIGGER:
# Ensures the trigger template entity updates
hass.states.async_set("light.test_state", STATE_OFF)
await hass.async_block_till_done()
state = hass.states.get("light.test_template_light")
supported_features = state.attributes.get("supported_features")
assert (
@ -2302,6 +2612,12 @@ async def test_supports_transition_template_updates(
hass.states.async_set("sensor.test", 0)
await hass.async_block_till_done()
if style == ConfigurationStyle.TRIGGER:
# Ensures the trigger template entity updates
hass.states.async_set("light.test_state", STATE_ON)
await hass.async_block_till_done()
state = hass.states.get("light.test_template_light")
supported_features = state.attributes.get("supported_features")
assert supported_features == LightEntityFeature.EFFECT
@ -2322,16 +2638,22 @@ async def test_supports_transition_template_updates(
[
(ConfigurationStyle.LEGACY, "availability_template"),
(ConfigurationStyle.MODERN, "availability"),
(ConfigurationStyle.TRIGGER, "availability"),
],
)
async def test_available_template_with_entities(
hass: HomeAssistant, setup_single_attribute_light
hass: HomeAssistant, style: ConfigurationStyle, setup_single_attribute_light
) -> None:
"""Test availability templates with values from other entities."""
# When template returns true..
hass.states.async_set(_STATE_AVAILABILITY_BOOLEAN, STATE_ON)
await hass.async_block_till_done()
if style == ConfigurationStyle.TRIGGER:
# Ensures the trigger template entity updates
hass.states.async_set("light.test_state", STATE_ON)
await hass.async_block_till_done()
# Device State should not be unavailable
assert hass.states.get("light.test_template_light").state != STATE_UNAVAILABLE
@ -2339,6 +2661,11 @@ async def test_available_template_with_entities(
hass.states.async_set(_STATE_AVAILABILITY_BOOLEAN, STATE_OFF)
await hass.async_block_till_done()
if style == ConfigurationStyle.TRIGGER:
# Ensures the trigger template entity updates
hass.states.async_set("light.test_state", STATE_OFF)
await hass.async_block_till_done()
# device state should be unavailable
assert hass.states.get("light.test_template_light").state == STATE_UNAVAILABLE
@ -2361,7 +2688,9 @@ async def test_available_template_with_entities(
],
)
async def test_invalid_availability_template_keeps_component_available(
hass: HomeAssistant, setup_single_attribute_light, caplog_setup_text
hass: HomeAssistant,
setup_single_attribute_light,
caplog_setup_text,
) -> None:
"""Test that an invalid availability keeps the device available."""
assert hass.states.get("light.test_template_light").state != STATE_UNAVAILABLE
@ -2392,6 +2721,19 @@ async def test_invalid_availability_template_keeps_component_available(
],
ConfigurationStyle.MODERN,
),
(
[
{
"name": "test_template_light_01",
**TEST_UNIQUE_ID_CONFIG,
},
{
"name": "test_template_light_02",
**TEST_UNIQUE_ID_CONFIG,
},
],
ConfigurationStyle.TRIGGER,
),
],
)
async def test_unique_id(hass: HomeAssistant, setup_light) -> None: