adaptive-lighting/custom_components/adaptive_lighting/const.py
Charles-HL 238d189f40 feat: add custom evening plateau color temperature
Add the optional color_temp_fixed_plateau_kelvin setting (between
min_color_temp and max_color_temp) for the fixed evening schedule. The
daytime maximum is kept; after it the color temperature descends along the
regular afternoon curve, reaches the target exactly and holds it for the last
hour before color_temp_fixed_plateau_end, whatever the season, then eases to
min_color_temp as before. The curve is sped up when the regular one would be
too late (summer), without any step. Unset keeps the previous behavior.

Validated in the options flow, YAML, the stored configuration and the
change_switch_settings service, with services, translations and docs.
2026-10-19 17:50:23 +02:00

724 lines
28 KiB
Python

"""Constants for the Adaptive Lighting integration."""
from datetime import date, datetime, time, timedelta
from enum import Enum
from typing import Any
import homeassistant.helpers.config_validation as cv
import voluptuous as vol
from homeassistant.components.light import VALID_TRANSITION
from homeassistant.const import CONF_ENTITY_ID
from homeassistant.helpers import selector
ICON_MAIN = "mdi:theme-light-dark"
ICON_BRIGHTNESS = "mdi:brightness-4"
ICON_COLOR_TEMP = "mdi:sun-thermometer"
ICON_SLEEP = "mdi:sleep"
DOMAIN = "adaptive_lighting"
class TakeOverControlMode(Enum):
"""Modes for pausing adaptation when control of a light is taken over externally."""
PAUSE_ALL = "pause_all"
PAUSE_CHANGED = "pause_changed"
DOCS = {CONF_ENTITY_ID: "Entity ID of the switch. 📝"}
CONF_NAME, DEFAULT_NAME = "name", "default"
DOCS[CONF_NAME] = "Display name for this switch. 📝"
CONF_LIGHTS, DEFAULT_LIGHTS = "lights", []
DOCS[CONF_LIGHTS] = "List of light entity_ids to be controlled (may be empty). 🌟"
CONF_DETECT_NON_HA_CHANGES, DEFAULT_DETECT_NON_HA_CHANGES = (
"detect_non_ha_changes",
False,
)
DOCS[CONF_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."
)
CONF_INCLUDE_CONFIG_IN_ATTRIBUTES, DEFAULT_INCLUDE_CONFIG_IN_ATTRIBUTES = (
"include_config_in_attributes",
False,
)
DOCS[CONF_INCLUDE_CONFIG_IN_ATTRIBUTES] = (
"Show all options as attributes on the switch in "
"Home Assistant when set to `true`. 📝"
)
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. ⏲️"
)
CONF_SLEEP_TRANSITION, DEFAULT_SLEEP_TRANSITION = "sleep_transition", 1
DOCS[CONF_SLEEP_TRANSITION] = (
'Duration of transition when "sleep mode" is toggled in seconds. 😴'
)
CONF_INTERVAL, DEFAULT_INTERVAL = "interval", 90
DOCS[CONF_INTERVAL] = "Frequency to adapt the lights, in seconds. 🔄"
CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS = "max_brightness", 100
DOCS[CONF_MAX_BRIGHTNESS] = "Maximum brightness percentage. 💡"
CONF_MAX_COLOR_TEMP, DEFAULT_MAX_COLOR_TEMP = "max_color_temp", 5500
DOCS[CONF_MAX_COLOR_TEMP] = "Coldest color temperature in Kelvin. ❄️"
CONF_MIN_BRIGHTNESS, DEFAULT_MIN_BRIGHTNESS = "min_brightness", 1
DOCS[CONF_MIN_BRIGHTNESS] = "Minimum brightness percentage. 💡"
CONF_MIN_COLOR_TEMP, DEFAULT_MIN_COLOR_TEMP = "min_color_temp", 2000
DOCS[CONF_MIN_COLOR_TEMP] = "Warmest color temperature in Kelvin. 🔥"
CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE = "only_once", False
DOCS[CONF_ONLY_ONCE] = (
"Adapt lights only when they are turned on (`true`) or keep adapting them "
"(`false`). 🔄"
)
CONF_ADAPT_ONLY_ON_BARE_TURN_ON, DEFAULT_ADAPT_ONLY_ON_BARE_TURN_ON = (
"adapt_only_on_bare_turn_on",
False,
)
DOCS[CONF_ADAPT_ONLY_ON_BARE_TURN_ON] = (
"When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is "
"invoked without specifying color or brightness. ❌🌈 "
"This e.g., prevents adaptation when activating a scene and marks the light as manually controlled. "
"If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. "
"Needs `take_over_control` enabled. 🕵️"
)
CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON, DEFAULT_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON = (
"manual_control_on_external_turn_on",
False,
)
DOCS[CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON] = (
"Treat turn-ons without a matching Home Assistant `light.turn_on` context as "
"manual control. Normal manual-control resets apply. Still allows "
"`detect_non_ha_changes` for already-on lights. Needs `take_over_control` enabled. 🕵️"
)
CONF_PREFER_RGB_COLOR, DEFAULT_PREFER_RGB_COLOR = "prefer_rgb_color", False
DOCS[CONF_PREFER_RGB_COLOR] = (
"Whether to prefer RGB color adjustment over "
"light color temperature when possible. 🌈"
)
CONF_SEPARATE_TURN_ON_COMMANDS, DEFAULT_SEPARATE_TURN_ON_COMMANDS = (
"separate_turn_on_commands",
False,
)
DOCS[CONF_SEPARATE_TURN_ON_COMMANDS] = (
"Use separate `light.turn_on` calls for color and brightness, needed for "
"some light types. 🔀"
)
CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS = "sleep_brightness", 1
DOCS[CONF_SLEEP_BRIGHTNESS] = "Brightness percentage of lights in sleep mode. 😴"
CONF_SLEEP_COLOR_TEMP, DEFAULT_SLEEP_COLOR_TEMP = "sleep_color_temp", 1000
DOCS[CONF_SLEEP_COLOR_TEMP] = (
"Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is "
"`color_temp`) in Kelvin. 😴"
)
CONF_SLEEP_RGB_COLOR, DEFAULT_SLEEP_RGB_COLOR = "sleep_rgb_color", [255, 56, 0]
DOCS[CONF_SLEEP_RGB_COLOR] = (
'RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈'
)
CONF_SLEEP_RGB_OR_COLOR_TEMP, DEFAULT_SLEEP_RGB_OR_COLOR_TEMP = (
"sleep_rgb_or_color_temp",
"color_temp",
)
DOCS[CONF_SLEEP_RGB_OR_COLOR_TEMP] = (
'Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙'
)
CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET = "sunrise_offset", 0
DOCS[CONF_SUNRISE_OFFSET] = (
"Adjust sunrise time with a positive or negative offset in seconds. ⏰"
)
CONF_SUNRISE_TIME = "sunrise_time"
DOCS[CONF_SUNRISE_TIME] = "Set a fixed time (HH:MM:SS) for sunrise. 🌅"
CONF_MIN_SUNRISE_TIME = "min_sunrise_time"
DOCS[CONF_MIN_SUNRISE_TIME] = (
"Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅"
)
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. 🌅"
)
CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET = "sunset_offset", 0
DOCS[CONF_SUNSET_OFFSET] = (
"Adjust sunset time with a positive or negative offset in seconds. ⏰"
)
CONF_SUNSET_TIME = "sunset_time"
DOCS[CONF_SUNSET_TIME] = "Set a fixed time (HH:MM:SS) for sunset. 🌇"
CONF_MIN_SUNSET_TIME = "min_sunset_time"
DOCS[CONF_MIN_SUNSET_TIME] = (
"Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇"
)
CONF_MAX_SUNSET_TIME = "max_sunset_time"
DOCS[CONF_MAX_SUNSET_TIME] = (
"Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇"
)
CONF_BRIGHTNESS_MODE, DEFAULT_BRIGHTNESS_MODE = "brightness_mode", "default"
DOCS[CONF_BRIGHTNESS_MODE] = (
"Brightness mode to use. Possible values are `default`, `linear`, and `tanh` "
"(uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈"
)
CONF_BRIGHTNESS_MODE_TIME_DARK, DEFAULT_BRIGHTNESS_MODE_TIME_DARK = (
"brightness_mode_time_dark",
900,
)
DOCS[CONF_BRIGHTNESS_MODE_TIME_DARK] = (
"(Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down "
"the brightness before/after sunrise/sunset. 📈📉"
)
CONF_BRIGHTNESS_MODE_TIME_LIGHT, DEFAULT_BRIGHTNESS_MODE_TIME_LIGHT = (
"brightness_mode_time_light",
3600,
)
DOCS[CONF_BRIGHTNESS_MODE_TIME_LIGHT] = (
"(Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down "
"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_COLOR_TEMP_FIXED_PLATEAU_END = "color_temp_fixed_plateau_end"
DOCS[CONF_COLOR_TEMP_FIXED_PLATEAU_END] = (
"Local time (HH:MM:SS, 12:00 or later) until which the color temperature stays at "
"`max_color_temp` in the evening, every day of the year. Needs "
"`color_temp_fixed_descent_end`. Leave unset to keep the default evening curve. "
"Only affects the color temperature. 🌙"
)
CONF_COLOR_TEMP_FIXED_DESCENT_END = "color_temp_fixed_descent_end"
DOCS[CONF_COLOR_TEMP_FIXED_DESCENT_END] = (
"Local time (HH:MM:SS) at which the color temperature has smoothly descended from "
"`max_color_temp` to `min_color_temp`, starting at `color_temp_fixed_plateau_end`. "
"May be after midnight but at most 12 hours after the plateau end. Needs "
"`color_temp_fixed_plateau_end`. Leave unset to keep the default evening curve. 🌙"
)
CONF_COLOR_TEMP_FIXED_PLATEAU_KELVIN = "color_temp_fixed_plateau_kelvin"
DOCS[CONF_COLOR_TEMP_FIXED_PLATEAU_KELVIN] = (
"Evening temperature in Kelvin (between `min_color_temp` and `max_color_temp`), "
"reached and held for the last hour before `color_temp_fixed_plateau_end`; the "
"daytime maximum is not capped. Needs both fixed times and is ignored when the "
"plateau end is less than 1 h after solar noon. Applied in steps of 5 K. "
"Unset holds `max_color_temp`. 🌙"
)
# The descent from the plateau end to the descent end lasts at most this long.
MAX_COLOR_TEMP_FIXED_DESCENT = timedelta(hours=12)
MIN_COLOR_TEMP_FIXED_PLATEAU_END = time(12, 0)
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 "
"issue `light.turn_on` calls for lights that are on. 🔒"
)
CONF_TAKE_OVER_CONTROL_MODE, DEFAULT_TAKE_OVER_CONTROL_MODE = (
"take_over_control_mode",
TakeOverControlMode.PAUSE_ALL.value,
)
DOCS[CONF_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."
)
CONF_TRANSITION, DEFAULT_TRANSITION = "transition", 45
DOCS[CONF_TRANSITION] = "Duration of transition when lights change, in seconds. 🕑"
CONF_ADAPT_UNTIL_SLEEP, DEFAULT_ADAPT_UNTIL_SLEEP = (
"transition_until_sleep",
False,
)
DOCS[CONF_ADAPT_UNTIL_SLEEP] = (
"When enabled, Adaptive Lighting will treat sleep settings as the minimum, "
"transitioning to these values after sunset. 🌙"
)
CONF_ADAPT_DELAY, DEFAULT_ADAPT_DELAY = "adapt_delay", 0
DOCS[CONF_ADAPT_DELAY] = (
"Wait time (seconds) between light turn on and Adaptive Lighting applying "
"changes. Might help to avoid flickering. ⏲️"
)
CONF_SEND_SPLIT_DELAY, DEFAULT_SEND_SPLIT_DELAY = "send_split_delay", 0
DOCS[CONF_SEND_SPLIT_DELAY] = (
"Delay (ms) between `separate_turn_on_commands` for lights that don't support "
"simultaneous brightness and color setting. ⏲️"
)
CONF_AUTORESET_CONTROL, DEFAULT_AUTORESET_CONTROL = "autoreset_control_seconds", 0
DOCS[CONF_AUTORESET_CONTROL] = (
"Automatically reset the manual control after a number of seconds. "
"Set to 0 to disable. ⏲️"
)
(
CONF_RESET_MANUAL_CONTROL_ON_SLEEP_MODE_CHANGE,
DEFAULT_RESET_MANUAL_CONTROL_ON_SLEEP_MODE_CHANGE,
) = ("reset_manual_control_on_sleep_mode_change", True)
DOCS[CONF_RESET_MANUAL_CONTROL_ON_SLEEP_MODE_CHANGE] = (
"Reset manual control when the sleep mode switch is toggled. "
"Set to `false` to preserve manual control across sleep mode changes. 😴"
)
CONF_SKIP_REDUNDANT_COMMANDS, DEFAULT_SKIP_REDUNDANT_COMMANDS = (
"skip_redundant_commands",
False,
)
DOCS[CONF_SKIP_REDUNDANT_COMMANDS] = (
"Skip sending adaptation commands whose target state already "
"equals the light's known state. Minimizes network traffic and improves the "
"adaptation responsivity in some situations. 📉"
"Disable if physical light states get out of sync with HA's recorded state."
)
CONF_INTERCEPT, DEFAULT_INTERCEPT = "intercept", True
DOCS[CONF_INTERCEPT] = (
"Intercept and adapt `light.turn_on` calls to enabling instantaneous color "
"and brightness adaptation. 🏎️ Disable for lights that do not "
"support `light.turn_on` with color and brightness."
)
CONF_MULTI_LIGHT_INTERCEPT, DEFAULT_MULTI_LIGHT_INTERCEPT = (
"multi_light_intercept",
True,
)
DOCS[CONF_MULTI_LIGHT_INTERCEPT] = (
"Intercept and adapt `light.turn_on` calls that target multiple lights. ➗"
"⚠️ This might result in splitting up a single `light.turn_on` call "
"into multiple calls, e.g., when lights are in different switches. "
"Requires `intercept` to be enabled."
)
CONF_EXPAND_LIGHT_GROUPS, DEFAULT_EXPAND_LIGHT_GROUPS = "expand_light_groups", True
DOCS[CONF_EXPAND_LIGHT_GROUPS] = (
"Expand light groups to their members (`true`, default). Set `false` to send "
"commands to the group and track manual control for the group. Explicit member "
"targets in services stay individual targets."
)
SLEEP_MODE_SWITCH = "sleep_mode_switch"
ADAPT_COLOR_SWITCH = "adapt_color_switch"
ADAPT_BRIGHTNESS_SWITCH = "adapt_brightness_switch"
ATTR_ADAPTIVE_LIGHTING_MANAGER = "manager"
UNDO_UPDATE_LISTENER = "undo_update_listener"
NONE_STR = "None"
ATTR_ADAPT_COLOR = "adapt_color"
DOCS[ATTR_ADAPT_COLOR] = "Whether to adapt the color on supporting lights. 🌈"
ATTR_ADAPT_BRIGHTNESS = "adapt_brightness"
DOCS[ATTR_ADAPT_BRIGHTNESS] = "Whether to adapt the brightness of the light. 🌞"
SERVICE_SET_MANUAL_CONTROL = "set_manual_control"
CONF_MANUAL_CONTROL = "manual_control"
DOCS[CONF_MANUAL_CONTROL] = "Whether to manually control the lights. 🔒"
SERVICE_APPLY = "apply"
CONF_TURN_ON_LIGHTS = "turn_on_lights"
DOCS[CONF_TURN_ON_LIGHTS] = "Whether to turn on lights that are currently off. 🔆"
SERVICE_CHANGE_SWITCH_SETTINGS = "change_switch_settings"
CONF_USE_DEFAULTS = "use_defaults"
DOCS[CONF_USE_DEFAULTS] = (
"Sets the default values not specified in this service call. Options: "
'"current" (default, retains current values), "factory" (resets to '
'documented defaults), or "configuration" (reverts to switch config defaults). ⚙️'
)
TURNING_OFF_DELAY = 5
DOCS_MANUAL_CONTROL = {
CONF_ENTITY_ID: "The `entity_id` of the switch in which to (un)mark the "
"light as being `manually controlled`. 📝",
CONF_LIGHTS: "entity_id(s) of lights, if not specified, all lights in the "
"switch are selected. 💡",
CONF_MANUAL_CONTROL: 'Whether to add ("true") or remove ("false") all '
'adapted attributes of the light from the "manual_control" list, or the '
"name of an attribute for selective addition. 🔒",
}
DOCS_APPLY = {
CONF_ENTITY_ID: "The `entity_id` of the switch with the settings to apply. 📝",
CONF_LIGHTS: "A light (or list of lights) to apply the settings to. 💡",
}
# Basic options shown at top level in options flow (not in collapsed section)
BASIC_OPTIONS: set[str] = {
CONF_LIGHTS,
CONF_MIN_BRIGHTNESS,
CONF_MAX_BRIGHTNESS,
CONF_MIN_COLOR_TEMP,
CONF_MAX_COLOR_TEMP,
CONF_SLEEP_BRIGHTNESS,
CONF_SLEEP_COLOR_TEMP,
CONF_TRANSITION,
CONF_INTERVAL,
}
def int_between(min_int: int, max_int: int) -> vol.All:
"""Return an integer between 'min_int' and 'max_int'."""
return vol.All(vol.Coerce(int), vol.Range(min=min_int, max=max_int))
VALIDATION_TUPLES: list[tuple[str, Any, Any]] = [
(CONF_LIGHTS, DEFAULT_LIGHTS, cv.entity_ids), # type: ignore[arg-type]
(CONF_INTERVAL, DEFAULT_INTERVAL, cv.positive_int),
(CONF_TRANSITION, DEFAULT_TRANSITION, VALID_TRANSITION),
(CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION, VALID_TRANSITION),
(CONF_MIN_BRIGHTNESS, DEFAULT_MIN_BRIGHTNESS, int_between(1, 100)),
(CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS, int_between(1, 100)),
(CONF_MIN_COLOR_TEMP, DEFAULT_MIN_COLOR_TEMP, int_between(1000, 10000)),
(CONF_MAX_COLOR_TEMP, DEFAULT_MAX_COLOR_TEMP, int_between(1000, 10000)),
(CONF_PREFER_RGB_COLOR, DEFAULT_PREFER_RGB_COLOR, bool),
(CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS, int_between(1, 100)),
(
CONF_SLEEP_RGB_OR_COLOR_TEMP,
DEFAULT_SLEEP_RGB_OR_COLOR_TEMP,
selector.SelectSelector( # type: ignore[arg-type]
selector.SelectSelectorConfig(
options=["color_temp", "rgb_color"],
multiple=False,
mode=selector.SelectSelectorMode.DROPDOWN,
),
),
),
(CONF_SLEEP_COLOR_TEMP, DEFAULT_SLEEP_COLOR_TEMP, int_between(1000, 10000)),
(
CONF_SLEEP_RGB_COLOR,
DEFAULT_SLEEP_RGB_COLOR,
selector.ColorRGBSelector(selector.ColorRGBSelectorConfig()), # type: ignore[arg-type]
),
(CONF_SLEEP_TRANSITION, DEFAULT_SLEEP_TRANSITION, VALID_TRANSITION),
(CONF_ADAPT_UNTIL_SLEEP, DEFAULT_ADAPT_UNTIL_SLEEP, bool),
(CONF_SUNRISE_TIME, NONE_STR, str),
(CONF_MIN_SUNRISE_TIME, NONE_STR, str),
(CONF_MAX_SUNRISE_TIME, NONE_STR, str),
(CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET, int),
(CONF_SUNSET_TIME, NONE_STR, str),
(CONF_MIN_SUNSET_TIME, NONE_STR, str),
(CONF_MAX_SUNSET_TIME, NONE_STR, str),
(CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET, int),
(
CONF_BRIGHTNESS_MODE,
DEFAULT_BRIGHTNESS_MODE,
selector.SelectSelector( # type: ignore[arg-type]
selector.SelectSelectorConfig(
options=["default", "linear", "tanh"],
multiple=False,
mode=selector.SelectSelectorMode.DROPDOWN,
),
),
),
(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_COLOR_TEMP_FIXED_PLATEAU_END, NONE_STR, str),
(CONF_COLOR_TEMP_FIXED_DESCENT_END, NONE_STR, str),
(CONF_COLOR_TEMP_FIXED_PLATEAU_KELVIN, NONE_STR, str),
(CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL, bool),
(
CONF_TAKE_OVER_CONTROL_MODE,
DEFAULT_TAKE_OVER_CONTROL_MODE,
selector.SelectSelector( # type: ignore[arg-type]
selector.SelectSelectorConfig(
options=[
TakeOverControlMode.PAUSE_ALL.value,
TakeOverControlMode.PAUSE_CHANGED.value,
],
multiple=False,
mode=selector.SelectSelectorMode.DROPDOWN,
),
),
),
(CONF_DETECT_NON_HA_CHANGES, DEFAULT_DETECT_NON_HA_CHANGES, bool),
(
CONF_AUTORESET_CONTROL,
DEFAULT_AUTORESET_CONTROL,
int_between(0, 365 * 24 * 60 * 60), # 1 year max
),
(CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE, bool),
(CONF_ADAPT_ONLY_ON_BARE_TURN_ON, DEFAULT_ADAPT_ONLY_ON_BARE_TURN_ON, bool),
(
CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON,
DEFAULT_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON,
bool,
),
(
CONF_RESET_MANUAL_CONTROL_ON_SLEEP_MODE_CHANGE,
DEFAULT_RESET_MANUAL_CONTROL_ON_SLEEP_MODE_CHANGE,
bool,
),
(CONF_SEPARATE_TURN_ON_COMMANDS, DEFAULT_SEPARATE_TURN_ON_COMMANDS, bool),
(CONF_SEND_SPLIT_DELAY, DEFAULT_SEND_SPLIT_DELAY, int_between(0, 10000)),
(CONF_ADAPT_DELAY, DEFAULT_ADAPT_DELAY, cv.positive_float),
(
CONF_SKIP_REDUNDANT_COMMANDS,
DEFAULT_SKIP_REDUNDANT_COMMANDS,
bool,
),
(CONF_INTERCEPT, DEFAULT_INTERCEPT, bool),
(CONF_MULTI_LIGHT_INTERCEPT, DEFAULT_MULTI_LIGHT_INTERCEPT, bool),
(CONF_INCLUDE_CONFIG_IN_ATTRIBUTES, DEFAULT_INCLUDE_CONFIG_IN_ATTRIBUTES, bool),
(CONF_EXPAND_LIGHT_GROUPS, DEFAULT_EXPAND_LIGHT_GROUPS, bool),
]
def timedelta_as_int(value: timedelta) -> float:
"""Convert a `datetime.timedelta` object to an integer.
This integer can be serialized to json but a timedelta cannot.
"""
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]] = {
CONF_INTERVAL: (cv.time_period, timedelta_as_int),
CONF_SUNRISE_OFFSET: (cv.time_period, timedelta_as_int),
CONF_SUNRISE_TIME: (cv.time, str),
CONF_MIN_SUNRISE_TIME: (cv.time, str),
CONF_MAX_SUNRISE_TIME: (cv.time, str),
CONF_SUNSET_OFFSET: (cv.time_period, timedelta_as_int),
CONF_SUNSET_TIME: (cv.time, str),
CONF_MIN_SUNSET_TIME: (cv.time, str),
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),
CONF_COLOR_TEMP_FIXED_PLATEAU_END: (cv.time, str),
CONF_COLOR_TEMP_FIXED_DESCENT_END: (cv.time, str),
CONF_COLOR_TEMP_FIXED_PLATEAU_KELVIN: (int_between(1000, 10000), int),
}
def validate_fixed_evening(plateau_end: Any, descent_end: Any) -> None:
"""Check the pair of fixed evening color temperature times.
Both are local times of day (strings or `datetime.time`), or unset
(`None` or "None"). Raises `vol.Invalid` for an incoherent pair.
"""
plateau_end, descent_end = (
None if value in (None, NONE_STR) else cv.time(value)
for value in (plateau_end, descent_end)
)
if plateau_end is None and descent_end is None:
return
if plateau_end is None or descent_end is None:
msg = (
f"{CONF_COLOR_TEMP_FIXED_PLATEAU_END} and"
f" {CONF_COLOR_TEMP_FIXED_DESCENT_END} must be set together"
)
raise vol.Invalid(msg)
if plateau_end < MIN_COLOR_TEMP_FIXED_PLATEAU_END:
msg = (
f"{CONF_COLOR_TEMP_FIXED_PLATEAU_END} must be"
f" {MIN_COLOR_TEMP_FIXED_PLATEAU_END:%H:%M} or later (it is an evening time)"
)
raise vol.Invalid(msg)
if plateau_end == descent_end:
msg = (
f"{CONF_COLOR_TEMP_FIXED_DESCENT_END} must differ from"
f" {CONF_COLOR_TEMP_FIXED_PLATEAU_END}"
)
raise vol.Invalid(msg)
# A descent end that is not after the plateau end is on the next day.
duration = (
datetime.combine(date(2000, 1, 1), descent_end)
- datetime.combine(date(2000, 1, 1), plateau_end)
) % timedelta(days=1)
if duration > MAX_COLOR_TEMP_FIXED_DESCENT:
msg = (
f"{CONF_COLOR_TEMP_FIXED_DESCENT_END} must be at most"
f" {MAX_COLOR_TEMP_FIXED_DESCENT} after {CONF_COLOR_TEMP_FIXED_PLATEAU_END}"
)
raise vol.Invalid(msg)
def validate_fixed_plateau_kelvin(
kelvin: Any,
plateau_end: Any,
descent_end: Any,
min_color_temp: int,
max_color_temp: int,
) -> None:
"""Check the custom evening plateau temperature, unset being `None`/"None".
It needs the fixed evening times and must lie between the minimum and
maximum color temperature. Raises `vol.Invalid` otherwise.
"""
if kelvin in (None, NONE_STR):
return
kelvin = int_between(1000, 10000)(kelvin)
if plateau_end in (None, NONE_STR) or descent_end in (None, NONE_STR):
msg = (
f"{CONF_COLOR_TEMP_FIXED_PLATEAU_KELVIN} needs"
f" {CONF_COLOR_TEMP_FIXED_PLATEAU_END} and"
f" {CONF_COLOR_TEMP_FIXED_DESCENT_END}"
)
raise vol.Invalid(msg)
if not min_color_temp <= kelvin <= max_color_temp:
msg = (
f"{CONF_COLOR_TEMP_FIXED_PLATEAU_KELVIN} ({kelvin}) must be between"
f" {CONF_MIN_COLOR_TEMP} ({min_color_temp}) and"
f" {CONF_MAX_COLOR_TEMP} ({max_color_temp})"
)
raise vol.Invalid(msg)
def maybe_coerce(key: str, validation: Any) -> vol.All | Any:
"""Coerce the validation into a json serializable type."""
validation, coerce = EXTRA_VALIDATION.get(key, (validation, None))
if coerce is not None:
return vol.All(validation, vol.Coerce(coerce))
return validation
def replace_none_str(value: Any, replace_with: Any | None = None) -> Any:
"""Replace "None" -> replace_with."""
return value if value != NONE_STR else replace_with
_yaml_validation_tuples = [
(key, default, maybe_coerce(key, validation))
for key, default, validation in VALIDATION_TUPLES
] + [(CONF_NAME, DEFAULT_NAME, cv.string)]
def _validate_yaml_fixed_evening(config: dict[str, Any]) -> dict[str, Any]:
validate_fixed_evening(
config.get(CONF_COLOR_TEMP_FIXED_PLATEAU_END),
config.get(CONF_COLOR_TEMP_FIXED_DESCENT_END),
)
validate_fixed_plateau_kelvin(
config.get(CONF_COLOR_TEMP_FIXED_PLATEAU_KELVIN),
config.get(CONF_COLOR_TEMP_FIXED_PLATEAU_END),
config.get(CONF_COLOR_TEMP_FIXED_DESCENT_END),
config.get(CONF_MIN_COLOR_TEMP, DEFAULT_MIN_COLOR_TEMP),
config.get(CONF_MAX_COLOR_TEMP, DEFAULT_MAX_COLOR_TEMP),
)
return config
_DOMAIN_SCHEMA = vol.All(
vol.Schema(
{
vol.Optional(
key,
default=replace_none_str(default, vol.UNDEFINED),
): validation
for key, default, validation in _yaml_validation_tuples
},
),
_validate_yaml_fixed_evening,
)
def apply_service_schema() -> vol.Schema:
"""Return the schema for the apply service."""
return vol.Schema(
{
vol.Optional(CONF_ENTITY_ID): cv.entity_ids, # type: ignore[arg-type]
vol.Optional(CONF_LIGHTS, default=[]): cv.entity_ids, # type: ignore[arg-type]
vol.Optional(CONF_TRANSITION): VALID_TRANSITION,
vol.Optional(ATTR_ADAPT_BRIGHTNESS, default=True): cv.boolean,
vol.Optional(ATTR_ADAPT_COLOR, default=True): cv.boolean,
vol.Optional(CONF_PREFER_RGB_COLOR, default=False): cv.boolean,
vol.Optional(CONF_TURN_ON_LIGHTS, default=False): cv.boolean,
},
)
# The service takes the numbers of its selectors, where the options flow only
# has a (serializable) string field; the strict check is made by `validate`.
_SERVICE_VALIDATION: dict[str, Any] = {
CONF_COLOR_TEMP_FIXED_PLATEAU_KELVIN: vol.Any(vol.Coerce(int), str),
}
def change_switch_settings_schema() -> dict[vol.Marker, Any]:
"""Return the schema for the change_switch_settings service."""
args: dict[vol.Marker, Any] = {
vol.Optional(CONF_USE_DEFAULTS, default="current"): cv.string,
}
# Modifying these after init isn't possible
skip = (CONF_INTERVAL, CONF_NAME, CONF_LIGHTS)
for k, _, valid in VALIDATION_TUPLES:
if k not in skip:
args[vol.Optional(k)] = _SERVICE_VALIDATION.get(k, valid)
return args
SET_MANUAL_CONTROL_SCHEMA = vol.Schema(
{
vol.Optional(CONF_ENTITY_ID): cv.entity_ids, # type: ignore[arg-type]
vol.Optional(CONF_LIGHTS, default=[]): cv.entity_ids, # type: ignore[arg-type]
vol.Optional(CONF_MANUAL_CONTROL, default=True): vol.Any(
cv.boolean,
vol.In(["brightness", "color"]),
),
},
)