diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index b3f276240b5..89c4826f1e6 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -11,6 +11,9 @@ from homeassistant.components.light import ( ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR, + ATTR_RGB_COLOR, + ATTR_RGBW_COLOR, + ATTR_RGBWW_COLOR, ATTR_TRANSITION, ENTITY_ID_FORMAT, ColorMode, @@ -46,8 +49,18 @@ from .template_entity import ( _LOGGER = logging.getLogger(__name__) _VALID_STATES = [STATE_ON, STATE_OFF, "true", "false"] +# Legacy CONF_COLOR_ACTION = "set_color" CONF_COLOR_TEMPLATE = "color_template" + +CONF_HS_ACTION = "set_hs" +CONF_HS_TEMPLATE = "hs_template" +CONF_RGB_ACTION = "set_rgb" +CONF_RGB_TEMPLATE = "rgb_template" +CONF_RGBW_ACTION = "set_rgbw" +CONF_RGBW_TEMPLATE = "rgbw_template" +CONF_RGBWW_ACTION = "set_rgbww" +CONF_RGBWW_TEMPLATE = "rgbww_template" CONF_EFFECT_ACTION = "set_effect" CONF_EFFECT_LIST_TEMPLATE = "effect_list_template" CONF_EFFECT_TEMPLATE = "effect_template" @@ -67,8 +80,16 @@ LIGHT_SCHEMA = vol.All( cv.deprecated(CONF_ENTITY_ID), vol.Schema( { - vol.Optional(CONF_COLOR_ACTION): cv.SCRIPT_SCHEMA, - vol.Optional(CONF_COLOR_TEMPLATE): cv.template, + vol.Exclusive(CONF_COLOR_ACTION, "hs_legacy_action"): cv.SCRIPT_SCHEMA, + vol.Exclusive(CONF_COLOR_TEMPLATE, "hs_legacy_template"): cv.template, + vol.Exclusive(CONF_HS_ACTION, "hs_legacy_action"): cv.SCRIPT_SCHEMA, + vol.Exclusive(CONF_HS_TEMPLATE, "hs_legacy_template"): cv.template, + vol.Optional(CONF_RGB_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_RGB_TEMPLATE): cv.template, + vol.Optional(CONF_RGBW_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_RGBW_TEMPLATE): cv.template, + vol.Optional(CONF_RGBWW_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_RGBWW_TEMPLATE): cv.template, vol.Inclusive(CONF_EFFECT_ACTION, "effect"): cv.SCRIPT_SCHEMA, vol.Inclusive(CONF_EFFECT_LIST_TEMPLATE, "effect"): cv.template, vol.Inclusive(CONF_EFFECT_TEMPLATE, "effect"): cv.template, @@ -166,6 +187,22 @@ class LightTemplate(TemplateEntity, LightEntity): if (color_action := config.get(CONF_COLOR_ACTION)) is not None: self._color_script = Script(hass, color_action, friendly_name, DOMAIN) self._color_template = config.get(CONF_COLOR_TEMPLATE) + self._hs_script = None + if (hs_action := config.get(CONF_HS_ACTION)) is not None: + self._hs_script = Script(hass, hs_action, friendly_name, DOMAIN) + self._hs_template = config.get(CONF_HS_TEMPLATE) + self._rgb_script = None + if (rgb_action := config.get(CONF_RGB_ACTION)) is not None: + self._rgb_script = Script(hass, rgb_action, friendly_name, DOMAIN) + self._rgb_template = config.get(CONF_RGB_TEMPLATE) + self._rgbw_script = None + if (rgbw_action := config.get(CONF_RGBW_ACTION)) is not None: + self._rgbw_script = Script(hass, rgbw_action, friendly_name, DOMAIN) + self._rgbw_template = config.get(CONF_RGBW_TEMPLATE) + self._rgbww_script = None + if (rgbww_action := config.get(CONF_RGBWW_ACTION)) is not None: + self._rgbww_script = Script(hass, rgbww_action, friendly_name, DOMAIN) + self._rgbww_template = config.get(CONF_RGBWW_TEMPLATE) self._effect_script = None if (effect_action := config.get(CONF_EFFECT_ACTION)) is not None: self._effect_script = Script(hass, effect_action, friendly_name, DOMAIN) @@ -178,24 +215,39 @@ class LightTemplate(TemplateEntity, LightEntity): self._state = False self._brightness = None self._temperature = None - self._color = None + self._hs_color = None + self._rgb_color = None + self._rgbw_color = None + self._rgbww_color = None self._effect = None self._effect_list = None - self._fixed_color_mode = None + self._color_mode = None self._max_mireds = None self._min_mireds = None self._supports_transition = False + self._supported_color_modes = None color_modes = {ColorMode.ONOFF} if self._level_script is not None: color_modes.add(ColorMode.BRIGHTNESS) if self._temperature_script is not None: color_modes.add(ColorMode.COLOR_TEMP) + if self._hs_script is not None: + color_modes.add(ColorMode.HS) if self._color_script is not None: color_modes.add(ColorMode.HS) + if self._rgb_script is not None: + color_modes.add(ColorMode.RGB) + if self._rgbw_script is not None: + color_modes.add(ColorMode.RGBW) + if self._rgbww_script is not None: + color_modes.add(ColorMode.RGBWW) + 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._fixed_color_mode = next(iter(self._supported_color_modes)) + self._color_mode = next(iter(self._supported_color_modes)) self._attr_supported_features = LightEntityFeature(0) if self._effect_script is not None: @@ -232,7 +284,22 @@ class LightTemplate(TemplateEntity, LightEntity): @property def hs_color(self) -> tuple[float, float] | None: """Return the hue and saturation color value [float, float].""" - return self._color + return self._hs_color + + @property + def rgb_color(self) -> tuple[int, int, int] | None: + """Return the rgb color value.""" + return self._rgb_color + + @property + def rgbw_color(self) -> tuple[int, int, int, int] | None: + """Return the rgbw color value.""" + return self._rgbw_color + + @property + def rgbww_color(self) -> tuple[int, int, int, int, int] | None: + """Return the rgbww color value.""" + return self._rgbww_color @property def effect(self) -> str | None: @@ -247,12 +314,7 @@ class LightTemplate(TemplateEntity, LightEntity): @property def color_mode(self): """Return current color mode.""" - if self._fixed_color_mode: - return self._fixed_color_mode - # Support for ct + hs, prioritize hs - if self._color is not None: - return ColorMode.HS - return ColorMode.COLOR_TEMP + return self._color_mode @property def supported_color_modes(self): @@ -305,10 +367,42 @@ class LightTemplate(TemplateEntity, LightEntity): ) if self._color_template: self.add_template_attribute( - "_color", + "_hs_color", self._color_template, None, - self._update_color, + self._update_hs, + 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: @@ -337,7 +431,7 @@ class LightTemplate(TemplateEntity, LightEntity): ) super()._async_setup_templates() - async def async_turn_on(self, **kwargs: Any) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: # noqa: C901 """Turn the light on.""" optimistic_set = False # set optimistic states @@ -357,19 +451,88 @@ class LightTemplate(TemplateEntity, LightEntity): "Optimistically setting color temperature to %s", kwargs[ATTR_COLOR_TEMP], ) + self._color_mode = ColorMode.COLOR_TEMP self._temperature = kwargs[ATTR_COLOR_TEMP] - if self._color_template is None: - self._color = None + if self._hs_template is None and self._color_template is None: + self._hs_color = None + if self._rgb_template is None: + self._rgb_color = None + if self._rgbw_template is None: + self._rgbw_color = None + if self._rgbww_template is None: + self._rgbww_color = None optimistic_set = True - if self._color_template is None and ATTR_HS_COLOR in kwargs: + if ( + self._hs_template is None + and self._color_template is None + and ATTR_HS_COLOR in kwargs + ): _LOGGER.debug( - "Optimistically setting color to %s", + "Optimistically setting hs color to %s", kwargs[ATTR_HS_COLOR], ) - self._color = kwargs[ATTR_HS_COLOR] + self._color_mode = ColorMode.HS + self._hs_color = kwargs[ATTR_HS_COLOR] if self._temperature_template is None: self._temperature = None + if self._rgb_template is None: + self._rgb_color = None + if self._rgbw_template is None: + self._rgbw_color = None + if self._rgbww_template is None: + self._rgbww_color = None + optimistic_set = True + + if self._rgb_template is None and ATTR_RGB_COLOR in kwargs: + _LOGGER.debug( + "Optimistically setting rgb color to %s", + kwargs[ATTR_RGB_COLOR], + ) + self._color_mode = ColorMode.RGB + self._rgb_color = kwargs[ATTR_RGB_COLOR] + if self._temperature_template is None: + self._temperature = None + if self._hs_template is None and self._color_template is None: + self._hs_color = None + if self._rgbw_template is None: + self._rgbw_color = None + if self._rgbww_template is None: + self._rgbww_color = None + optimistic_set = True + + if self._rgbw_template is None and ATTR_RGBW_COLOR in kwargs: + _LOGGER.debug( + "Optimistically setting rgbw color to %s", + kwargs[ATTR_RGBW_COLOR], + ) + self._color_mode = ColorMode.RGBW + self._rgbw_color = kwargs[ATTR_RGBW_COLOR] + if self._temperature_template is None: + self._temperature = None + if self._hs_template is None and self._color_template is None: + self._hs_color = None + if self._rgb_template is None: + self._rgb_color = None + if self._rgbww_template is None: + self._rgbww_color = None + optimistic_set = True + + if self._rgbww_template is None and ATTR_RGBWW_COLOR in kwargs: + _LOGGER.debug( + "Optimistically setting rgbww color to %s", + kwargs[ATTR_RGBWW_COLOR], + ) + self._color_mode = ColorMode.RGBWW + self._rgbww_color = kwargs[ATTR_RGBWW_COLOR] + if self._temperature_template is None: + self._temperature = None + if self._hs_template is None and self._color_template is None: + self._hs_color = None + if self._rgb_template is None: + self._rgb_color = None + if self._rgbw_template is None: + self._rgbw_color = None optimistic_set = True common_params = {} @@ -413,6 +576,58 @@ class LightTemplate(TemplateEntity, LightEntity): await self.async_run_script( self._color_script, run_variables=common_params, context=self._context ) + elif ATTR_HS_COLOR in kwargs and self._hs_script: + 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( + self._hs_script, run_variables=common_params, context=self._context + ) + elif ATTR_RGBWW_COLOR in kwargs and self._rgbww_script: + 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]) + + await self.async_run_script( + self._rgbww_script, run_variables=common_params, context=self._context + ) + elif ATTR_RGBW_COLOR in kwargs and self._rgbw_script: + 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]) + + await self.async_run_script( + self._rgbw_script, run_variables=common_params, context=self._context + ) + elif ATTR_RGB_COLOR in kwargs and self._rgb_script: + 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]) + + await self.async_run_script( + self._rgb_script, run_variables=common_params, context=self._context + ) elif ATTR_BRIGHTNESS in kwargs and self._level_script: await self.async_run_script( self._level_script, run_variables=common_params, context=self._context @@ -560,18 +775,19 @@ class LightTemplate(TemplateEntity, LightEntity): " this light, or 'None'" ) self._temperature = None + self._color_mode = ColorMode.COLOR_TEMP @callback - def _update_color(self, render): - """Update the hs_color from the template.""" + def _update_hs(self, render): + """Update the color from the template.""" if render is None: - self._color = None + self._hs_color = None return h_str = s_str = None if isinstance(render, str): if render in ("None", ""): - self._color = None + self._hs_color = None return h_str, s_str = map( float, render.replace("(", "").replace(")", "").split(",", 1) @@ -582,10 +798,12 @@ class LightTemplate(TemplateEntity, LightEntity): if ( h_str is not None and s_str is not None + and isinstance(h_str, (int, float)) + and isinstance(s_str, (int, float)) and 0 <= h_str <= 360 and 0 <= s_str <= 100 ): - self._color = (h_str, s_str) + self._hs_color = (h_str, s_str) elif h_str is not None and s_str is not None: _LOGGER.error( ( @@ -596,12 +814,151 @@ class LightTemplate(TemplateEntity, LightEntity): s_str, self.entity_id, ) - self._color = None + self._hs_color = None else: _LOGGER.error( "Received invalid hs_color : (%s) for entity %s", render, self.entity_id ) - self._color = None + self._hs_color = None + self._color_mode = ColorMode.HS + + @callback + def _update_rgb(self, render): + """Update the color from the template.""" + if render is None: + self._rgb_color = None + return + + r_int = g_int = b_int = None + if isinstance(render, str): + if render in ("None", ""): + self._rgb_color = None + return + cleanup_char = ["(", ")", "[", "]", " "] + for char in cleanup_char: + render = render.replace(char, "") + r_int, g_int, b_int = map(int, render.split(",", 3)) + elif isinstance(render, (list, tuple)) and len(render) == 3: + r_int, g_int, b_int = render + + if all( + value is not None and isinstance(value, (int, float)) and 0 <= value <= 255 + for value in (r_int, g_int, b_int) + ): + self._rgb_color = (r_int, g_int, b_int) + elif any( + isinstance(value, (int, float)) and not 0 <= value <= 255 + for value in (r_int, g_int, b_int) + ): + _LOGGER.error( + "Received invalid rgb_color : (%s, %s, %s) for entity %s. Expected: (0-255, 0-255, 0-255)", + r_int, + g_int, + b_int, + self.entity_id, + ) + self._rgb_color = None + else: + _LOGGER.error( + "Received invalid rgb_color : (%s) for entity %s", + render, + self.entity_id, + ) + self._rgb_color = None + self._color_mode = ColorMode.RGB + + @callback + def _update_rgbw(self, render): + """Update the color from the template.""" + if render is None: + self._rgbw_color = None + return + + r_int = g_int = b_int = w_int = None + if isinstance(render, str): + if render in ("None", ""): + self._rgb_color = None + return + cleanup_char = ["(", ")", "[", "]", " "] + for char in cleanup_char: + render = render.replace(char, "") + r_int, g_int, b_int, w_int = map(int, render.split(",", 4)) + elif isinstance(render, (list, tuple)) and len(render) == 4: + r_int, g_int, b_int, w_int = render + + if all( + value is not None and isinstance(value, (int, float)) and 0 <= value <= 255 + for value in (r_int, g_int, b_int, w_int) + ): + self._rgbw_color = (r_int, g_int, b_int, w_int) + elif any( + isinstance(value, (int, float)) and not 0 <= value <= 255 + for value in (r_int, g_int, b_int, w_int) + ): + _LOGGER.error( + "Received invalid rgb_color : (%s, %s, %s, %s) for entity %s. Expected: (0-255, 0-255, 0-255, 0-255)", + r_int, + g_int, + b_int, + w_int, + self.entity_id, + ) + self._rgbw_color = None + else: + _LOGGER.error( + "Received invalid rgb_color : (%s) for entity %s", + render, + self.entity_id, + ) + self._rgbw_color = None + self._color_mode = ColorMode.RGBW + + @callback + def _update_rgbww(self, render): + """Update the color from the template.""" + if render is None: + self._rgbww_color = None + return + + r_int = g_int = b_int = cw_int = ww_int = None + if isinstance(render, str): + if render in ("None", ""): + self._rgb_color = None + return + cleanup_char = ["(", ")", "[", "]", " "] + for char in cleanup_char: + render = render.replace(char, "") + r_int, g_int, b_int, cw_int, ww_int = map(int, render.split(",", 5)) + elif isinstance(render, (list, tuple)) and len(render) == 5: + r_int, g_int, b_int, cw_int, ww_int = render + + if all( + value is not None and isinstance(value, (int, float)) and 0 <= value <= 255 + for value in (r_int, g_int, b_int, cw_int, ww_int) + ): + self._rgbww_color = (r_int, g_int, b_int, cw_int, ww_int) + elif any( + isinstance(value, (int, float)) and not 0 <= value <= 255 + for value in (r_int, g_int, b_int, cw_int, ww_int) + ): + _LOGGER.error( + "Received invalid rgb_color : (%s, %s, %s, %s, %s) for entity %s. Expected: (0-255, 0-255, 0-255, 0-255)", + r_int, + g_int, + b_int, + cw_int, + ww_int, + self.entity_id, + ) + self._rgbww_color = None + else: + _LOGGER.error( + "Received invalid rgb_color : (%s) for entity %s", + render, + self.entity_id, + ) + self._rgbww_color = None + self._color_mode = ColorMode.RGBWW @callback def _update_max_mireds(self, render): diff --git a/tests/components/template/test_light.py b/tests/components/template/test_light.py index f807b185c45..ec830d4daf6 100644 --- a/tests/components/template/test_light.py +++ b/tests/components/template/test_light.py @@ -7,6 +7,9 @@ from homeassistant.components.light import ( ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR, + ATTR_RGB_COLOR, + ATTR_RGBW_COLOR, + ATTR_RGBWW_COLOR, ATTR_TRANSITION, ColorMode, LightEntityFeature, @@ -72,7 +75,7 @@ OPTIMISTIC_COLOR_TEMP_LIGHT_CONFIG = { } -OPTIMISTIC_HS_COLOR_LIGHT_CONFIG = { +OPTIMISTIC_LEGACY_COLOR_LIGHT_CONFIG = { **OPTIMISTIC_ON_OFF_LIGHT_CONFIG, "set_color": { "service": "test.automation", @@ -86,6 +89,68 @@ OPTIMISTIC_HS_COLOR_LIGHT_CONFIG = { } +OPTIMISTIC_HS_COLOR_LIGHT_CONFIG = { + **OPTIMISTIC_ON_OFF_LIGHT_CONFIG, + "set_hs": { + "service": "test.automation", + "data_template": { + "action": "set_hs", + "caller": "{{ this.entity_id }}", + "s": "{{s}}", + "h": "{{h}}", + }, + }, +} + + +OPTIMISTIC_RGB_COLOR_LIGHT_CONFIG = { + **OPTIMISTIC_ON_OFF_LIGHT_CONFIG, + "set_rgb": { + "service": "test.automation", + "data_template": { + "action": "set_rgb", + "caller": "{{ this.entity_id }}", + "r": "{{r}}", + "g": "{{g}}", + "b": "{{b}}", + }, + }, +} + + +OPTIMISTIC_RGBW_COLOR_LIGHT_CONFIG = { + **OPTIMISTIC_ON_OFF_LIGHT_CONFIG, + "set_rgbw": { + "service": "test.automation", + "data_template": { + "action": "set_rgbw", + "caller": "{{ this.entity_id }}", + "r": "{{r}}", + "g": "{{g}}", + "b": "{{b}}", + "w": "{{w}}", + }, + }, +} + + +OPTIMISTIC_RGBWW_COLOR_LIGHT_CONFIG = { + **OPTIMISTIC_ON_OFF_LIGHT_CONFIG, + "set_rgbww": { + "service": "test.automation", + "data_template": { + "action": "set_rgbww", + "caller": "{{ this.entity_id }}", + "r": "{{r}}", + "g": "{{g}}", + "b": "{{b}}", + "cw": "{{cw}}", + "ww": "{{ww}}", + }, + }, +} + + async def async_setup_light(hass, count, light_config): """Do setup of light integration.""" config = {"light": {"platform": "template", "lights": light_config}} @@ -607,6 +672,7 @@ async def test_level_action_no_template( "{{ state_attr('light.nolight', 'brightness') }}", ColorMode.BRIGHTNESS, ), + (None, "{{'one'}}", ColorMode.BRIGHTNESS), ], ) async def test_level_template( @@ -643,6 +709,7 @@ async def test_level_template( (None, "None", ColorMode.COLOR_TEMP), (None, "{{ none }}", ColorMode.COLOR_TEMP), (None, "", ColorMode.COLOR_TEMP), + (None, "{{ 'one' }}", ColorMode.COLOR_TEMP), ], ) async def test_temperature_template( @@ -797,17 +864,17 @@ async def test_entity_picture_template(hass: HomeAssistant, setup_light) -> None [ { "test_template_light": { - **OPTIMISTIC_HS_COLOR_LIGHT_CONFIG, + **OPTIMISTIC_LEGACY_COLOR_LIGHT_CONFIG, "value_template": "{{1 == 1}}", } }, ], ) -async def test_color_action_no_template( - hass: HomeAssistant, +async def test_legacy_color_action_no_template( + hass, setup_light, calls, -) -> None: +): """Test setting color with optimistic template.""" state = hass.states.get("light.test_template_light") assert state.attributes.get("hs_color") is None @@ -833,6 +900,186 @@ async def test_color_action_no_template( assert state.attributes["supported_features"] == 0 +@pytest.mark.parametrize("count", [1]) +@pytest.mark.parametrize( + "light_config", + [ + { + "test_template_light": { + **OPTIMISTIC_HS_COLOR_LIGHT_CONFIG, + "value_template": "{{1 == 1}}", + } + }, + ], +) +async def test_hs_color_action_no_template( + hass: HomeAssistant, + setup_light, + calls, +) -> None: + """Test setting hs color with optimistic template.""" + state = hass.states.get("light.test_template_light") + assert state.attributes.get("hs_color") is None + + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.test_template_light", ATTR_HS_COLOR: (40, 50)}, + blocking=True, + ) + + assert len(calls) == 1 + assert calls[-1].data["action"] == "set_hs" + assert calls[-1].data["caller"] == "light.test_template_light" + assert calls[-1].data["h"] == 40 + assert calls[-1].data["s"] == 50 + + state = hass.states.get("light.test_template_light") + assert state.state == STATE_ON + assert state.attributes["color_mode"] == ColorMode.HS + assert state.attributes.get("hs_color") == (40, 50) + assert state.attributes["supported_color_modes"] == [ColorMode.HS] + assert state.attributes["supported_features"] == 0 + + +@pytest.mark.parametrize("count", [1]) +@pytest.mark.parametrize( + "light_config", + [ + { + "test_template_light": { + **OPTIMISTIC_RGB_COLOR_LIGHT_CONFIG, + "value_template": "{{1 == 1}}", + } + }, + ], +) +async def test_rgb_color_action_no_template( + hass: HomeAssistant, + setup_light, + calls, +) -> None: + """Test setting rgb color with optimistic template.""" + state = hass.states.get("light.test_template_light") + assert state.attributes.get("rgb_color") is None + + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.test_template_light", ATTR_RGB_COLOR: (160, 78, 192)}, + blocking=True, + ) + + assert len(calls) == 1 + assert calls[-1].data["action"] == "set_rgb" + assert calls[-1].data["caller"] == "light.test_template_light" + assert calls[-1].data["r"] == 160 + assert calls[-1].data["g"] == 78 + assert calls[-1].data["b"] == 192 + + state = hass.states.get("light.test_template_light") + assert state.state == STATE_ON + assert state.attributes["color_mode"] == ColorMode.RGB + assert state.attributes.get("rgb_color") == (160, 78, 192) + assert state.attributes["supported_color_modes"] == [ColorMode.RGB] + assert state.attributes["supported_features"] == 0 + + +@pytest.mark.parametrize("count", [1]) +@pytest.mark.parametrize( + "light_config", + [ + { + "test_template_light": { + **OPTIMISTIC_RGBW_COLOR_LIGHT_CONFIG, + "value_template": "{{1 == 1}}", + } + }, + ], +) +async def test_rgbw_color_action_no_template( + hass: HomeAssistant, + setup_light, + calls, +) -> None: + """Test setting rgbw color with optimistic template.""" + state = hass.states.get("light.test_template_light") + assert state.attributes.get("rgbw_color") is None + + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "light.test_template_light", + ATTR_RGBW_COLOR: (160, 78, 192, 25), + }, + blocking=True, + ) + + assert len(calls) == 1 + assert calls[-1].data["action"] == "set_rgbw" + assert calls[-1].data["caller"] == "light.test_template_light" + assert calls[-1].data["r"] == 160 + assert calls[-1].data["g"] == 78 + assert calls[-1].data["b"] == 192 + assert calls[-1].data["w"] == 25 + + state = hass.states.get("light.test_template_light") + assert state.state == STATE_ON + assert state.attributes["color_mode"] == ColorMode.RGBW + assert state.attributes.get("rgbw_color") == (160, 78, 192, 25) + assert state.attributes["supported_color_modes"] == [ColorMode.RGBW] + assert state.attributes["supported_features"] == 0 + + +@pytest.mark.parametrize("count", [1]) +@pytest.mark.parametrize( + "light_config", + [ + { + "test_template_light": { + **OPTIMISTIC_RGBWW_COLOR_LIGHT_CONFIG, + "value_template": "{{1 == 1}}", + } + }, + ], +) +async def test_rgbww_color_action_no_template( + hass: HomeAssistant, + setup_light, + calls, +) -> None: + """Test setting rgbww color with optimistic template.""" + state = hass.states.get("light.test_template_light") + assert state.attributes.get("rgbww_color") is None + + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "light.test_template_light", + ATTR_RGBWW_COLOR: (160, 78, 192, 25, 55), + }, + blocking=True, + ) + + assert len(calls) == 1 + assert calls[-1].data["action"] == "set_rgbww" + assert calls[-1].data["caller"] == "light.test_template_light" + assert calls[-1].data["r"] == 160 + assert calls[-1].data["g"] == 78 + assert calls[-1].data["b"] == 192 + assert calls[-1].data["cw"] == 25 + assert calls[-1].data["ww"] == 55 + + state = hass.states.get("light.test_template_light") + assert state.state == STATE_ON + assert state.attributes["color_mode"] == ColorMode.RGBWW + assert state.attributes.get("rgbww_color") == (160, 78, 192, 25, 55) + assert state.attributes["supported_color_modes"] == [ColorMode.RGBWW] + assert state.attributes["supported_features"] == 0 + + @pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( ("expected_hs", "color_template", "expected_color_mode"), @@ -845,19 +1092,20 @@ async def test_color_action_no_template( (None, "{{x - 12}}", ColorMode.HS), (None, "", ColorMode.HS), (None, "{{ none }}", ColorMode.HS), + (None, "{{('one','two')}}", ColorMode.HS), ], ) -async def test_color_template( - hass: HomeAssistant, +async def test_legacy_color_template( + hass, expected_hs, expected_color_mode, count, color_template, -) -> None: +): """Test the template for the color.""" light_config = { "test_template_light": { - **OPTIMISTIC_HS_COLOR_LIGHT_CONFIG, + **OPTIMISTIC_LEGACY_COLOR_LIGHT_CONFIG, "value_template": "{{ 1 == 1 }}", "color_template": color_template, } @@ -871,6 +1119,176 @@ async def test_color_template( assert state.attributes["supported_features"] == 0 +@pytest.mark.parametrize("count", [1]) +@pytest.mark.parametrize( + ("expected_hs", "hs_template", "expected_color_mode"), + [ + ((360, 100), "{{(360, 100)}}", ColorMode.HS), + ((360, 100), "(360, 100)", ColorMode.HS), + ((359.9, 99.9), "{{(359.9, 99.9)}}", ColorMode.HS), + (None, "{{(361, 100)}}", ColorMode.HS), + (None, "{{(360, 101)}}", ColorMode.HS), + (None, "[{{(360)}},{{null}}]", ColorMode.HS), + (None, "{{x - 12}}", ColorMode.HS), + (None, "", ColorMode.HS), + (None, "{{ none }}", ColorMode.HS), + (None, "{{('one','two')}}", ColorMode.HS), + ], +) +async def test_hs_template( + hass: HomeAssistant, + expected_hs, + expected_color_mode, + count, + hs_template, +) -> None: + """Test the template for the color.""" + light_config = { + "test_template_light": { + **OPTIMISTIC_HS_COLOR_LIGHT_CONFIG, + "value_template": "{{ 1 == 1 }}", + "hs_template": hs_template, + } + } + await async_setup_light(hass, count, light_config) + state = hass.states.get("light.test_template_light") + assert state.attributes.get("hs_color") == expected_hs + assert state.state == STATE_ON + assert state.attributes["color_mode"] == expected_color_mode + assert state.attributes["supported_color_modes"] == [ColorMode.HS] + assert state.attributes["supported_features"] == 0 + + +@pytest.mark.parametrize("count", [1]) +@pytest.mark.parametrize( + ("expected_rgb", "rgb_template", "expected_color_mode"), + [ + ((160, 78, 192), "{{(160, 78, 192)}}", ColorMode.RGB), + ((160, 78, 192), "{{[160, 78, 192]}}", ColorMode.RGB), + ((160, 78, 192), "(160, 78, 192)", ColorMode.RGB), + ((159, 77, 191), "{{(159.9, 77.9, 191.9)}}", ColorMode.RGB), + (None, "{{(256, 100, 100)}}", ColorMode.RGB), + (None, "{{(100, 256, 100)}}", ColorMode.RGB), + (None, "{{(100, 100, 256)}}", ColorMode.RGB), + (None, "{{x - 12}}", ColorMode.RGB), + (None, "", ColorMode.RGB), + (None, "{{ none }}", ColorMode.RGB), + (None, "{{('one','two','tree')}}", ColorMode.RGB), + ], +) +async def test_rgb_template( + hass: HomeAssistant, + expected_rgb, + expected_color_mode, + count, + rgb_template, +) -> None: + """Test the template for the color.""" + light_config = { + "test_template_light": { + **OPTIMISTIC_RGB_COLOR_LIGHT_CONFIG, + "value_template": "{{ 1 == 1 }}", + "rgb_template": rgb_template, + } + } + await async_setup_light(hass, count, light_config) + state = hass.states.get("light.test_template_light") + assert state.attributes.get("rgb_color") == expected_rgb + assert state.state == STATE_ON + assert state.attributes["color_mode"] == expected_color_mode + assert state.attributes["supported_color_modes"] == [ColorMode.RGB] + assert state.attributes["supported_features"] == 0 + + +@pytest.mark.parametrize("count", [1]) +@pytest.mark.parametrize( + ("expected_rgbw", "rgbw_template", "expected_color_mode"), + [ + ((160, 78, 192, 25), "{{(160, 78, 192, 25)}}", ColorMode.RGBW), + ((160, 78, 192, 25), "{{[160, 78, 192, 25]}}", ColorMode.RGBW), + ((160, 78, 192, 25), "(160, 78, 192, 25)", ColorMode.RGBW), + ((159, 77, 191, 24), "{{(159.9, 77.9, 191.9, 24.9)}}", ColorMode.RGBW), + (None, "{{(256, 100, 100, 100)}}", ColorMode.RGBW), + (None, "{{(100, 256, 100, 100)}}", ColorMode.RGBW), + (None, "{{(100, 100, 256, 100)}}", ColorMode.RGBW), + (None, "{{(100, 100, 100, 256)}}", ColorMode.RGBW), + (None, "{{x - 12}}", ColorMode.RGBW), + (None, "", ColorMode.RGBW), + (None, "{{ none }}", ColorMode.RGBW), + (None, "{{('one','two','tree','four')}}", ColorMode.RGBW), + ], +) +async def test_rgbw_template( + hass: HomeAssistant, + expected_rgbw, + expected_color_mode, + count, + rgbw_template, +) -> None: + """Test the template for the color.""" + light_config = { + "test_template_light": { + **OPTIMISTIC_RGBW_COLOR_LIGHT_CONFIG, + "value_template": "{{ 1 == 1 }}", + "rgbw_template": rgbw_template, + } + } + await async_setup_light(hass, count, light_config) + state = hass.states.get("light.test_template_light") + assert state.attributes.get("rgbw_color") == expected_rgbw + assert state.state == STATE_ON + assert state.attributes["color_mode"] == expected_color_mode + assert state.attributes["supported_color_modes"] == [ColorMode.RGBW] + assert state.attributes["supported_features"] == 0 + + +@pytest.mark.parametrize("count", [1]) +@pytest.mark.parametrize( + ("expected_rgbww", "rgbww_template", "expected_color_mode"), + [ + ((160, 78, 192, 25, 55), "{{(160, 78, 192, 25, 55)}}", ColorMode.RGBWW), + ((160, 78, 192, 25, 55), "(160, 78, 192, 25, 55)", ColorMode.RGBWW), + ((160, 78, 192, 25, 55), "{{[160, 78, 192, 25, 55]}}", ColorMode.RGBWW), + ( + (159, 77, 191, 24, 54), + "{{(159.9, 77.9, 191.9, 24.9, 54.9)}}", + ColorMode.RGBWW, + ), + (None, "{{(256, 100, 100, 100, 100)}}", ColorMode.RGBWW), + (None, "{{(100, 256, 100, 100, 100)}}", ColorMode.RGBWW), + (None, "{{(100, 100, 256, 100, 100)}}", ColorMode.RGBWW), + (None, "{{(100, 100, 100, 256, 100)}}", ColorMode.RGBWW), + (None, "{{(100, 100, 100, 100, 256)}}", ColorMode.RGBWW), + (None, "{{x - 12}}", ColorMode.RGBWW), + (None, "", ColorMode.RGBWW), + (None, "{{ none }}", ColorMode.RGBWW), + (None, "{{('one','two','tree','four','five')}}", ColorMode.RGBWW), + ], +) +async def test_rgbww_template( + hass: HomeAssistant, + expected_rgbww, + expected_color_mode, + count, + rgbww_template, +) -> None: + """Test the template for the color.""" + light_config = { + "test_template_light": { + **OPTIMISTIC_RGBWW_COLOR_LIGHT_CONFIG, + "value_template": "{{ 1 == 1 }}", + "rgbww_template": rgbww_template, + } + } + await async_setup_light(hass, count, light_config) + state = hass.states.get("light.test_template_light") + assert state.attributes.get("rgbww_color") == expected_rgbww + assert state.state == STATE_ON + assert state.attributes["color_mode"] == expected_color_mode + assert state.attributes["supported_color_modes"] == [ColorMode.RGBWW] + assert state.attributes["supported_features"] == 0 + + @pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( "light_config", @@ -879,16 +1297,14 @@ async def test_color_template( "test_template_light": { **OPTIMISTIC_ON_OFF_LIGHT_CONFIG, "value_template": "{{1 == 1}}", - "set_color": [ - { - "service": "test.automation", - "data_template": { - "entity_id": "test.test_state", - "h": "{{h}}", - "s": "{{s}}", - }, + "set_hs": { + "service": "test.automation", + "data_template": { + "entity_id": "test.test_state", + "h": "{{h}}", + "s": "{{s}}", }, - ], + }, "set_temperature": { "service": "test.automation", "data_template": { @@ -896,18 +1312,48 @@ async def test_color_template( "color_temp": "{{color_temp}}", }, }, + "set_rgb": { + "service": "test.automation", + "data_template": { + "entity_id": "test.test_state", + "r": "{{r}}", + "g": "{{g}}", + "b": "{{b}}", + }, + }, + "set_rgbw": { + "service": "test.automation", + "data_template": { + "entity_id": "test.test_state", + "r": "{{r}}", + "g": "{{g}}", + "b": "{{b}}", + "w": "{{w}}", + }, + }, + "set_rgbww": { + "service": "test.automation", + "data_template": { + "entity_id": "test.test_state", + "r": "{{r}}", + "g": "{{g}}", + "b": "{{b}}", + "cw": "{{cw}}", + "ww": "{{ww}}", + }, + }, } }, ], ) -async def test_color_and_temperature_actions_no_template( +async def test_all_colors_mode_no_template( hass: HomeAssistant, setup_light, calls ) -> None: """Test setting color and color temperature with optimistic template.""" state = hass.states.get("light.test_template_light") assert state.attributes.get("hs_color") is None - # Optimistically set color, light should be in hs_color mode + # Optimistically set hs color, light should be in hs_color mode await hass.services.async_call( light.DOMAIN, SERVICE_TURN_ON, @@ -926,6 +1372,9 @@ async def test_color_and_temperature_actions_no_template( assert state.attributes["supported_color_modes"] == [ ColorMode.COLOR_TEMP, ColorMode.HS, + ColorMode.RGB, + ColorMode.RGBW, + ColorMode.RGBWW, ] assert state.attributes["supported_features"] == 0 @@ -947,10 +1396,100 @@ async def test_color_and_temperature_actions_no_template( assert state.attributes["supported_color_modes"] == [ ColorMode.COLOR_TEMP, ColorMode.HS, + ColorMode.RGB, + ColorMode.RGBW, + ColorMode.RGBWW, ] assert state.attributes["supported_features"] == 0 - # Optimistically set color, light should again be in hs_color mode + # Optimistically set rgb color, light should be in rgb_color mode + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.test_template_light", ATTR_RGB_COLOR: (160, 78, 192)}, + blocking=True, + ) + + assert len(calls) == 3 + assert calls[-1].data["r"] == 160 + assert calls[-1].data["g"] == 78 + assert calls[-1].data["b"] == 192 + + state = hass.states.get("light.test_template_light") + assert state.attributes["color_mode"] == ColorMode.RGB + assert state.attributes["color_temp"] is None + assert state.attributes["rgb_color"] == (160, 78, 192) + assert state.attributes["supported_color_modes"] == [ + ColorMode.COLOR_TEMP, + ColorMode.HS, + ColorMode.RGB, + ColorMode.RGBW, + ColorMode.RGBWW, + ] + assert state.attributes["supported_features"] == 0 + + # Optimistically set rgbw color, light should be in rgb_color mode + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "light.test_template_light", + ATTR_RGBW_COLOR: (160, 78, 192, 25), + }, + blocking=True, + ) + + assert len(calls) == 4 + assert calls[-1].data["r"] == 160 + assert calls[-1].data["g"] == 78 + assert calls[-1].data["b"] == 192 + assert calls[-1].data["w"] == 25 + + state = hass.states.get("light.test_template_light") + assert state.attributes["color_mode"] == ColorMode.RGBW + assert state.attributes["color_temp"] is None + assert state.attributes["rgbw_color"] == (160, 78, 192, 25) + assert state.attributes["supported_color_modes"] == [ + ColorMode.COLOR_TEMP, + ColorMode.HS, + ColorMode.RGB, + ColorMode.RGBW, + ColorMode.RGBWW, + ] + assert state.attributes["supported_features"] == 0 + + # Optimistically set rgbww color, light should be in rgb_color mode + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "light.test_template_light", + ATTR_RGBWW_COLOR: (160, 78, 192, 25, 55), + }, + blocking=True, + ) + + assert len(calls) == 5 + assert calls[-1].data["r"] == 160 + assert calls[-1].data["g"] == 78 + assert calls[-1].data["b"] == 192 + assert calls[-1].data["cw"] == 25 + assert calls[-1].data["ww"] == 55 + + state = hass.states.get("light.test_template_light") + assert state.attributes["color_mode"] == ColorMode.RGBWW + assert state.attributes["color_temp"] is None + assert state.attributes["rgbww_color"] == (160, 78, 192, 25, 55) + assert state.attributes["supported_color_modes"] == [ + ColorMode.COLOR_TEMP, + ColorMode.HS, + ColorMode.RGB, + ColorMode.RGBW, + ColorMode.RGBWW, + ] + assert state.attributes["supported_features"] == 0 + + # Optimistically set hs color, light should again be in hs_color mode await hass.services.async_call( light.DOMAIN, SERVICE_TURN_ON, @@ -958,7 +1497,7 @@ async def test_color_and_temperature_actions_no_template( blocking=True, ) - assert len(calls) == 3 + assert len(calls) == 6 assert calls[-1].data["h"] == 10 assert calls[-1].data["s"] == 20 @@ -969,6 +1508,9 @@ async def test_color_and_temperature_actions_no_template( assert state.attributes["supported_color_modes"] == [ ColorMode.COLOR_TEMP, ColorMode.HS, + ColorMode.RGB, + ColorMode.RGBW, + ColorMode.RGBWW, ] assert state.attributes["supported_features"] == 0 @@ -980,7 +1522,7 @@ async def test_color_and_temperature_actions_no_template( blocking=True, ) - assert len(calls) == 4 + assert len(calls) == 7 assert calls[-1].data["color_temp"] == 234 state = hass.states.get("light.test_template_light") @@ -990,6 +1532,9 @@ async def test_color_and_temperature_actions_no_template( assert state.attributes["supported_color_modes"] == [ ColorMode.COLOR_TEMP, ColorMode.HS, + ColorMode.RGB, + ColorMode.RGBW, + ColorMode.RGBWW, ] assert state.attributes["supported_features"] == 0