adaptive-lighting/openspec/specs/runtime-range-controls/spec.md
Casey ada3538449 Rename range sliders to lower/upper + friendlier new-profile defaults
Device page sorts entities alphabetically by name, so 'Min/Max …' always
listed max above min ('a' < 'i'). Rename the four range numbers to
'Brightness lower/upper' and 'Color temp lower/upper' so each quantity's
lower bound sorts first. Pin suggested_object_id to the field key so new
profiles keep slugging number.<profile>_min_brightness etc., identical to
profiles created before the rename; existing entity_ids are registry-
preserved via unchanged unique_ids.

New-profile defaults: brightness 5-100% -> 10-90% (5% proved too dim on
gloomy days; 90% leaves manual-boost headroom), min color temp 2200K ->
2000K (deeper warm tone won in practice). Existing profiles unaffected —
saved options and restored slider values take precedence.

Specs synced: runtime-range-controls naming table + scenarios,
output-sensors collision references. Two new tests cover the slug pin
and the lower/upper friendly-name composition.
2026-06-05 10:49:40 +02:00

10 KiB

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 <entry.entry_id>_<suffix>. 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_<name>_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(<entity_id>).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(<entity_id>) 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_<name>_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_<name>_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_<name>_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(<entity_id>).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_<name>_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 "Brightness lower" Dining MVP Brightness lower
Max brightness number "Brightness upper" Dining MVP Brightness upper
Min color temp number "Color temp lower" Dining MVP Color temp lower
Max color temp number "Color temp upper" Dining MVP Color temp upper

The range numbers use "lower/upper" wording (not "Min/Max") so the HA device page — which sorts entities alphabetically by friendly name — lists each quantity's lower bound before its upper bound. The number entities SHALL additionally pin suggested_object_id to their field key (min_brightness, max_brightness, min_color_temp, max_color_temp) so newly created profiles slug the same entity_ids as profiles created before the rename.

Existing unique_ids SHALL remain unchanged; the entity registry SHALL preserve existing entity_ids 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 Brightness lower", "Dining MVP Brightness upper", "Dining MVP Color temp lower", "Dining MVP Color temp upper"
  • AND the four range number entities' entity_ids SHALL slug from the field keys (e.g. number.dining_mvp_min_brightness), not from the display names

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