"""Support for Template lights.""" from collections.abc import Callable import contextlib from dataclasses import dataclass import logging from typing import TYPE_CHECKING, Any, Self, override import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP_KELVIN, ATTR_EFFECT, ATTR_HS_COLOR, ATTR_RGB_COLOR, ATTR_RGBW_COLOR, ATTR_RGBWW_COLOR, ATTR_TRANSITION, ATTR_XY_COLOR, DEFAULT_MAX_KELVIN, DEFAULT_MIN_KELVIN, DOMAIN as LIGHT_DOMAIN, ENTITY_ID_FORMAT, ColorMode, LightEntity, LightEntityFeature, filter_supported_color_modes, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_EFFECT, CONF_HS, CONF_NAME, CONF_RGB, CONF_STATE, CONF_XY, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import ( AddConfigEntryEntitiesCallback, AddEntitiesCallback, ) from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import color as color_util from . import TriggerUpdateCoordinator, validators as template_validators from .const import DOMAIN from .entity import AbstractTemplateEntity from .helpers import ( async_setup_template_entry, async_setup_template_platform, async_setup_template_preview, ) from .schemas import ( TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA, TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA, make_template_entity_common_modern_schema, ) from .template_entity import TemplateEntity from .trigger_entity import TriggerEntity _LOGGER = logging.getLogger(__name__) # Legacy ATTR_COLOR_TEMP = "color_temp" CONF_HS_ACTION = "set_hs" CONF_RGB_ACTION = "set_rgb" CONF_RGBW = "rgbw" CONF_RGBW_ACTION = "set_rgbw" CONF_RGBWW = "rgbww" CONF_RGBWW_ACTION = "set_rgbww" CONF_EFFECT_ACTION = "set_effect" CONF_EFFECT_LIST = "effect_list" CONF_LEVEL = "level" CONF_LEVEL_ACTION = "set_level" CONF_MAX_MIREDS = "max_mireds" CONF_MIN_MIREDS = "min_mireds" CONF_OFF_ACTION = "turn_off" CONF_ON_ACTION = "turn_on" CONF_SUPPORTS_TRANSITION = "supports_transition" CONF_TEMPERATURE_ACTION = "set_temperature" CONF_TEMPERATURE = "temperature" CONF_XY_ACTION = "set_xy" DEFAULT_MIN_MIREDS = 153 DEFAULT_MAX_MIREDS = 500 DEFAULT_NAME = "Template Light" SCRIPT_FIELDS = ( CONF_EFFECT_ACTION, CONF_HS_ACTION, CONF_LEVEL_ACTION, CONF_OFF_ACTION, CONF_ON_ACTION, CONF_RGB_ACTION, CONF_RGBW_ACTION, CONF_RGBWW_ACTION, CONF_TEMPERATURE_ACTION, CONF_XY_ACTION, ) LIGHT_COMMON_SCHEMA = vol.Schema( { vol.Inclusive(CONF_EFFECT_ACTION, "effect"): cv.SCRIPT_SCHEMA, vol.Inclusive(CONF_EFFECT_LIST, "effect"): cv.template, vol.Inclusive(CONF_EFFECT, "effect"): cv.template, vol.Optional(CONF_HS_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_HS): cv.template, vol.Optional(CONF_LEVEL_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_LEVEL): cv.template, vol.Optional(CONF_MAX_MIREDS): cv.template, vol.Optional(CONF_MIN_MIREDS): cv.template, vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA, vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_RGB_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_RGB): cv.template, vol.Optional(CONF_RGBW_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_RGBW): cv.template, vol.Optional(CONF_RGBWW_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_RGBWW): cv.template, vol.Optional(CONF_STATE): cv.template, vol.Optional(CONF_SUPPORTS_TRANSITION): cv.template, vol.Optional(CONF_TEMPERATURE_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_TEMPERATURE): cv.template, vol.Optional(CONF_XY): cv.template, vol.Optional(CONF_XY_ACTION): cv.SCRIPT_SCHEMA, } ) LIGHT_YAML_SCHEMA = LIGHT_COMMON_SCHEMA.extend( TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA ).extend(make_template_entity_common_modern_schema(LIGHT_DOMAIN, DEFAULT_NAME).schema) LIGHT_CONFIG_ENTRY_SCHEMA = LIGHT_COMMON_SCHEMA.extend( TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA.schema ) async def async_setup_platform( hass: HomeAssistant, config: ConfigType, async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the template lights.""" await async_setup_template_platform( hass, LIGHT_DOMAIN, config, StateLightEntity, TriggerLightEntity, async_add_entities, discovery_info, script_options=SCRIPT_FIELDS, ) async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Initialize config entry.""" await async_setup_template_entry( hass, config_entry, async_add_entities, StateLightEntity, LIGHT_CONFIG_ENTRY_SCHEMA, True, script_options=SCRIPT_FIELDS, ) @callback def async_create_preview_light( hass: HomeAssistant, name: str, config: dict[str, Any] ) -> StateLightEntity: """Create a preview.""" return async_setup_template_preview( hass, name, config, StateLightEntity, LIGHT_CONFIG_ENTRY_SCHEMA, True, ) def _string_to_list(result: str) -> list[float]: for char in "()[] ": result = result.replace(char, "") return [float(v) for v in result.split(",")] def two_color_list( entity: AbstractTemplateLight, option: str, option_one: str, min_1: int, max_1: int, option_two: str, min_2: int, max_2: int, ) -> Callable[[Any], list[int | float] | None]: """Convert the result to a list of 2 numbers that represent a color.""" option_range = f"({min_1}-{max_1}, {min_2}-{max_2})" def convert(result: Any) -> list[int | float] | None: if template_validators.check_result_for_none(result): return None if isinstance(result, str): with contextlib.suppress(ValueError): result = _string_to_list(result) if ( isinstance(result, (list, tuple)) and len(result) == 2 and all(isinstance(value, (int, float)) for value in result) ): one, two = result if not (min_1 <= one <= max_1) or not (min_2 <= two <= max_2): template_validators.log_validation_result_error( entity, option, result, ( f"expected {option_one} value between {min_1} and {max_1} and " f"{option_two} value between {min_2} and {max_2}: {option_range}" ), ) return None return list(result) template_validators.log_validation_result_error( entity, option, result, f"expected a list of numbers: {option_range}", ) return None return convert def rgb_color_list( entity: AbstractTemplateLight, attribute: str, length: int ) -> Callable[[Any], list[int] | None]: """Convert the result to a list of numbers that represent a color.""" example = "[" + ", ".join(("0-255",) * length) + "]" message = f"expected a list of {length} numbers between 0 and 255: {example}" def convert(result: Any) -> list[int] | None: if template_validators.check_result_for_none(result): return None if isinstance(result, str): with contextlib.suppress(ValueError): result = _string_to_list(result) if ( isinstance(result, (list, tuple)) and len(result) == length and all(isinstance(value, (int, float)) for value in result) ): # Ensure the result are numbers between 0 and 255. if all(0 <= value <= 255 for value in result): return list(result) template_validators.log_validation_result_error( entity, attribute, result, message ) return None return convert @dataclass(kw_only=True) class LightExtraStoredData(ExtraStoredData): """Object to hold extra stored data.""" is_on: bool | None brightness: int | None color_mode: ColorMode | None color_temp_kelvin: int | None effect_list: list[str] | None effect: str | None hs_color: tuple[float, float] | None max_color_temp_kelvin: int min_color_temp_kelvin: int rgb_color: tuple[int, int, int] | None rgbw_color: tuple[int, int, int, int] | None rgbww_color: tuple[int, int, int, int, int] | None supported_color_modes: set[ColorMode] | None xy_color: tuple[float, float] | None @override def as_dict(self) -> dict[str, Any]: """Return a dict representation of the extra data.""" return { "is_on": self.is_on, "brightness": self.brightness, "color_mode": self.color_mode.value if self.color_mode else None, "color_temp_kelvin": self.color_temp_kelvin, "effect_list": self.effect_list, "effect": self.effect, "hs_color": self.hs_color, "max_color_temp_kelvin": self.max_color_temp_kelvin, "min_color_temp_kelvin": self.min_color_temp_kelvin, "rgb_color": self.rgb_color, "rgbw_color": self.rgbw_color, "rgbww_color": self.rgbww_color, "supported_color_modes": ( [mode.value for mode in self.supported_color_modes] if self.supported_color_modes else None ), "xy_color": self.xy_color, } @classmethod def from_dict(cls, restored: dict[str, Any]) -> Self | None: """Initialize a stored state from a dict.""" try: color_mode: ColorMode | None = None if _color_mode := restored["color_mode"]: color_mode = ColorMode(_color_mode) supported_color_modes: set[ColorMode] | None = None if _supported_color_modes := restored["supported_color_modes"]: supported_color_modes = { ColorMode(item) for item in _supported_color_modes } return cls( is_on=restored["is_on"], brightness=restored["brightness"], color_mode=color_mode, color_temp_kelvin=restored["color_temp_kelvin"], effect_list=restored["effect_list"], effect=restored["effect"], hs_color=restored["hs_color"], max_color_temp_kelvin=restored["max_color_temp_kelvin"], min_color_temp_kelvin=restored["min_color_temp_kelvin"], rgb_color=restored["rgb_color"], rgbw_color=restored["rgbw_color"], rgbww_color=restored["rgbww_color"], supported_color_modes=supported_color_modes, xy_color=restored["xy_color"], ) except KeyError, ValueError: return None class AbstractTemplateLight(AbstractTemplateEntity, LightEntity, RestoreEntity): """Representation of a template lights features.""" _entity_id_format = ENTITY_ID_FORMAT _optimistic_entity = True _attr_max_color_temp_kelvin = DEFAULT_MAX_KELVIN _attr_min_color_temp_kelvin = DEFAULT_MIN_KELVIN _state_option = CONF_STATE _restore_state_extra_data = LightExtraStoredData _restore_state_properties = ("_attr_is_on",) # The super init is not called because TemplateEntity # and TriggerEntity will call # AbstractTemplateEntity.__init__. This ensures that # the __init__ on AbstractTemplateEntity is not # called twice. def __init__( # pylint: disable=super-init-not-called self, name: str, config: dict[str, Any] ) -> None: """Initialize the features.""" # Setup state and brightness self.setup_state_template( "_attr_is_on", template_validators.boolean(self, CONF_STATE) ) self.setup_template( CONF_LEVEL, "_attr_brightness", template_validators.number(self, CONF_LEVEL, 0, 255, int), ) # Setup Color temperature self.setup_template( CONF_TEMPERATURE, "_attr_color_temp_kelvin", self._validate_temperature, self._update_color("_attr_color_temp_kelvin", ColorMode.COLOR_TEMP), ) # Setup Hue Saturation self.setup_template( CONF_HS, "_attr_hs_color", two_color_list(self, CONF_HS, "a hue", 0, 360, "a saturation", 0, 100), self._update_color("_attr_hs_color", ColorMode.HS), render_complex=True, ) # Setup RGB Colors for option, attribute, length, colormode in ( (CONF_RGB, "_attr_rgb_color", 3, ColorMode.RGB), (CONF_RGBW, "_attr_rgbw_color", 4, ColorMode.RGBW), (CONF_RGBWW, "_attr_rgbww_color", 5, ColorMode.RGBWW), ): self.setup_template( option, attribute, rgb_color_list(self, option, length), self._update_color(attribute, colormode), render_complex=True, ) # Setup XY Color self.setup_template( CONF_XY, "_attr_xy_color", two_color_list(self, CONF_XY, "an x", 0, 1, "a y", 0, 1), self._update_color("_attr_xy_color", ColorMode.XY), render_complex=True, ) # Setup Effect templates self.setup_template( CONF_EFFECT_LIST, "_attr_effect_list", template_validators.list_of_strings( self, CONF_EFFECT_LIST, none_on_empty=True ), render_complex=True, ) self.setup_template( CONF_EFFECT, "_attr_effect", template_validators.item_in_list( self, "_attr_effect", "_attr_effect_list", CONF_EFFECT_LIST ), ) # Min/Max temperature templates self.setup_template( CONF_MAX_MIREDS, "_attr_max_color_temp_kelvin", template_validators.number(self, CONF_MAX_MIREDS), self._update_max_mireds, ) self.setup_template( CONF_MIN_MIREDS, "_attr_min_color_temp_kelvin", template_validators.number(self, CONF_MIN_MIREDS), self._update_min_mireds, ) # Transition self.setup_template( CONF_SUPPORTS_TRANSITION, "_supports_transition_template", template_validators.boolean(self, CONF_SUPPORTS_TRANSITION), self._update_supports_transition, ) # Stored values for template attributes self._supports_transition = False color_modes = {ColorMode.ONOFF} 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), (CONF_RGB_ACTION, ColorMode.RGB), (CONF_RGBW_ACTION, ColorMode.RGBW), (CONF_RGBWW_ACTION, ColorMode.RGBWW), (CONF_XY_ACTION, ColorMode.XY), ): if (action_config := config.get(action_id)) is not None: self.add_script(action_id, action_config, name, DOMAIN) if color_mode: color_modes.add(color_mode) self._attr_supported_color_modes = filter_supported_color_modes(color_modes) if len(self._attr_supported_color_modes) > 1: self._attr_color_mode = ColorMode.UNKNOWN if len(self._attr_supported_color_modes) == 1: self._attr_color_mode = next(iter(self._attr_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 @override 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() @override 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._attr_assumed_state: self._attr_is_on = False self.async_write_ha_state() def set_optimistic_attributes(self, **kwargs) -> bool: """Update attributes which should be set optimistically. Returns True if any attribute was updated. """ optimistic_set = False if self._attr_assumed_state: self._attr_is_on = True optimistic_set = True if CONF_LEVEL not in self._templates and ATTR_BRIGHTNESS in kwargs: _LOGGER.debug( "Optimistically setting brightness to %s", kwargs[ATTR_BRIGHTNESS] ) self._attr_brightness = kwargs[ATTR_BRIGHTNESS] optimistic_set = True if CONF_TEMPERATURE not in self._templates and ATTR_COLOR_TEMP_KELVIN in kwargs: self._set_optimistic_color( "color temperature", "_attr_color_temp_kelvin", kwargs[ATTR_COLOR_TEMP_KELVIN], ColorMode.COLOR_TEMP, ) optimistic_set = True if CONF_TEMPERATURE not in self._templates and ATTR_COLOR_TEMP in kwargs: self._set_optimistic_color( "color temperature", "_attr_color_temp_kelvin", color_util.color_temperature_mired_to_kelvin(kwargs[ATTR_COLOR_TEMP]), ColorMode.COLOR_TEMP, ) optimistic_set = True if CONF_HS not in self._templates and ATTR_HS_COLOR in kwargs: self._set_optimistic_color( "hs color", "_attr_hs_color", kwargs[ATTR_HS_COLOR], ColorMode.HS ) optimistic_set = True if CONF_RGB not in self._templates and ATTR_RGB_COLOR in kwargs: self._set_optimistic_color( "rgb color", "_attr_rgb_color", kwargs[ATTR_RGB_COLOR], ColorMode.RGB ) optimistic_set = True if CONF_RGBW not in self._templates and ATTR_RGBW_COLOR in kwargs: self._set_optimistic_color( "rgbw color", "_attr_rgbw_color", kwargs[ATTR_RGBW_COLOR], ColorMode.RGBW, ) optimistic_set = True if CONF_RGBWW not in self._templates and ATTR_RGBWW_COLOR in kwargs: self._set_optimistic_color( "rgbww color", "_attr_rgbww_color", kwargs[ATTR_RGBWW_COLOR], ColorMode.RGBWW, ) optimistic_set = True if CONF_XY not in self._templates and ATTR_XY_COLOR in kwargs: self._set_optimistic_color( "xy color", "_attr_xy_color", kwargs[ATTR_XY_COLOR], ColorMode.XY, ) optimistic_set = True if optimistic_set and not self._attr_assumed_state: # If we are optmistically setting color or level but the state template # has not rendered, optimisically set the state to 'on'. self._attr_is_on = True return optimistic_set def _set_optimistic_color( self, description: str, attribute: str, value: Any, color_mode: ColorMode ) -> None: _LOGGER.debug( "Optimistically setting %s to %s", description, value, ) self._attr_color_mode = color_mode setattr(self, attribute, value) for option, attr in ( (CONF_TEMPERATURE, "_attr_color_temp_kelvin"), (CONF_HS, "_attr_hs_color"), (CONF_RGB, "_attr_rgb_color"), (CONF_RGBW, "_attr_rgbw_color"), (CONF_RGBWW, "_attr_rgbww_color"), (CONF_XY, "_attr_xy_color"), ): if attribute == attr: continue if option not in self._templates: setattr(self, attr, None) def get_registered_script(self, **kwargs) -> tuple[str, dict]: """Get registered script for turn_on.""" common_params = {} if ATTR_BRIGHTNESS in kwargs: brightness = kwargs[ATTR_BRIGHTNESS] common_params["brightness"] = brightness common_params["brightness_pct"] = round(brightness / 255 * 100) 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 (script := CONF_TEMPERATURE_ACTION) in self._action_scripts ): kelvin = kwargs[ATTR_COLOR_TEMP_KELVIN] common_params[ATTR_COLOR_TEMP_KELVIN] = kelvin common_params[ATTR_COLOR_TEMP] = ( color_util.color_temperature_kelvin_to_mired(kelvin) ) return (script, common_params) if ( ATTR_EFFECT in kwargs and (script := CONF_EFFECT_ACTION) in self._action_scripts ): assert self._attr_effect_list is not None effect = kwargs[ATTR_EFFECT] if ( self._attr_effect_list is not None and effect not in self._attr_effect_list ): _LOGGER.error( "Received invalid effect: %s for entity %s. Expected one of: %s", effect, self.entity_id, self._attr_effect_list, ) common_params["effect"] = effect return (script, common_params) if ( ATTR_HS_COLOR in kwargs and (script := CONF_HS_ACTION) in self._action_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]) return (script, common_params) if ( ATTR_RGBWW_COLOR in kwargs and (script := CONF_RGBWW_ACTION) in self._action_scripts ): rgbww_value = kwargs[ATTR_RGBWW_COLOR] common_params["rgbww"] = rgbww_value common_params["rgb"] = ( int(rgbww_value[0]), int(rgbww_value[1]), int(rgbww_value[2]), ) common_params["r"] = int(rgbww_value[0]) common_params["g"] = int(rgbww_value[1]) common_params["b"] = int(rgbww_value[2]) common_params["cw"] = int(rgbww_value[3]) common_params["ww"] = int(rgbww_value[4]) return (script, common_params) if ( ATTR_RGBW_COLOR in kwargs and (script := CONF_RGBW_ACTION) in self._action_scripts ): rgbw_value = kwargs[ATTR_RGBW_COLOR] common_params["rgbw"] = rgbw_value common_params["rgb"] = ( int(rgbw_value[0]), int(rgbw_value[1]), int(rgbw_value[2]), ) common_params["r"] = int(rgbw_value[0]) common_params["g"] = int(rgbw_value[1]) common_params["b"] = int(rgbw_value[2]) common_params["w"] = int(rgbw_value[3]) return (script, common_params) if ( ATTR_RGB_COLOR in kwargs and (script := CONF_RGB_ACTION) in self._action_scripts ): rgb_value = kwargs[ATTR_RGB_COLOR] common_params["rgb"] = rgb_value common_params["r"] = int(rgb_value[0]) common_params["g"] = int(rgb_value[1]) common_params["b"] = int(rgb_value[2]) return (script, common_params) if ( ATTR_XY_COLOR in kwargs and (script := CONF_XY_ACTION) in self._action_scripts ): xy_value = kwargs[ATTR_XY_COLOR] common_params["xy"] = xy_value common_params["x"] = float(xy_value[0]) common_params["y"] = float(xy_value[1]) return (script, common_params) if ( ATTR_BRIGHTNESS in kwargs and (script := CONF_LEVEL_ACTION) in self._action_scripts ): return (script, common_params) return (CONF_ON_ACTION, common_params) def _update_color( self, attribute: str, color_mode: ColorMode ) -> Callable[[Any], None]: """Update the color.""" def update(render) -> None: if render is None: setattr(self, attribute, None) return setattr(self, attribute, render) self._attr_color_mode = color_mode return update @callback def _validate_temperature(self, result: Any) -> int | None: """Validate the temperature from the template.""" if template_validators.check_result_for_none(result): return None if (min_kelvin := self._attr_min_color_temp_kelvin) is not None: max_mireds = color_util.color_temperature_kelvin_to_mired(min_kelvin) else: max_mireds = DEFAULT_MAX_MIREDS if (max_kelvin := self._attr_max_color_temp_kelvin) is not None: min_mireds = color_util.color_temperature_kelvin_to_mired(max_kelvin) else: min_mireds = DEFAULT_MIN_MIREDS if isinstance(result, (int, float)) and min_mireds <= result <= max_mireds: return color_util.color_temperature_mired_to_kelvin(result) template_validators.log_validation_result_error( self, CONF_TEMPERATURE, result, f"expected a number between {min_mireds} and {max_mireds}", ) return None @callback def _update_max_mireds(self, render): """Update the max mireds from the template.""" if render is None: self._attr_min_color_temp_kelvin = DEFAULT_MIN_KELVIN return try: self._attr_min_color_temp_kelvin = ( color_util.color_temperature_mired_to_kelvin(int(render)) ) except ValueError: _LOGGER.exception( "Template must supply an integer temperature within the range for" " this light, or 'None'" ) self._attr_min_color_temp_kelvin = DEFAULT_MIN_KELVIN @callback def _update_min_mireds(self, render): """Update the min mireds from the template.""" if render is None: self._attr_max_color_temp_kelvin = DEFAULT_MAX_KELVIN return try: self._attr_max_color_temp_kelvin = ( color_util.color_temperature_mired_to_kelvin(int(render)) ) except ValueError: _LOGGER.exception( "Template must supply an integer temperature within the range for" " this light, or 'None'" ) self._attr_max_color_temp_kelvin = DEFAULT_MAX_KELVIN @callback def _update_supports_transition(self, render): """Update the supports transition from the template.""" if render is None: self._supports_transition = False return self._attr_supported_features &= ~LightEntityFeature.TRANSITION self._supports_transition = bool(render) if self._supports_transition: self._attr_supported_features |= LightEntityFeature.TRANSITION @property @override def extra_restore_state_data(self) -> LightExtraStoredData: """Return weather specific state data to be restored.""" return LightExtraStoredData( is_on=self._attr_is_on, brightness=self._attr_brightness, color_mode=self._attr_color_mode, color_temp_kelvin=self._attr_color_temp_kelvin, effect_list=self._attr_effect_list, effect=self._attr_effect, hs_color=self._attr_hs_color, max_color_temp_kelvin=self._attr_max_color_temp_kelvin, min_color_temp_kelvin=self._attr_min_color_temp_kelvin, rgb_color=self._attr_rgb_color, rgbw_color=self._attr_rgbw_color, rgbww_color=self._attr_rgbww_color, supported_color_modes=self._attr_supported_color_modes, xy_color=self._attr_xy_color, ) @override def restore_extra_data(self, extra_data: LightExtraStoredData) -> None: """Restore the extra data.""" self._attr_is_on = extra_data.is_on self._attr_brightness = extra_data.brightness self._attr_color_mode = extra_data.color_mode self._attr_color_temp_kelvin = extra_data.color_temp_kelvin self._attr_effect_list = extra_data.effect_list self._attr_effect = extra_data.effect self._attr_hs_color = extra_data.hs_color self._attr_max_color_temp_kelvin = extra_data.max_color_temp_kelvin self._attr_min_color_temp_kelvin = extra_data.min_color_temp_kelvin self._attr_rgb_color = extra_data.rgb_color self._attr_rgbw_color = extra_data.rgbw_color self._attr_rgbww_color = extra_data.rgbww_color self._attr_supported_color_modes = extra_data.supported_color_modes self._attr_xy_color = extra_data.xy_color class StateLightEntity(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, unique_id) name = self._attr_name if TYPE_CHECKING: assert name is not None AbstractTemplateLight.__init__(self, name, config) 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) # Render the _attr_name before initializing TemplateLightEntity self._attr_name = name = self._rendered.get(CONF_NAME, DEFAULT_NAME) AbstractTemplateLight.__init__(self, name, config)