diff --git a/homeassistant/components/template/config.py b/homeassistant/components/template/config.py index 5038114b8ab..9d0cf148f3f 100644 --- a/homeassistant/components/template/config.py +++ b/homeassistant/components/template/config.py @@ -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( diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index c58709eba5e..3b64cca26b4 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -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() diff --git a/tests/components/template/test_light.py b/tests/components/template/test_light.py index c0aade84e0f..f240c2412e0 100644 --- a/tests/components/template/test_light.py +++ b/tests/components/template/test_light.py @@ -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: