mirror of
https://github.com/basnijholt/adaptive-lighting.git
synced 2026-09-16 08:44:03 +02:00
Scaffold CDiT fork: planning artifacts and tooling
- openspec/ — three changes scoped:
• cdit-config-redesign: full artifact set (proposal + design + specs +
tasks), strict-validate green. Prunes 21 fields from the upstream
options flow, switches sun timing to entity-driven sources, hardcodes
a tanh curve, and breaks compat with upstream config entries.
• add-runtime-range-controls: proposal stub for promoting the 4
brightness/color-temp ranges to live number entities.
• house-mode-modes: proposal stub for per-AL house-mode behavior matrix
driving the runtime switches.
- .claude/ — opsx slash commands and openspec skill bundles for driving
the artifact-driven workflow.
- CLAUDE.md — fork orientation, remote topology, dev commands, and the
active change pointer.
This commit is contained in:
parent
ddaf851be3
commit
80f99db16f
31 changed files with 4569 additions and 0 deletions
|
|
@ -0,0 +1,2 @@
|
|||
schema: spec-driven
|
||||
created: 2026-05-16
|
||||
43
openspec/changes/add-runtime-range-controls/proposal.md
Normal file
43
openspec/changes/add-runtime-range-controls/proposal.md
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
## Why
|
||||
|
||||
The four brightness/color-temperature ranges (`min_brightness`, `max_brightness`, `min_color_temp`, `max_color_temp`) are exactly the values a CDiT household wants to live-tune from a Lovelace card — the difference between "kitchen feels too warm" and "kitchen feels right" is one slider away. Today they are setup-time options buried in the integration's options dialog; every tweak forces a click trail through Settings → Devices → Adaptive Lighting → Configure → Save, and triggers a full integration reload via `OptionsFlowWithReload`.
|
||||
|
||||
This change promotes the four ranges to first-class runtime entities (HA's `number` platform), with the config flow remaining the place to seed the initial defaults and any subsequent UI edit writing through transparently.
|
||||
|
||||
> **Status**: stub proposal. Specs, design, and tasks to be written when work on this change starts. Depends on `cdit-config-redesign` landing first.
|
||||
|
||||
## What Changes
|
||||
|
||||
- **Add a `number` platform** to the integration. Each AL config entry creates four `number` entities:
|
||||
- `number.adaptive_lighting_<name>_min_brightness` (range 1–100, step 1, %, slider mode)
|
||||
- `number.adaptive_lighting_<name>_max_brightness` (same)
|
||||
- `number.adaptive_lighting_<name>_min_color_temp` (range 1000–10000, step 100, K, slider mode)
|
||||
- `number.adaptive_lighting_<name>_max_color_temp` (same)
|
||||
- **Source-of-truth model: write-through** (Decision C from the prior session). The number entity is canonical at runtime; config flow values seed the entity on first creation and act as a mirror afterwards.
|
||||
- On config-entry creation, the four number entities are initialized from `entry.options`.
|
||||
- When the user moves a slider on the number entity, the new value is persisted back to `entry.options` via `async_update_entry`.
|
||||
- When the user saves the options flow, the four corresponding number entities have their state updated to match.
|
||||
- **Curve math reads from number entities**, not from `entry.options` directly. Single read path at evaluation time.
|
||||
- **Options flow keeps the four fields visible** in the Daytime curve section so users can still tune them from the config screen (especially first-time setup). Both surfaces stay in sync.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `runtime-range-controls`: live-tunable brightness and color-temperature range entities, with bidirectional sync to the config entry options. Covers entity creation, write-through semantics, curve-math read path, and conflict resolution between the two surfaces.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `options-flow`: the Daytime curve section's four range fields gain write-through behavior to the new number entities. Spec delta will capture this as a MODIFIED requirement.
|
||||
|
||||
## Impact
|
||||
|
||||
- **`custom_components/adaptive_lighting/number.py`** — new file, `number` platform implementation.
|
||||
- **`custom_components/adaptive_lighting/const.py`** — `Platform.NUMBER` appended to platform list; entity unique-id pattern constants.
|
||||
- **`custom_components/adaptive_lighting/__init__.py`** — curve math switches its reads for the four ranges from `entry.options[...]` to `hass.states.get(<number_entity_id>)`. `async_setup_entry` registers number platform.
|
||||
- **`custom_components/adaptive_lighting/config_flow.py`** — save path also writes through to the four number entities (if they exist). Initial setup creates them.
|
||||
- **`tests/test_number_platform.py`** — new file. Tests: entity creation on first setup, slider change persists to options, options save writes to entity, curve reads pick up latest value, removal cleans up entities.
|
||||
- **`tests/test_config_flow.py`** — extends existing tests to cover the write-through path.
|
||||
- **No new runtime dependencies.**
|
||||
|
||||
**Sequencing**: this change MUST land after `cdit-config-redesign` — relies on its pruned schema, native selectors, and `OptionsFlowWithReload` pattern.
|
||||
2
openspec/changes/cdit-config-redesign/.openspec.yaml
Normal file
2
openspec/changes/cdit-config-redesign/.openspec.yaml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
schema: spec-driven
|
||||
created: 2026-05-16
|
||||
212
openspec/changes/cdit-config-redesign/design.md
Normal file
212
openspec/changes/cdit-config-redesign/design.md
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
## Context
|
||||
|
||||
Upstream `basnijholt/adaptive-lighting` is a mature HA custom component (~10k LOC, 800+ stars). Its options flow is built by iterating over `VALIDATION_TUPLES` in `const.py` — 39 entries, each a `(key, default, voluptuous_validator)` tuple. The result is one flat `vol.Schema` rendered as a single tall form with no grouping, no conditional visibility, custom `int_between` validators (no slider UI), and silent no-op behavior when the entry was YAML-configured.
|
||||
|
||||
CDiT's install is one household, opinionated about scenes and house modes, and uses Sun2 for precise twilight events. Half of upstream's options are dead weight in that context (sleep mode, take-over-control, manual sun-time math). The other half is hard to find because nothing is grouped.
|
||||
|
||||
This change resets the dialog. It also removes the sleep-mode switch entity from the switch platform — sleep mode is dropped wholesale, not just hidden.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- 18-field dialog that fits on one screen via collapsible sections.
|
||||
- Sun curve driven by HA entities, so Sun2 (or any other twilight source) plugs in via the existing entity selector.
|
||||
- Native HA selectors for everything (sliders, dropdowns, entity pickers) instead of voluptuous `int_between` wrappers.
|
||||
- Reload-on-save without manual `async_unload_entry` / `async_setup_entry` plumbing.
|
||||
- Honest message when the entry is YAML-managed (currently silently no-ops).
|
||||
- One-shot break: no migration code, manifest version bump, clear "recreate your entry" message.
|
||||
|
||||
**Non-Goals:**
|
||||
- Multi-step wizard. Editing dominates over setup in this integration's lifetime; sections beat steps.
|
||||
- Backwards compatibility with upstream config entries.
|
||||
- Translations beyond `en.json`. Other locales are out of scope; upstream's translations can be culled in a follow-up.
|
||||
- Replacing the simulator webapp at `webapp/`. It targets the upstream curve model and is decoupled from the HA integration. Out of scope.
|
||||
- Removing `astral` as a transitive dep. It's still imported elsewhere; pruning it is a follow-up.
|
||||
- Touching `tests/` for unaffected features (color helpers, light state diffing, etc.). Only config-flow, sleep-mode, and take-over-control tests get rewritten.
|
||||
|
||||
## Decisions
|
||||
|
||||
### Decision 1: Collapsible sections via HA's `section()` helper, not a multi-step flow
|
||||
|
||||
**What we chose:** One `async_step_init` that returns a single schema with grouped sections built via the HA Frontend `section()` helper. All fields visible (or conditionally visible) on one screen.
|
||||
|
||||
**Why:** Setup happens once per AL config; editing happens dozens of times over the install's lifetime. Wizards optimize for first-run cost (guidance) at the expense of every subsequent edit (clicks). Sections give us guidance through grouping while keeping editing fast.
|
||||
|
||||
**Alternatives considered:**
|
||||
- **Multi-step flow** (`async_step_basics` → `async_step_curve` → ...). Rejected — every edit becomes a click-through. Also, conditional visibility across steps is messier than within a single section-grouped form.
|
||||
- **One flat dialog with field-order reshuffling.** Rejected — without visual grouping, ordering alone doesn't communicate which fields belong together.
|
||||
|
||||
### Decision 2: Entity-driven sun timing, no `astral` in the curve path
|
||||
|
||||
**What we chose:** Two entity selectors (`sunrise_entity`, `sunset_entity`) of `domain: sensor, device_class: timestamp`. Defaults to built-in `sensor.sun_next_rising` / `sensor.sun_next_setting`. The curve math reads `hass.states.get(<entity_id>)` for the timestamps, not `astral`.
|
||||
|
||||
**Why:** Lets Sun2 (or any custom sun source) plug in without a code change. Matches HA convention ("everything is an entity"). Decouples us from `astral`'s quirks at high latitudes (irrelevant to Germany, but it's free elegance). Single source of truth — your `sensor.sun_next_rising` is also what every other automation in the house reads.
|
||||
|
||||
**Alternatives considered:**
|
||||
- **Keep `astral` and offer a "sun source" toggle.** Rejected — code path bifurcation, two implementations to test, defeats the simplification goal.
|
||||
- **Hardcode `sun.sun` and drop the selector.** Rejected — explicitly precludes Sun2, which is the whole reason to refactor sun timing.
|
||||
|
||||
### Decision 3: Hardcoded tanh curve with 30-minute half-width
|
||||
|
||||
**What we chose:** `brightness_mode` field is deleted. Curve is always tanh. The half-width (`brightness_mode_time_dark` / `_time_light` upstream) is hardcoded as a single constant `RAMP_HALF_WIDTH_SECONDS = 1800` in `const.py`.
|
||||
|
||||
**Why:** Three modes (default / linear / tanh) were a research-era artifact; tanh is the eye-friendly choice for any household use case. The half-width sweet spot (30 min) is well-studied for circadian-friendly transitions and looks reasonable from solstice (sunrise 04:30) to equinox (sunrise 06:00). Removing the knob removes a UX maze.
|
||||
|
||||
**Alternatives considered:**
|
||||
- **Keep `brightness_mode` as a hidden constant, expose half-width.** Rejected — same maintenance cost as keeping both, with no real win.
|
||||
- **Expose half-width as one Advanced slider.** Rejected (initially considered). One less knob is genuinely better than one customizable knob — advanced fields rot when unused.
|
||||
|
||||
### Decision 4: Strict break, no migration code
|
||||
|
||||
**What we chose:** Bump `manifest.json` major version (1.x → 2.0.0-cdit.1). On upgrade, HA fails to load old config entries with a "this version is incompatible" toast. User recreates the entry manually.
|
||||
|
||||
**Why:** Single-household fork. The cost of one manual re-setup is ~5 minutes. Migration code that strips removed keys and silently translates old values would be ~50 LOC, would carry across the next two breaks, and is the kind of code that hides edge-case bugs.
|
||||
|
||||
**Alternatives considered:**
|
||||
- **Silent strip on `async_setup_entry`.** Rejected — looks magical, hides which fields the user thought were configured.
|
||||
- **`async_migrate_entry` with explicit version bump.** Rejected — correct for a public fork, overkill for one user. Re-evaluate if anyone outside CDiT installs this.
|
||||
|
||||
### Decision 5: Native HA selectors instead of voluptuous `int_between`
|
||||
|
||||
**What we chose:** `NumberSelector` with `min`, `max`, `step`, `unit_of_measurement`, `mode=NumberSelectorMode.SLIDER` (or `BOX` for fine-grained); `EntitySelector` with domain/device_class filters; `BooleanSelector`; `DurationSelector` for time intervals. All in `homeassistant.helpers.selector`.
|
||||
|
||||
**Why:** Selectors render as proper UI (sliders, dropdowns, color pickers, entity pickers). `int_between(1, 100)` validates correctly but renders as a plain text field — same UX whether the range is brightness, color temp, or seconds. Selectors are the direction HA core is moving; staying on voluptuous custom validators means re-implementing this every minor HA release.
|
||||
|
||||
**Alternatives considered:**
|
||||
- **Keep voluptuous wrappers, restyle via custom frontend card.** Rejected — explicitly pulls us into frontend territory, which is out of scope for this change.
|
||||
|
||||
### Decision 6: `OptionsFlowWithReload` instead of manual reload
|
||||
|
||||
**What we chose:** Subclass `homeassistant.config_entries.OptionsFlowWithReload` (added to HA core in 2024.10). Override `async_step_init` only. No `async_unload_entry` / `async_setup_entry` reload plumbing.
|
||||
|
||||
**Why:** The documented, supported path. Manual reload has subtle bugs around entity registry stale state and listener leakage. `OptionsFlowWithReload` handles them by tearing down and rebuilding the entry atomically.
|
||||
|
||||
**Alternatives considered:**
|
||||
- **Stay on `OptionsFlow` and call `hass.config_entries.async_reload(...)` manually after save.** Rejected — duplicates HA framework code, easy to forget on edge paths.
|
||||
|
||||
### Decision 7: Delete the sleep mode switch entity, don't hide it
|
||||
|
||||
**What we chose:** Remove the sleep-mode switch class from `switch.py` entirely. Each AL config now creates 3 switches (master, adapt_brightness, adapt_color), not 4. Sleep-mode state machine is also stripped from the master switch class.
|
||||
|
||||
**Why:** Keeping unused entities pollutes the device page, accumulates state, and creates ambiguity ("will this wake up if I flip it?"). Hiding from UI but keeping the code path means the state machine still runs, just invisibly — worst of both worlds.
|
||||
|
||||
**Alternatives considered:**
|
||||
- **Keep the entity, hide from UI.** Rejected — see above.
|
||||
- **Keep the class, gate behind a feature flag.** Rejected — dead code with a flag is still dead code.
|
||||
|
||||
### Decision 8: Delete take-over-control, don't gate it
|
||||
|
||||
**What we chose:** Remove `take_over_control`, `take_over_control_mode`, `detect_non_ha_changes`, `autoreset_control`, `only_once`, `adapt_only_on_bare_turn_on`. Strip the take-over-control state machine from `switch.py`.
|
||||
|
||||
**Why:** Manual-override semantics live at the scene/automation layer in CDiT's house. The take-over-control code threads through every adapter call in `switch.py` — making it conditional via a feature flag doesn't reduce complexity, just relocates it. Delete is the only honest option.
|
||||
|
||||
**Alternatives considered:**
|
||||
- **Gate behind `enable_legacy_overrides` flag.** Rejected — preserves the code, defeats the simplification goal, accumulates "advanced settings" that nobody ever turns on.
|
||||
- **Keep `intercept` and `multi_light_intercept`** — these *are* kept. They're transport behavior (hooking `light.turn_on` to merge adaptive values), not manual-override semantics. Different concern.
|
||||
|
||||
### Decision 9: Conditional visibility costs one submit cycle
|
||||
|
||||
**What we chose:** Fields with drivers (`send_split_delay` driven by `separate_turn_on_commands`; sun-time min/max pairs driven by whether the sun-entity is set) are conditionally included in the schema. When the driver changes, the user submits the form and HA re-renders with the new fields.
|
||||
|
||||
**Why:** HA's config flow can't re-render a single step dynamically when a field changes within that step. The supported path is to submit and let the next render reflect the new state. This costs one extra submit when a driver changes — accepted because driver changes are rare (you set `separate_turn_on_commands` once and forget).
|
||||
|
||||
**Alternatives considered:**
|
||||
- **Always show all fields.** Rejected — the whole point is to hide noise.
|
||||
- **Split into multiple steps when a driver changes.** Rejected — back to the wizard problem.
|
||||
|
||||
### Decision 10: Capability slug is `options-flow`, scoped tight
|
||||
|
||||
**What we chose:** This change defines one capability, `options-flow`, covering the integration's setup/reconfigure UI. Future changes get their own capabilities (`runtime-controls` for the number entities, `house-mode-binding` for the per-mode matrix).
|
||||
|
||||
**Why:** Keeps each spec testable and each requirement focused. A single `integration-configuration` umbrella would collect every config-related requirement across multiple changes — hard to read, hard to maintain.
|
||||
|
||||
### Decision 11: Sun curve math — max brightness lives strictly between the two entities
|
||||
|
||||
**What we chose:** Given `t_sunrise` and `t_sunset` from the entities, the brightness curve is:
|
||||
|
||||
```
|
||||
t < t_sunrise - 1800s : brightness = min
|
||||
t in [t_sunrise - 1800s, t_sunrise + 1800s] : tanh ramp from min → max
|
||||
t in [t_sunrise + 1800s, t_sunset - 1800s] : brightness = max
|
||||
t in [t_sunset - 1800s, t_sunset + 1800s] : tanh ramp from max → min
|
||||
t > t_sunset + 1800s : brightness = min
|
||||
```
|
||||
|
||||
Same shape applied to color temperature, scaled between `min_color_temp` and `max_color_temp`.
|
||||
|
||||
**Why:** Symmetric, deterministic, easy to test. The user's pick of entity is the semantic anchor — pick `sun2_dawn` and full brightness arrives 30 min after civil dawn.
|
||||
|
||||
**Alternatives considered:**
|
||||
- **Treat the entities as ramp midpoints.** Rejected — less intuitive; "when does the day start" should map to "when does max brightness start," not "when am I halfway to max."
|
||||
- **Sun-elevation-style curve interpolated between events.** Rejected — adds astral-style math complexity without proportional benefit. The user picked entity-driven explicitly to avoid that.
|
||||
|
||||
### Decision 12: Auto-remove leftover sleep-mode switch entities on first load
|
||||
|
||||
**What we chose:** In `async_setup_entry`, scan the entity registry for any `switch.adaptive_lighting_sleep_mode_*` entries tied to this integration's config entry, remove them via `entity_registry.async_remove(entity_id)`, and log each removal at INFO level.
|
||||
|
||||
**Why:** Without this, upgrading users see orphan switches in Settings → Entities forever. The cleanup is bounded (only entities this integration created, identifiable by `unique_id` prefix), idempotent (runs every setup, no-op after the first), and small (~10 LOC). The log line gives auditability.
|
||||
|
||||
**Alternatives considered:**
|
||||
- **Log + leave for manual cleanup.** Rejected — pollutes the device page; CDiT install has half a dozen AL configs, that's half a dozen orphans.
|
||||
- **One-time cleanup script.** Rejected — extra step the user has to run.
|
||||
|
||||
### Decision 13: Pin `homeassistant` minimum version to 2025.1.0
|
||||
|
||||
**What we chose:** `manifest.json` declares `"homeassistant": "2025.1.0"` as the minimum supported HA version.
|
||||
|
||||
**Why:** Lets us use the latest stable selector API (`NumberSelector` with `BOX_OR_SLIDER` mode, `EntitySelector` with multi-domain filtering), `section()` helper with no rendering quirks, and `OptionsFlowWithReload` with all 2024.x bug fixes applied. Excludes pre-2025.1 installs intentionally — running HA more than ~6 months behind is its own problem.
|
||||
|
||||
**Alternatives considered:**
|
||||
- **Pin 2024.10.** Rejected (initially recommended) — gives back the headroom for no clear benefit; user explicitly chose to go bolder.
|
||||
- **No pin (`"homeassistant"` absent).** Rejected — silently runs on incompatible versions, fails confusingly when `section()` doesn't render right.
|
||||
|
||||
### Decision 14: Sun-entity selectors are strictly typed
|
||||
|
||||
**What we chose:** Both `sunrise_entity` and `sunset_entity` use `EntitySelector(EntitySelectorConfig(domain="sensor", device_class="timestamp"))`. Only sensors with `device_class: timestamp` show up in the picker.
|
||||
|
||||
**Why:** Covers all real use cases (built-in `sensor.sun_next_rising`, Sun2's `sensor.sun2_*`, any custom timestamp sensor) while preventing foot-guns — `input_datetime` entities don't auto-update for next sunrise, and picking one would silently produce wrong curves. Strict typing makes the wrong choice impossible.
|
||||
|
||||
**Alternatives considered:**
|
||||
- **Loose typing (any datetime entity).** Rejected — invites the `input_datetime` foot-gun.
|
||||
- **Strict + escape hatch constant.** Rejected — no real use case for loose mode; adding the knob now is YAGNI.
|
||||
|
||||
### Decision 15: `RAMP_HALF_WIDTH_SECONDS` lives in `const.py`
|
||||
|
||||
**What we chose:** Define `RAMP_HALF_WIDTH_SECONDS = 1800` in `const.py` near the other tuning defaults, with an inline comment explaining the 30-minute choice (eye-friendly circadian transition, robust across solstice-to-equinox sunrise drift).
|
||||
|
||||
**Why:** `const.py` is the canonical "knobs you might tweak" location in HA custom components. Discoverable to anyone reading the integration. The comment captures the rationale so future-CDiT doesn't relitigate.
|
||||
|
||||
**Alternatives considered:**
|
||||
- **`__init__.py` near curve math.** Rejected — cohesive but harder to find when you want to tune.
|
||||
- **Buried in `config_flow.py`.** Rejected — treats the constant as flow-private, but the curve math is the actual consumer.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **[Sun2 isn't installed in HA]** → Defaults work fine — built-in `sensor.sun_next_*` is always present. Mitigation: README mentions Sun2 as a recommended-but-not-required HACS install.
|
||||
- **[`sensor.sun_next_rising` flips to tomorrow's date the moment sunrise passes]** → The curve math must handle this: after `t_sunrise` has passed, read `sun.sun.last_changed`/historical state, or anchor today's curve from `t_sunset` and the implicit "today is between yesterday's sunset and tomorrow's sunrise." Mitigation: write a small helper `today_sun_events()` that returns `(today_sunrise_dt, today_sunset_dt)` based on current time vs entity values; specs/tasks will cover this explicitly.
|
||||
- **[Removed sleep-mode switch entity lingers in HA's entity registry after upgrade]** → Old `switch.adaptive_lighting_sleep_mode_<name>` entries remain in `core.entity_registry` until manually deleted. Mitigation: document in CHANGELOG; provide a one-line `hass.config_entries.async_remove(...)` snippet for cleanup, or accept manual deletion via UI.
|
||||
- **[Strict break inconveniences any non-CDiT installer]** → Anyone forking the CDiT fork to use upstream config entries will hit the break. Mitigation: explicit "this fork is opinionated; existing AL entries will not load" notice in README and on the fork's GitHub description.
|
||||
- **[HA `section()` API is relatively young (added 2024.5)]** → Risk of styling/render quirks on older HA versions. Mitigation: bump `manifest.json` `homeassistant` minimum version to ≥ 2024.10 (when `OptionsFlowWithReload` also landed, so we're pinning both together).
|
||||
- **[The synthetic tanh curve drifts from astronomical reality near solstices]** → Won't perfectly match astral-computed elevation. Mitigation: this is by design — the user picked Sun2 entities for accuracy at the *endpoints*; smooth dimming between them is what matters, not physical accuracy.
|
||||
- **[`include_config_in_attributes` buried in Diagnostics could trip debug-time users]** → Mitigation: section is collapsed by default but not hidden; surfaces on expand. Mention in README's "Debugging" subsection.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
This change is delivered as a single PR on the fork's `main` branch. No phased rollout; CDiT-dev is the only consumer.
|
||||
|
||||
1. Land the PR.
|
||||
2. Tag the release (`v2.0.0-cdit.1`).
|
||||
3. On the running HA install:
|
||||
- Snapshot current AL config (Settings → Integrations → Adaptive Lighting → ⋮ → Download Diagnostics, or just screenshot the options dialog).
|
||||
- Update via HACS or manual copy of `custom_components/adaptive_lighting/`.
|
||||
- Restart HA.
|
||||
- Old config entry shows as "failed to load."
|
||||
- Delete the failed entry.
|
||||
- Recreate via Settings → Add Integration → Adaptive Lighting, copying the snapshot's values into the new section layout.
|
||||
4. Manually delete leftover `switch.adaptive_lighting_sleep_mode_<name>` entries via Settings → Devices → Entities (filter by integration, sort by "missing").
|
||||
|
||||
**Rollback:** `git revert` the PR merge commit. Old upstream config entries do *not* auto-recover — you'd reinstall upstream AL from HACS and re-create the entry there.
|
||||
|
||||
## Open Questions
|
||||
|
||||
None — all resolved and folded into Decisions 12–15.
|
||||
50
openspec/changes/cdit-config-redesign/proposal.md
Normal file
50
openspec/changes/cdit-config-redesign/proposal.md
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
## Why
|
||||
|
||||
The upstream Adaptive Lighting options dialog is a 40-field flat schema that mixes setup-time decisions, runtime-tunable values, and features CDiT does not use — sleep mode, automatic manual-override detection, three interchangeable brightness curve modes, and eight fields of manual sun-time bookkeeping. With no grouping, no conditional visibility, and expert-only field names, the dialog is hard to read and risky to edit; meaningful changes are buried in noise.
|
||||
|
||||
This change resets the integration config to what CDiT actually needs: an 18-field, sectioned dialog with conditional visibility, native HA selectors, and entity-driven sun timing that plugs into Sun2 cleanly.
|
||||
|
||||
## What Changes
|
||||
|
||||
**BREAKING** — existing upstream config entries will not load. `manifest.json` major version is bumped; users (CDiT only — one household) recreate the entry once.
|
||||
|
||||
- **Sectioned layout via `section()` helper** — 5 collapsible groups (Targets, Daytime curve, Sun schedule, Light control, Advanced) plus a collapsed Diagnostics subsection. One screen, grouped logically, no multi-step wizard.
|
||||
- **`OptionsFlowWithReload`** — saving reloads the integration without a full HA restart and without the stale-state bugs of manual reload.
|
||||
- **Conditional visibility** — fields appear only when their driver is set:
|
||||
- `send_split_delay` only when `separate_turn_on_commands` is true.
|
||||
- `include_config_in_attributes` lives under a collapsed Diagnostics subsection.
|
||||
- **Entity-driven sun timing** — two new fields replace eight:
|
||||
- `sunrise_entity`: any `sensor` with `device_class: timestamp`. Default `sensor.sun_next_rising`.
|
||||
- `sunset_entity`: same. Default `sensor.sun_next_setting`.
|
||||
- Users with Sun2 installed can point to civil / nautical / astronomical twilight sensors without code changes.
|
||||
- **Synthetic tanh brightness curve** — derived from `sunrise_entity` and `sunset_entity` with a hardcoded 30-minute half-width ramp at each end. Curve shape is no longer user-configurable.
|
||||
- **Native HA selectors throughout** — `EntitySelector` (lights and sun events), `NumberSelector` with explicit ranges (brightness, color temp, durations), `BooleanSelector` (flags). Replaces the upstream `int_between(...)` voluptuous custom validators with introspectable UI primitives.
|
||||
- **Explicit YAML-managed entry message** — currently the dialog appears editable but silently no-ops for YAML-configured entries. Replace with an `async_abort` and a clear "this entry is managed by YAML; edit `configuration.yaml`" message.
|
||||
- **Remove 21 fields and 1 entity:**
|
||||
- **Sleep cluster (6 fields + 1 entity)**: `sleep_brightness`, `sleep_rgb_or_color_temp`, `sleep_color_temp`, `sleep_rgb_color`, `sleep_transition`, `adapt_until_sleep`. Plus `switch.adaptive_lighting_sleep_mode_<name>` from the switch platform — runtime drops from 4 toggles to 3.
|
||||
- **Manual sun timing (8 fields)**: `sunrise_time`, `min_sunrise_time`, `max_sunrise_time`, `sunrise_offset`, and the four sunset counterparts. Replaced by `sunrise_entity` / `sunset_entity`.
|
||||
- **Curve shape (3 fields)**: `brightness_mode`, `brightness_mode_time_dark`, `brightness_mode_time_light`. Hardcoded to tanh with 30-min half-width.
|
||||
- **Take-over-control cluster (4 fields)**: `take_over_control`, `take_over_control_mode`, `detect_non_ha_changes`, `autoreset_control`. Manual overrides handled at the scene/automation layer; `only_once` and `adapt_only_on_bare_turn_on` removed as anti-AL flags.
|
||||
- **Kept from upstream override cluster (2 fields)**: `intercept` and `multi_light_intercept` — these are transport behavior (hooking `light.turn_on`), not manual-override semantics.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `options-flow`: integration setup and reconfiguration UI. Covers section layout, conditional field visibility, entity-driven sun timing, native HA selector usage, reload-on-save semantics, YAML-managed entry messaging, and the surface of fields that exist in the config dialog.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
None. `openspec/specs/` is empty — this is the first capability defined in the fork.
|
||||
|
||||
## Impact
|
||||
|
||||
- **`custom_components/adaptive_lighting/const.py`** — `VALIDATION_TUPLES` shrinks from 39 to 18 entries; `CONF_*` / `DEFAULT_*` constants for removed fields are deleted; `EXTRA_VALIDATION` shrinks accordingly. New constants for the hardcoded tanh half-width and the two entity-config keys.
|
||||
- **`custom_components/adaptive_lighting/config_flow.py`** — schema build switches from a flat `vol.Schema({...})` loop over `VALIDATION_TUPLES` to a hand-shaped layout using `section()` and HA selector classes. `OptionsFlowWithReload` replaces `OptionsFlow`. YAML-managed branch replaces the silent no-op with `async_abort(reason="yaml_managed")`.
|
||||
- **`custom_components/adaptive_lighting/switch.py`** — sleep mode switch entity class and its state machine are removed from the master switch class. Take-over-control state machine and its `autoreset` timer are removed. Two fewer entities per AL config.
|
||||
- **`custom_components/adaptive_lighting/__init__.py`** — `astral` calls for sun timing are replaced with `hass.states.get(<sunrise_entity>)` reads; tanh curve math moves here as a stable, hardcoded function. Astral remains a transitive dep but is no longer called in the curve path.
|
||||
- **`custom_components/adaptive_lighting/manifest.json`** — `version` bumped to `2.0.0-cdit.1` (or equivalent CDiT-tagged major) to force config-entry rejection on upgrade.
|
||||
- **`custom_components/adaptive_lighting/strings.json` + `translations/en.json`** — re-shaped to match new section labels; keys for deleted fields removed; new keys for sun-entity labels, the YAML-managed abort reason, and section titles. Other locales left as upstream — out of scope for this change.
|
||||
- **`tests/test_config_flow.py`** — substantially rewritten against the new schema. Sleep-mode tests and take-over-control tests are deleted, not migrated. New tests cover: entity selector validation, conditional visibility, tanh curve output at fixed timestamps, YAML-managed abort path, and `OptionsFlowWithReload` behavior.
|
||||
- **No new runtime dependencies.** `astral` stays in `pyproject.toml` for now (used elsewhere); can be pruned in a follow-up.
|
||||
- **Future changes unlocked**: `add-runtime-range-controls` (number entities for the 4 brightness/color-temp ranges) and `house-mode-modes` (per-AL behavior matrix driven by an external mode entity) both layer on top of this redesign cleanly.
|
||||
195
openspec/changes/cdit-config-redesign/specs/options-flow/spec.md
Normal file
195
openspec/changes/cdit-config-redesign/specs/options-flow/spec.md
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Options dialog presents fields in named collapsible sections
|
||||
|
||||
The integration options dialog SHALL group its 18 configurable fields into five named sections plus a Diagnostics subsection, rendered using Home Assistant's `section()` schema helper. Section names and field membership SHALL match the layout below.
|
||||
|
||||
| Section | Default state | Fields |
|
||||
|---|---|---|
|
||||
| Targets | expanded | `lights` |
|
||||
| Daytime curve | expanded | `min_brightness`, `max_brightness`, `min_color_temp`, `max_color_temp`, `prefer_rgb_color` |
|
||||
| Sun schedule | expanded | `sunrise_entity`, `sunset_entity` |
|
||||
| Light control | expanded | `intercept`, `multi_light_intercept` |
|
||||
| Advanced | collapsed | `interval`, `transition`, `initial_transition`, `adapt_delay`, `separate_turn_on_commands`, `send_split_delay`, `skip_redundant_commands` |
|
||||
| Diagnostics | collapsed | `include_config_in_attributes` |
|
||||
|
||||
#### Scenario: User opens options dialog on a UI-managed entry
|
||||
|
||||
- **WHEN** the user navigates to Settings → Devices & Services → Adaptive Lighting → Configure
|
||||
- **THEN** the form SHALL render six sections in the order: Targets, Daytime curve, Sun schedule, Light control, Advanced, Diagnostics
|
||||
- **AND** the Advanced and Diagnostics sections SHALL be rendered in their collapsed state
|
||||
- **AND** the Targets, Daytime curve, Sun schedule, and Light control sections SHALL be rendered expanded
|
||||
|
||||
#### Scenario: Each section contains only the fields specified for it
|
||||
|
||||
- **WHEN** the user expands any section in the options dialog
|
||||
- **THEN** the fields shown in that section SHALL exactly match the field list in the table above for that section
|
||||
- **AND** no field SHALL appear in more than one section
|
||||
|
||||
### Requirement: Conditional fields hide when their driver makes them irrelevant
|
||||
|
||||
Fields whose configuration is meaningful only under a specific value of another field ("driver") SHALL be omitted from the rendered schema when the driver value makes them inapplicable. When the driver value changes, the form SHALL be re-submitted to re-render with the updated field set.
|
||||
|
||||
The conditional pairs are:
|
||||
- `send_split_delay` is conditional on `separate_turn_on_commands` being `true`.
|
||||
|
||||
#### Scenario: send_split_delay hidden when transport mode disables it
|
||||
|
||||
- **WHEN** the user opens the options dialog with `separate_turn_on_commands` set to `false`
|
||||
- **THEN** the Advanced section SHALL NOT include the `send_split_delay` field
|
||||
|
||||
#### Scenario: send_split_delay revealed when transport mode enables it
|
||||
|
||||
- **WHEN** the user toggles `separate_turn_on_commands` to `true` and submits the form
|
||||
- **THEN** the options dialog SHALL re-render with `send_split_delay` present in the Advanced section
|
||||
- **AND** the field SHALL accept values in the range 0–10000 milliseconds
|
||||
|
||||
### Requirement: Sun event timing is read from configurable HA entities
|
||||
|
||||
The integration SHALL read sunrise and sunset event timestamps from two user-configured HA sensor entities exposed in the options dialog as `sunrise_entity` and `sunset_entity`. Both fields SHALL use an entity selector strictly typed to `domain: sensor` and `device_class: timestamp`. The integration SHALL NOT compute sun events from `astral` or any other internal sun-position library when both entities are configured.
|
||||
|
||||
#### Scenario: Default sun entities point to the built-in sun integration
|
||||
|
||||
- **WHEN** the user creates a new Adaptive Lighting config entry
|
||||
- **THEN** `sunrise_entity` SHALL default to `sensor.sun_next_rising`
|
||||
- **AND** `sunset_entity` SHALL default to `sensor.sun_next_setting`
|
||||
|
||||
#### Scenario: Selector filters to timestamp sensors only
|
||||
|
||||
- **WHEN** the user opens the entity picker for `sunrise_entity` or `sunset_entity`
|
||||
- **THEN** only entities with `domain == "sensor"` and `device_class == "timestamp"` SHALL appear in the picker
|
||||
- **AND** entities of domain `input_datetime` SHALL NOT appear
|
||||
|
||||
#### Scenario: Curve math reads from the configured entity
|
||||
|
||||
- **WHEN** the user sets `sunrise_entity` to `sensor.sun2_dawn` and saves
|
||||
- **THEN** subsequent brightness curve calculations SHALL use the timestamp value of `sensor.sun2_dawn` as the morning sun event
|
||||
- **AND** no call to `astral` SHALL occur in the curve evaluation path for this config entry
|
||||
|
||||
### Requirement: Brightness and color temperature follow a synthetic tanh curve
|
||||
|
||||
For each config entry, the integration SHALL synthesize the daytime brightness and color-temperature values using a hyperbolic-tangent curve anchored at the timestamps from `sunrise_entity` and `sunset_entity`, with a hardcoded half-width of 30 minutes (`RAMP_HALF_WIDTH_SECONDS = 1800`) at each event. The brightness value at time `t` SHALL follow:
|
||||
|
||||
- `t ≤ t_sunrise − 1800s`: value = configured minimum
|
||||
- `t_sunrise − 1800s < t < t_sunrise + 1800s`: value = tanh-interpolated minimum → maximum
|
||||
- `t_sunrise + 1800s ≤ t ≤ t_sunset − 1800s`: value = configured maximum
|
||||
- `t_sunset − 1800s < t < t_sunset + 1800s`: value = tanh-interpolated maximum → minimum
|
||||
- `t ≥ t_sunset + 1800s`: value = configured minimum
|
||||
|
||||
The same curve shape SHALL be applied to color temperature using `min_color_temp` and `max_color_temp` as the curve bounds.
|
||||
|
||||
#### Scenario: Brightness is at minimum well before sunrise
|
||||
|
||||
- **WHEN** the current time is more than 30 minutes before the `sunrise_entity` timestamp
|
||||
- **THEN** the computed brightness SHALL equal the configured `min_brightness`
|
||||
|
||||
#### Scenario: Brightness is exactly at the midpoint at the sunrise event
|
||||
|
||||
- **WHEN** the current time equals the `sunrise_entity` timestamp
|
||||
- **THEN** the computed brightness SHALL equal `(min_brightness + max_brightness) / 2`
|
||||
|
||||
#### Scenario: Brightness is at maximum during the day
|
||||
|
||||
- **WHEN** the current time is between `sunrise_entity + 30min` and `sunset_entity − 30min`
|
||||
- **THEN** the computed brightness SHALL equal the configured `max_brightness`
|
||||
|
||||
#### Scenario: Color temperature follows the same curve shape
|
||||
|
||||
- **WHEN** the current time is at any point on the curve
|
||||
- **THEN** the computed color temperature SHALL follow the same tanh interpolation between `min_color_temp` and `max_color_temp` as the brightness curve does between `min_brightness` and `max_brightness`
|
||||
|
||||
### Requirement: All configurable fields use native HA selectors
|
||||
|
||||
Every field in the options dialog SHALL be rendered using a class from `homeassistant.helpers.selector`. The integration SHALL NOT use bare voluptuous primitive types (such as `vol.Coerce(int)` or custom `int_between`) as schema values for user-facing fields. Each field type SHALL be backed by the selector specified below.
|
||||
|
||||
| Field type | Selector |
|
||||
|---|---|
|
||||
| Numeric range (brightness, color temp) | `NumberSelector` with explicit `min`, `max`, `step`, `unit_of_measurement`, `mode=SLIDER` |
|
||||
| Duration (seconds) | `NumberSelector` with `unit_of_measurement="s"`, `mode=BOX` |
|
||||
| Duration (milliseconds) | `NumberSelector` with `unit_of_measurement="ms"`, `mode=BOX` |
|
||||
| Boolean | `BooleanSelector` |
|
||||
| Entity (lights) | `EntitySelector` with `domain="light"`, `multiple=True` |
|
||||
| Entity (sun events) | `EntitySelector` with `domain="sensor"`, `device_class="timestamp"` |
|
||||
|
||||
#### Scenario: Brightness ranges render as sliders
|
||||
|
||||
- **WHEN** the user opens the options dialog
|
||||
- **THEN** `min_brightness` and `max_brightness` SHALL render as slider controls with range 1–100 and step 1
|
||||
- **AND** both fields SHALL display the unit "%"
|
||||
|
||||
#### Scenario: Color temperature ranges render with explicit unit
|
||||
|
||||
- **WHEN** the user opens the options dialog
|
||||
- **THEN** `min_color_temp` and `max_color_temp` SHALL render as numeric inputs with range 1000–10000 and step 100
|
||||
- **AND** both fields SHALL display the unit "K"
|
||||
|
||||
#### Scenario: Booleans render as toggles
|
||||
|
||||
- **WHEN** the user opens the options dialog
|
||||
- **THEN** every boolean field (`prefer_rgb_color`, `intercept`, `multi_light_intercept`, `separate_turn_on_commands`, `skip_redundant_commands`, `include_config_in_attributes`) SHALL render as a toggle switch control
|
||||
|
||||
### Requirement: Saving options reloads the integration via OptionsFlowWithReload
|
||||
|
||||
The options flow class SHALL extend `homeassistant.config_entries.OptionsFlowWithReload`. Saving changes through the options dialog SHALL trigger an integration reload without the integration manually calling `hass.config_entries.async_reload()`. Custom `async_unload_entry` plumbing for reload purposes SHALL NOT exist in the integration.
|
||||
|
||||
#### Scenario: Saving valid options reloads the integration
|
||||
|
||||
- **WHEN** the user changes any field in the options dialog and submits the form
|
||||
- **THEN** the integration's `async_unload_entry` and `async_setup_entry` SHALL be invoked exactly once each as part of the reload
|
||||
- **AND** the user SHALL NOT see a "restart Home Assistant" prompt
|
||||
|
||||
#### Scenario: Reload preserves entity registry identity
|
||||
|
||||
- **WHEN** the integration reloads after a save
|
||||
- **THEN** the entity IDs of the AL device's switches SHALL remain unchanged
|
||||
- **AND** no duplicate entities SHALL appear in the entity registry
|
||||
|
||||
### Requirement: YAML-managed config entries cannot be edited via the options dialog
|
||||
|
||||
When a config entry was created from `configuration.yaml` rather than the UI, the options flow SHALL abort with `async_abort(reason="yaml_managed")` instead of presenting an editable form. The abort SHALL produce a translation-keyed message in the HA UI that directs the user to edit `configuration.yaml`.
|
||||
|
||||
#### Scenario: Opening options on a YAML-managed entry shows an abort message
|
||||
|
||||
- **WHEN** the user navigates to Configure on a config entry whose `source == SOURCE_IMPORT`
|
||||
- **THEN** the options flow SHALL abort with reason `yaml_managed`
|
||||
- **AND** the HA UI SHALL display a message indicating the entry is YAML-managed and must be edited in `configuration.yaml`
|
||||
- **AND** no editable form SHALL be shown
|
||||
|
||||
### Requirement: Incompatible config entry versions fail to load with a clear error
|
||||
|
||||
The integration's `manifest.json` SHALL declare a major `version` that increments on every breaking config-schema change. On `async_setup_entry`, the integration SHALL reject any config entry whose stored `version` is older than the current major and SHALL raise `ConfigEntryError` with a user-facing message instructing the user to recreate the entry. No silent migration of dropped fields SHALL occur.
|
||||
|
||||
#### Scenario: Loading an upstream config entry on first upgrade
|
||||
|
||||
- **WHEN** Home Assistant attempts to set up a config entry whose `version` is 1 and the current integration version is 2
|
||||
- **THEN** `async_setup_entry` SHALL raise `ConfigEntryError` with a message that names the incompatibility and instructs the user to delete and recreate the entry
|
||||
- **AND** the integration SHALL NOT silently drop or migrate any fields from the old entry
|
||||
|
||||
#### Scenario: Loading a current-version entry succeeds
|
||||
|
||||
- **WHEN** Home Assistant attempts to set up a config entry whose `version` matches the current integration version
|
||||
- **THEN** `async_setup_entry` SHALL complete without error
|
||||
|
||||
### Requirement: Sleep-mode switch entities left behind by upstream are auto-removed
|
||||
|
||||
On `async_setup_entry`, the integration SHALL scan the entity registry for entities whose `unique_id` matches the historical sleep-mode switch pattern owned by this integration's config entry and SHALL remove each match via `entity_registry.async_remove`. Each removal SHALL be logged at `INFO` level with the entity ID. The cleanup SHALL be idempotent: subsequent setups of the same entry SHALL find no matches and SHALL no-op.
|
||||
|
||||
#### Scenario: First load after upgrade removes the orphan sleep switch
|
||||
|
||||
- **WHEN** Home Assistant sets up a config entry on first launch after the version bump
|
||||
- **AND** the entity registry contains a `switch.adaptive_lighting_sleep_mode_<name>` entity owned by this config entry
|
||||
- **THEN** that entity SHALL be removed from the entity registry
|
||||
- **AND** an `INFO` log entry SHALL be emitted naming the removed entity ID
|
||||
|
||||
#### Scenario: Subsequent loads find nothing to remove
|
||||
|
||||
- **WHEN** the integration has already removed the orphan sleep switch on a prior setup
|
||||
- **AND** Home Assistant sets up the same config entry again
|
||||
- **THEN** the entity registry scan SHALL find no matching entities
|
||||
- **AND** no `INFO` log entry about sleep-switch removal SHALL be emitted
|
||||
|
||||
#### Scenario: Cleanup does not touch entities owned by other integrations
|
||||
|
||||
- **WHEN** the integration runs the sleep-switch cleanup
|
||||
- **AND** another integration owns a similarly named entity (e.g., a user-created `switch.adaptive_lighting_sleep_mode_demo` template switch)
|
||||
- **THEN** that foreign entity SHALL NOT be removed from the entity registry
|
||||
86
openspec/changes/cdit-config-redesign/tasks.md
Normal file
86
openspec/changes/cdit-config-redesign/tasks.md
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
<!--
|
||||
Annotations:
|
||||
R1–R9 = Requirements in specs/options-flow/spec.md
|
||||
D1–D15 = Decisions in design.md
|
||||
|
||||
Build order: groups 1 and 2 are foundation (run sequentially or in parallel,
|
||||
no shared file). Groups 3-5 layer on top. Group 6 is the setup-entry guard.
|
||||
Group 7-9 are tests, translations, and docs (final polish).
|
||||
-->
|
||||
|
||||
## 1. Foundation — `const.py` and `manifest.json`
|
||||
|
||||
- [ ] 1.1 Remove the 21 retired `CONF_*` / `DEFAULT_*` constants from `const.py`: `sleep_brightness`, `sleep_rgb_or_color_temp`, `sleep_color_temp`, `sleep_rgb_color`, `sleep_transition`, `adapt_until_sleep`, `sunrise_time`, `min_sunrise_time`, `max_sunrise_time`, `sunrise_offset`, `sunset_time`, `min_sunset_time`, `max_sunset_time`, `sunset_offset`, `brightness_mode`, `brightness_mode_time_dark`, `brightness_mode_time_light`, `take_over_control`, `take_over_control_mode`, `detect_non_ha_changes`, `autoreset_control`, `only_once`, `adapt_only_on_bare_turn_on`. [R8]
|
||||
- [ ] 1.2 Trim `VALIDATION_TUPLES` to the 16 retained upstream entries plus 2 new sun-entity entries (target: 18 total). Also prune `EXTRA_VALIDATION` of orphaned keys. [R8]
|
||||
- [ ] 1.3 Add `CONF_SUNRISE_ENTITY = "sunrise_entity"` / `DEFAULT_SUNRISE_ENTITY = "sensor.sun_next_rising"` and the `_SUNSET_` pair. [R3]
|
||||
- [ ] 1.4 Add `RAMP_HALF_WIDTH_SECONDS = 1800` with an inline comment naming the design choice (30-min eye-friendly transition). [R4, D15]
|
||||
- [ ] 1.5 Bump `manifest.json` to `version: "2.0.0-cdit.1"`, add `"homeassistant": "2025.1.0"`, update `codeowners` to CDiT-dev maintainers, point `documentation` / `issue_tracker` at the fork URL. [R8, D4, D13]
|
||||
|
||||
## 2. Foundation — strip dead code from `switch.py`
|
||||
|
||||
- [ ] 2.1 Delete the sleep-mode switch entity class and its `unique_id` pattern from `switch.py`. The integration now creates 3 switches per AL config (master, adapt_brightness, adapt_color), not 4. [D7]
|
||||
- [ ] 2.2 Remove sleep-mode state machine from the master `AdaptiveSwitch` class (sleep transition logic, sleep brightness override, `adapt_until_sleep` handling). [D7]
|
||||
- [ ] 2.3 Remove the take-over-control state machine: `_manual_control` tracking, `_autoreset_handle`, `detect_non_ha_changes` polling, `only_once` short-circuits, `adapt_only_on_bare_turn_on` checks. [D8]
|
||||
- [ ] 2.4 Audit remaining `switch.py` for references to removed `CONF_*` keys and delete dead branches. Lint must pass. [D7, D8]
|
||||
|
||||
## 3. Config flow — sectioned schema
|
||||
|
||||
- [ ] 3.1 Replace the flat `VALIDATION_TUPLES` loop in `config_flow.py` with a hand-shaped builder that returns a `vol.Schema` containing six `section()`-wrapped subschemas: Targets, Daytime curve, Sun schedule, Light control, Advanced (collapsed), Diagnostics (collapsed). [R1, D1]
|
||||
- [ ] 3.2 Wire each field's selector per the table in R5: `NumberSelector` (slider mode for brightness 1–100 / step 1 / %; box-or-slider for color temp 1000–10000 / step 100 / K), `BooleanSelector`, `EntitySelector(domain="light", multiple=True)` for `lights`, `EntitySelector(domain="sensor", device_class="timestamp")` for the two sun entities. [R5, D5, D14]
|
||||
- [ ] 3.3 Implement conditional visibility for `send_split_delay` (driver: `separate_turn_on_commands`). The schema builder reads the current options/draft state and omits `send_split_delay` when the driver is false. [R2, D9]
|
||||
- [ ] 3.4 Move `include_config_in_attributes` into the Diagnostics collapsed subsection. [R1]
|
||||
- [ ] 3.5 Verify that no field appears in more than one section (spec R1 scenario 2). [R1]
|
||||
|
||||
## 4. Config flow — reload-on-save and YAML-managed abort
|
||||
|
||||
- [ ] 4.1 Change `OptionsFlow` → `OptionsFlowWithReload` in `config_flow.py`. Drop any custom `async_reload` / `async_unload_entry` plumbing that exists today for reload purposes. [R6, D6]
|
||||
- [ ] 4.2 In the options flow's `async_step_init`, detect `config_entry.source == SOURCE_IMPORT` and return `self.async_abort(reason="yaml_managed")`. [R7]
|
||||
- [ ] 4.3 Confirm via manual test that toggling a field and saving reloads the integration with no "restart HA" prompt, and that entity IDs of the AL switches are preserved across the reload. [R6]
|
||||
|
||||
## 5. Curve math — entity reads + synthetic tanh
|
||||
|
||||
- [ ] 5.1 Add a `today_sun_events(hass, entry)` helper in `__init__.py` (or a new `sun.py` module) that reads `entry.options[CONF_SUNRISE_ENTITY]` and `_SUNSET_ENTITY`, fetches their state via `hass.states.get(...)`, parses the timestamp, and handles the "next_rising flipped to tomorrow after sunrise" case by anchoring today's curve from whichever event is in the past. Returns `(today_sunrise_dt, today_sunset_dt)`. [R3, D2]
|
||||
- [ ] 5.2 Add a pure `tanh_curve(now, t_start, t_end, value_min, value_max, half_width=RAMP_HALF_WIDTH_SECONDS)` function. Returns `value_min` outside the active window, ramps via `tanh` between (`t_start - half_width`, `t_start + half_width`), holds at `value_max` between (`t_start + half_width`, `t_end - half_width`), ramps back via `tanh` between (`t_end - half_width`, `t_end + half_width`). [R4, D11]
|
||||
- [ ] 5.3 Replace upstream's `astral`-driven brightness and color-temp computation with two calls to `tanh_curve` — one for brightness using `min_brightness` / `max_brightness`, one for color temp using `min_color_temp` / `max_color_temp`. Same `(t_sunrise, t_sunset)` inputs for both. [R4]
|
||||
- [ ] 5.4 Verify the curve evaluation path no longer imports `astral.sun`. (`astral` may remain a transitive dep for now; pruning it is a follow-up.) [D2]
|
||||
|
||||
## 6. `async_setup_entry` guards
|
||||
|
||||
- [ ] 6.1 At the top of `async_setup_entry` in `__init__.py`, check `config_entry.version` against the current major (2). If older, raise `ConfigEntryError` with a user-facing message: "Adaptive Lighting v2 (CDiT fork) is incompatible with the existing config entry. Delete and recreate the entry from Settings → Devices & Services." [R8, D4]
|
||||
- [ ] 6.2 Add a sleep-switch tombstone helper: scan `entity_registry` for entities whose `unique_id` matches the historical `<entry.entry_id>_sleep_mode_*` pattern, call `entity_registry.async_remove(entity_id)` on each match, log `INFO` per removal with the entity ID. [R9, D12]
|
||||
- [ ] 6.3 Ensure the tombstone helper is idempotent — a second `async_setup_entry` call finds nothing and emits no log lines. [R9]
|
||||
- [ ] 6.4 Confirm the helper only removes entities whose `config_entry_id` matches the current entry (does not touch foreign entities matching the name pattern). [R9, D12]
|
||||
|
||||
## 7. Tests
|
||||
|
||||
- [ ] 7.1 Delete obsolete test files: `tests/test_*sleep*`, `tests/test_*take_over*`, `tests/test_*manual_control*`. Update `tests/conftest.py` to drop fixtures that referenced those features. [D7, D8]
|
||||
- [ ] 7.2 Add test: section layout — opening options on a UI-managed entry returns a flow result with six labeled sections in the specified order; Advanced and Diagnostics collapsed. [R1]
|
||||
- [ ] 7.3 Add test: each field appears in exactly one section, matching the R1 table. [R1]
|
||||
- [ ] 7.4 Add test: `send_split_delay` is absent from the schema when `separate_turn_on_commands` is false; present when true. [R2]
|
||||
- [ ] 7.5 Add test: `sunrise_entity` / `sunset_entity` default to `sensor.sun_next_rising` / `sensor.sun_next_setting` on a freshly created entry. [R3]
|
||||
- [ ] 7.6 Add test: entity selectors for the two sun fields are configured with `domain="sensor"` and `device_class="timestamp"`. [R3, D14]
|
||||
- [ ] 7.7 Add test: `tanh_curve` returns `value_min` more than `half_width` before `t_start`, midpoint at exactly `t_start`, `value_max` more than `half_width` after `t_start` (and the symmetric trio around `t_end`). [R4]
|
||||
- [ ] 7.8 Add test: color temperature uses the same curve shape as brightness, with `min_color_temp` / `max_color_temp` as bounds. [R4]
|
||||
- [ ] 7.9 Add test: `NumberSelector` types, ranges, units, and modes match the R5 table for brightness, color temp, durations, and milliseconds. [R5]
|
||||
- [ ] 7.10 Add test: saving valid options invokes `async_unload_entry` and `async_setup_entry` exactly once each (use mock spies) and produces no "restart HA" prompt. [R6]
|
||||
- [ ] 7.11 Add test: opening the options flow on a `SOURCE_IMPORT` config entry returns `async_abort(reason="yaml_managed")`. [R7]
|
||||
- [ ] 7.12 Add test: `async_setup_entry` raises `ConfigEntryError` when `config_entry.version == 1` and current is 2. [R8]
|
||||
- [ ] 7.13 Add test: `async_setup_entry` succeeds and runs no tombstone log line when entry version is current. [R8]
|
||||
- [ ] 7.14 Add test: the tombstone helper removes a seeded `switch.adaptive_lighting_sleep_mode_<name>` entity owned by this config entry, emits one INFO log line. [R9, D12]
|
||||
- [ ] 7.15 Add test: tombstone helper is idempotent — second run finds nothing, no log line. [R9]
|
||||
- [ ] 7.16 Add test: tombstone helper does not remove a foreign-owned entity matching the name pattern (different `config_entry_id`). [R9, D12]
|
||||
|
||||
## 8. Strings and translations
|
||||
|
||||
- [ ] 8.1 In `strings.json`, add section labels (`section.targets.name`, `section.daytime_curve.name`, etc.) and short descriptions per section. Add field labels for `sunrise_entity` / `sunset_entity`. [R1, R3]
|
||||
- [ ] 8.2 In `strings.json`, add the `yaml_managed` abort reason text: "This Adaptive Lighting config entry is managed from `configuration.yaml`. Edit it there to change options." [R7]
|
||||
- [ ] 8.3 In `strings.json`, add the version-incompatible error: "This entry was created with an older, incompatible version. Delete it and create a new one." [R8]
|
||||
- [ ] 8.4 Remove `strings.json` keys for the 21 deleted fields and the sleep switch entity. [R1, D7]
|
||||
- [ ] 8.5 Mirror the additions/removals in `translations/en.json`. Other locales (de, fr, etc.) are out of scope and may diverge until a follow-up change. [R1]
|
||||
|
||||
## 9. Docs — README and CHANGELOG
|
||||
|
||||
- [ ] 9.1 Replace the upstream README's "Installation" / "Configuration" sections (or add a CDiT-specific preamble at the top) explicitly stating: "This is a CDiT fork. Existing upstream config entries WILL NOT load; recreate them after upgrade." Reference the migration steps from design.md §Migration Plan. [D4]
|
||||
- [ ] 9.2 Add a "Recommended companions" section to README mentioning Sun2 as a HACS-installable source for precise civil / nautical / astronomical twilight sensors. Show the example of pointing `sunrise_entity` at `sensor.sun2_astro_dawn`. [D2, D14]
|
||||
- [ ] 9.3 Write `CHANGELOG.md` (or append to existing) entry for `2.0.0-cdit.1`: the 21 removed fields by name, the 2 added fields, the 1 removed entity, the breaking config-entry behavior, and the explicit recreate workflow. [D4]
|
||||
- [ ] 9.4 Update the fork's GitHub repo description and topics to mark it as opinionated/fork (not a drop-in replacement). [D4]
|
||||
2
openspec/changes/house-mode-modes/.openspec.yaml
Normal file
2
openspec/changes/house-mode-modes/.openspec.yaml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
schema: spec-driven
|
||||
created: 2026-05-16
|
||||
41
openspec/changes/house-mode-modes/proposal.md
Normal file
41
openspec/changes/house-mode-modes/proposal.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
## Why
|
||||
|
||||
A CDiT house already organizes itself around a "house mode" (`input_select.house_mode` or similar) with values like `morning`, `day`, `evening`, `night`, `away`, `guest`. Adaptive Lighting today is mode-blind: enabling AL only during `morning` / `day` / `evening` while disabling adaptation during `away` and forcing fixed brightness in `guest` requires N HA automations per AL × M modes, with no single place to read the truth.
|
||||
|
||||
This change makes the house mode binding native to each AL config: pick the house mode source entity once, fill in a per-mode behavior matrix, and the integration drives its three runtime switches (`master`, `adapt_color`, `adapt_brightness`) on every `state_changed` event from the source entity.
|
||||
|
||||
> **Status**: stub proposal. Specs, design, and tasks to be written when work on this change starts. Depends on `cdit-config-redesign` landing first; can land in parallel with `add-runtime-range-controls`.
|
||||
|
||||
## What Changes
|
||||
|
||||
- **New "House mode" section in the options flow** (added to the layout established by `cdit-config-redesign`). Lives between "Sun schedule" and "Light control". Collapsed by default if no source entity is configured, expanded once one is selected.
|
||||
- **New field `house_mode_entity`** — entity selector. Accepts any entity (`input_select`, `sensor`, `input_text`) — strict typing isn't appropriate because users build mode entities from many sources.
|
||||
- **Per-mode behavior matrix** — dynamically rendered once a source entity is selected. For each mode value (auto-discovered from `input_select.options` when available; manually enumerable for other entity types), three booleans: `active`, `adapt_color`, `adapt_brightness`.
|
||||
- **State listener** — integration subscribes to `state_changed` events for the configured `house_mode_entity`. On change, looks up the new state value in the matrix and asserts the three runtime switches accordingly.
|
||||
- **Strict semantics** (TBD in design): every mode change re-asserts the table; manual user overrides between mode transitions are not preserved across the next transition. Alternative "sticky" semantics deferred until design.
|
||||
- **Graceful degradation**:
|
||||
- Source entity unavailable → no switch changes, log a warning once.
|
||||
- State value not in the matrix → log an info entry, leave switches untouched.
|
||||
- Matrix is empty (no `house_mode_entity` configured) → integration behaves as if this feature does not exist.
|
||||
- **No new runtime control entities** — this change operates only on the existing three switches created per AL config.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `house-mode-binding`: per-AL coupling between an external HA mode entity and the three runtime switches. Covers entity-source configuration, mode discovery, behavior-matrix data model, state-change handling, override semantics (strict by default), and degradation under missing/unavailable source entities.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `options-flow`: gains a new "House mode" section with a driver field (`house_mode_entity`) and a conditional matrix renderer. Spec delta will capture this as ADDED Requirements + a MODIFIED Requirement on the section layout list.
|
||||
|
||||
## Impact
|
||||
|
||||
- **`custom_components/adaptive_lighting/config_flow.py`** — new section, new field, dynamic matrix rendering based on the discovered mode list of the selected entity.
|
||||
- **`custom_components/adaptive_lighting/__init__.py`** — `async_setup_entry` subscribes to `state_changed` for the configured `house_mode_entity` via `hass.helpers.event.async_track_state_change_event`.
|
||||
- **`custom_components/adaptive_lighting/switch.py`** — `AdaptiveSwitch` (the master class) gains an `_apply_house_mode(mode_value)` method that flips the three runtime switches per the configured matrix.
|
||||
- **`custom_components/adaptive_lighting/const.py`** — `CONF_HOUSE_MODE_ENTITY`, `CONF_HOUSE_MODE_MATRIX` constants; default matrix shape (empty dict).
|
||||
- **`tests/test_house_mode_binding.py`** — new file. Tests: mode change flips switches per matrix; unknown mode value is logged but doesn't crash; entity unavailable triggers warn-once behavior; empty matrix is a no-op; mode-discovery from `input_select.options`; manual mode list for non-select entities.
|
||||
- **No new runtime dependencies.**
|
||||
|
||||
**Sequencing**: depends on `cdit-config-redesign`. May land in parallel with `add-runtime-range-controls` (no overlap). Open architectural question for design: strict vs sticky override semantics.
|
||||
Loading…
Add table
Add a link
Reference in a new issue