mirror of
https://github.com/basnijholt/adaptive-lighting.git
synced 2026-10-28 14:17:07 +01:00
Merge 0a768be866 into b2ca188df3
This commit is contained in:
commit
87d30e7e54
9 changed files with 411 additions and 8 deletions
|
|
@ -145,6 +145,8 @@ 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_mode` | How color-temperature-only lights transition after sunset when `transition_until_sleep` is enabled and `sleep_rgb_or_color_temp` is `rgb_color`. `linear`: interpolate `min_color_temp` → `sleep_color_temp` in Kelvin. `match_rgb`: use the color temperature of the RGB blend that RGB lights show at the same moment, so mixed setups look uniform. 🌡️ | `linear` | one of `['linear', 'match_rgb']` |
|
||||
| `transition_until_sleep_ct_curve` | Only used with `transition_until_sleep_ct_mode: match_rgb`. Calibrates 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 still look colder than your RGB lights), `<1` later. 🎛️ | `1.0` | `float` 0.1-10 |
|
||||
| `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,8 @@ 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_mode: Literal["linear", "match_rgb"] = "linear"
|
||||
adapt_until_sleep_ct_curve: float = 1.0
|
||||
sunrise_offset: datetime.timedelta = datetime.timedelta()
|
||||
sunset_offset: datetime.timedelta = datetime.timedelta()
|
||||
timezone: datetime.tzinfo = UTC
|
||||
|
|
@ -410,6 +412,47 @@ class SunLightSettings:
|
|||
msg = "Should not happen"
|
||||
raise ValueError(msg)
|
||||
|
||||
def _color_temp_kelvin_matching_rgb(
|
||||
self,
|
||||
rgb_color: tuple[int, int, int],
|
||||
xy_color: tuple[float, float],
|
||||
linear_color_temp_kelvin: int,
|
||||
) -> int:
|
||||
"""Color temperature for color-temp-only lights following the RGB blend.
|
||||
|
||||
Keeps color-temp-only lights on the same perceived trajectory as the
|
||||
RGB blend after sunset: the CCT is derived from the blended color
|
||||
instead of interpolating in kelvin space, which diverges perceptually
|
||||
from the HSV interpolation used for `rgb_color` (see #624).
|
||||
"""
|
||||
if not any(self.sleep_rgb_color) or not any(rgb_color):
|
||||
# Black has no chromaticity, so there is no color temperature to
|
||||
# match; fall back to the linear interpolation, which ends at
|
||||
# `sleep_color_temp` when the blend reaches black.
|
||||
return linear_color_temp_kelvin
|
||||
mired_min = 1e6 / self.min_color_temp
|
||||
mired_sleep = 1e6 / self.sleep_color_temp
|
||||
mired = 1e6 / color_xy_to_cct(*xy_color)
|
||||
# The kelvin -> RGB -> CCT round trip is approximate, so the CCT of the
|
||||
# RGB color shown right after sunset is not exactly `min_color_temp`.
|
||||
# Anchor the start of the trajectory to that CCT, so color-temp-only
|
||||
# lights keep `min_color_temp` at sunset instead of stepping to a
|
||||
# slightly different value.
|
||||
r, g, b = color_temperature_to_rgb(self.min_color_temp)
|
||||
min_rgb_xy = color_RGB_to_xy(round(r), round(g), round(b))
|
||||
mired_start = 1e6 / color_xy_to_cct(*min_rgb_xy)
|
||||
if mired_sleep <= mired_start:
|
||||
# Degenerate configuration (sleep_color_temp >= min_color_temp).
|
||||
cct = clamp(1e6 / mired, self.sleep_color_temp, self.min_color_temp)
|
||||
return 5 * round(cct / 5)
|
||||
progress = clamp((mired - mired_start) / (mired_sleep - mired_start), 0, 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.
|
||||
progress **= 1 / self.adapt_until_sleep_ct_curve
|
||||
cct = 1e6 / (mired_min + progress * (mired_sleep - mired_min))
|
||||
return 5 * round(cct / 5)
|
||||
|
||||
def brightness_and_color(
|
||||
self,
|
||||
dt: datetime.datetime,
|
||||
|
|
@ -445,9 +488,15 @@ class SunLightSettings:
|
|||
color_temp_kelvin = self.color_temp_kelvin(sun_position)
|
||||
r, g, b = color_temperature_to_rgb(color_temp_kelvin)
|
||||
rgb_color = (round(r), round(g), round(b))
|
||||
xy_color: tuple[float, float] = color_RGB_to_xy(*rgb_color)
|
||||
if force_rgb_color and self.adapt_until_sleep_ct_mode == "match_rgb":
|
||||
color_temp_kelvin = self._color_temp_kelvin_matching_rgb(
|
||||
rgb_color,
|
||||
xy_color,
|
||||
color_temp_kelvin,
|
||||
)
|
||||
# backwards compatibility for versions < 1.3.1 - see #403
|
||||
color_temp_mired: float = math.floor(1000000 / color_temp_kelvin)
|
||||
xy_color: tuple[float, float] = color_RGB_to_xy(*rgb_color)
|
||||
hs_color: tuple[float, float] = color_xy_to_hs(*xy_color)
|
||||
return {
|
||||
"brightness_pct": brightness_pct,
|
||||
|
|
@ -594,6 +643,41 @@ def lerp_color_hsv(
|
|||
return cast("tuple[int, int, int]", rgb)
|
||||
|
||||
|
||||
def color_xy_to_cct(x: float, y: float) -> float:
|
||||
"""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
|
||||
to turn a color temperature into an RGB color) is closest to the given
|
||||
chromaticity in the CIE 1960 uv plane, which is how CCT is defined.
|
||||
This stays accurate for saturated colors far below the Planckian locus,
|
||||
where polynomial approximations like McCamy's break down.
|
||||
"""
|
||||
|
||||
def _uv(x: float, y: float) -> tuple[float, float]:
|
||||
d = -2 * x + 12 * y + 3
|
||||
return 4 * x / d, 6 * y / d
|
||||
|
||||
u0, v0 = _uv(x, y)
|
||||
|
||||
def _dist_sq(kelvin: float) -> float:
|
||||
xk, yk = color_RGB_to_xy(*color_temperature_to_rgb(kelvin))
|
||||
uk, vk = _uv(xk, yk)
|
||||
return (u0 - uk) ** 2 + (v0 - vk) ** 2
|
||||
|
||||
# Ternary search in mired space (perceptually uniform). 1000-13000 K covers
|
||||
# every configurable color temperature with a wide margin.
|
||||
lo_mired, hi_mired = 1e6 / 13000, 1e6 / 1000
|
||||
for _ in range(40):
|
||||
m1 = lo_mired + (hi_mired - lo_mired) / 3
|
||||
m2 = hi_mired - (hi_mired - lo_mired) / 3
|
||||
if _dist_sq(1e6 / m1) < _dist_sq(1e6 / m2):
|
||||
hi_mired = m2
|
||||
else:
|
||||
lo_mired = m1
|
||||
return 1e6 / ((lo_mired + hi_mired) / 2)
|
||||
|
||||
|
||||
def lerp(x: float, x1: float, x2: float, y1: float, y2: float) -> float:
|
||||
"""Linearly interpolate between two values."""
|
||||
return y1 + (x - x1) * (y2 - y1) / (x2 - x1)
|
||||
|
|
|
|||
|
|
@ -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,30 @@ DOCS[CONF_ADAPT_UNTIL_SLEEP] = (
|
|||
"transitioning to these values after sunset. 🌙"
|
||||
)
|
||||
|
||||
CONF_ADAPT_UNTIL_SLEEP_CT_MODE, DEFAULT_ADAPT_UNTIL_SLEEP_CT_MODE = (
|
||||
"transition_until_sleep_ct_mode",
|
||||
"linear",
|
||||
)
|
||||
DOCS[CONF_ADAPT_UNTIL_SLEEP_CT_MODE] = (
|
||||
"How color-temperature-only lights transition after sunset when "
|
||||
"`transition_until_sleep` is enabled and `sleep_rgb_or_color_temp` is "
|
||||
"`rgb_color`. `linear`: interpolate `min_color_temp` → `sleep_color_temp` "
|
||||
"in Kelvin. `match_rgb`: use the color temperature of the RGB blend that "
|
||||
"RGB lights show at the same moment, so mixed setups look uniform. 🌡️"
|
||||
)
|
||||
|
||||
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_ct_mode: match_rgb`. Calibrates 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 still look colder than your RGB "
|
||||
"lights), `<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 +412,22 @@ 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_MODE,
|
||||
DEFAULT_ADAPT_UNTIL_SLEEP_CT_MODE,
|
||||
selector.SelectSelector( # type: ignore[arg-type]
|
||||
selector.SelectSelectorConfig(
|
||||
options=["linear", "match_rgb"],
|
||||
multiple=False,
|
||||
mode=selector.SelectSelectorMode.DROPDOWN,
|
||||
),
|
||||
),
|
||||
),
|
||||
(
|
||||
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,8 @@
|
|||
"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_mode": "transition_until_sleep_ct_mode",
|
||||
"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 +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_ct_mode: match_rgb`. Calibrates 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 still look colder than your RGB lights), `<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. 🌅",
|
||||
|
|
@ -100,7 +103,8 @@
|
|||
"take_over_control_mode": "The adaptation pausing mode when other sources change brightness and/or color of lights. `pause_all` always pauses both brightness and color adaptation. `pause_changed` pauses the adaptation of only the changed attributes and continues adapting unchanged attributes, e.g., continues color adaptation when only brightness was changed.",
|
||||
"autoreset_control_seconds": "Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️",
|
||||
"send_split_delay": "Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️",
|
||||
"adapt_delay": "Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️"
|
||||
"adapt_delay": "Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️",
|
||||
"transition_until_sleep_ct_mode": "How color-temperature-only lights transition after sunset when `transition_until_sleep` is enabled and `sleep_rgb_or_color_temp` is `rgb_color`. `linear`: interpolate `min_color_temp` → `sleep_color_temp` in Kelvin. `match_rgb`: use the color temperature of the RGB blend that RGB lights show at the same moment, so mixed setups look uniform. 🌡️"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,6 +96,8 @@ from .const import (
|
|||
CONF_ADAPT_DELAY,
|
||||
CONF_ADAPT_ONLY_ON_BARE_TURN_ON,
|
||||
CONF_ADAPT_UNTIL_SLEEP,
|
||||
CONF_ADAPT_UNTIL_SLEEP_CT_CURVE,
|
||||
CONF_ADAPT_UNTIL_SLEEP_CT_MODE,
|
||||
CONF_AUTORESET_CONTROL,
|
||||
CONF_BRIGHTNESS_MODE,
|
||||
CONF_BRIGHTNESS_MODE_TIME_DARK,
|
||||
|
|
@ -1018,6 +1020,8 @@ 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],
|
||||
adapt_until_sleep_ct_mode=data[CONF_ADAPT_UNTIL_SLEEP_CT_MODE],
|
||||
max_brightness=data[CONF_MAX_BRIGHTNESS],
|
||||
max_color_temp=data[CONF_MAX_COLOR_TEMP],
|
||||
min_brightness=data[CONF_MIN_BRIGHTNESS],
|
||||
|
|
|
|||
|
|
@ -54,6 +54,8 @@
|
|||
"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_mode": "transition_until_sleep_ct_mode",
|
||||
"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 +89,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_ct_mode: match_rgb`. Calibrates 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 still look colder than your RGB lights), `<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. 🌅",
|
||||
|
|
@ -101,7 +104,8 @@
|
|||
"take_over_control_mode": "The adaptation pausing mode when other sources change brightness and/or color of lights. `pause_all` always pauses both brightness and color adaptation. `pause_changed` pauses the adaptation of only the changed attributes and continues adapting unchanged attributes, e.g., continues color adaptation when only brightness was changed.",
|
||||
"autoreset_control_seconds": "Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️",
|
||||
"send_split_delay": "Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️",
|
||||
"adapt_delay": "Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️"
|
||||
"adapt_delay": "Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️",
|
||||
"transition_until_sleep_ct_mode": "How color-temperature-only lights transition after sunset when `transition_until_sleep` is enabled and `sleep_rgb_or_color_temp` is `rgb_color`. `linear`: interpolate `min_color_temp` → `sleep_color_temp` in Kelvin. `match_rgb`: use the color temperature of the RGB blend that RGB lights show at the same moment, so mixed setups look uniform. 🌡️"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,8 @@
|
|||
"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_mode": "transition_until_sleep_ct_mode",
|
||||
"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 +64,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_ct_mode: match_rgb`. Kalibruje, jak szybko żarówki CT 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 nadal wyglądają 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. 🌅",
|
||||
|
|
@ -75,7 +78,8 @@
|
|||
"brightness_mode_time_light": "(Pomijany, gdy `brightness_mode='default'`). Czas w sekundach, kiedy jasność będzie zwiększana po wschodzie słońca/zmniejszana przed zachodem słońca. 📈📉",
|
||||
"autoreset_control_seconds": "Czas, po którym manualna kontrola zostanie wyłączona (w sekundach). Ustaw 0, aby wyłączyć. ⏲️",
|
||||
"send_split_delay": "Opóźnienie (w ms) pomiędzy `separate_turn_on_commands` dla świateł, które nie akceptują jednoczesnego ustawiania jasności i koloru. ⏲️",
|
||||
"adapt_delay": "Czas (w sekundach) pomiędzy włączeniem światła, a rozpoczęciem adaptowania przez Adaptacyjne oświetlenie. Może pomóc zredukować migotanie. ⏲️"
|
||||
"adapt_delay": "Czas (w sekundach) pomiędzy włączeniem światła, a rozpoczęciem adaptowania przez Adaptacyjne oświetlenie. Może pomóc zredukować migotanie. ⏲️",
|
||||
"transition_until_sleep_ct_mode": "Jak żarówki obsługujące tylko temperaturę barwową zmieniają się po zachodzie słońca, gdy włączone jest `transition_until_sleep` a `sleep_rgb_or_color_temp` to `rgb_color`. `linear`: interpolacja `min_color_temp` → `sleep_color_temp` w kelwinach. `match_rgb`: temperatura barwowa mieszanki RGB, którą w tym momencie pokazują żarówki RGB — mieszane instalacje wyglądają jednolicie. 🌡️"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,6 +49,8 @@ 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_mode` | How color-temperature-only lights transition after sunset when `transition_until_sleep` is enabled and `sleep_rgb_or_color_temp` is `rgb_color`. `linear`: interpolate `min_color_temp` → `sleep_color_temp` in Kelvin. `match_rgb`: use the color temperature of the RGB blend that RGB lights show at the same moment, so mixed setups look uniform. 🌡️ | `linear` | one of `['linear', 'match_rgb']` |
|
||||
| `transition_until_sleep_ct_curve` | Only used with `transition_until_sleep_ct_mode: match_rgb`. Calibrates 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 still look colder than your RGB lights), `<1` later. 🎛️ | `1.0` | `float` 0.1-10 |
|
||||
| `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` |
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from homeassistant.components.adaptive_lighting.color_and_brightness import (
|
|||
SunEvents,
|
||||
SunLightSettings,
|
||||
clamp,
|
||||
color_xy_to_cct,
|
||||
)
|
||||
|
||||
# Create a mock astral location object (its `.observer` is passed to `SunEvents`)
|
||||
|
|
@ -547,3 +548,263 @@ def test_brightness_and_color_on_polar_days(date):
|
|||
at_midnight = settings.brightness_and_color(midnight, is_sleep=False)
|
||||
assert at_midnight["brightness_pct"] == 30
|
||||
assert at_midnight["color_temp_kelvin"] == 2000
|
||||
|
||||
|
||||
def test_color_xy_to_cct_roundtrip():
|
||||
"""CCT derived from a blackbody RGB color must match the original kelvins."""
|
||||
from homeassistant.util.color import (
|
||||
color_RGB_to_xy,
|
||||
color_temperature_to_rgb,
|
||||
)
|
||||
|
||||
for kelvin in (1500, 2000, 2700, 3000, 4000, 5500, 6500):
|
||||
xy = color_RGB_to_xy(*color_temperature_to_rgb(kelvin))
|
||||
cct = color_xy_to_cct(*xy)
|
||||
assert abs(cct - kelvin) / kelvin < 0.02, (kelvin, cct)
|
||||
|
||||
|
||||
def test_adapt_until_sleep_ct_follows_rgb_blend():
|
||||
"""With `adapt_until_sleep` and an RGB sleep color, color-temp-only lights
|
||||
must follow the perceived color of the RGB blend (CCT derived from the
|
||||
blended color) instead of a separate linear interpolation in kelvin space,
|
||||
which diverges perceptually from the HSV blend used for `rgb_color` (#624).
|
||||
"""
|
||||
from homeassistant.util.color import color_RGB_to_xy
|
||||
|
||||
settings = SunLightSettings(
|
||||
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,
|
||||
adapt_until_sleep_ct_mode="match_rgb",
|
||||
)
|
||||
date = dt.datetime(2022, 6, 1, tzinfo=dt.timezone.utc)
|
||||
sunset = settings.sun.sunset(date.date())
|
||||
_, midnight = settings.sun.noon_and_midnight(date)
|
||||
if midnight < sunset:
|
||||
midnight += dt.timedelta(days=1)
|
||||
|
||||
previous_kelvin = None
|
||||
seen_forced = 0
|
||||
for fraction in (0.15, 0.3, 0.5, 0.7, 0.9):
|
||||
moment = sunset + (midnight - sunset) * fraction
|
||||
result = settings.brightness_and_color(moment, is_sleep=False)
|
||||
if not result["force_rgb_color"]:
|
||||
continue
|
||||
seen_forced += 1
|
||||
expected_cct = clamp(
|
||||
color_xy_to_cct(*color_RGB_to_xy(*result["rgb_color"])),
|
||||
settings.sleep_color_temp,
|
||||
settings.min_color_temp,
|
||||
)
|
||||
# The trajectory is anchored at `min_color_temp` for the RGB color
|
||||
# shown at sunset (whose CCT is only approximately `min_color_temp`),
|
||||
# so allow for that small, constant offset.
|
||||
assert abs(result["color_temp_kelvin"] - expected_cct) <= 40, (
|
||||
moment,
|
||||
result,
|
||||
)
|
||||
assert (
|
||||
settings.sleep_color_temp
|
||||
<= result["color_temp_kelvin"]
|
||||
<= settings.min_color_temp
|
||||
)
|
||||
if previous_kelvin is not None:
|
||||
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,
|
||||
"adapt_until_sleep_ct_mode": "match_rgb",
|
||||
}
|
||||
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
|
||||
|
||||
|
||||
def test_adapt_until_sleep_ct_mode_linear_is_default_and_unchanged():
|
||||
"""The default `linear` mode keeps the historical kelvin interpolation."""
|
||||
settings = SunLightSettings(
|
||||
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,
|
||||
)
|
||||
assert settings.adapt_until_sleep_ct_mode == "linear"
|
||||
date = dt.datetime(2022, 6, 1, tzinfo=dt.timezone.utc)
|
||||
sunset = settings.sun.sunset(date.date())
|
||||
_, midnight = settings.sun.noon_and_midnight(date)
|
||||
if midnight < sunset:
|
||||
midnight += dt.timedelta(days=1)
|
||||
moment = sunset + (midnight - sunset) * 0.5
|
||||
result = settings.brightness_and_color(moment, is_sleep=False)
|
||||
assert result["force_rgb_color"]
|
||||
assert result["color_temp_kelvin"] == settings.color_temp_kelvin(
|
||||
result["sun_position"],
|
||||
)
|
||||
|
||||
|
||||
def _settings_until_sleep(**overrides):
|
||||
kwargs = {
|
||||
"name": "test",
|
||||
"astral_observer": location.observer,
|
||||
"adapt_until_sleep": True,
|
||||
"max_brightness": 100,
|
||||
"max_color_temp": 5500,
|
||||
"min_brightness": 30,
|
||||
"min_color_temp": 2700,
|
||||
"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,
|
||||
"adapt_until_sleep_ct_mode": "match_rgb",
|
||||
}
|
||||
kwargs.update(overrides)
|
||||
return SunLightSettings(**kwargs)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("min_color_temp", [2000, 2700, 3000, 4000])
|
||||
@pytest.mark.parametrize("curve", [1.0, 2.5])
|
||||
def test_adapt_until_sleep_match_rgb_no_step_at_sunset(min_color_temp, curve):
|
||||
"""`match_rgb` must keep `min_color_temp` right after sunset.
|
||||
|
||||
The kelvin -> RGB -> CCT round trip is approximate, so without anchoring
|
||||
the trajectory the color temperature would step to a slightly different
|
||||
value the moment the sun goes below the horizon.
|
||||
"""
|
||||
settings = _settings_until_sleep(
|
||||
min_color_temp=min_color_temp,
|
||||
adapt_until_sleep_ct_curve=curve,
|
||||
)
|
||||
date = dt.datetime(2022, 6, 1, tzinfo=dt.timezone.utc)
|
||||
sunset = settings.sun.sunset(date.date())
|
||||
at_sunset = settings.brightness_and_color(sunset, is_sleep=False)
|
||||
assert not at_sunset["force_rgb_color"]
|
||||
assert at_sunset["color_temp_kelvin"] == min_color_temp
|
||||
just_after = settings.brightness_and_color(
|
||||
sunset + dt.timedelta(seconds=1),
|
||||
is_sleep=False,
|
||||
)
|
||||
assert just_after["force_rgb_color"]
|
||||
assert just_after["color_temp_kelvin"] == min_color_temp
|
||||
|
||||
|
||||
def test_adapt_until_sleep_match_rgb_black_sleep_color():
|
||||
"""A black `sleep_rgb_color` has no chromaticity to match.
|
||||
|
||||
Color-temp-only lights must still end at `sleep_color_temp` (not at
|
||||
`min_color_temp`) and warm up monotonically in between.
|
||||
"""
|
||||
settings = _settings_until_sleep(sleep_rgb_color=(0, 0, 0))
|
||||
date = dt.datetime(2022, 6, 1, tzinfo=dt.timezone.utc)
|
||||
sunset = settings.sun.sunset(date.date())
|
||||
_, midnight = settings.sun.noon_and_midnight(date)
|
||||
if midnight < sunset:
|
||||
midnight += dt.timedelta(days=1)
|
||||
|
||||
previous = None
|
||||
for fraction in (0.01, 0.25, 0.5, 0.75, 0.99, 1.0):
|
||||
result = settings.brightness_and_color(
|
||||
sunset + (midnight - sunset) * fraction,
|
||||
is_sleep=False,
|
||||
)
|
||||
assert result["force_rgb_color"]
|
||||
assert 1000 <= result["color_temp_kelvin"] <= 2700
|
||||
if previous is not None:
|
||||
assert result["color_temp_kelvin"] <= previous
|
||||
previous = result["color_temp_kelvin"]
|
||||
assert result["rgb_color"] == (0, 0, 0)
|
||||
assert result["color_temp_kelvin"] == settings.sleep_color_temp
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue