From b8497b500fc9464cd8268f27893eb21cf2aae68a Mon Sep 17 00:00:00 2001 From: Casey Date: Sun, 17 May 2026 21:58:32 +0200 Subject: [PATCH] Archive add-runtime-range-controls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 47/47 tasks complete, including live verification on production HA: - 4 number entities × 6 profiles registered (42 AL entities total) - Slider drag does not reload integration (entry.modified_at stable) - Curve picks up new slider value on next recompute - Options flow seeds from entity state, not entry.options - Save snaps sliders to typed values via reload - RestoreNumber persists slider position across HA restart - Friendly names now read as " Brightness" / "Color" / etc. Delta synced into canonical specs: - options-flow MR4 (curve bound values come from entities) - runtime-range-controls capability added (7 requirements) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../.openspec.yaml | 0 .../design.md | 0 .../proposal.md | 0 .../specs/options-flow/spec.md | 0 .../specs/runtime-range-controls/spec.md | 0 .../tasks.md | 12 +- openspec/specs/options-flow/spec.md | 19 ++- openspec/specs/runtime-range-controls/spec.md | 142 ++++++++++++++++++ 8 files changed, 163 insertions(+), 10 deletions(-) rename openspec/changes/{add-runtime-range-controls => archive/2026-05-17-add-runtime-range-controls}/.openspec.yaml (100%) rename openspec/changes/{add-runtime-range-controls => archive/2026-05-17-add-runtime-range-controls}/design.md (100%) rename openspec/changes/{add-runtime-range-controls => archive/2026-05-17-add-runtime-range-controls}/proposal.md (100%) rename openspec/changes/{add-runtime-range-controls => archive/2026-05-17-add-runtime-range-controls}/specs/options-flow/spec.md (100%) rename openspec/changes/{add-runtime-range-controls => archive/2026-05-17-add-runtime-range-controls}/specs/runtime-range-controls/spec.md (100%) rename openspec/changes/{add-runtime-range-controls => archive/2026-05-17-add-runtime-range-controls}/tasks.md (96%) create mode 100644 openspec/specs/runtime-range-controls/spec.md diff --git a/openspec/changes/add-runtime-range-controls/.openspec.yaml b/openspec/changes/archive/2026-05-17-add-runtime-range-controls/.openspec.yaml similarity index 100% rename from openspec/changes/add-runtime-range-controls/.openspec.yaml rename to openspec/changes/archive/2026-05-17-add-runtime-range-controls/.openspec.yaml diff --git a/openspec/changes/add-runtime-range-controls/design.md b/openspec/changes/archive/2026-05-17-add-runtime-range-controls/design.md similarity index 100% rename from openspec/changes/add-runtime-range-controls/design.md rename to openspec/changes/archive/2026-05-17-add-runtime-range-controls/design.md diff --git a/openspec/changes/add-runtime-range-controls/proposal.md b/openspec/changes/archive/2026-05-17-add-runtime-range-controls/proposal.md similarity index 100% rename from openspec/changes/add-runtime-range-controls/proposal.md rename to openspec/changes/archive/2026-05-17-add-runtime-range-controls/proposal.md diff --git a/openspec/changes/add-runtime-range-controls/specs/options-flow/spec.md b/openspec/changes/archive/2026-05-17-add-runtime-range-controls/specs/options-flow/spec.md similarity index 100% rename from openspec/changes/add-runtime-range-controls/specs/options-flow/spec.md rename to openspec/changes/archive/2026-05-17-add-runtime-range-controls/specs/options-flow/spec.md diff --git a/openspec/changes/add-runtime-range-controls/specs/runtime-range-controls/spec.md b/openspec/changes/archive/2026-05-17-add-runtime-range-controls/specs/runtime-range-controls/spec.md similarity index 100% rename from openspec/changes/add-runtime-range-controls/specs/runtime-range-controls/spec.md rename to openspec/changes/archive/2026-05-17-add-runtime-range-controls/specs/runtime-range-controls/spec.md diff --git a/openspec/changes/add-runtime-range-controls/tasks.md b/openspec/changes/archive/2026-05-17-add-runtime-range-controls/tasks.md similarity index 96% rename from openspec/changes/add-runtime-range-controls/tasks.md rename to openspec/changes/archive/2026-05-17-add-runtime-range-controls/tasks.md index 7a031de7..e701ce47 100644 --- a/openspec/changes/add-runtime-range-controls/tasks.md +++ b/openspec/changes/archive/2026-05-17-add-runtime-range-controls/tasks.md @@ -47,7 +47,7 @@ Build order: group 1 is platform foundation. Group 2 implements the entity class - [x] 6.2 Set `_attr_name` per the D11 table: `AdaptiveSwitch._attr_name = None` (master takes device name), `AdaptBrightnessSwitch._attr_name = "Brightness"`, `AdaptColorSwitch._attr_name = "Color"`. Delete any code that hand-composes "Adaptive Lighting …" into the friendly name. [R7, D11] - [x] 6.3 Confirm the shared `device_info` block sets `name = entry.title` (or `entry.data[CONF_NAME]`, whichever is the user-facing string). This is the anchor for the composed friendly names. [R7, D11] - [x] 6.4 Verify `unique_id`s are NOT changed by this group — only `_attr_name` and `_attr_has_entity_name`. The entity registry must keep existing entity_ids stable. [R7, D11] -- [ ] 6.5 Manual check on live HA after deploy: pre-existing entity_ids unchanged (no duplicates, no broken automations), friendly names now read as "Dining MVP Brightness" / "Dining MVP Color" instead of "Adaptive Lighting Adapt Brightness dining_mvp_lights". [R7, D11] +- [x] 6.5 Manual check on live HA after deploy: pre-existing entity_ids unchanged (no duplicates, no broken automations), friendly names now read as "Dining MVP Brightness" / "Dining MVP Color" instead of "Adaptive Lighting Adapt Brightness dining_mvp_lights". [R7, D11] ## 7. Tests — `tests/test_number_platform.py` @@ -74,11 +74,11 @@ Build order: group 1 is platform foundation. Group 2 implements the entity class ## 9. Manual verification on live HA -- [ ] 9.1 Deploy to `homeassistant.onca-blenny.ts.net` via HACS. Verify the four `number.adaptive_lighting_*` entities appear under each of the 6 profiles' devices. [R1] -- [ ] 9.2 Move a slider on one profile via the dashboard. Verify (a) no integration reload occurs (check Integration page → no "reloading" banner; entity IDs unchanged), (b) the next curve tick uses the new value (watch the master switch's `brightness_pct` attribute over ~90 s). [R3, R4] -- [ ] 9.3 Open the options flow on the same profile. Verify the four range fields show the just-moved slider values, not the original setup defaults. [R6] -- [ ] 9.4 Save the options flow with different values. Verify the sliders snap to the new values after reload. [R5, D3] -- [ ] 9.5 Restart HA. Verify the slider values persist (RestoreNumber works). [R2] +- [x] 9.1 Deploy to `homeassistant.onca-blenny.ts.net` via HACS. Verify the four `number.adaptive_lighting_*` entities appear under each of the 6 profiles' devices. [R1] +- [x] 9.2 Move a slider on one profile via the dashboard. Verify (a) no integration reload occurs (check Integration page → no "reloading" banner; entity IDs unchanged), (b) the next curve tick uses the new value (watch the master switch's `brightness_pct` attribute over ~90 s). [R3, R4] +- [x] 9.3 Open the options flow on the same profile. Verify the four range fields show the just-moved slider values, not the original setup defaults. [R6] +- [x] 9.4 Save the options flow with different values. Verify the sliders snap to the new values after reload. [R5, D3] +- [x] 9.5 Restart HA. Verify the slider values persist (RestoreNumber works). [R2] ## 10. Validation gate diff --git a/openspec/specs/options-flow/spec.md b/openspec/specs/options-flow/spec.md index 06cbed91..e0afb0c0 100644 --- a/openspec/specs/options-flow/spec.md +++ b/openspec/specs/options-flow/spec.md @@ -81,25 +81,35 @@ For each config entry, the integration SHALL synthesize the daytime brightness a The same curve shape SHALL be applied to color temperature using `min_color_temp` and `max_color_temp` as the curve bounds. +The four bound values (`min_brightness`, `max_brightness`, `min_color_temp`, `max_color_temp`) SHALL be read at evaluation time from the four runtime range entities defined in the `runtime-range-controls` capability, with fallback to `entry.options[CONF_*]` when an entity is unavailable. The curve evaluation SHALL NOT read these four values directly from `entry.options` during normal operation. + #### 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` +- **THEN** the computed brightness SHALL equal the current value of `number.adaptive_lighting__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` +- **THEN** the computed brightness SHALL equal `(current_min_brightness + current_max_brightness) / 2` +- **WHERE** `current_min_brightness` and `current_max_brightness` are the current states of the corresponding number entities #### 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` +- **THEN** the computed brightness SHALL equal the current value of `number.adaptive_lighting__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` +- **THEN** the computed color temperature SHALL follow the same tanh interpolation between the current values of `number.adaptive_lighting__min_color_temp` and `_max_color_temp` as the brightness curve does between the two brightness entities + +#### Scenario: Bound values are taken from runtime entities, not from entry.options + +- **GIVEN** `entry.options[CONF_MIN_BRIGHTNESS]` is 5 +- **AND** `number.adaptive_lighting__min_brightness` is at 30 +- **WHEN** the curve is evaluated at a time before `sunrise_entity − 1800s` +- **THEN** the computed brightness SHALL equal 30 (the entity state), not 5 (`entry.options`) ### Requirement: All configurable fields use native HA selectors @@ -196,3 +206,4 @@ On `async_setup_entry`, the integration SHALL scan the entity registry for entit - **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 + diff --git a/openspec/specs/runtime-range-controls/spec.md b/openspec/specs/runtime-range-controls/spec.md new file mode 100644 index 00000000..7e6e377f --- /dev/null +++ b/openspec/specs/runtime-range-controls/spec.md @@ -0,0 +1,142 @@ +# runtime-range-controls Specification + +## Purpose +TBD - created by archiving change add-runtime-range-controls. Update Purpose after archive. +## Requirements +### Requirement: Each AL profile exposes four runtime range entities + +For each Adaptive Lighting config entry, the integration SHALL create exactly four `number` entities, one for each of `min_brightness`, `max_brightness`, `min_color_temp`, `max_color_temp`. The entities SHALL be registered on the `number` platform during `async_setup_entry` and torn down during `async_unload_entry`. Each entity SHALL share the same device record as the profile's three switches. + +| Field | unique_id suffix | native_min | native_max | step | unit | mode | +|---|---|---|---|---|---|---| +| `min_brightness` | `_min_brightness` | 1 | 100 | 1 | `%` | `SLIDER` | +| `max_brightness` | `_max_brightness` | 1 | 100 | 1 | `%` | `SLIDER` | +| `min_color_temp` | `_min_color_temp` | 1000 | 10000 | 100 | `K` | `SLIDER` | +| `max_color_temp` | `_max_color_temp` | 1000 | 10000 | 100 | `K` | `SLIDER` | + +The full unique_id SHALL be `_`. The device record SHALL be the same `(DOMAIN, entry.entry_id)` identifier used by the profile's switches. + +#### Scenario: A new config entry produces four number entities + +- **WHEN** the user creates a new Adaptive Lighting config entry +- **AND** `async_setup_entry` completes +- **THEN** the entity registry SHALL contain four `number` entities owned by this entry +- **AND** their unique_ids SHALL end with `_min_brightness`, `_max_brightness`, `_min_color_temp`, `_max_color_temp` respectively +- **AND** all four entities SHALL be attached to the same device as the profile's switches + +#### Scenario: Number entity bounds match the options-flow selector bounds + +- **WHEN** any of the four range number entities are inspected +- **THEN** the brightness entities SHALL declare `native_min_value=1`, `native_max_value=100`, `native_step=1`, `native_unit_of_measurement="%"`, and `mode=NumberMode.SLIDER` +- **AND** the color-temperature entities SHALL declare `native_min_value=1000`, `native_max_value=10000`, `native_step=100`, `native_unit_of_measurement="K"`, and `mode=NumberMode.SLIDER` + +### Requirement: Entities persist their value across Home Assistant restarts + +Each of the four range entities SHALL extend `homeassistant.components.number.RestoreNumber` so that its last known value is preserved across Home Assistant restarts without an explicit `Store` helper. On `async_added_to_hass`, each entity SHALL prefer in order: (1) the value present in `entry.options[CONF_*]` if that value is newer than the restored value (which is the case immediately after an options-flow save), (2) the restored value from `RestoreNumber.async_get_last_number_data()`, (3) the value present in `entry.options[CONF_*]` as the first-creation fallback. + +#### Scenario: Slider value survives an HA restart + +- **GIVEN** the user has moved `number.adaptive_lighting__min_brightness` to 30 via the dashboard +- **WHEN** Home Assistant is restarted +- **AND** the integration re-runs `async_setup_entry` +- **THEN** the entity's state SHALL be 30 once it finishes loading + +#### Scenario: Options-flow save value wins over restored value + +- **GIVEN** the user has previously moved the slider to 30 (restored state) +- **WHEN** the user opens the options flow, sets `min_brightness` to 50, and saves +- **AND** the integration reloads +- **THEN** the entity's state SHALL be 50, not 30 + +### Requirement: Curve math reads runtime ranges from the number entities + +The brightness and color-temperature curve evaluation SHALL read its four bound values (`min_brightness`, `max_brightness`, `min_color_temp`, `max_color_temp`) by calling `hass.states.get().state` for the corresponding number entity, casting to `int`, on every curve evaluation. The curve evaluation SHALL NOT read these four values from `entry.options` during normal operation. + +When `hass.states.get()` returns `None` or the state is `unavailable` or `unknown`, the curve evaluation SHALL fall back to the value in `entry.options[CONF_*]` and SHALL log the fallback at `DEBUG` level. + +#### Scenario: Slider change takes effect on next curve tick + +- **GIVEN** the integration is running with `number.adaptive_lighting__max_brightness` at 100 +- **WHEN** the user moves the slider to 70 +- **AND** the next curve evaluation tick fires +- **THEN** the brightness curve SHALL be computed with `value_max = 70` + +#### Scenario: Entity-unavailable fallback uses entry.options + +- **GIVEN** the four number entities are not yet available (e.g., during early setup race) +- **WHEN** the curve evaluation runs +- **THEN** the curve SHALL be computed using the values from `entry.options[CONF_*]` +- **AND** a `DEBUG` log entry SHALL be emitted naming the missing entity + +### Requirement: Slider changes do not trigger an integration reload + +Moving a slider on any of the four range entities SHALL NOT call `hass.config_entries.async_update_entry` for the owning config entry. The entity's new value SHALL take effect on the next curve evaluation tick without any reload of the integration, the device, or other entities. + +#### Scenario: Slider drag does not reload the integration + +- **GIVEN** the integration is loaded +- **WHEN** the user moves `number.adaptive_lighting__min_brightness` from 10 to 20 via the dashboard +- **THEN** `async_unload_entry` SHALL NOT be invoked +- **AND** `async_setup_entry` SHALL NOT be invoked +- **AND** the profile's switch entity IDs SHALL remain unchanged +- **AND** the entity's state SHALL update to 20 + +### Requirement: Options-flow save propagates new range values to the entities + +When the user saves the options flow with new values for any of the four range fields, the resulting integration reload SHALL cause the four entities to be recreated with the just-saved values as their initial state. Once the reload completes, each entity's state SHALL match the value submitted in the options flow. + +#### Scenario: Saving updated ranges in the options flow updates the sliders + +- **GIVEN** `number.adaptive_lighting__min_brightness` is currently at 30 +- **WHEN** the user opens the options flow, sets `min_brightness` to 55, and saves +- **THEN** the integration SHALL reload +- **AND** after the reload, the entity state SHALL be 55 + +### Requirement: Options-flow open seeds range fields from current entity state + +When the options dialog is rendered, the default values shown for the four range fields (`min_brightness`, `max_brightness`, `min_color_temp`, `max_color_temp`) SHALL be read from the corresponding number entity's current state via `hass.states.get().state` cast to `int`. The dialog SHALL NOT seed these four fields from `entry.options[CONF_*]` when entities exist and are available. If an entity is unavailable, the dialog SHALL fall back to `entry.options[CONF_*]` for that field. + +The other ~14 fields in the options dialog SHALL continue to seed from `entry.options` as defined by the `options-flow` capability. + +#### Scenario: Open options after live tuning shows live values + +- **GIVEN** the user has moved `number.adaptive_lighting__max_brightness` from 100 (default) to 80 via the dashboard +- **WHEN** the user opens the options flow +- **THEN** the `max_brightness` field in the Daytime curve section SHALL show 80 as its default +- **AND** the other fields in the dialog SHALL show their `entry.options` values + +### Requirement: All AL entities use HA's `has_entity_name` composition + +Every entity created by this integration — the three switches (`AdaptiveSwitch`, `AdaptBrightnessSwitch`, `AdaptColorSwitch`) and the four range number entities — SHALL set `_attr_has_entity_name = True` and SHALL register under a device whose `name` matches the profile's display name (`entry.title`). The per-entity `_attr_name` SHALL carry only the entity's role, not the integration name or the profile name. The master switch (`AdaptiveSwitch`) SHALL set `_attr_name = None` so HA renders its friendly name as the device name alone. + +The resulting friendly names SHALL follow this table for a profile named `Dining MVP`: + +| Entity | `_attr_name` | Friendly name | +|---|---|---| +| Master switch | `None` | `Dining MVP` | +| Adapt-brightness switch | `"Brightness"` | `Dining MVP Brightness` | +| Adapt-color switch | `"Color"` | `Dining MVP Color` | +| Min brightness number | `"Min brightness"` | `Dining MVP Min brightness` | +| Max brightness number | `"Max brightness"` | `Dining MVP Max brightness` | +| Min color temp number | `"Min color temp"` | `Dining MVP Min color temp` | +| Max color temp number | `"Max color temp"` | `Dining MVP Max color temp` | + +Existing `unique_id`s SHALL remain unchanged; the entity registry SHALL preserve existing `entity_id`s for any deployed install. + +#### Scenario: Friendly names compose from device name + entity role + +- **GIVEN** an AL profile is configured with display name "Dining MVP" +- **WHEN** the integration is loaded +- **THEN** the master switch's friendly name SHALL be exactly "Dining MVP" +- **AND** the adapt-brightness switch's friendly name SHALL be exactly "Dining MVP Brightness" +- **AND** the adapt-color switch's friendly name SHALL be exactly "Dining MVP Color" +- **AND** the four range number entities' friendly names SHALL be "Dining MVP Min brightness", "Dining MVP Max brightness", "Dining MVP Min color temp", "Dining MVP Max color temp" + +#### Scenario: Existing entity_ids survive the rename + +- **GIVEN** an entity registry contains a pre-existing `switch.adaptive_lighting_adapt_brightness_dining_mvp_lights` owned by this integration +- **WHEN** the integration is upgraded to a version that ships this `has_entity_name` change +- **AND** HA reloads the config entry +- **THEN** the entity's `entity_id` SHALL remain `switch.adaptive_lighting_adapt_brightness_dining_mvp_lights` (preserved by the registry via stable `unique_id`) +- **AND** only the entity's friendly name SHALL update to follow the new composition +