mirror of
https://github.com/basnijholt/adaptive-lighting.git
synced 2026-09-27 04:14:47 +02:00
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.
90 lines
12 KiB
Markdown
90 lines
12 KiB
Markdown
<!--
|
||
Annotations:
|
||
R1–R9 = Requirements in specs/options-flow/spec.md
|
||
D1–D15 = Decisions in design.md
|
||
polish = Quality/UX tasks captured during design review, not spec-driven
|
||
|
||
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`
|
||
|
||
- [x] 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]
|
||
- [x] 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]
|
||
- [x] 1.3 Add `CONF_SUNRISE_ENTITY = "sunrise_entity"` / `DEFAULT_SUNRISE_ENTITY = "sensor.sun_next_rising"` and the `_SUNSET_` pair. [R3]
|
||
- [x] 1.4 Add `RAMP_HALF_WIDTH_SECONDS = 1800` with an inline comment naming the design choice (30-min eye-friendly transition). [R4, D15]
|
||
- [x] 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]
|
||
- [x] 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`
|
||
|
||
- [x] 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]
|
||
- [x] 2.2 Remove sleep-mode state machine from the master `AdaptiveSwitch` class (sleep transition logic, sleep brightness override, `adapt_until_sleep` handling). [D7]
|
||
- [x] 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.)_
|
||
- [x] 2.4 Audit remaining `switch.py` for references to removed `CONF_*` keys and delete dead branches. Lint must pass. [D7, D8]
|
||
- [x] 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
|
||
|
||
- [x] 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]
|
||
- [x] 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]
|
||
- [x] 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]
|
||
- [x] 3.4 Move `include_config_in_attributes` into the Diagnostics collapsed subsection. [R1]
|
||
- [x] 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
|
||
|
||
- [x] 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]
|
||
- [x] 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
|
||
|
||
- [x] 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.)_
|
||
- [x] 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`.)_
|
||
- [x] 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]
|
||
- [x] 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
|
||
|
||
- [x] 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]
|
||
- [x] 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]
|
||
- [x] 6.3 Ensure the tombstone helper is idempotent — a second `async_setup_entry` call finds nothing and emits no log lines. [R9]
|
||
- [x] 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
|
||
|
||
- [x] 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]
|
||
- [x] 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]
|
||
- [x] 7.3 Add test: each field appears in exactly one section, matching the R1 table. [R1]
|
||
- [x] 7.4 Add test: `send_split_delay` is absent from the schema when `separate_turn_on_commands` is false; present when true. [R2]
|
||
- [x] 7.5 Add test: `sunrise_entity` / `sunset_entity` default to `sensor.sun_next_rising` / `sensor.sun_next_setting` on a freshly created entry. [R3]
|
||
- [x] 7.6 Add test: entity selectors for the two sun fields are configured with `domain="sensor"` and `device_class="timestamp"`. [R3, D14]
|
||
- [x] 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]
|
||
- [x] 7.8 Add test: color temperature uses the same curve shape as brightness, with `min_color_temp` / `max_color_temp` as bounds. [R4]
|
||
- [x] 7.9 Add test: `NumberSelector` types, ranges, units, and modes match the R5 table for brightness, color temp, durations, and milliseconds. [R5]
|
||
- [x] 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]
|
||
- [x] 7.11 Add test: opening the options flow on a `SOURCE_IMPORT` config entry returns `async_abort(reason="yaml_managed")`. [R7]
|
||
- [x] 7.12 Add test: `async_setup_entry` raises `ConfigEntryError` when `config_entry.version == 1` and current is 2. [R8]
|
||
- [x] 7.13 Add test: `async_setup_entry` succeeds and runs no tombstone log line when entry version is current. [R8]
|
||
- [x] 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]
|
||
- [x] 7.15 Add test: tombstone helper is idempotent — second run finds nothing, no log line. [R9]
|
||
- [x] 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
|
||
|
||
- [x] 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]
|
||
- [x] 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]
|
||
- [x] 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]
|
||
- [x] 8.4 Remove `strings.json` keys for the 21 deleted fields and the sleep switch entity. [R1, D7]
|
||
- [x] 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]
|
||
- [x] 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
|
||
|
||
- [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.)_
|