17 KiB
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 configurable fields into seven named sections, 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 |
| Ambient lux | collapsed | lux_sensor, target_lux |
| 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 seven sections in the order: Targets, Daytime curve, Sun schedule, Ambient lux, Light control, Advanced, Diagnostics
- AND the Ambient lux, 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_delayis conditional onseparate_turn_on_commandsbeingtrue.target_luxis conditional onlux_sensorbeing a non-empty string.
Scenario: send_split_delay hidden when transport mode disables it
- WHEN the user opens the options dialog with
separate_turn_on_commandsset tofalse - THEN the Advanced section SHALL NOT include the
send_split_delayfield
Scenario: send_split_delay revealed when transport mode enables it
- WHEN the user toggles
separate_turn_on_commandstotrueand submits the form - THEN the options dialog SHALL re-render with
send_split_delaypresent in the Advanced section - AND the field SHALL accept values in the range 0–10000 milliseconds
Scenario: target_lux hidden when no lux sensor is selected
- WHEN the user opens the options dialog with
lux_sensorset to""(empty) - THEN the Ambient lux section SHALL show only the
lux_sensorentity selector - AND
target_luxSHALL NOT appear
Scenario: target_lux revealed when lux sensor is selected
- WHEN the user selects a
lux_sensorentity and submits the form - THEN the options dialog SHALL re-render with
target_luxpresent in the Ambient lux section - AND the field SHALL accept values in the range 1–10000 lux
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_entitySHALL default tosensor.sun_next_rising - AND
sunset_entitySHALL default tosensor.sun_next_setting
Scenario: Selector filters to timestamp sensors only
- WHEN the user opens the entity picker for
sunrise_entityorsunset_entity - THEN only entities with
domain == "sensor"anddevice_class == "timestamp"SHALL appear in the picker - AND entities of domain
input_datetimeSHALL NOT appear
Scenario: Curve math reads from the configured entity
- WHEN the user sets
sunrise_entitytosensor.sun2_dawnand saves - THEN subsequent brightness curve calculations SHALL use the timestamp value of
sensor.sun2_dawnas the morning sun event - AND no call to
astralSHALL 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 half-width W at each event. W SHALL be read at evaluation time from the profile's ramp half-width runtime entity as defined in the runtime-range-controls capability (default 30 minutes, falling back to RAMP_HALF_WIDTH_SECONDS = 1800 when the entity is unavailable). The brightness value at time t SHALL follow:
t ≤ t_sunrise − W: value = configured minimumt_sunrise − W < t < t_sunrise + W: value = tanh-interpolated minimum → maximumt_sunrise + W ≤ t ≤ t_sunset − W: value = configured maximumt_sunset − W < t < t_sunset + W: value = tanh-interpolated maximum → minimumt ≥ t_sunset + W: 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. Both channels SHALL use the same W within a single evaluation.
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
Wbefore thesunrise_entitytimestamp - THEN the computed brightness SHALL equal the current value of
number.adaptive_lighting_<name>_min_brightness
Scenario: Brightness is exactly at the midpoint at the sunrise event
- WHEN the current time equals the
sunrise_entitytimestamp - THEN the computed brightness SHALL equal
(current_min_brightness + current_max_brightness) / 2 - WHERE
current_min_brightnessandcurrent_max_brightnessare the current states of the corresponding number entities
Scenario: Brightness is at maximum during the day
- WHEN the current time is between
sunrise_entity + Wandsunset_entity − W - THEN the computed brightness SHALL equal the current value of
number.adaptive_lighting_<name>_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 the current values of
number.adaptive_lighting_<name>_min_color_tempand_max_color_tempas 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_<name>_min_brightnessis at 30 - WHEN the curve is evaluated at a time before
sunrise_entity − W - THEN the computed brightness SHALL equal 30 (the entity state), not 5 (
entry.options)
Scenario: Curve width follows the ramp half-width entity
- GIVEN the profile's ramp half-width entity is at 60
- WHEN the curve is evaluated 45 minutes before the
sunset_entitytimestamp - THEN the computed brightness SHALL lie strictly between the configured minimum and maximum (inside the widened down-ramp)
- AND with the entity at 30 the same instant would have produced the configured maximum (outside the default-width ramp)
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" |
| Entity (lux sensor) | EntitySelector with domain="sensor", device_class="illuminance" |
| Lux target | NumberSelector with min=1, max=10000, step=10, unit_of_measurement="lx", mode=BOX |
Scenario: Brightness ranges render as sliders
- WHEN the user opens the options dialog
- THEN
min_brightnessandmax_brightnessSHALL 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_tempandmax_color_tempSHALL 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
Scenario: Lux sensor selector filters to illuminance sensors only
- WHEN the user opens the entity picker for
lux_sensor - THEN only entities with
domain == "sensor"anddevice_class == "illuminance"SHALL appear in the picker - AND temperature sensors, humidity sensors, and other non-illuminance sensors SHALL NOT appear
Scenario: Target lux renders as a number box with lux unit
- WHEN the user expands the Ambient lux section with a lux sensor configured
- THEN
target_luxSHALL render as a numeric box input with range 1–10000, step 10 - AND the field SHALL display the unit "lx"
Requirement: Ambient lux section shows the sensor's current reading
When a lux_sensor is configured and its state is numeric, the Ambient lux section description SHALL include the sensor's current reading via description_placeholders. This helps the user calibrate their target_lux to their actual space.
Scenario: Current lux reading shown in section description
- GIVEN
lux_sensoris set tosensor.office_illuminance - AND that sensor's current state is
"340" - WHEN the user opens the options dialog
- THEN the Ambient lux section description SHALL include the text "340 lx"
Scenario: No reading shown when sensor is not configured
- GIVEN
lux_sensoris empty (not configured) - WHEN the user opens the options dialog
- THEN the Ambient lux section description SHALL NOT include any lux reading number
Scenario: Fallback when sensor is unavailable
- GIVEN
lux_sensoris configured but its state is"unavailable" - WHEN the user opens the options dialog
- THEN the Ambient lux section description SHALL show a dash or "unavailable" in place of a numeric reading
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_entryandasync_setup_entrySHALL 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
versionis 1 and the current integration version is 2 - THEN
async_setup_entrySHALL raiseConfigEntryErrorwith 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
versionmatches the current integration version - THEN
async_setup_entrySHALL 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_<name>entity owned by this config entry - THEN that entity SHALL be removed from the entity registry
- AND an
INFOlog 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
INFOlog 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_demotemplate switch) - THEN that foreign entity SHALL NOT be removed from the entity registry