mirror of
https://github.com/basnijholt/adaptive-lighting.git
synced 2026-10-28 22:27:06 +01:00
feat: add configurable color temperature plateau
Add `color_temp_plateau_morning` and `color_temp_plateau_evening` (seconds, default 0, max 12h). They keep `max_color_temp` for that long before / after solar noon, and compress the ramp from/to `min_color_temp` at sunrise/sunset so the curve stays continuous and smooth. Each side is independent, only the color temperature is affected (not brightness, sleep mode or the night), and the default (0) is bit-for-bit identical to the previous behavior. Each plateau is clamped to 90% of the half-day so the ramp never collapses.
This commit is contained in:
parent
283fec08ae
commit
72a7c4875e
13 changed files with 656 additions and 5 deletions
|
|
@ -156,6 +156,8 @@ The YAML and frontend configuration methods support all of the options listed be
|
|||
| `brightness_mode` | Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈 | `default` | one of `['default', 'linear', 'tanh']` |
|
||||
| `brightness_mode_time_dark` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉 | `900` | `int` |
|
||||
| `brightness_mode_time_light` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉. | `3600` | `int` |
|
||||
| `color_temp_plateau_morning` | The duration in seconds before solar noon during which the color temperature stays at `max_color_temp` instead of only reaching it at noon. The ramp from `min_color_temp` at sunrise to `max_color_temp` is compressed accordingly. `0` keeps the default curve. Only affects the morning. 🌤️ | `0` | `int` 0-43200 |
|
||||
| `color_temp_plateau_evening` | The duration in seconds after solar noon during which the color temperature stays at `max_color_temp` before ramping down to `min_color_temp` at sunset. `0` keeps the default curve. Only affects the evening. 🌇 | `0` | `int` 0-43200 |
|
||||
| `take_over_control` | Pause adaptation of individual lights and hand over (manual) control to other sources that issue `light.turn_on` calls for lights that are on. 🔒 | `True` | `bool` |
|
||||
| `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. | `pause_all` | one of `['pause_all', 'pause_changed']` |
|
||||
| `detect_non_ha_changes` | Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Note that this calls `homeassistant.update_entity` every `interval`! Disable this feature if you encounter such issues. | `False` | `bool` |
|
||||
|
|
|
|||
|
|
@ -41,6 +41,10 @@ _ALLOWED_ORDERS = {_ORDER[i:] + _ORDER[:i] for i in range(len(_ORDER))}
|
|||
_POLAR_SUN_EVENT_OFFSET = timedelta(minutes=30)
|
||||
_POLAR_SUN_EVENT_EPSILON = timedelta(seconds=1)
|
||||
|
||||
# The color temperature plateau is limited to this fraction of the time between
|
||||
# solar noon and sunrise/sunset, such that the ramp never collapses.
|
||||
_PLATEAU_MAX_FRACTION = 0.9
|
||||
|
||||
utcnow: partial[datetime.datetime] = partial(datetime.datetime.now, UTC)
|
||||
utcnow.__doc__ = "Get now in UTC time."
|
||||
|
||||
|
|
@ -243,15 +247,46 @@ class SunEvents:
|
|||
i_now = bisect.bisect([ts for _, ts in events], dt.timestamp())
|
||||
return events[i_now - 1 : i_now + 1]
|
||||
|
||||
def sun_position(self, dt: datetime.datetime) -> float:
|
||||
"""Calculate the position of the sun, between [-1, 1]."""
|
||||
def sun_position(
|
||||
self,
|
||||
dt: datetime.datetime,
|
||||
plateau_morning: datetime.timedelta = datetime.timedelta(),
|
||||
plateau_evening: datetime.timedelta = datetime.timedelta(),
|
||||
) -> float:
|
||||
"""Calculate the position of the sun, between [-1, 1].
|
||||
|
||||
With a non-zero `plateau_morning` (`plateau_evening`) the position stays
|
||||
at its maximum of 1 for that long before (after) solar noon, and the
|
||||
parabola between sunrise (sunset) and the end of the plateau is
|
||||
compressed to still start (end) at 0 at sunrise (sunset). This is a
|
||||
monotonic and continuous remapping of the time axis that only applies
|
||||
to the part of the day where the sun is up; the position below the
|
||||
horizon is never changed. With both plateaus zero this is the
|
||||
unmodified position.
|
||||
"""
|
||||
target_ts = dt.timestamp()
|
||||
(_, prev_ts), (next_event, next_ts) = self.prev_and_next_events(dt)
|
||||
(prev_event, prev_ts), (next_event, next_ts) = self.prev_and_next_events(dt)
|
||||
h, x = (
|
||||
(prev_ts, next_ts)
|
||||
if next_event in (SunEvent.SUNSET, SunEvent.SUNRISE)
|
||||
else (next_ts, prev_ts)
|
||||
)
|
||||
plateau = None
|
||||
if next_event == SunEvent.NOON and prev_event == SunEvent.SUNRISE:
|
||||
plateau = plateau_morning
|
||||
elif next_event == SunEvent.SUNSET and prev_event == SunEvent.NOON:
|
||||
plateau = plateau_evening
|
||||
if plateau:
|
||||
# `h` is solar noon and `x` the sunrise or sunset bounding this half
|
||||
half_day = abs(h - x)
|
||||
# Always keep part of the half-day for the ramp so that it has a
|
||||
# non-zero duration, which also prevents dividing by zero.
|
||||
hold = min(plateau.total_seconds(), _PLATEAU_MAX_FRACTION * half_day)
|
||||
distance = abs(target_ts - h)
|
||||
if hold > 0 and half_day > 0:
|
||||
if distance <= hold:
|
||||
return 1.0
|
||||
return 1 - ((distance - hold) / (half_day - hold)) ** 2
|
||||
# k = -1 between sunset and sunrise (sun below horizon)
|
||||
# k = 1 between sunrise and sunset (sun above horizon)
|
||||
k = 1 if next_event in (SunEvent.SUNSET, SunEvent.NOON) else -1
|
||||
|
|
@ -297,6 +332,8 @@ class SunLightSettings:
|
|||
brightness_mode_time_dark: datetime.timedelta
|
||||
brightness_mode_time_light: datetime.timedelta
|
||||
brightness_mode: Literal["default", "linear", "tanh"] = "default"
|
||||
color_temp_plateau_morning: datetime.timedelta = datetime.timedelta()
|
||||
color_temp_plateau_evening: datetime.timedelta = datetime.timedelta()
|
||||
sunrise_offset: datetime.timedelta = datetime.timedelta()
|
||||
sunset_offset: datetime.timedelta = datetime.timedelta()
|
||||
timezone: datetime.tzinfo = UTC
|
||||
|
|
@ -417,6 +454,16 @@ class SunLightSettings:
|
|||
) -> dict[str, Any]:
|
||||
"""Calculate the brightness and color."""
|
||||
sun_position = self.sun.sun_position(dt)
|
||||
# Only the color temperature (not the brightness) has the plateaus.
|
||||
color_sun_position = (
|
||||
self.sun.sun_position(
|
||||
dt,
|
||||
self.color_temp_plateau_morning,
|
||||
self.color_temp_plateau_evening,
|
||||
)
|
||||
if self.color_temp_plateau_morning or self.color_temp_plateau_evening
|
||||
else sun_position
|
||||
)
|
||||
rgb_color: tuple[int, int, int]
|
||||
# Variable `force_rgb_color` is needed for RGB color after sunset (if enabled)
|
||||
force_rgb_color = False
|
||||
|
|
@ -439,10 +486,10 @@ class SunLightSettings:
|
|||
self.sleep_rgb_color,
|
||||
sun_position,
|
||||
)
|
||||
color_temp_kelvin = self.color_temp_kelvin(sun_position)
|
||||
color_temp_kelvin = self.color_temp_kelvin(color_sun_position)
|
||||
force_rgb_color = True
|
||||
else:
|
||||
color_temp_kelvin = self.color_temp_kelvin(sun_position)
|
||||
color_temp_kelvin = self.color_temp_kelvin(color_sun_position)
|
||||
r, g, b = color_temperature_to_rgb(color_temp_kelvin)
|
||||
rgb_color = (round(r), round(g), round(b))
|
||||
# backwards compatibility for versions < 1.3.1 - see #403
|
||||
|
|
|
|||
|
|
@ -206,6 +206,28 @@ DOCS[CONF_BRIGHTNESS_MODE_TIME_LIGHT] = (
|
|||
"the brightness after/before sunrise/sunset. 📈📉."
|
||||
)
|
||||
|
||||
CONF_COLOR_TEMP_PLATEAU_MORNING, DEFAULT_COLOR_TEMP_PLATEAU_MORNING = (
|
||||
"color_temp_plateau_morning",
|
||||
0,
|
||||
)
|
||||
DOCS[CONF_COLOR_TEMP_PLATEAU_MORNING] = (
|
||||
"The duration in seconds before solar noon during which the color temperature "
|
||||
"stays at `max_color_temp` instead of only reaching it at noon. The ramp from "
|
||||
"`min_color_temp` at sunrise to `max_color_temp` is compressed accordingly. "
|
||||
"`0` keeps the default curve. Only affects the morning. 🌤️"
|
||||
)
|
||||
CONF_COLOR_TEMP_PLATEAU_EVENING, DEFAULT_COLOR_TEMP_PLATEAU_EVENING = (
|
||||
"color_temp_plateau_evening",
|
||||
0,
|
||||
)
|
||||
DOCS[CONF_COLOR_TEMP_PLATEAU_EVENING] = (
|
||||
"The duration in seconds after solar noon during which the color temperature "
|
||||
"stays at `max_color_temp` before ramping down to `min_color_temp` at sunset. "
|
||||
"`0` keeps the default curve. Only affects the evening. 🌇"
|
||||
)
|
||||
# Each side of the plateau is limited to half a day.
|
||||
MAX_COLOR_TEMP_PLATEAU = 12 * 60 * 60
|
||||
|
||||
CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL = "take_over_control", True
|
||||
DOCS[CONF_TAKE_OVER_CONTROL] = (
|
||||
"Pause adaptation of individual lights and hand over (manual) control to other sources that "
|
||||
|
|
@ -411,6 +433,16 @@ VALIDATION_TUPLES: list[tuple[str, Any, Any]] = [
|
|||
),
|
||||
(CONF_BRIGHTNESS_MODE_TIME_DARK, DEFAULT_BRIGHTNESS_MODE_TIME_DARK, int),
|
||||
(CONF_BRIGHTNESS_MODE_TIME_LIGHT, DEFAULT_BRIGHTNESS_MODE_TIME_LIGHT, int),
|
||||
(
|
||||
CONF_COLOR_TEMP_PLATEAU_MORNING,
|
||||
DEFAULT_COLOR_TEMP_PLATEAU_MORNING,
|
||||
int_between(0, MAX_COLOR_TEMP_PLATEAU),
|
||||
),
|
||||
(
|
||||
CONF_COLOR_TEMP_PLATEAU_EVENING,
|
||||
DEFAULT_COLOR_TEMP_PLATEAU_EVENING,
|
||||
int_between(0, MAX_COLOR_TEMP_PLATEAU),
|
||||
),
|
||||
(CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL, bool),
|
||||
(
|
||||
CONF_TAKE_OVER_CONTROL_MODE,
|
||||
|
|
@ -467,6 +499,11 @@ def timedelta_as_int(value: timedelta) -> float:
|
|||
return value.total_seconds()
|
||||
|
||||
|
||||
_PLATEAU_PERIOD = vol.All(
|
||||
cv.time_period,
|
||||
vol.Range(min=timedelta(0), max=timedelta(seconds=MAX_COLOR_TEMP_PLATEAU)),
|
||||
)
|
||||
|
||||
# conf_option: (validator, coerce) tuples
|
||||
# these validators cannot be serialized but can be serialized when coerced by coerce.
|
||||
EXTRA_VALIDATION: dict[str, tuple[Any, Any]] = {
|
||||
|
|
@ -481,6 +518,8 @@ EXTRA_VALIDATION: dict[str, tuple[Any, Any]] = {
|
|||
CONF_MAX_SUNSET_TIME: (cv.time, str),
|
||||
CONF_BRIGHTNESS_MODE_TIME_LIGHT: (cv.time_period, timedelta_as_int),
|
||||
CONF_BRIGHTNESS_MODE_TIME_DARK: (cv.time_period, timedelta_as_int),
|
||||
CONF_COLOR_TEMP_PLATEAU_MORNING: (_PLATEAU_PERIOD, timedelta_as_int),
|
||||
CONF_COLOR_TEMP_PLATEAU_EVENING: (_PLATEAU_PERIOD, timedelta_as_int),
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -205,6 +205,24 @@ change_switch_settings:
|
|||
number:
|
||||
min: -86400
|
||||
max: 86300
|
||||
color_temp_plateau_morning:
|
||||
description: The duration in seconds before solar noon during which the color temperature stays at `max_color_temp` instead of only reaching it at noon. The ramp from `min_color_temp` at sunrise to `max_color_temp` is compressed accordingly. `0` keeps the default curve. Only affects the morning. 🌤️
|
||||
required: false
|
||||
example: ''
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 43200
|
||||
unit_of_measurement: s
|
||||
color_temp_plateau_evening:
|
||||
description: The duration in seconds after solar noon during which the color temperature stays at `max_color_temp` before ramping down to `min_color_temp` at sunset. `0` keeps the default curve. Only affects the evening. 🌇
|
||||
required: false
|
||||
example: ''
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 43200
|
||||
unit_of_measurement: s
|
||||
sunset_time:
|
||||
description: Set a fixed time (HH:MM:SS) for sunset. 🌇
|
||||
example: ''
|
||||
|
|
|
|||
|
|
@ -64,6 +64,8 @@
|
|||
"brightness_mode": "brightness_mode",
|
||||
"brightness_mode_time_dark": "brightness_mode_time_dark",
|
||||
"brightness_mode_time_light": "brightness_mode_time_light",
|
||||
"color_temp_plateau_morning": "color_temp_plateau_morning",
|
||||
"color_temp_plateau_evening": "color_temp_plateau_evening",
|
||||
"take_over_control": "take_over_control: Pause adaptation of individual lights and hand over (manual) control to other sources that issue `light.turn_on` calls for lights that are on. 🔒",
|
||||
"take_over_control_mode": "take_over_control_mode",
|
||||
"detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Note that this calls `homeassistant.update_entity` every `interval`! Disable this feature if you encounter such issues.",
|
||||
|
|
@ -97,6 +99,8 @@
|
|||
"brightness_mode": "Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈",
|
||||
"brightness_mode_time_dark": "(Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉",
|
||||
"brightness_mode_time_light": "(Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉.",
|
||||
"color_temp_plateau_morning": "The duration in seconds before solar noon during which the color temperature stays at `max_color_temp` instead of only reaching it at noon. The ramp from `min_color_temp` at sunrise to `max_color_temp` is compressed accordingly. `0` keeps the default curve. Only affects the morning. 🌤️",
|
||||
"color_temp_plateau_evening": "The duration in seconds after solar noon during which the color temperature stays at `max_color_temp` before ramping down to `min_color_temp` at sunset. `0` keeps the default curve. Only affects the evening. 🌇",
|
||||
"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. ⏲️",
|
||||
|
|
@ -252,6 +256,14 @@
|
|||
"description": "Adjust sunset time with a positive or negative offset in seconds. ⏰",
|
||||
"name": "sunset_offset"
|
||||
},
|
||||
"color_temp_plateau_morning": {
|
||||
"description": "The duration in seconds before solar noon during which the color temperature stays at `max_color_temp` instead of only reaching it at noon. The ramp from `min_color_temp` at sunrise to `max_color_temp` is compressed accordingly. `0` keeps the default curve. Only affects the morning. 🌤️",
|
||||
"name": "color_temp_plateau_morning"
|
||||
},
|
||||
"color_temp_plateau_evening": {
|
||||
"description": "The duration in seconds after solar noon during which the color temperature stays at `max_color_temp` before ramping down to `min_color_temp` at sunset. `0` keeps the default curve. Only affects the evening. 🌇",
|
||||
"name": "color_temp_plateau_evening"
|
||||
},
|
||||
"sunset_time": {
|
||||
"description": "Set a fixed time (HH:MM:SS) for sunset. 🌇",
|
||||
"name": "sunset_time"
|
||||
|
|
|
|||
|
|
@ -100,6 +100,8 @@ from .const import (
|
|||
CONF_BRIGHTNESS_MODE,
|
||||
CONF_BRIGHTNESS_MODE_TIME_DARK,
|
||||
CONF_BRIGHTNESS_MODE_TIME_LIGHT,
|
||||
CONF_COLOR_TEMP_PLATEAU_EVENING,
|
||||
CONF_COLOR_TEMP_PLATEAU_MORNING,
|
||||
CONF_DETECT_NON_HA_CHANGES,
|
||||
CONF_EXPAND_LIGHT_GROUPS,
|
||||
CONF_INCLUDE_CONFIG_IN_ATTRIBUTES,
|
||||
|
|
@ -1037,6 +1039,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
brightness_mode=data[CONF_BRIGHTNESS_MODE],
|
||||
brightness_mode_time_dark=data[CONF_BRIGHTNESS_MODE_TIME_DARK],
|
||||
brightness_mode_time_light=data[CONF_BRIGHTNESS_MODE_TIME_LIGHT],
|
||||
color_temp_plateau_morning=data[CONF_COLOR_TEMP_PLATEAU_MORNING],
|
||||
color_temp_plateau_evening=data[CONF_COLOR_TEMP_PLATEAU_EVENING],
|
||||
timezone=zoneinfo.ZoneInfo(self.hass.config.time_zone),
|
||||
)
|
||||
_LOGGER.debug(
|
||||
|
|
|
|||
|
|
@ -65,6 +65,8 @@
|
|||
"brightness_mode": "brightness_mode",
|
||||
"brightness_mode_time_dark": "brightness_mode_time_dark",
|
||||
"brightness_mode_time_light": "brightness_mode_time_light",
|
||||
"color_temp_plateau_morning": "color_temp_plateau_morning",
|
||||
"color_temp_plateau_evening": "color_temp_plateau_evening",
|
||||
"take_over_control": "take_over_control: Pause adaptation of individual lights and hand over (manual) control to other sources that issue `light.turn_on` calls for lights that are on. 🔒",
|
||||
"take_over_control_mode": "take_over_control_mode",
|
||||
"detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Note that this calls `homeassistant.update_entity` every `interval`! Disable this feature if you encounter such issues.",
|
||||
|
|
@ -98,6 +100,8 @@
|
|||
"brightness_mode": "Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈",
|
||||
"brightness_mode_time_dark": "(Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉",
|
||||
"brightness_mode_time_light": "(Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉.",
|
||||
"color_temp_plateau_morning": "The duration in seconds before solar noon during which the color temperature stays at `max_color_temp` instead of only reaching it at noon. The ramp from `min_color_temp` at sunrise to `max_color_temp` is compressed accordingly. `0` keeps the default curve. Only affects the morning. 🌤️",
|
||||
"color_temp_plateau_evening": "The duration in seconds after solar noon during which the color temperature stays at `max_color_temp` before ramping down to `min_color_temp` at sunset. `0` keeps the default curve. Only affects the evening. 🌇",
|
||||
"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. ⏲️",
|
||||
|
|
@ -253,6 +257,14 @@
|
|||
"description": "Adjust sunset time with a positive or negative offset in seconds. ⏰",
|
||||
"name": "sunset_offset"
|
||||
},
|
||||
"color_temp_plateau_morning": {
|
||||
"description": "The duration in seconds before solar noon during which the color temperature stays at `max_color_temp` instead of only reaching it at noon. The ramp from `min_color_temp` at sunrise to `max_color_temp` is compressed accordingly. `0` keeps the default curve. Only affects the morning. 🌤️",
|
||||
"name": "color_temp_plateau_morning"
|
||||
},
|
||||
"color_temp_plateau_evening": {
|
||||
"description": "The duration in seconds after solar noon during which the color temperature stays at `max_color_temp` before ramping down to `min_color_temp` at sunset. `0` keeps the default curve. Only affects the evening. 🌇",
|
||||
"name": "color_temp_plateau_evening"
|
||||
},
|
||||
"sunset_time": {
|
||||
"description": "Set a fixed time (HH:MM:SS) for sunset. 🌇",
|
||||
"name": "sunset_time"
|
||||
|
|
|
|||
36
docs/advanced/color-temperature-plateau.md
Normal file
36
docs/advanced/color-temperature-plateau.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
---
|
||||
icon: lucide/thermometer-sun
|
||||
---
|
||||
|
||||
# Color Temperature Plateau
|
||||
|
||||
By default the color temperature follows the sun position: it rises from `min_color_temp` at sunrise to `max_color_temp` at solar noon, and falls back to `min_color_temp` at sunset. The light therefore only *touches* `max_color_temp` at a single instant.
|
||||
|
||||
`color_temp_plateau_morning` and `color_temp_plateau_evening` hold `max_color_temp` for a while around solar noon, while still ramping smoothly from and to `min_color_temp` at sunrise and sunset. Both are independent, in seconds, and default to `0` (the unchanged default curve).
|
||||
|
||||
| Option | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `color_temp_plateau_morning` | `0` | Seconds *before* solar noon that the color temperature already stays at `max_color_temp` |
|
||||
| `color_temp_plateau_evening` | `0` | Seconds *after* solar noon that the color temperature stays at `max_color_temp` |
|
||||
|
||||
## How it works
|
||||
|
||||
The ramp is the same curve as without a plateau, but compressed into the time between sunrise (or sunset) and the start (or end) of the plateau. The color temperature is therefore continuous, has no jump at the edge of the plateau, and still equals `min_color_temp` exactly at the (offset) sunrise and sunset.
|
||||
|
||||
- A plateau is measured from the same "noon" as the rest of the integration: solar noon, or the middle between sunrise and sunset when `sunrise_time`/`sunset_time` (or their min/max variants) are set. Sunrise and sunset offsets move the far end of the ramp; without fixed times, the anchor stays at solar noon.
|
||||
- Each plateau is limited to 12 hours (43200 seconds) and to 90% of the time between noon and sunrise/sunset, so that a ramp of at least 10% of that time always remains. Larger values are clamped, never an error at runtime.
|
||||
- Only the color temperature is affected. The brightness, the sleep mode, and the color after sunset (including `adapt_until_sleep`) are unchanged.
|
||||
|
||||
## Example
|
||||
|
||||
```yaml
|
||||
adaptive_lighting:
|
||||
- name: kitchen
|
||||
min_color_temp: 2700
|
||||
max_color_temp: 4000
|
||||
sunset_offset: 7200 # 2 hours
|
||||
color_temp_plateau_morning: 3600 # at 4000 K from 1 hour before noon
|
||||
color_temp_plateau_evening: 14400 # stays at 4000 K until 4 hours after noon
|
||||
```
|
||||
|
||||
The same options can be changed at runtime with `adaptive_lighting.change_switch_settings`.
|
||||
|
|
@ -60,6 +60,8 @@ All configuration options are listed below with their default values. These opti
|
|||
| `brightness_mode` | Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈 | `default` | one of `['default', 'linear', 'tanh']` |
|
||||
| `brightness_mode_time_dark` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉 | `900` | `int` |
|
||||
| `brightness_mode_time_light` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉. | `3600` | `int` |
|
||||
| `color_temp_plateau_morning` | The duration in seconds before solar noon during which the color temperature stays at `max_color_temp` instead of only reaching it at noon. The ramp from `min_color_temp` at sunrise to `max_color_temp` is compressed accordingly. `0` keeps the default curve. Only affects the morning. 🌤️ | `0` | `int` 0-43200 |
|
||||
| `color_temp_plateau_evening` | The duration in seconds after solar noon during which the color temperature stays at `max_color_temp` before ramping down to `min_color_temp` at sunset. `0` keeps the default curve. Only affects the evening. 🌇 | `0` | `int` 0-43200 |
|
||||
| `take_over_control` | Pause adaptation of individual lights and hand over (manual) control to other sources that issue `light.turn_on` calls for lights that are on. 🔒 | `True` | `bool` |
|
||||
| `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. | `pause_all` | one of `['pause_all', 'pause_changed']` |
|
||||
| `detect_non_ha_changes` | Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Note that this calls `homeassistant.update_entity` every `interval`! Disable this feature if you encounter such issues. | `False` | `bool` |
|
||||
|
|
|
|||
|
|
@ -547,3 +547,409 @@ 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
|
||||
|
||||
|
||||
# --- Color temperature plateau --------------------------------------------
|
||||
|
||||
PLATEAU_DAY = dt.date(2022, 6, 1)
|
||||
H = dt.timedelta(hours=1)
|
||||
|
||||
|
||||
def _plateau_settings(
|
||||
morning=dt.timedelta(),
|
||||
evening=dt.timedelta(),
|
||||
*,
|
||||
sunset_offset=dt.timedelta(),
|
||||
sunrise_offset=dt.timedelta(),
|
||||
adapt_until_sleep=False,
|
||||
brightness_mode="default",
|
||||
sleep_rgb_or_color_temp="color_temp",
|
||||
min_color_temp=2700,
|
||||
max_color_temp=4000,
|
||||
):
|
||||
"""Settings with fixed sunrise 06:00 and sunset 18:00 UTC (noon is 12:00)."""
|
||||
return SunLightSettings(
|
||||
name="test",
|
||||
astral_observer=location.observer,
|
||||
adapt_until_sleep=adapt_until_sleep,
|
||||
max_brightness=100,
|
||||
max_color_temp=max_color_temp,
|
||||
min_brightness=30,
|
||||
min_color_temp=min_color_temp,
|
||||
sleep_brightness=1,
|
||||
sleep_rgb_or_color_temp=sleep_rgb_or_color_temp,
|
||||
sleep_color_temp=1000,
|
||||
sleep_rgb_color=(255, 56, 0),
|
||||
sunrise_time=dt.time(6, 0),
|
||||
min_sunrise_time=None,
|
||||
max_sunrise_time=None,
|
||||
sunset_time=dt.time(18, 0),
|
||||
min_sunset_time=None,
|
||||
max_sunset_time=None,
|
||||
brightness_mode_time_dark=dt.timedelta(minutes=30),
|
||||
brightness_mode_time_light=dt.timedelta(minutes=30),
|
||||
brightness_mode=brightness_mode,
|
||||
sunrise_offset=sunrise_offset,
|
||||
sunset_offset=sunset_offset,
|
||||
color_temp_plateau_morning=morning,
|
||||
color_temp_plateau_evening=evening,
|
||||
timezone=dt.UTC,
|
||||
)
|
||||
|
||||
|
||||
def _at(hour, minute=0, second=0):
|
||||
return dt.datetime.combine(PLATEAU_DAY, dt.time(hour, minute, second), dt.UTC)
|
||||
|
||||
|
||||
def _kelvin(settings, when, is_sleep=False):
|
||||
return settings.brightness_and_color(when, is_sleep)["color_temp_kelvin"]
|
||||
|
||||
|
||||
def _day_grid(step_minutes=7):
|
||||
start = _at(0)
|
||||
return [start + dt.timedelta(minutes=m) for m in range(0, 24 * 60, step_minutes)]
|
||||
|
||||
|
||||
def _reference_sun_position(sun, when):
|
||||
"""The original (pre-plateau) formula, kept here as the reference."""
|
||||
target_ts = when.timestamp()
|
||||
(_, prev_ts), (next_event, next_ts) = sun.prev_and_next_events(when)
|
||||
h, x = (
|
||||
(prev_ts, next_ts)
|
||||
if next_event in (SunEvent.SUNSET, SunEvent.SUNRISE)
|
||||
else (next_ts, prev_ts)
|
||||
)
|
||||
k = 1 if next_event in (SunEvent.SUNSET, SunEvent.NOON) else -1
|
||||
return k * (1 - ((target_ts - h) / (h - x)) ** 2)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sunset_offset", [dt.timedelta(), 2 * H, -2 * H])
|
||||
@pytest.mark.parametrize("adapt_until_sleep", [True, False])
|
||||
def test_plateau_defaults_are_bit_for_bit_identical(sunset_offset, adapt_until_sleep):
|
||||
default = _plateau_settings(
|
||||
sunset_offset=sunset_offset,
|
||||
adapt_until_sleep=adapt_until_sleep,
|
||||
)
|
||||
explicit_zero = _plateau_settings(
|
||||
dt.timedelta(0),
|
||||
dt.timedelta(seconds=0),
|
||||
sunset_offset=sunset_offset,
|
||||
adapt_until_sleep=adapt_until_sleep,
|
||||
)
|
||||
for when in _day_grid():
|
||||
reference = _reference_sun_position(default.sun, when)
|
||||
assert default.sun.sun_position(when) == reference
|
||||
assert default.sun.sun_position(when, dt.timedelta(), dt.timedelta()) == (
|
||||
reference
|
||||
)
|
||||
for is_sleep in (False, True):
|
||||
assert default.brightness_and_color(
|
||||
when,
|
||||
is_sleep,
|
||||
) == explicit_zero.brightness_and_color(when, is_sleep)
|
||||
assert default.color_temp_kelvin(reference) == _kelvin(default, when)
|
||||
|
||||
|
||||
def test_plateau_morning_shape():
|
||||
settings = _plateau_settings(morning=3 * H)
|
||||
assert _kelvin(settings, _at(6)) == 2700 # sunrise: still the minimum
|
||||
assert _kelvin(settings, _at(9)) == 4000 # plateau starts 3h before noon
|
||||
assert _kelvin(settings, _at(9, 0, 1)) == 4000
|
||||
assert _kelvin(settings, _at(10, 30)) == 4000
|
||||
assert _kelvin(settings, _at(12)) == 4000
|
||||
# the ramp is compressed: reaching 4000 earlier than without plateau
|
||||
default = _plateau_settings()
|
||||
assert _kelvin(settings, _at(8)) > _kelvin(default, _at(8))
|
||||
# and is monotonic
|
||||
values = [_kelvin(settings, _at(6) + dt.timedelta(minutes=m)) for m in range(361)]
|
||||
assert values == sorted(values)
|
||||
|
||||
|
||||
def test_plateau_evening_shape():
|
||||
settings = _plateau_settings(evening=4 * H)
|
||||
assert _kelvin(settings, _at(12)) == 4000
|
||||
assert _kelvin(settings, _at(16)) == 4000 # plateau ends 4h after noon
|
||||
assert _kelvin(settings, _at(16, 0, 1)) >= 3995
|
||||
assert _kelvin(settings, _at(18)) == 2700 # sunset: the minimum again
|
||||
default = _plateau_settings()
|
||||
assert _kelvin(settings, _at(15)) > _kelvin(default, _at(15))
|
||||
values = [_kelvin(settings, _at(12) + dt.timedelta(minutes=m)) for m in range(361)]
|
||||
assert values == sorted(values, reverse=True)
|
||||
|
||||
|
||||
def test_plateau_sides_are_independent():
|
||||
default = _plateau_settings()
|
||||
morning = _plateau_settings(morning=3 * H)
|
||||
evening = _plateau_settings(evening=3 * H)
|
||||
both = _plateau_settings(morning=3 * H, evening=3 * H)
|
||||
noon = _at(12)
|
||||
for when in _day_grid(5):
|
||||
before_noon = when < noon
|
||||
after_noon = when > noon
|
||||
if after_noon:
|
||||
# a morning plateau never changes the evening
|
||||
assert morning.brightness_and_color(
|
||||
when,
|
||||
False,
|
||||
) == default.brightness_and_color(when, False)
|
||||
assert both.brightness_and_color(when, False) == (
|
||||
evening.brightness_and_color(when, False)
|
||||
)
|
||||
if before_noon:
|
||||
# an evening plateau never changes the morning
|
||||
assert evening.brightness_and_color(
|
||||
when,
|
||||
False,
|
||||
) == default.brightness_and_color(when, False)
|
||||
assert both.brightness_and_color(when, False) == (
|
||||
morning.brightness_and_color(when, False)
|
||||
)
|
||||
|
||||
|
||||
def test_plateau_is_continuous_at_its_boundaries():
|
||||
settings = _plateau_settings(morning=2 * H, evening=2 * H)
|
||||
eps = dt.timedelta(milliseconds=1)
|
||||
for boundary in (_at(10), _at(14)):
|
||||
before = settings.sun.sun_position(boundary - eps, 2 * H, 2 * H)
|
||||
after = settings.sun.sun_position(boundary + eps, 2 * H, 2 * H)
|
||||
assert abs(before - after) < 1e-6
|
||||
assert max(before, after) == pytest.approx(1, abs=1e-6)
|
||||
# sunrise and sunset still map to exactly 0
|
||||
for event in (_at(6), _at(18)):
|
||||
assert settings.sun.sun_position(event, 2 * H, 2 * H) == pytest.approx(
|
||||
0,
|
||||
abs=1e-9,
|
||||
)
|
||||
|
||||
|
||||
def test_plateau_never_decreases_color_temp():
|
||||
default = _plateau_settings()
|
||||
plateau = _plateau_settings(morning=5 * H, evening=3 * H)
|
||||
for when in _day_grid(3):
|
||||
assert _kelvin(plateau, when) >= _kelvin(default, when)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("plateau", [dt.timedelta(hours=12), dt.timedelta(days=3)])
|
||||
def test_plateau_larger_than_the_half_day_is_clamped(plateau):
|
||||
settings = _plateau_settings(plateau, plateau)
|
||||
sun = settings.sun
|
||||
assert _kelvin(settings, _at(6)) == 2700
|
||||
assert _kelvin(settings, _at(18)) == 2700
|
||||
assert _kelvin(settings, _at(12)) == 4000
|
||||
previous = None
|
||||
for when in _day_grid(1):
|
||||
position = sun.sun_position(when, plateau, plateau)
|
||||
assert -1 <= position <= 1
|
||||
if previous is not None and _at(6) <= when <= _at(18):
|
||||
# no jump bigger than what a smooth ramp over 10% of the half-day
|
||||
# (36 minutes) allows between consecutive minutes
|
||||
assert abs(position - previous) < 0.1
|
||||
previous = position
|
||||
# a ramp of 10% of the half-day (36 minutes) remains at both ends
|
||||
assert _kelvin(settings, _at(17, 59)) < 4000
|
||||
assert _kelvin(settings, _at(17, 30)) < 4000
|
||||
assert _kelvin(settings, _at(6, 30)) < 4000
|
||||
|
||||
|
||||
def test_plateau_with_a_collapsed_day():
|
||||
# sunrise and sunset only a few seconds apart around noon
|
||||
settings = SunLightSettings(
|
||||
name="test",
|
||||
astral_observer=location.observer,
|
||||
adapt_until_sleep=False,
|
||||
max_brightness=100,
|
||||
max_color_temp=4000,
|
||||
min_brightness=30,
|
||||
min_color_temp=2700,
|
||||
sleep_brightness=1,
|
||||
sleep_rgb_or_color_temp="color_temp",
|
||||
sleep_color_temp=1000,
|
||||
sleep_rgb_color=(255, 56, 0),
|
||||
sunrise_time=dt.time(11, 59, 58),
|
||||
min_sunrise_time=None,
|
||||
max_sunrise_time=None,
|
||||
sunset_time=dt.time(12, 0, 2),
|
||||
min_sunset_time=None,
|
||||
max_sunset_time=None,
|
||||
brightness_mode_time_dark=dt.timedelta(minutes=30),
|
||||
brightness_mode_time_light=dt.timedelta(minutes=30),
|
||||
color_temp_plateau_morning=12 * H,
|
||||
color_temp_plateau_evening=12 * H,
|
||||
timezone=dt.UTC,
|
||||
)
|
||||
for seconds in range(-5, 6):
|
||||
when = _at(12) + dt.timedelta(seconds=seconds)
|
||||
assert 2700 <= _kelvin(settings, when) <= 4000
|
||||
assert _kelvin(settings, _at(12)) == 4000
|
||||
assert _kelvin(settings, _at(3)) == 2700
|
||||
|
||||
|
||||
def test_plateau_does_not_touch_the_night_or_sleep():
|
||||
default = _plateau_settings(adapt_until_sleep=True)
|
||||
plateau = _plateau_settings(5 * H, 5 * H, adapt_until_sleep=True)
|
||||
for when in _day_grid(11):
|
||||
if when < _at(6) or when > _at(18):
|
||||
# below the horizon (including the transition to the sleep color)
|
||||
assert plateau.brightness_and_color(when, False) == (
|
||||
default.brightness_and_color(when, False)
|
||||
)
|
||||
# sleep mode ignores the color temperature curve entirely
|
||||
sleep = plateau.brightness_and_color(when, True)
|
||||
assert sleep == default.brightness_and_color(when, True)
|
||||
assert sleep["color_temp_kelvin"] == 1000
|
||||
assert sleep["brightness_pct"] == 1
|
||||
|
||||
|
||||
def test_plateau_with_rgb_sleep_color_after_sunset():
|
||||
default = _plateau_settings(
|
||||
adapt_until_sleep=True,
|
||||
sleep_rgb_or_color_temp="rgb_color",
|
||||
)
|
||||
plateau = _plateau_settings(
|
||||
4 * H,
|
||||
4 * H,
|
||||
adapt_until_sleep=True,
|
||||
sleep_rgb_or_color_temp="rgb_color",
|
||||
)
|
||||
for when in (_at(19), _at(22), _at(1), _at(5, 30)):
|
||||
assert plateau.brightness_and_color(when, False) == (
|
||||
default.brightness_and_color(when, False)
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("brightness_mode", ["default", "linear", "tanh"])
|
||||
def test_plateau_does_not_change_brightness(brightness_mode):
|
||||
default = _plateau_settings(brightness_mode=brightness_mode)
|
||||
plateau = _plateau_settings(
|
||||
5 * H,
|
||||
5 * H,
|
||||
brightness_mode=brightness_mode,
|
||||
adapt_until_sleep=True,
|
||||
)
|
||||
reference = _plateau_settings(
|
||||
brightness_mode=brightness_mode,
|
||||
adapt_until_sleep=True,
|
||||
)
|
||||
for when in _day_grid(9):
|
||||
for is_sleep in (False, True):
|
||||
assert plateau.brightness_pct(when, is_sleep) == (
|
||||
reference.brightness_pct(when, is_sleep)
|
||||
)
|
||||
result = plateau.brightness_and_color(when, is_sleep)
|
||||
ref = reference.brightness_and_color(when, is_sleep)
|
||||
assert result["brightness_pct"] == ref["brightness_pct"]
|
||||
# the (brightness-driving) sun position itself is left untouched
|
||||
assert result["sun_position"] == ref["sun_position"]
|
||||
assert default.brightness_pct(when, False) == reference.brightness_pct(
|
||||
when,
|
||||
False,
|
||||
)
|
||||
|
||||
|
||||
def test_plateau_with_sunset_offset_is_anchored_on_noon():
|
||||
# sunrise 06:00 and sunset 18:00 + 2h offset = 20:00, so the (adjusted) noon,
|
||||
# the middle of the two, is 13:00 and the evening ramp spans 13:00 -> 20:00
|
||||
settings = _plateau_settings(evening=4 * H, sunset_offset=2 * H)
|
||||
default = _plateau_settings(sunset_offset=2 * H)
|
||||
assert _kelvin(settings, _at(11)) == _kelvin(default, _at(11))
|
||||
assert _kelvin(settings, _at(13)) == 4000
|
||||
assert _kelvin(settings, _at(17)) == 4000 # plateau ends 4h after noon
|
||||
assert _kelvin(settings, _at(18)) < 4000
|
||||
assert _kelvin(settings, _at(18)) > _kelvin(default, _at(18))
|
||||
assert _kelvin(settings, _at(20)) == 2700 # the offset sunset is respected
|
||||
# the morning is not affected by the sunset offset nor the other way around
|
||||
morning = _plateau_settings(morning=2 * H, sunset_offset=2 * H)
|
||||
assert _kelvin(morning, _at(11)) == 4000
|
||||
assert _kelvin(morning, _at(8)) > _kelvin(default, _at(8))
|
||||
assert _kelvin(morning, _at(14)) == _kelvin(default, _at(14))
|
||||
|
||||
|
||||
def test_plateau_with_sunrise_and_sunset_times_and_limits():
|
||||
settings = _plateau_settings(morning=2 * H, evening=2 * H, sunrise_offset=-1 * H)
|
||||
# sunrise is 05:00 and sunset 18:00: noon is their midpoint (11:30)
|
||||
sunrise = settings.sun.sunrise(PLATEAU_DAY)
|
||||
sunset = settings.sun.sunset(PLATEAU_DAY)
|
||||
noon, _ = settings.sun.noon_and_midnight(_at(12))
|
||||
assert noon == sunrise + (sunset - sunrise) / 2
|
||||
assert _kelvin(settings, noon) == 4000
|
||||
assert _kelvin(settings, noon - 2 * H) == 4000
|
||||
assert _kelvin(settings, noon + 2 * H) == 4000
|
||||
assert _kelvin(settings, sunrise) == 2700
|
||||
assert _kelvin(settings, sunset) == 2700
|
||||
|
||||
|
||||
def test_plateau_astral_sunrise_and_sunset(tzinfo_and_location):
|
||||
tzinfo, loc = tzinfo_and_location
|
||||
kwargs = {
|
||||
"name": "test",
|
||||
"astral_observer": loc.observer,
|
||||
"adapt_until_sleep": False,
|
||||
"max_brightness": 100,
|
||||
"max_color_temp": 4000,
|
||||
"min_brightness": 30,
|
||||
"min_color_temp": 2700,
|
||||
"sleep_brightness": 1,
|
||||
"sleep_rgb_or_color_temp": "color_temp",
|
||||
"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(minutes=30),
|
||||
"brightness_mode_time_light": dt.timedelta(minutes=30),
|
||||
"sunset_offset": 2 * H,
|
||||
"timezone": tzinfo,
|
||||
}
|
||||
default = SunLightSettings(**kwargs)
|
||||
plateau = SunLightSettings(
|
||||
**kwargs,
|
||||
color_temp_plateau_morning=2 * H,
|
||||
color_temp_plateau_evening=4 * H,
|
||||
)
|
||||
day = dt.datetime(2022, 3, 20, tzinfo=dt.UTC)
|
||||
noon, _ = default.sun.noon_and_midnight(day)
|
||||
assert _kelvin(plateau, noon) == 4000
|
||||
assert _kelvin(plateau, noon - 2 * H) == 4000
|
||||
assert _kelvin(plateau, noon + 4 * H) == 4000
|
||||
assert _kelvin(plateau, default.sun.sunset(day.date())) == 2700
|
||||
assert _kelvin(plateau, default.sun.sunrise(day.date())) == 2700
|
||||
|
||||
|
||||
@pytest.mark.parametrize("date", [POLAR_NIGHT_DATE, MIDNIGHT_SUN_DATE])
|
||||
def test_plateau_on_polar_days(date):
|
||||
kwargs = {
|
||||
"name": "test",
|
||||
"astral_observer": TROMSO.observer,
|
||||
"adapt_until_sleep": False,
|
||||
"max_brightness": 100,
|
||||
"max_color_temp": 4000,
|
||||
"min_brightness": 30,
|
||||
"min_color_temp": 2700,
|
||||
"sleep_brightness": 1,
|
||||
"sleep_rgb_or_color_temp": "color_temp",
|
||||
"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": zoneinfo.ZoneInfo("Europe/Oslo"),
|
||||
}
|
||||
plateau = SunLightSettings(
|
||||
**kwargs,
|
||||
color_temp_plateau_morning=12 * H,
|
||||
color_temp_plateau_evening=12 * H,
|
||||
)
|
||||
start = dt.datetime(date.year, date.month, date.day, tzinfo=dt.UTC)
|
||||
noon, _ = plateau.sun.noon_and_midnight(start)
|
||||
assert _kelvin(plateau, noon) == 4000
|
||||
for minutes in range(0, 24 * 60, 5):
|
||||
when = start + dt.timedelta(minutes=minutes)
|
||||
assert 2700 <= _kelvin(plateau, when) <= 4000
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ except ImportError:
|
|||
from voluptuous_serialize import convert as to_field_list
|
||||
from homeassistant.components.adaptive_lighting.const import (
|
||||
BASIC_OPTIONS,
|
||||
CONF_COLOR_TEMP_PLATEAU_EVENING,
|
||||
CONF_COLOR_TEMP_PLATEAU_MORNING,
|
||||
CONF_EXPAND_LIGHT_GROUPS,
|
||||
CONF_INITIAL_TRANSITION,
|
||||
CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON,
|
||||
|
|
@ -108,6 +110,8 @@ async def test_options(hass):
|
|||
advanced_data = ADVANCED_DATA.copy()
|
||||
advanced_data[CONF_INITIAL_TRANSITION] = 23
|
||||
advanced_data[CONF_EXPAND_LIGHT_GROUPS] = False
|
||||
advanced_data[CONF_COLOR_TEMP_PLATEAU_MORNING] = 1800
|
||||
advanced_data[CONF_COLOR_TEMP_PLATEAU_EVENING] = 10800
|
||||
advanced_data[CONF_SUNRISE_TIME] = NONE_STR
|
||||
advanced_data[CONF_SUNSET_TIME] = NONE_STR
|
||||
basic_data = {**BASIC_DATA, "min_brightness": 12}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ from homeassistant.components.adaptive_lighting.const import (
|
|||
CONF_BRIGHTNESS_MODE,
|
||||
CONF_BRIGHTNESS_MODE_TIME_DARK,
|
||||
CONF_BRIGHTNESS_MODE_TIME_LIGHT,
|
||||
CONF_COLOR_TEMP_PLATEAU_EVENING,
|
||||
CONF_COLOR_TEMP_PLATEAU_MORNING,
|
||||
CONF_DETECT_NON_HA_CHANGES,
|
||||
CONF_EXPAND_LIGHT_GROUPS,
|
||||
CONF_INITIAL_TRANSITION,
|
||||
|
|
@ -2904,6 +2906,72 @@ async def test_change_switch_settings_service(hass):
|
|||
assert switch._sun_light_settings.min_color_temp == 2500
|
||||
|
||||
|
||||
async def test_color_temp_plateau_defaults_and_service(hass):
|
||||
"""The plateau options default to 0 and can be changed at runtime."""
|
||||
switch, _ = await setup_lights_and_switch(hass)
|
||||
settings = switch._sun_light_settings
|
||||
assert settings.color_temp_plateau_morning == datetime.timedelta(0)
|
||||
assert settings.color_temp_plateau_evening == datetime.timedelta(0)
|
||||
|
||||
async def change_switch_settings(**kwargs):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_CHANGE_SWITCH_SETTINGS,
|
||||
{ATTR_ENTITY_ID: switch.entity_id, **kwargs},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await change_switch_settings(**{CONF_COLOR_TEMP_PLATEAU_EVENING: 7200})
|
||||
settings = switch._sun_light_settings
|
||||
assert settings.color_temp_plateau_evening == datetime.timedelta(hours=2)
|
||||
# the other side is untouched
|
||||
assert settings.color_temp_plateau_morning == datetime.timedelta(0)
|
||||
|
||||
await change_switch_settings(**{CONF_COLOR_TEMP_PLATEAU_MORNING: 3600})
|
||||
settings = switch._sun_light_settings
|
||||
assert settings.color_temp_plateau_morning == datetime.timedelta(hours=1)
|
||||
assert settings.color_temp_plateau_evening == datetime.timedelta(hours=2)
|
||||
|
||||
for key in (CONF_COLOR_TEMP_PLATEAU_MORNING, CONF_COLOR_TEMP_PLATEAU_EVENING):
|
||||
for invalid in (-1, 43201):
|
||||
with pytest.raises(voluptuous.error.MultipleInvalid):
|
||||
await change_switch_settings(**{key: invalid})
|
||||
|
||||
await change_switch_settings(**{CONF_USE_DEFAULTS: "factory"})
|
||||
settings = switch._sun_light_settings
|
||||
assert settings.color_temp_plateau_morning == datetime.timedelta(0)
|
||||
assert settings.color_temp_plateau_evening == datetime.timedelta(0)
|
||||
|
||||
|
||||
async def test_color_temp_plateau_from_configuration(hass):
|
||||
"""The options from the config entry reach the sun light settings."""
|
||||
_, switch = await setup_switch(
|
||||
hass,
|
||||
{
|
||||
CONF_COLOR_TEMP_PLATEAU_MORNING: 1800,
|
||||
CONF_COLOR_TEMP_PLATEAU_EVENING: datetime.timedelta(hours=3),
|
||||
},
|
||||
)
|
||||
settings = switch._sun_light_settings
|
||||
assert settings.color_temp_plateau_morning == datetime.timedelta(minutes=30)
|
||||
assert settings.color_temp_plateau_evening == datetime.timedelta(hours=3)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("invalid", [-1, 43201])
|
||||
@pytest.mark.parametrize(
|
||||
"key",
|
||||
[CONF_COLOR_TEMP_PLATEAU_MORNING, CONF_COLOR_TEMP_PLATEAU_EVENING],
|
||||
)
|
||||
def test_color_temp_plateau_yaml_validation(invalid, key):
|
||||
"""Out of range plateaus are rejected in the YAML schema."""
|
||||
from homeassistant.components.adaptive_lighting.const import _DOMAIN_SCHEMA
|
||||
|
||||
assert _DOMAIN_SCHEMA({key: 3600})[key] == 3600
|
||||
with pytest.raises(voluptuous.error.MultipleInvalid):
|
||||
_DOMAIN_SCHEMA({key: invalid})
|
||||
|
||||
|
||||
@pytest.mark.parametrize("target", ["entity", "area", "device", "all"])
|
||||
async def test_change_switch_settings_entity_targets(hass, device_registry, target):
|
||||
"""Test settings changes through Home Assistant entity targets."""
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ nav = [
|
|||
{ "Troubleshooting" = "troubleshooting.md" },
|
||||
{ "Advanced" = [
|
||||
{ "Brightness Modes" = "advanced/brightness-modes.md" },
|
||||
{ "Color Temperature Plateau" = "advanced/color-temperature-plateau.md" },
|
||||
{ "Manual Control" = "advanced/manual-control.md" },
|
||||
{ "Sleep Mode" = "advanced/sleep-mode.md" },
|
||||
] },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue