adaptive-lighting/openspec/changes/cdit-config-redesign/tasks.md
Casey 03d2748cd4 Group 7: tests — 94 passing in 0.57s (53/53 + 9.4 deferred)
Test infrastructure:
- Add `pytest-homeassistant-custom-component` (PHACC) as a `test`
  dependency group. PHACC ships `hass`, `enable_custom_integrations`,
  `MockConfigEntry`, and friends without needing to clone HA core as a
  sibling directory. Modernizes the test setup from upstream's
  `setup-symlinks` pattern.
- conftest.py: add `auto_enable_custom_integrations` autouse fixture
  so HA discovers the integration under `custom_components/` during
  tests. Keep the upstream template-deprecation no-op.
- Import paths: all tests now import from `custom_components.adaptive_lighting`
  (not `homeassistant.components.adaptive_lighting`) and from
  `pytest_homeassistant_custom_component.common` (not `tests.common`).

New tests (groups 7.2-7.16):
- tests/test_color_and_brightness.py (16 tests): TestBrightnessCurve
  asserts min-before-sunrise, midpoint-at-event, max-during-day, sunset
  ramp symmetry. TestColorTempCurve verifies the same shape applies to
  K. TestSunPosition checks the synthetic +1/-1/0 derivation.
  TestTanhDayCurveDirect exercises the helper directly.
- tests/test_config_flow.py (12 tests): six sections in order, each
  section contains only its specified fields, conditional visibility
  of send_split_delay, default sun entities, strict-typed entity
  selectors, NumberSelector slider/box configs, BooleanSelector for
  every flag, full user→create-entry flow, YAML-managed entry aborts
  with `yaml_managed` reason, options flow renders the sectioned schema.
- tests/test_init.py (6 tests): successful setup on current version,
  stale version raises ConfigEntryError via async_migrate_entry,
  unload is clean, tombstone removes orphan sleep entity + logs INFO,
  tombstone is idempotent, tombstone respects config_entry ownership.

Source modernizations driven by the tests:
- __init__.py: add `async_migrate_entry` that surfaces the
  "incompatible — delete and recreate" message and sets the entry to
  MIGRATION_ERROR. Removed direct `ConfigEntryError` from
  `async_setup_entry` (HA routes version mismatches through the
  migration handler now).
- switch.py: removed upstream's YAML-managed-entry auto-remove hack.
  YAML profiles now load normally and the options flow handles the
  "you must edit configuration.yaml" message (spec R7).

Existing tests updated to PHACC paths: test_adaptation_utils.py
(38 tests), test_hass_utils.py (22 tests). Both pass without
modification beyond the import fix.

Deleted: tests/test_switch.py (2,999 LOC of upstream tests, most
covering sleep mode / take-over-control / manual-control state
machines that no longer exist; CDiT-specific switch tests deferred
to a follow-up change).

Result: `uv run --group test pytest tests/` → 94 passed in 0.57s.

Deferred from this change:
- 4.3: manual UI test that toggling a field and saving reloads cleanly
  (requires a real HA instance, can't be done from CLI).
- 9.4: GitHub repo description / topics update (do via `gh repo edit`
  outside the change scope).

openspec status: 4/4 artifacts complete; strict-validate green.
2026-05-16 15:11:49 +02:00

12 KiB
Raw Blame History

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]
  • 1.6 Re-default values that felt off in upstream: DEFAULT_MIN_BRIGHTNESS = 5 (was 1 — 1% reads as off on most bulbs), DEFAULT_MIN_COLOR_TEMP = 2200 (was 2000 — less sodium-vapor orange). Document the change in a one-line comment per constant. [polish]

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] (Note: implemented via bridge stubs _take_over_control = False etc. on AdaptiveSwitch — Manager-side branches now dead but not yet pruned; follow-up cleanup.)
  • 2.4 Audit remaining switch.py for references to removed CONF_* keys and delete dead branches. Lint must pass. [D7, D8]
  • 2.5 Set distinct _attr_icon on each of the three switch classes: master mdi:weather-sunny-alert (signals automatic behavior), adapt_color mdi:invert-colors, adapt_brightness mdi:brightness-percent. [polish]

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 1100 / step 1 / %; box-or-slider for color temp 100010000 / 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 OptionsFlowOptionsFlowWithReload 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] (Implemented as AdaptiveSwitch._today_sun_events() instance method rather than module-level helper — same behavior, scoped to the switch.)
  • 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] (Implemented as _tanh_day_curve() in color_and_brightness.py.)
  • 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]
  • 8.6 Plain-language pass on every field label and description in strings.json. Each label reads like a sentence a human wrote — not engineer shorthand. Add a one-sentence "what this section is for" framer at the top of each section. [R1, R7, R8, polish]

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] (GitHub-side metadata; do via gh repo edit in a separate step after merge.)