From c1a98a5c915b3b0c1978f276680857f9efa9110a Mon Sep 17 00:00:00 2001 From: Casey Date: Sat, 16 May 2026 17:08:53 +0200 Subject: [PATCH] Archive cdit-config-redesign: 53/53 + repo description MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task 9.4: GitHub repo description and topics set via `gh repo edit`. Description: "CDiT-opinionated fork of basnijholt/adaptive-lighting — sectioned config, entity-driven sun timing (Sun2-friendly), synthetic tanh curve. Not a drop-in replacement; star upstream too." Topics: home-assistant, hacs, adaptive-lighting, circadian-lighting, cdit, opinionated-fork, custom-component, home-automation. Archive: change moved to openspec/changes/archive/2026-05-16-cdit- config-redesign/. The 9 ADDED Requirements from the delta were synced into openspec/specs/options-flow/spec.md as the canonical capability spec — this is the first long-lived spec in the repo. Result: cdit-config-redesign is 53/53 complete, archived, validated, synced. The three remaining active changes (add-lovelace-card, add-runtime-range-controls, house-mode-modes) sit in the roadmap. --- .../.openspec.yaml | 0 .../design.md | 0 .../proposal.md | 0 .../specs/options-flow/spec.md | 0 .../2026-05-16-cdit-config-redesign}/tasks.md | 2 +- openspec/specs/options-flow/spec.md | 199 ++++++++++++++++++ 6 files changed, 200 insertions(+), 1 deletion(-) rename openspec/changes/{cdit-config-redesign => archive/2026-05-16-cdit-config-redesign}/.openspec.yaml (100%) rename openspec/changes/{cdit-config-redesign => archive/2026-05-16-cdit-config-redesign}/design.md (100%) rename openspec/changes/{cdit-config-redesign => archive/2026-05-16-cdit-config-redesign}/proposal.md (100%) rename openspec/changes/{cdit-config-redesign => archive/2026-05-16-cdit-config-redesign}/specs/options-flow/spec.md (100%) rename openspec/changes/{cdit-config-redesign => archive/2026-05-16-cdit-config-redesign}/tasks.md (97%) create mode 100644 openspec/specs/options-flow/spec.md diff --git a/openspec/changes/cdit-config-redesign/.openspec.yaml b/openspec/changes/archive/2026-05-16-cdit-config-redesign/.openspec.yaml similarity index 100% rename from openspec/changes/cdit-config-redesign/.openspec.yaml rename to openspec/changes/archive/2026-05-16-cdit-config-redesign/.openspec.yaml diff --git a/openspec/changes/cdit-config-redesign/design.md b/openspec/changes/archive/2026-05-16-cdit-config-redesign/design.md similarity index 100% rename from openspec/changes/cdit-config-redesign/design.md rename to openspec/changes/archive/2026-05-16-cdit-config-redesign/design.md diff --git a/openspec/changes/cdit-config-redesign/proposal.md b/openspec/changes/archive/2026-05-16-cdit-config-redesign/proposal.md similarity index 100% rename from openspec/changes/cdit-config-redesign/proposal.md rename to openspec/changes/archive/2026-05-16-cdit-config-redesign/proposal.md diff --git a/openspec/changes/cdit-config-redesign/specs/options-flow/spec.md b/openspec/changes/archive/2026-05-16-cdit-config-redesign/specs/options-flow/spec.md similarity index 100% rename from openspec/changes/cdit-config-redesign/specs/options-flow/spec.md rename to openspec/changes/archive/2026-05-16-cdit-config-redesign/specs/options-flow/spec.md diff --git a/openspec/changes/cdit-config-redesign/tasks.md b/openspec/changes/archive/2026-05-16-cdit-config-redesign/tasks.md similarity index 97% rename from openspec/changes/cdit-config-redesign/tasks.md rename to openspec/changes/archive/2026-05-16-cdit-config-redesign/tasks.md index a444035f..eda0ceb4 100644 --- a/openspec/changes/cdit-config-redesign/tasks.md +++ b/openspec/changes/archive/2026-05-16-cdit-config-redesign/tasks.md @@ -87,4 +87,4 @@ Group 7-9 are tests, translations, and docs (final polish). - [x] 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] - [x] 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] - [x] 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] _(GitHub-side metadata; do via `gh repo edit` in a separate step after merge.)_ +- [x] 9.4 Update the fork's GitHub repo description and topics to mark it as opinionated/fork (not a drop-in replacement). [D4] _(Done via `gh repo edit`: description names upstream + CDiT differentiators + "not a drop-in replacement; star upstream too"; topics: home-assistant, hacs, adaptive-lighting, circadian-lighting, cdit, opinionated-fork, custom-component, home-automation.)_ diff --git a/openspec/specs/options-flow/spec.md b/openspec/specs/options-flow/spec.md new file mode 100644 index 00000000..b2861b7e --- /dev/null +++ b/openspec/specs/options-flow/spec.md @@ -0,0 +1,199 @@ +# options-flow Specification + +## Purpose +TBD - created by archiving change cdit-config-redesign. Update Purpose after archive. +## 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_` 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 +