Fix LIFX effect action defaults and clamp values above zero (#182923)

Signed-off-by: Avi Miller <me@dje.li>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Avi Miller
2026-09-24 21:48:03 +02:00
committed by GitHub
co-authored by Claude Opus 5
parent e8bab8c41f
commit c52809cec7
3 changed files with 75 additions and 5 deletions
+10 -4
View File
@@ -115,8 +115,13 @@ LIFX_EFFECT_SCHEMA = {
LIFX_EFFECT_PULSE_SCHEMA = cv.make_entity_service_schema(
{
**LIFX_EFFECT_SCHEMA,
probatio.Exclusive(ATTR_BRIGHTNESS, ATTR_BRIGHTNESS): VALID_BRIGHTNESS,
probatio.Exclusive(ATTR_BRIGHTNESS_PCT, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_PCT,
# A brightness of zero would pulse to black
probatio.Exclusive(ATTR_BRIGHTNESS, ATTR_BRIGHTNESS): probatio.All(
VALID_BRIGHTNESS, probatio.Clamp(min=1)
),
probatio.Exclusive(ATTR_BRIGHTNESS_PCT, ATTR_BRIGHTNESS): probatio.All(
VALID_BRIGHTNESS_PCT, probatio.Clamp(min=1)
),
probatio.Exclusive(ATTR_COLOR_NAME, COLOR_GROUP): cv.string,
probatio.Exclusive(ATTR_RGB_COLOR, COLOR_GROUP): probatio.All(
probatio.Coerce(tuple), probatio.ExactSequence((cv.byte, cv.byte, cv.byte))
@@ -152,11 +157,12 @@ LIFX_EFFECT_COLORLOOP_SCHEMA = cv.make_entity_service_schema(
**LIFX_EFFECT_SCHEMA,
probatio.Exclusive(ATTR_BRIGHTNESS, ATTR_BRIGHTNESS): VALID_BRIGHTNESS,
probatio.Exclusive(ATTR_BRIGHTNESS_PCT, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_PCT,
# A saturation of zero switches the bulb to color temperature mode
ATTR_SATURATION_MAX: probatio.All(
probatio.Coerce(int), probatio.Clamp(min=0, max=100)
probatio.Coerce(int), probatio.Clamp(min=1, max=100)
),
ATTR_SATURATION_MIN: probatio.All(
probatio.Coerce(int), probatio.Clamp(min=0, max=100)
probatio.Coerce(int), probatio.Clamp(min=1, max=100)
),
ATTR_PERIOD: probatio.All(probatio.Coerce(float), probatio.Clamp(min=0.05)),
ATTR_CHANGE: probatio.All(
@@ -186,7 +186,6 @@ effect_move:
- left
theme:
example: exciting
default: exciting
selector:
select:
mode: dropdown
+65
View File
@@ -341,6 +341,33 @@ async def test_effect_pulse_reads_color_before_merging(
assert effect.color == HSBK(120.0, 1.0, 128 / 255, 3500)
@pytest.mark.parametrize(
("brightness", "expected"),
[
pytest.param({ATTR_BRIGHTNESS: 0}, 1 / 255, id="brightness"),
pytest.param({ATTR_BRIGHTNESS_PCT: 0}, 0.01, id="brightness_pct"),
],
)
async def test_effect_pulse_clamps_brightness_above_zero(
hass: HomeAssistant,
mock_effect_conductor: MagicMock,
brightness: dict[str, int],
expected: float,
) -> None:
"""Test a zero brightness is raised, as it would pulse to black."""
await async_setup_lifx_entry(hass, create_mock_light())
await hass.services.async_call(
DOMAIN,
SERVICE_EFFECT_PULSE,
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_HS_COLOR: (120.0, 50.0), **brightness},
blocking=True,
)
effect, _ = mock_effect_conductor.start.await_args.args
assert effect.color == HSBK(120.0, 0.5, expected, 3500)
async def test_effect_colorloop_uses_public_effect_defaults(
hass: HomeAssistant, mock_effect_conductor: MagicMock
) -> None:
@@ -390,6 +417,24 @@ async def test_effect_colorloop_orders_saturation_bounds(
assert effect.saturation_max == 0.8
async def test_effect_colorloop_clamps_saturation_above_zero(
hass: HomeAssistant, mock_effect_conductor: MagicMock
) -> None:
"""Test a zero saturation is raised, as it switches the bulb to white."""
await async_setup_lifx_entry(hass, create_mock_light())
await hass.services.async_call(
DOMAIN,
SERVICE_EFFECT_COLORLOOP,
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_SATURATION_MIN: 0, ATTR_SATURATION_MAX: 0},
blocking=True,
)
effect = mock_effect_conductor.start.await_args.args[0]
assert effect.saturation_min == 0.01
assert effect.saturation_max == 0.01
async def test_effect_colorloop_accepts_absolute_brightness(
hass: HomeAssistant, mock_effect_conductor: MagicMock
) -> None:
@@ -1670,6 +1715,26 @@ def test_theme_selector_options_match_the_library(service: str) -> None:
assert options == sorted(ThemeLibrary.get_available_themes())
@pytest.mark.parametrize(
("service", "default"),
[
pytest.param(SERVICE_EFFECT_MOVE, None, id="move"),
pytest.param(SERVICE_PAINT_THEME, "exciting", id="paint_theme"),
],
)
def test_theme_field_default_matches_the_action(
service: str, default: str | None
) -> None:
"""Test the UI pre-fills the theme the action applies when none is given.
Move applies no theme without one, so a default would suggest a color
change the action does not otherwise make.
"""
services = load_yaml_dict(f"{lifx.__path__[0]}/services.yaml")
assert services[service]["fields"][ATTR_THEME].get("default") == default
@pytest.mark.parametrize(
("service", "factory", "palette"),
[