[light] Add color option to strobe and other color state (#19874)

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
Clyde Stubbs
2026-10-01 19:02:58 +00:00
committed by GitHub
co-authored by Claude Sonnet 5 J. Nick Koston
parent 0ed997da92
commit bfb7e76b2e
6 changed files with 222 additions and 47 deletions
@@ -0,0 +1,22 @@
esphome:
name: test-color-wipe-color
esp32:
board: esp32dev
framework:
type: arduino
light:
- platform: fastled_clockless
id: wipe_light
chipset: WS2811
pin: GPIO13
num_leds: 10
effects:
- addressable_color_wipe:
name: Wipe
colors:
- color: darkred
num_leds: 1
- red: 50%
num_leds: 1
@@ -0,0 +1,40 @@
"""Regression test: addressable_color_wipe's `color:` must scale by color_brightness.
The per-item schema tests in test_effect_color.py only check the validated config
dict; they would still pass if addressable_color_wipe_effect_to_code dropped
color_brightness on the floor (as it originally did), since a dark color's red/green/
blue are already peak-normalized to 1.0 by the time codegen sees them. Generate the
actual C++ and check the AddressableColorWipeEffectColor byte values instead, so a
regression in the scaling multiplication itself is caught.
"""
from __future__ import annotations
from collections.abc import Callable
from pathlib import Path
import pytest
@pytest.fixture
def main_cpp(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> str:
return generate_main(
component_config_path("addressable_color_wipe_color_test.yaml")
)
def test_dark_color_name_is_scaled_by_color_brightness(main_cpp: str) -> None:
# color: darkred (0x8B0000) normalizes to r=1.0 but color_brightness=0x8B/0xFF,
# so the byte value must come back down to 0x8B (139), not 255.
assert ".r = 139," in main_cpp
assert ".g = 0," in main_cpp
assert ".b = 0," in main_cpp
def test_explicit_channel_is_unaffected(main_cpp: str) -> None:
# red: 50% with no color: or color_brightness: set must still scale by the
# default color_brightness of 1.0, i.e. come out as plain 50% of 255.
assert ".r = 128," in main_cpp
@@ -0,0 +1,69 @@
"""Tests for the `color` option in per-item effect color lists."""
from __future__ import annotations
import pytest
from esphome import config_validation as cv
from esphome.components.light.effects import (
ADDRESSABLE_COLOR_WIPE_COLOR_SCHEMA,
STROBE_COLOR_SCHEMA,
)
from esphome.const import CONF_BLUE, CONF_COLOR_BRIGHTNESS, CONF_GREEN, CONF_RED
def test_strobe_plain_defaults_full_level() -> None:
result = STROBE_COLOR_SCHEMA({"duration": "1s"})
assert (result[CONF_RED], result[CONF_GREEN], result[CONF_BLUE]) == (1.0, 1.0, 1.0)
assert result[CONF_COLOR_BRIGHTNESS] == 1.0
def test_strobe_color_name() -> None:
result = STROBE_COLOR_SCHEMA({"duration": "1s", "color": "darkred"})
assert "color" not in result
assert (result[CONF_RED], result[CONF_GREEN], result[CONF_BLUE]) == (1.0, 0.0, 0.0)
assert result[CONF_COLOR_BRIGHTNESS] == pytest.approx(0x8B / 0xFF)
def test_strobe_hex_color() -> None:
result = STROBE_COLOR_SCHEMA({"duration": "1s", "color": "0x223344"})
assert result[CONF_BLUE] == 1.0
assert result[CONF_COLOR_BRIGHTNESS] == pytest.approx(0x44 / 0xFF)
def test_strobe_explicit_channels_unaffected() -> None:
result = STROBE_COLOR_SCHEMA({"duration": "1s", "red": "50%"})
assert result[CONF_RED] == 0.5
assert (result[CONF_GREEN], result[CONF_BLUE]) == (1.0, 1.0)
assert result[CONF_COLOR_BRIGHTNESS] == 1.0
def test_strobe_color_conflicts_with_rgb() -> None:
with pytest.raises(cv.Invalid, match="cannot be used with"):
STROBE_COLOR_SCHEMA({"duration": "1s", "color": "red", "red": "10%"})
def test_addressable_color_wipe_plain_defaults_full_level() -> None:
result = ADDRESSABLE_COLOR_WIPE_COLOR_SCHEMA({"num_leds": 1})
assert (result[CONF_RED], result[CONF_GREEN], result[CONF_BLUE]) == (1.0, 1.0, 1.0)
assert result[CONF_COLOR_BRIGHTNESS] == 1.0
def test_addressable_color_wipe_color_name() -> None:
result = ADDRESSABLE_COLOR_WIPE_COLOR_SCHEMA({"num_leds": 1, "color": "darkred"})
assert "color" not in result
assert (result[CONF_RED], result[CONF_GREEN], result[CONF_BLUE]) == (1.0, 0.0, 0.0)
assert result[CONF_COLOR_BRIGHTNESS] == pytest.approx(0x8B / 0xFF)
def test_addressable_color_wipe_explicit_channels_unaffected() -> None:
result = ADDRESSABLE_COLOR_WIPE_COLOR_SCHEMA({"num_leds": 1, "red": "50%"})
assert result[CONF_RED] == 0.5
assert result[CONF_COLOR_BRIGHTNESS] == 1.0
def test_addressable_color_wipe_color_conflicts_with_rgb() -> None:
with pytest.raises(cv.Invalid, match="cannot be used with"):
ADDRESSABLE_COLOR_WIPE_COLOR_SCHEMA(
{"num_leds": 1, "color": "blue", "blue": "10%"}
)