mirror of
https://github.com/basnijholt/adaptive-lighting.git
synced 2026-09-13 07:14:04 +02:00
Use the sleep RGB colour as the floor whenever it is configured
A switch with `sleep_rgb_or_color_temp: rgb_color` expresses its sleep colour as RGB. `_apply_intensity` only walked RGB towards `sleep_rgb_color` on the `adapt_until_sleep` path; everywhere else it re-derived RGB from the interpolated colour temperature. Intensity 0 therefore produced `color_temperature_to_rgb(sleep_color_temp)` rather than the configured `sleep_rgb_color`, breaking the guarantee that 0 reproduces sleep mode. The condition is now the switch's own configuration -- the sleep floor being in force, and the sleep colour being expressed as RGB -- which subsumes the `adapt_until_sleep` case and makes the `keep_rgb` parameter redundant. The `minimum` floor still derives RGB from the colour temperature, because its anchor is `min_color_temp`, a Kelvin, and the sleep colour is not the floor there. The existing test asserted the RGB match only when `force_rgb_color` was set, so it stepped over this. Replaced with a test that asserts what was actually promised, under both values of `adapt_until_sleep`, plus one pinning the `minimum` floor's Kelvin-derived behaviour. Reported by greptile-apps on #1593. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
3b1b84e7e8
commit
51e0b11860
2 changed files with 40 additions and 36 deletions
|
|
@ -433,7 +433,6 @@ class SunLightSettings:
|
|||
rgb_color: tuple[int, int, int],
|
||||
*,
|
||||
is_sleep: bool,
|
||||
keep_rgb: bool,
|
||||
) -> tuple[float | None, int, tuple[int, int, int]]:
|
||||
"""Scale the adaptive result towards this switch's floor settings.
|
||||
|
||||
|
|
@ -473,12 +472,15 @@ class SunLightSettings:
|
|||
)
|
||||
color_temp_kelvin = 5 * round(color_temp_kelvin / 5) # round to nearest 5
|
||||
|
||||
if keep_rgb:
|
||||
# This switch drives colour as RGB after sunset, so walk the RGB value
|
||||
# towards the configured sleep colour rather than re-deriving it from
|
||||
# the (unused) colour temperature. `keep_rgb` is only ever set when
|
||||
# `adapt_until_sleep` is on, which forces the sleep floor, so the
|
||||
# sleep colour is always the right target here.
|
||||
if (
|
||||
self.intensity_floor_is_sleep
|
||||
and self.sleep_rgb_or_color_temp == "rgb_color"
|
||||
):
|
||||
# This switch expresses its sleep colour as RGB, so walk the RGB value
|
||||
# towards `sleep_rgb_color` rather than re-deriving it from the
|
||||
# interpolated colour temperature. Deriving it would land 0% on
|
||||
# `color_temperature_to_rgb(sleep_color_temp)`, which is not the
|
||||
# colour sleep mode actually uses.
|
||||
rgb_color = lerp_color_hsv(self.sleep_rgb_color, rgb_color, factor)
|
||||
else:
|
||||
r, g, b = color_temperature_to_rgb(color_temp_kelvin)
|
||||
|
|
@ -526,7 +528,6 @@ class SunLightSettings:
|
|||
color_temp_kelvin,
|
||||
rgb_color,
|
||||
is_sleep=is_sleep,
|
||||
keep_rgb=force_rgb_color,
|
||||
)
|
||||
|
||||
# backwards compatibility for versions < 1.3.1 - see #403
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from astral.location import Location
|
|||
from homeassistant.components.adaptive_lighting.color_and_brightness import (
|
||||
SunLightSettings,
|
||||
)
|
||||
from homeassistant.util.color import color_temperature_to_rgb
|
||||
|
||||
TZINFO = zoneinfo.ZoneInfo("Europe/Amsterdam")
|
||||
LOCATION = Location(
|
||||
|
|
@ -95,10 +96,7 @@ def test_apply_intensity_short_circuits_at_full(intensity_floor):
|
|||
"""At 100 the interpolation is skipped outright, not merely a no-op."""
|
||||
settings = make_settings(intensity=100, intensity_floor=intensity_floor)
|
||||
arguments = (50.0, 3000, (255, 180, 100))
|
||||
assert (
|
||||
settings._apply_intensity(*arguments, is_sleep=False, keep_rgb=False)
|
||||
== arguments
|
||||
)
|
||||
assert settings._apply_intensity(*arguments, is_sleep=False) == arguments
|
||||
|
||||
|
||||
@pytest.mark.parametrize("intensity", [0, 25, 50, 75, 100])
|
||||
|
|
@ -213,36 +211,41 @@ def test_adapt_until_sleep_overrides_the_floor(datetime, intensity):
|
|||
assert results[0] == results[1]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("intensity_floor", ["sleep", "minimum"])
|
||||
@pytest.mark.parametrize("adapt_until_sleep", [True, False])
|
||||
@pytest.mark.parametrize("datetime", TIMES)
|
||||
def test_rgb_path_always_has_the_sleep_floor(
|
||||
datetime,
|
||||
adapt_until_sleep,
|
||||
intensity_floor,
|
||||
):
|
||||
"""`_apply_intensity` walks RGB towards `sleep_rgb_color` unconditionally.
|
||||
def test_zero_intensity_matches_sleep_rgb(datetime, adapt_until_sleep):
|
||||
"""0 must reproduce sleep mode's RGB, not a colour derived from Kelvin.
|
||||
|
||||
That is only sound because the RGB path implies `adapt_until_sleep`, which
|
||||
forces the sleep floor.
|
||||
A switch using `sleep_rgb_or_color_temp: rgb_color` expresses its sleep
|
||||
colour as RGB. Interpolating the colour temperature and re-deriving RGB from
|
||||
it lands 0% on `color_temperature_to_rgb(sleep_color_temp)`, which is a
|
||||
different colour from the configured `sleep_rgb_color`. This held only on
|
||||
the `adapt_until_sleep` path before.
|
||||
"""
|
||||
kwargs = {
|
||||
"sleep_rgb_or_color_temp": "rgb_color",
|
||||
"adapt_until_sleep": adapt_until_sleep,
|
||||
}
|
||||
asleep = make_settings(**kwargs).brightness_and_color(datetime, is_sleep=True)
|
||||
dialled = make_settings(intensity=0, **kwargs).brightness_and_color(
|
||||
datetime,
|
||||
is_sleep=False,
|
||||
)
|
||||
assert dialled["rgb_color"] == asleep["rgb_color"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("datetime", TIMES)
|
||||
def test_minimum_floor_derives_rgb_from_color_temp(datetime):
|
||||
"""With the `minimum` floor the anchor is a Kelvin, so RGB follows it.
|
||||
|
||||
The sleep RGB colour is deliberately NOT used here -- it is not the floor.
|
||||
"""
|
||||
settings = make_settings(
|
||||
intensity=50,
|
||||
intensity_floor=intensity_floor,
|
||||
adapt_until_sleep=adapt_until_sleep,
|
||||
intensity=0,
|
||||
intensity_floor="minimum",
|
||||
adapt_until_sleep=False,
|
||||
sleep_rgb_or_color_temp="rgb_color",
|
||||
)
|
||||
if settings.brightness_and_color(datetime, is_sleep=False)["force_rgb_color"]:
|
||||
assert settings.intensity_floor_is_sleep
|
||||
|
||||
|
||||
def test_rgb_path_is_covered():
|
||||
"""Guard against `test_rgb_path_always_has_the_sleep_floor` going vacuous."""
|
||||
settings = make_settings(
|
||||
adapt_until_sleep=True,
|
||||
sleep_rgb_or_color_temp="rgb_color",
|
||||
)
|
||||
assert any(
|
||||
settings.brightness_and_color(datetime, is_sleep=False)["force_rgb_color"]
|
||||
for datetime in TIMES
|
||||
)
|
||||
result = settings.brightness_and_color(datetime, is_sleep=False)
|
||||
expected = color_temperature_to_rgb(result["color_temp_kelvin"])
|
||||
assert result["rgb_color"] == tuple(round(c) for c in expected)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue