mirror of
https://github.com/basnijholt/adaptive-lighting.git
synced 2026-09-16 16:54:04 +02:00
const.py:
- Drop 21 retired CONF_*/DEFAULT_* (sleep cluster, manual sun timing,
brightness curve variants, take-over-control cluster).
- Add CONF_SUNRISE_ENTITY / CONF_SUNSET_ENTITY (default
sensor.sun_next_rising / _setting).
- Add RAMP_HALF_WIDTH_SECONDS = 1800.
- Add CONFIG_ENTRY_VERSION = 2 (gate for strict version-break).
- Re-default DEFAULT_MIN_BRIGHTNESS 1→5, DEFAULT_MIN_COLOR_TEMP 2000→2200.
- Update ICON_MAIN / _BRIGHTNESS / _COLOR_TEMP to the CDiT picks.
manifest.json: bump to 2.0.0-cdit.1, pin homeassistant: 2025.1.0,
re-point codeowners/documentation/issue_tracker to the CaseyRo fork.
switch.py:
- Drop sleep_mode_switch creation in async_setup_entry; integration
now creates 3 switches per profile (master, adapt_color, adapt_brightness).
- Strip sleep-mode state machine from AdaptiveSwitch (sleep_mode_switch
attribute, sleep_transition, adapt_until_sleep, the state-change
listener, the _sleep_mode_switch_state_event_action method).
- Bridge-stub _take_over_control / _detect_non_ha_changes /
_adapt_only_on_bare_turn_on / _only_once / _auto_reset_manual_control_time
as class-level False/0 so Manager-side branches become dead code without
needing a full rewrite of AdaptiveLightingManager.
- Remove handle_set_manual_control service + its registration.
- Replace astral-driven curve init with the new SunLightSettings call
shape; add AdaptiveSwitch._today_sun_events() helper that reads the
configured sunrise/sunset entities.
- Set _attr_icon on AdaptiveSwitch.
color_and_brightness.py:
- Rewrite SunLightSettings as a pure curve-math wrapper (5 fields:
name + bounds + ramp half-width); methods take t_sunrise/t_sunset
as args, never reads HA state.
- Implement piecewise tanh ramp curve per spec R4 / design D11 in
_tanh_day_curve(). Both brightness and color-temp use the same shape.
- Drop SunEvents, sleep-mode branches, brightness_mode-switch, astral
dependency, force_rgb_color, lerp_color_hsv sleep-tinted color blending.
config_flow.py:
- Rewrite as sectioned schema (Targets / Daytime curve / Sun schedule /
Light control / Advanced / Diagnostics) via HA's section() helper.
- Native HA selectors throughout (NumberSelector, EntitySelector,
BooleanSelector). Strict typing on sun-event entity pickers.
- Conditional visibility for send_split_delay (driver:
separate_turn_on_commands).
- Extend OptionsFlowWithReload (with ImportError fallback for HA < 2025.1
dev environment); async_abort(reason="yaml_managed") for SOURCE_IMPORT.
- VERSION = CONFIG_ENTRY_VERSION.
__init__.py:
- Reject older config entries with ConfigEntryError ("recreate the entry")
per spec R8 / design D4.
- Add _remove_orphan_sleep_entities tombstone helper that scans the
entity registry for sleep-mode entities owned by this entry and
removes them on first setup. Idempotent and config_entry-scoped.
_docs_helpers.py: stub the removed DOCS_MANUAL_CONTROL / SET_MANUAL_CONTROL_SCHEMA
so the docs-generator script keeps importing.
Validation:
- ALL modules import cleanly under HA 2024.12.5 (dev env).
- openspec validate cdit-config-redesign --strict: green.
- ruff check --select=F: clean.
Remaining: groups 7 (16 tests), 8 (strings + plain-language pass), 9 (docs).
257 lines
9.9 KiB
Python
257 lines
9.9 KiB
Python
"""Constants for the Adaptive Lighting integration (CDiT fork)."""
|
|
|
|
from datetime import timedelta
|
|
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
|
|
|
|
# CDiT icons (task 2.5). Master switch signals "this is an automatic system",
|
|
# the two adapt switches name the attribute they govern.
|
|
ICON_MAIN = "mdi:weather-sunny-alert"
|
|
ICON_BRIGHTNESS = "mdi:brightness-percent"
|
|
ICON_COLOR_TEMP = "mdi:invert-colors"
|
|
|
|
DOMAIN = "adaptive_lighting"
|
|
|
|
# Half-width of the tanh ramp around each sun event, in seconds.
|
|
# 30 minutes is the eye-friendly circadian-transition sweet spot; long enough
|
|
# to feel gradual, short enough to stay in-window across solstice-to-equinox
|
|
# sunrise drift. Documented in design.md decision 15.
|
|
RAMP_HALF_WIDTH_SECONDS = 1800
|
|
|
|
# CDiT config-entry schema version. Bumped from upstream's implicit v1.
|
|
# An entry with version < CONFIG_ENTRY_VERSION fails async_setup_entry with
|
|
# a "recreate this entry" message — see design.md decision 4.
|
|
CONFIG_ENTRY_VERSION = 2
|
|
|
|
DOCS = {CONF_ENTITY_ID: "Entity ID of the switch. 📝"}
|
|
|
|
|
|
CONF_NAME, DEFAULT_NAME = "name", "default"
|
|
DOCS[CONF_NAME] = "Display name for this profile."
|
|
|
|
CONF_LIGHTS, DEFAULT_LIGHTS = "lights", []
|
|
DOCS[CONF_LIGHTS] = "Light entities this profile controls."
|
|
|
|
CONF_INCLUDE_CONFIG_IN_ATTRIBUTES, DEFAULT_INCLUDE_CONFIG_IN_ATTRIBUTES = (
|
|
"include_config_in_attributes",
|
|
False,
|
|
)
|
|
DOCS[CONF_INCLUDE_CONFIG_IN_ATTRIBUTES] = (
|
|
"Expose all configuration options as attributes on the master switch "
|
|
"for debugging."
|
|
)
|
|
|
|
CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION = "initial_transition", 1
|
|
DOCS[CONF_INITIAL_TRANSITION] = (
|
|
"Fade time when a light first turns on, in seconds."
|
|
)
|
|
|
|
CONF_INTERVAL, DEFAULT_INTERVAL = "interval", 90
|
|
DOCS[CONF_INTERVAL] = "How often to recompute and re-apply the curve, in seconds."
|
|
|
|
CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS = "max_brightness", 100
|
|
DOCS[CONF_MAX_BRIGHTNESS] = "Brightness at the peak of the day, in percent."
|
|
|
|
CONF_MAX_COLOR_TEMP, DEFAULT_MAX_COLOR_TEMP = "max_color_temp", 5500
|
|
DOCS[CONF_MAX_COLOR_TEMP] = "Color temperature at the peak of the day, in Kelvin."
|
|
|
|
# CDiT default: 5 instead of upstream's 1. 1% reads as off on most bulbs;
|
|
# 5% is the dim-but-visible floor.
|
|
CONF_MIN_BRIGHTNESS, DEFAULT_MIN_BRIGHTNESS = "min_brightness", 5
|
|
DOCS[CONF_MIN_BRIGHTNESS] = "Brightness during the night, in percent."
|
|
|
|
# CDiT default: 2200 K instead of upstream's 2000 K. 2000 K reads as
|
|
# sodium-vapor orange; 2200 K is a softer warm-lamp tone.
|
|
CONF_MIN_COLOR_TEMP, DEFAULT_MIN_COLOR_TEMP = "min_color_temp", 2200
|
|
DOCS[CONF_MIN_COLOR_TEMP] = "Color temperature during the night, in Kelvin."
|
|
|
|
CONF_PREFER_RGB_COLOR, DEFAULT_PREFER_RGB_COLOR = "prefer_rgb_color", False
|
|
DOCS[CONF_PREFER_RGB_COLOR] = (
|
|
"Prefer RGB color over color temperature when a light supports both."
|
|
)
|
|
|
|
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. Required "
|
|
"by some lights that cannot accept both at once."
|
|
)
|
|
|
|
CONF_SUNRISE_ENTITY, DEFAULT_SUNRISE_ENTITY = (
|
|
"sunrise_entity",
|
|
"sensor.sun_next_rising",
|
|
)
|
|
DOCS[CONF_SUNRISE_ENTITY] = (
|
|
"Sensor whose state is the next sunrise timestamp. Defaults to the "
|
|
"built-in `sensor.sun_next_rising`; point at a Sun2 sensor for civil, "
|
|
"nautical, or astronomical twilight."
|
|
)
|
|
|
|
CONF_SUNSET_ENTITY, DEFAULT_SUNSET_ENTITY = (
|
|
"sunset_entity",
|
|
"sensor.sun_next_setting",
|
|
)
|
|
DOCS[CONF_SUNSET_ENTITY] = (
|
|
"Sensor whose state is the next sunset timestamp. Defaults to the "
|
|
"built-in `sensor.sun_next_setting`."
|
|
)
|
|
|
|
CONF_TRANSITION, DEFAULT_TRANSITION = "transition", 45
|
|
DOCS[CONF_TRANSITION] = "Fade time for each scheduled curve update, in seconds."
|
|
|
|
CONF_ADAPT_DELAY, DEFAULT_ADAPT_DELAY = "adapt_delay", 0
|
|
DOCS[CONF_ADAPT_DELAY] = (
|
|
"Wait time after a light turns on before applying the curve, in seconds. "
|
|
"Avoids flickering on lights that report 'on' before they have settled."
|
|
)
|
|
|
|
CONF_SEND_SPLIT_DELAY, DEFAULT_SEND_SPLIT_DELAY = "send_split_delay", 0
|
|
DOCS[CONF_SEND_SPLIT_DELAY] = (
|
|
"Pause between the two `light.turn_on` calls when "
|
|
"`separate_turn_on_commands` is enabled, in milliseconds."
|
|
)
|
|
|
|
CONF_SKIP_REDUNDANT_COMMANDS, DEFAULT_SKIP_REDUNDANT_COMMANDS = (
|
|
"skip_redundant_commands",
|
|
False,
|
|
)
|
|
DOCS[CONF_SKIP_REDUNDANT_COMMANDS] = (
|
|
"Don't send a command when the light is already at the target state. "
|
|
"Cuts traffic; disable if HA's recorded state drifts from the physical "
|
|
"light."
|
|
)
|
|
|
|
CONF_INTERCEPT, DEFAULT_INTERCEPT = "intercept", True
|
|
DOCS[CONF_INTERCEPT] = (
|
|
"Intercept `light.turn_on` calls to apply the curve instantly, instead "
|
|
"of waiting for the next scheduled update."
|
|
)
|
|
|
|
CONF_MULTI_LIGHT_INTERCEPT, DEFAULT_MULTI_LIGHT_INTERCEPT = (
|
|
"multi_light_intercept",
|
|
True,
|
|
)
|
|
DOCS[CONF_MULTI_LIGHT_INTERCEPT] = (
|
|
"Also intercept `light.turn_on` calls that target multiple lights. "
|
|
"Requires `intercept` to be enabled."
|
|
)
|
|
|
|
# Switch identifiers used as suffixes on each AL profile's entities.
|
|
# Sleep-mode switch (`SLEEP_MODE_SWITCH`) is intentionally removed in this
|
|
# fork — see design.md decision 7.
|
|
ADAPT_COLOR_SWITCH = "adapt_color_switch"
|
|
ADAPT_BRIGHTNESS_SWITCH = "adapt_brightness_switch"
|
|
ATTR_ADAPTIVE_LIGHTING_MANAGER = "manager"
|
|
UNDO_UPDATE_LISTENER = "undo_update_listener"
|
|
|
|
ATTR_ADAPT_COLOR = "adapt_color"
|
|
DOCS[ATTR_ADAPT_COLOR] = "Adjust the color of supporting lights over the day."
|
|
ATTR_ADAPT_BRIGHTNESS = "adapt_brightness"
|
|
DOCS[ATTR_ADAPT_BRIGHTNESS] = "Adjust the brightness of supporting lights over the day."
|
|
|
|
SERVICE_APPLY = "apply"
|
|
CONF_TURN_ON_LIGHTS = "turn_on_lights"
|
|
DOCS[CONF_TURN_ON_LIGHTS] = "Also turn on any targeted lights that are currently off."
|
|
SERVICE_CHANGE_SWITCH_SETTINGS = "change_switch_settings"
|
|
CONF_USE_DEFAULTS = "use_defaults"
|
|
DOCS[CONF_USE_DEFAULTS] = (
|
|
'Source for any field not supplied in this call: '
|
|
'"current" (keep existing values), "factory" (documented defaults), '
|
|
'or "configuration" (the profile\'s own configured defaults).'
|
|
)
|
|
|
|
TURNING_OFF_DELAY = 5
|
|
|
|
DOCS_APPLY = {
|
|
CONF_ENTITY_ID: "The master switch whose curve settings to apply.",
|
|
CONF_LIGHTS: "Lights to apply the settings to.",
|
|
}
|
|
|
|
|
|
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))
|
|
|
|
|
|
# Per-field validation tuples driving YAML import and (via maybe_coerce)
|
|
# the YAML schema. The UI options flow does not consume this list directly;
|
|
# config_flow.py hand-shapes its sections via HA selectors. Eighteen entries:
|
|
# 16 retained from upstream + 2 new sun-entity fields.
|
|
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_SUNRISE_ENTITY, DEFAULT_SUNRISE_ENTITY, cv.entity_id),
|
|
(CONF_SUNSET_ENTITY, DEFAULT_SUNSET_ENTITY, cv.entity_id),
|
|
(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),
|
|
]
|
|
|
|
|
|
def timedelta_as_int(value: timedelta) -> float:
|
|
"""Convert a `datetime.timedelta` object to an integer."""
|
|
return value.total_seconds()
|
|
|
|
|
|
# Only CONF_INTERVAL still needs YAML-time coercion (HA accepts strings like
|
|
# "00:01:30" and turns them into timedeltas). All other coerced fields
|
|
# (sun times, brightness-mode windows) were removed.
|
|
EXTRA_VALIDATION: dict[str, tuple[Any, Any]] = {
|
|
CONF_INTERVAL: (cv.time_period, timedelta_as_int),
|
|
}
|
|
|
|
|
|
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
|
|
|
|
|
|
_yaml_validation_tuples = [
|
|
(key, default, maybe_coerce(key, validation))
|
|
for key, default, validation in VALIDATION_TUPLES
|
|
] + [(CONF_NAME, DEFAULT_NAME, cv.string)]
|
|
|
|
_DOMAIN_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Optional(key, default=default): validation
|
|
for key, default, validation in _yaml_validation_tuples
|
|
},
|
|
)
|
|
|
|
|
|
def apply_service_schema(initial_transition: int = 1) -> 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,
|
|
default=initial_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,
|
|
},
|
|
)
|