Add RGB, RGBW and RGBWW capability to template.light (#86047)

* Add RGB, RGBW and RGBWW capability to template.light

Add the required unit test

Mute 'LightTemplate.async_turn_on' is too complex

Rename all HS color mode from a generic "Color" name to a specific "HS" name

* Bring back legacy "color" keyword

* Cleanup unrequested commented test

* Increase code coverage to 100%

* Remove confusing if that should never be false

* Apply suggestions from code review

---------

Co-authored-by: Erik Montnemery <erik@montnemery.com>
pull/104349/head
Vaarlion 2023-11-22 08:15:26 +01:00 committed by GitHub
parent edf18df0e6
commit 3929b0163c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 952 additions and 50 deletions

View File

@ -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):

View File

@ -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