adaptive-lighting/custom_components/adaptive_lighting/__init__.py
Casey 501e3d639e Implement cdit-config-redesign groups 1-6 (30/53 tasks)
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).
2026-05-16 14:27:39 +02:00

152 lines
5.1 KiB
Python

"""Adaptive Lighting integration in Home Assistant (CDiT fork)."""
import logging
from typing import Any
import homeassistant.helpers.config_validation as cv
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_SOURCE
from homeassistant.core import Event, HomeAssistant
from homeassistant.exceptions import ConfigEntryError
from homeassistant.helpers import entity_registry as er
from .const import (
_DOMAIN_SCHEMA, # pyright: ignore[reportPrivateUsage]
ATTR_ADAPTIVE_LIGHTING_MANAGER,
CONF_NAME,
CONFIG_ENTRY_VERSION,
DOMAIN,
UNDO_UPDATE_LISTENER,
)
_LOGGER = logging.getLogger(__name__)
PLATFORMS = ["switch"]
# unique_id suffix(es) that this fork no longer creates. Any entity in the
# registry whose unique_id ends with one of these strings AND that is owned
# by an Adaptive Lighting config entry is a leftover from upstream and is
# removed on first setup (spec R9, design D12).
_REMOVED_UNIQUE_ID_SUFFIXES = ("_sleep_mode",)
def _all_unique_names(value: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Validate that all entities have a unique profile name."""
hosts = [device[CONF_NAME] for device in value]
schema = vol.Schema(vol.Unique())
schema(hosts)
return value
CONFIG_SCHEMA = vol.Schema(
{DOMAIN: vol.All(cv.ensure_list, [_DOMAIN_SCHEMA], _all_unique_names)},
extra=vol.ALLOW_EXTRA,
)
async def reload_configuration_yaml(event: Event) -> None:
"""Reload configuration.yaml."""
hass: HomeAssistant | None = event.data.get("hass")
if hass is not None:
await hass.services.async_call("homeassistant", "check_config", {})
else:
_LOGGER.error("HomeAssistant instance not found in event data.")
async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool:
"""Import integration from config."""
if DOMAIN in config:
for entry in config[DOMAIN]:
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={CONF_SOURCE: SOURCE_IMPORT},
data=entry,
),
)
return True
def _remove_orphan_sleep_entities(
hass: HomeAssistant,
config_entry: ConfigEntry,
) -> None:
"""Remove sleep-mode switch entities left behind by upstream AL.
Idempotent: subsequent runs find nothing and emit no log lines. Only
removes entities whose config_entry_id matches the current entry, so
foreign entities matching the name pattern are not touched.
Spec R9, design D12.
"""
registry = er.async_get(hass)
entries_to_remove = [
entry.entity_id
for entry in registry.entities.values()
if entry.config_entry_id == config_entry.entry_id
and any(
entry.unique_id.endswith(suffix)
for suffix in _REMOVED_UNIQUE_ID_SUFFIXES
)
]
for entity_id in entries_to_remove:
_LOGGER.info(
"Removing orphan entity %s left behind by upstream Adaptive Lighting "
"(sleep mode is not supported in the CDiT fork).",
entity_id,
)
registry.async_remove(entity_id)
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up the component."""
# Spec R8 + design D4: reject entries from older, incompatible versions
# with a clear "recreate this entry" message rather than silently migrating.
if config_entry.version < CONFIG_ENTRY_VERSION:
msg = (
f"Adaptive Lighting v{CONFIG_ENTRY_VERSION} (CDiT fork) is incompatible "
f"with the existing config entry (version {config_entry.version}). "
"Delete and recreate the entry from Settings → Devices & Services."
)
raise ConfigEntryError(msg)
_remove_orphan_sleep_entities(hass, config_entry)
data = hass.data.setdefault(DOMAIN, {})
# Reload YAML configs on `hass.config.entry_updated` (covers `quick reload`
# and explicit `hass.reload_config_entry` calls).
hass.bus.async_listen("hass.config.entry_updated", reload_configuration_yaml)
undo_listener = config_entry.add_update_listener(async_update_options)
data[config_entry.entry_id] = {UNDO_UPDATE_LISTENER: undo_listener}
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
return True
async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
"""Update options."""
await hass.config_entries.async_reload(config_entry.entry_id)
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_forward_entry_unload(
config_entry,
"switch",
)
data = hass.data[DOMAIN]
data[config_entry.entry_id][UNDO_UPDATE_LISTENER]()
if unload_ok:
data.pop(config_entry.entry_id)
if len(data) == 1 and ATTR_ADAPTIVE_LIGHTING_MANAGER in data:
# no more config_entries
manager = data.pop(ATTR_ADAPTIVE_LIGHTING_MANAGER)
manager.disable()
if not data:
hass.data.pop(DOMAIN)
return unload_ok