mirror of
https://github.com/basnijholt/adaptive-lighting.git
synced 2026-10-29 22:47:46 +01:00
feat: add transition_until_sleep_ct_curve to calibrate CT-only lights
Deriving the CCT from the RGB blend is colorimetrically correct, but a color-temperature-only bulb cannot reproduce the saturation of the blended orange, and real bulbs render nominal kelvins differently. In practice CT-only lights can still look colder than the RGB lights next to them. Add an optional `transition_until_sleep_ct_curve` (default 1.0 = follow the CCT exactly) that reshapes the progress towards `sleep_color_temp` in mired space: `>1` reaches warm tones earlier, `<1` later. The endpoints (min_color_temp at sunset, sleep_color_temp at sleep) are unchanged, so there is still no jump at sunset. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
e3cbb4c0b3
commit
ba441747f7
9 changed files with 106 additions and 9 deletions
|
|
@ -145,6 +145,7 @@ The YAML and frontend configuration methods support all of the options listed be
|
|||
| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color |
|
||||
| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `float` 0-6553 |
|
||||
| `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` |
|
||||
| `transition_until_sleep_ct_curve` | Only used with `transition_until_sleep` and `sleep_rgb_or_color_temp: rgb_color`. Shapes how fast color-temperature-only lights warm up towards `sleep_color_temp` compared to the CCT of the RGB blend: `1` follows the CCT, `>1` warms up earlier (use if your color-temp lights look colder than your RGB lights in the evening), `<1` later. 🌡️ | `1.0` | `float` |
|
||||
| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` |
|
||||
| `min_sunrise_time` | Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅 | `None` | `str` |
|
||||
| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅 | `None` | `str` |
|
||||
|
|
|
|||
|
|
@ -297,6 +297,7 @@ class SunLightSettings:
|
|||
brightness_mode_time_dark: datetime.timedelta
|
||||
brightness_mode_time_light: datetime.timedelta
|
||||
brightness_mode: Literal["default", "linear", "tanh"] = "default"
|
||||
adapt_until_sleep_ct_curve: float = 1.0
|
||||
sunrise_offset: datetime.timedelta = datetime.timedelta()
|
||||
sunset_offset: datetime.timedelta = datetime.timedelta()
|
||||
timezone: datetime.tzinfo = UTC
|
||||
|
|
@ -451,10 +452,24 @@ class SunLightSettings:
|
|||
# the RGB blend: derive the CCT from the blended color instead of
|
||||
# interpolating in kelvin space, which diverges perceptually from
|
||||
# the HSV interpolation used for `rgb_color` (see #624).
|
||||
cct = color_xy_to_cct(*xy_color)
|
||||
color_temp_kelvin = 5 * round(
|
||||
clamp(cct, self.sleep_color_temp, self.min_color_temp) / 5,
|
||||
cct = clamp(
|
||||
color_xy_to_cct(*xy_color),
|
||||
self.sleep_color_temp,
|
||||
self.min_color_temp,
|
||||
)
|
||||
if self.adapt_until_sleep_ct_curve != 1:
|
||||
# Optional calibration: reshape the progress towards
|
||||
# `sleep_color_temp` in mired space (perceptually uniform), so
|
||||
# that `>1` reaches warm tones earlier and `<1` later.
|
||||
mired_min = 1e6 / self.min_color_temp
|
||||
mired_sleep = 1e6 / self.sleep_color_temp
|
||||
if mired_sleep != mired_min:
|
||||
progress = (1e6 / cct - mired_min) / (mired_sleep - mired_min)
|
||||
progress = clamp(progress, 0, 1) ** (
|
||||
1 / self.adapt_until_sleep_ct_curve
|
||||
)
|
||||
cct = 1e6 / (mired_min + progress * (mired_sleep - mired_min))
|
||||
color_temp_kelvin = 5 * round(cct / 5)
|
||||
# backwards compatibility for versions < 1.3.1 - see #403
|
||||
color_temp_mired: float = math.floor(1000000 / color_temp_kelvin)
|
||||
hs_color: tuple[float, float] = color_xy_to_hs(*xy_color)
|
||||
|
|
@ -604,8 +619,7 @@ def lerp_color_hsv(
|
|||
|
||||
|
||||
def color_xy_to_cct(x: float, y: float) -> float:
|
||||
"""Return the correlated color temperature (CCT) in Kelvin for a CIE 1931
|
||||
xy chromaticity.
|
||||
"""Return the correlated color temperature (CCT) in Kelvin of a CIE 1931 xy.
|
||||
|
||||
The CCT is found as the color temperature whose RGB representation (via
|
||||
`color_temperature_to_rgb`, i.e. the same conversion this component uses
|
||||
|
|
|
|||
|
|
@ -58,8 +58,7 @@ DOCS[CONF_INCLUDE_CONFIG_IN_ATTRIBUTES] = (
|
|||
|
||||
CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION = "initial_transition", 1
|
||||
DOCS[CONF_INITIAL_TRANSITION] = (
|
||||
"Duration of the first transition when lights turn "
|
||||
"from `off` to `on` in seconds. ⏲️"
|
||||
"Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️"
|
||||
)
|
||||
|
||||
CONF_SLEEP_TRANSITION, DEFAULT_SLEEP_TRANSITION = "sleep_transition", 1
|
||||
|
|
@ -162,8 +161,7 @@ DOCS[CONF_MIN_SUNRISE_TIME] = (
|
|||
|
||||
CONF_MAX_SUNRISE_TIME = "max_sunrise_time"
|
||||
DOCS[CONF_MAX_SUNRISE_TIME] = (
|
||||
"Set the latest virtual sunrise time (HH:MM:SS), allowing"
|
||||
" for earlier sunrises. 🌅"
|
||||
"Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅"
|
||||
)
|
||||
|
||||
CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET = "sunset_offset", 0
|
||||
|
|
@ -235,6 +233,18 @@ DOCS[CONF_ADAPT_UNTIL_SLEEP] = (
|
|||
"transitioning to these values after sunset. 🌙"
|
||||
)
|
||||
|
||||
CONF_ADAPT_UNTIL_SLEEP_CT_CURVE, DEFAULT_ADAPT_UNTIL_SLEEP_CT_CURVE = (
|
||||
"transition_until_sleep_ct_curve",
|
||||
1.0,
|
||||
)
|
||||
DOCS[CONF_ADAPT_UNTIL_SLEEP_CT_CURVE] = (
|
||||
"Only used with `transition_until_sleep` and `sleep_rgb_or_color_temp: "
|
||||
"rgb_color`. Shapes how fast color-temperature-only lights warm up towards "
|
||||
"`sleep_color_temp` compared to the CCT of the RGB blend: `1` follows the "
|
||||
"CCT, `>1` warms up earlier (use if your color-temp lights look colder than "
|
||||
"your RGB lights in the evening), `<1` later. 🌡️"
|
||||
)
|
||||
|
||||
CONF_ADAPT_DELAY, DEFAULT_ADAPT_DELAY = "adapt_delay", 0
|
||||
DOCS[CONF_ADAPT_DELAY] = (
|
||||
"Wait time (seconds) between light turn on and Adaptive Lighting applying "
|
||||
|
|
@ -390,6 +400,11 @@ VALIDATION_TUPLES: list[tuple[str, Any, Any]] = [
|
|||
),
|
||||
(CONF_SLEEP_TRANSITION, DEFAULT_SLEEP_TRANSITION, VALID_TRANSITION),
|
||||
(CONF_ADAPT_UNTIL_SLEEP, DEFAULT_ADAPT_UNTIL_SLEEP, bool),
|
||||
(
|
||||
CONF_ADAPT_UNTIL_SLEEP_CT_CURVE,
|
||||
DEFAULT_ADAPT_UNTIL_SLEEP_CT_CURVE,
|
||||
vol.All(vol.Coerce(float), vol.Range(min=0.1, max=10)),
|
||||
),
|
||||
(CONF_SUNRISE_TIME, NONE_STR, str),
|
||||
(CONF_MIN_SUNRISE_TIME, NONE_STR, str),
|
||||
(CONF_MAX_SUNRISE_TIME, NONE_STR, str),
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@
|
|||
"sleep_rgb_color": "sleep_rgb_color",
|
||||
"sleep_transition": "sleep_transition",
|
||||
"transition_until_sleep": "transition_until_sleep: When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙",
|
||||
"transition_until_sleep_ct_curve": "transition_until_sleep_ct_curve",
|
||||
"sunrise_time": "sunrise_time",
|
||||
"min_sunrise_time": "min_sunrise_time",
|
||||
"max_sunrise_time": "max_sunrise_time",
|
||||
|
|
@ -86,6 +87,7 @@
|
|||
"sleep_rgb_or_color_temp": "Use either `\"rgb_color\"` or `\"color_temp\"` in sleep mode. 🌙",
|
||||
"sleep_rgb_color": "RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is \"rgb_color\"). 🌈",
|
||||
"sleep_transition": "Duration of transition when \"sleep mode\" is toggled in seconds. 😴",
|
||||
"transition_until_sleep_ct_curve": "Only used with `transition_until_sleep` and `sleep_rgb_or_color_temp: rgb_color`. Shapes how fast color-temperature-only lights warm up towards `sleep_color_temp` compared to the CCT of the RGB blend: `1` follows the CCT, `>1` warms up earlier (use if your color-temp lights look colder than your RGB lights in the evening), `<1` later. 🌡️",
|
||||
"sunrise_time": "Set a fixed time (HH:MM:SS) for sunrise. 🌅",
|
||||
"min_sunrise_time": "Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅",
|
||||
"max_sunrise_time": "Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅",
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@ from .const import (
|
|||
CONF_ADAPT_DELAY,
|
||||
CONF_ADAPT_ONLY_ON_BARE_TURN_ON,
|
||||
CONF_ADAPT_UNTIL_SLEEP,
|
||||
CONF_ADAPT_UNTIL_SLEEP_CT_CURVE,
|
||||
CONF_AUTORESET_CONTROL,
|
||||
CONF_BRIGHTNESS_MODE,
|
||||
CONF_BRIGHTNESS_MODE_TIME_DARK,
|
||||
|
|
@ -1018,6 +1019,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
name=self._name,
|
||||
astral_observer=observer,
|
||||
adapt_until_sleep=data[CONF_ADAPT_UNTIL_SLEEP],
|
||||
adapt_until_sleep_ct_curve=data[CONF_ADAPT_UNTIL_SLEEP_CT_CURVE],
|
||||
max_brightness=data[CONF_MAX_BRIGHTNESS],
|
||||
max_color_temp=data[CONF_MAX_COLOR_TEMP],
|
||||
min_brightness=data[CONF_MIN_BRIGHTNESS],
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@
|
|||
"sleep_rgb_color": "sleep_rgb_color",
|
||||
"sleep_transition": "sleep_transition",
|
||||
"transition_until_sleep": "transition_until_sleep: When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙",
|
||||
"transition_until_sleep_ct_curve": "transition_until_sleep_ct_curve",
|
||||
"sunrise_time": "sunrise_time",
|
||||
"min_sunrise_time": "min_sunrise_time",
|
||||
"max_sunrise_time": "max_sunrise_time",
|
||||
|
|
@ -87,6 +88,7 @@
|
|||
"sleep_rgb_or_color_temp": "Use either `\"rgb_color\"` or `\"color_temp\"` in sleep mode. 🌙",
|
||||
"sleep_rgb_color": "RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is \"rgb_color\"). 🌈",
|
||||
"sleep_transition": "Duration of transition when \"sleep mode\" is toggled in seconds. 😴",
|
||||
"transition_until_sleep_ct_curve": "Only used with `transition_until_sleep` and `sleep_rgb_or_color_temp: rgb_color`. Shapes how fast color-temperature-only lights warm up towards `sleep_color_temp` compared to the CCT of the RGB blend: `1` follows the CCT, `>1` warms up earlier (use if your color-temp lights look colder than your RGB lights in the evening), `<1` later. 🌡️",
|
||||
"sunrise_time": "Set a fixed time (HH:MM:SS) for sunrise. 🌅",
|
||||
"min_sunrise_time": "Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅",
|
||||
"max_sunrise_time": "Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅",
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@
|
|||
"prefer_rgb_color": "prefer_rgb_color: Czy w miarę możliwości preferować regulację kolorów RGB zamiast regulacji temperatury barwowej światła. 🌈",
|
||||
"sleep_transition": "sleep_transition: When 'sleep_state' changes. (sekund)",
|
||||
"transition_until_sleep": "transition_until_sleep: Gdy włączone, Adaptacyjne oświetlenie będzie traktowało ustawienia spania jako minimalne i przejdzie do nich po zachodzie słońca. 🌙",
|
||||
"transition_until_sleep_ct_curve": "transition_until_sleep_ct_curve",
|
||||
"sunrise_time": "sunrise_time: Manual override of the sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)",
|
||||
"sunrise_offset": "sunrise_offset: How long before(-) or after(+) to define the sunrise point of the cycle (+/- sekund)",
|
||||
"sunset_time": "sunset_time: Manual override of the sunset time, if 'None', it uses the actual sunset time at your location (HH:MM:SS)",
|
||||
|
|
@ -62,6 +63,7 @@
|
|||
"sleep_rgb_or_color_temp": "Użyj `\"rgb_color\"` albo `\"color_temp\"` w trybie spania. 🌙",
|
||||
"sleep_rgb_color": "Kolor RGB w trybie spania (używane, gdy `sleep_rgb_or_color_temp` jest `rgb_color`). 🌈",
|
||||
"sleep_transition": "Długość przejścia, gdy nastąpi włączenie/wyłączenie \"trybu spania\" (w sekundach). 😴",
|
||||
"transition_until_sleep_ct_curve": "Używane tylko z `transition_until_sleep` i `sleep_rgb_or_color_temp: rgb_color`. Określa, jak szybko żarówki obsługujące tylko temperaturę barwową ocieplają się w stronę `sleep_color_temp` względem CCT mieszanki RGB: `1` podąża za CCT, `>1` ociepla wcześniej (użyj, gdy żarówki CT wyglądają wieczorem chłodniej niż RGB), `<1` później. 🌡️",
|
||||
"sunrise_time": "Ustaw stały czas wschodu słońca (HH:MM:SS). 🌅",
|
||||
"min_sunrise_time": "Ustaw czas najwcześniejszego wirtualnego wschodu słońca (HH:MM:SS), pozwala na opóźnienie wschodu słońca. 🌅",
|
||||
"max_sunrise_time": "Ustaw czas najpóźniejszego wirtualnego wschodu słońca (HH:MM:SS), pozwala na przyspieszenie wschodu słońca. 🌅",
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ All configuration options are listed below with their default values. These opti
|
|||
| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color |
|
||||
| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `float` 0-6553 |
|
||||
| `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` |
|
||||
| `transition_until_sleep_ct_curve` | Only used with `transition_until_sleep` and `sleep_rgb_or_color_temp: rgb_color`. Shapes how fast color-temperature-only lights warm up towards `sleep_color_temp` compared to the CCT of the RGB blend: `1` follows the CCT, `>1` warms up earlier (use if your color-temp lights look colder than your RGB lights in the evening), `<1` later. 🌡️ | `1.0` | `float` |
|
||||
| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` |
|
||||
| `min_sunrise_time` | Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅 | `None` | `str` |
|
||||
| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅 | `None` | `str` |
|
||||
|
|
|
|||
|
|
@ -625,3 +625,61 @@ def test_adapt_until_sleep_ct_follows_rgb_blend():
|
|||
assert result["color_temp_kelvin"] <= previous_kelvin
|
||||
previous_kelvin = result["color_temp_kelvin"]
|
||||
assert seen_forced > 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("curve", "expect_warmer"), [(3.0, True), (0.5, False)])
|
||||
def test_adapt_until_sleep_ct_curve(curve, expect_warmer):
|
||||
"""`transition_until_sleep_ct_curve` reshapes the color-temp trajectory:
|
||||
`>1` makes CT-only lights warmer than the plain CCT of the RGB blend at the
|
||||
same moment, `<1` colder, while the endpoints (sunset/sleep) are unchanged.
|
||||
"""
|
||||
kwargs = {
|
||||
"name": "test",
|
||||
"astral_observer": location.observer,
|
||||
"adapt_until_sleep": True,
|
||||
"max_brightness": 100,
|
||||
"max_color_temp": 5500,
|
||||
"min_brightness": 30,
|
||||
"min_color_temp": 3000,
|
||||
"sleep_brightness": 1,
|
||||
"sleep_rgb_or_color_temp": "rgb_color",
|
||||
"sleep_color_temp": 1000,
|
||||
"sleep_rgb_color": (255, 56, 0),
|
||||
"sunrise_time": None,
|
||||
"min_sunrise_time": None,
|
||||
"max_sunrise_time": None,
|
||||
"sunset_time": None,
|
||||
"min_sunset_time": None,
|
||||
"max_sunset_time": None,
|
||||
"brightness_mode_time_dark": dt.timedelta(hours=1),
|
||||
"brightness_mode_time_light": dt.timedelta(hours=1),
|
||||
"timezone": dt.timezone.utc,
|
||||
}
|
||||
plain = SunLightSettings(**kwargs)
|
||||
shaped = SunLightSettings(**kwargs, adapt_until_sleep_ct_curve=curve)
|
||||
|
||||
date = dt.datetime(2022, 6, 1, tzinfo=dt.timezone.utc)
|
||||
sunset = plain.sun.sunset(date.date())
|
||||
_, midnight = plain.sun.noon_and_midnight(date)
|
||||
if midnight < sunset:
|
||||
midnight += dt.timedelta(days=1)
|
||||
|
||||
checked = 0
|
||||
for fraction in (0.3, 0.5, 0.7):
|
||||
moment = sunset + (midnight - sunset) * fraction
|
||||
a = plain.brightness_and_color(moment, is_sleep=False)
|
||||
b = shaped.brightness_and_color(moment, is_sleep=False)
|
||||
if not a["force_rgb_color"]:
|
||||
continue
|
||||
assert a["rgb_color"] == b["rgb_color"]
|
||||
if 1000 < a["color_temp_kelvin"] < 3000:
|
||||
checked += 1
|
||||
if expect_warmer:
|
||||
assert b["color_temp_kelvin"] < a["color_temp_kelvin"]
|
||||
else:
|
||||
assert b["color_temp_kelvin"] > a["color_temp_kelvin"]
|
||||
assert 1000 <= b["color_temp_kelvin"] <= 3000
|
||||
assert checked > 0
|
||||
# endpoints unchanged
|
||||
at_sunset = shaped.brightness_and_color(sunset, is_sleep=False)
|
||||
assert at_sunset["color_temp_kelvin"] == 3000
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue