mirror of
https://github.com/basnijholt/adaptive-lighting.git
synced 2026-09-29 05:14:42 +02:00
New lux-feedback spec, updated options-flow (7 sections, lux conditionals, live reading), updated output-sensors (conditional ambient_lux + lux_reduction).
144 lines
7.3 KiB
Markdown
144 lines
7.3 KiB
Markdown
# lux-feedback Specification
|
||
|
||
## Purpose
|
||
Reduce-only ambient-lux gate that dims lights when daylight alone exceeds a user-set target. Uses a proportional `target/current` factor, turns lights off below `min_brightness`, and degrades gracefully when the sensor is unavailable or unconfigured. The gate only reduces brightness — it never boosts above the sun curve.
|
||
|
||
## Requirements
|
||
### Requirement: Reduce-only lux gate dims lights when ambient lux exceeds target
|
||
|
||
When a profile has both `lux_sensor` and `target_lux` configured, the integration SHALL apply a reduction factor to the curve-computed brightness on every adapt cycle. The factor SHALL be `target_lux / current_lux` when `current_lux > target_lux`, and `1.0` otherwise. The gate SHALL NOT increase brightness above the curve value under any circumstances.
|
||
|
||
The adjusted brightness SHALL be `curve_brightness × factor`, clamped to `[min_brightness, curve_brightness]`.
|
||
|
||
Color temperature SHALL NOT be affected by the lux gate — it SHALL remain on the sun curve.
|
||
|
||
#### Scenario: Ambient lux above target reduces brightness
|
||
|
||
- **WHEN** the curve computes brightness at 85%
|
||
- **AND** `target_lux` is 500
|
||
- **AND** `lux_sensor` reads 700
|
||
- **THEN** the integration SHALL send brightness of `85 × (500 / 700)` = 60.7%, rounded to the nearest integer (61%)
|
||
|
||
#### Scenario: Ambient lux at or below target passes curve through
|
||
|
||
- **WHEN** the curve computes brightness at 85%
|
||
- **AND** `target_lux` is 500
|
||
- **AND** `lux_sensor` reads 300
|
||
- **THEN** the integration SHALL send brightness of 85% (unchanged)
|
||
|
||
#### Scenario: Reduction never boosts above curve
|
||
|
||
- **WHEN** the curve computes brightness at 40%
|
||
- **AND** `target_lux` is 500
|
||
- **AND** `lux_sensor` reads 200
|
||
- **THEN** the integration SHALL send brightness of 40% (factor is 1.0, not 2.5)
|
||
|
||
#### Scenario: Exactly at target means no reduction
|
||
|
||
- **WHEN** `target_lux` is 500
|
||
- **AND** `lux_sensor` reads exactly 500
|
||
- **THEN** the factor SHALL be 1.0 and brightness SHALL equal the curve value
|
||
|
||
### Requirement: Lights turn off when lux reduction drives brightness below min_brightness
|
||
|
||
When the lux-adjusted brightness falls below the profile's `min_brightness`, the integration SHALL turn the light off instead of sending a negligible brightness value.
|
||
|
||
#### Scenario: Adjusted brightness below min_brightness turns light off
|
||
|
||
- **WHEN** the curve computes brightness at 80%
|
||
- **AND** `min_brightness` is 5
|
||
- **AND** `target_lux` is 500
|
||
- **AND** `lux_sensor` reads 10000
|
||
- **THEN** the adjusted brightness would be `80 × (500 / 10000)` = 4%, which is below `min_brightness` (5%)
|
||
- **AND** the integration SHALL send `light.turn_off` with the profile's `transition` time
|
||
|
||
#### Scenario: Lights turn back on when lux drops below target
|
||
|
||
- **GIVEN** the integration previously turned a light off due to lux reduction
|
||
- **WHEN** the next adapt cycle computes an adjusted brightness at or above `min_brightness`
|
||
- **THEN** the integration SHALL send `light.turn_on` with the adjusted brightness and the profile's `initial_transition` time
|
||
|
||
#### Scenario: Only lux-turned-off lights are restored
|
||
|
||
- **GIVEN** a light was manually turned off by the user
|
||
- **AND** the lux gate did not trigger the turn-off
|
||
- **WHEN** the lux sensor drops below target on a subsequent tick
|
||
- **THEN** the integration SHALL NOT turn that light back on
|
||
- **AND** only lights with the internal `_lux_turned_off` flag SHALL be eligible for lux-initiated turn-on
|
||
|
||
### Requirement: Graceful degradation when lux sensor is unavailable or unconfigured
|
||
|
||
When `lux_sensor` is not configured (empty string), OR the configured sensor's state is `unavailable` or `unknown`, the integration SHALL use the curve brightness without any lux adjustment. No error SHALL be logged for unconfigured sensors. A `WARNING`-level log SHALL be emitted once when a previously-available sensor becomes unavailable.
|
||
|
||
#### Scenario: No lux sensor configured
|
||
|
||
- **WHEN** `lux_sensor` is empty (not configured)
|
||
- **THEN** the integration SHALL skip the lux gate entirely
|
||
- **AND** brightness SHALL equal the curve value
|
||
- **AND** no error or warning SHALL be logged about lux
|
||
|
||
#### Scenario: Configured sensor becomes unavailable
|
||
|
||
- **GIVEN** `lux_sensor` is configured and was previously reporting a numeric value
|
||
- **WHEN** the sensor state changes to `unavailable`
|
||
- **THEN** the integration SHALL fall back to curve brightness
|
||
- **AND** a `WARNING` log SHALL be emitted once indicating the lux sensor is unavailable
|
||
|
||
#### Scenario: Sensor returns non-numeric state
|
||
|
||
- **GIVEN** `lux_sensor` is configured
|
||
- **WHEN** the sensor state is a non-numeric string (e.g. `"unknown"`)
|
||
- **THEN** the integration SHALL treat it as unavailable and fall back to curve brightness
|
||
|
||
### Requirement: Lux sensor state changes trigger re-adaptation
|
||
|
||
When `lux_sensor` is configured, the integration SHALL register an `async_track_state_change_event` listener on the lux sensor entity. On significant state changes, the listener SHALL trigger `_update_attrs_and_maybe_adapt_lights`.
|
||
|
||
A state change is significant when the resulting reduction factor changes by more than 5 percentage points compared to the last applied factor, OR the change crosses the target threshold (was below, now above — or vice versa).
|
||
|
||
The listener SHALL be removed during `async_will_remove_from_hass`.
|
||
|
||
#### Scenario: Lux crossing target triggers immediate re-adaptation
|
||
|
||
- **GIVEN** the lux sensor was reading 400 (below target of 500)
|
||
- **WHEN** the sensor reports 600 (above target)
|
||
- **THEN** the integration SHALL trigger a re-adaptation within the same event loop cycle
|
||
- **AND** the lights SHALL be dimmed according to the new factor
|
||
|
||
#### Scenario: Small lux fluctuation does not trigger re-adaptation
|
||
|
||
- **GIVEN** the lux sensor was reading 700 (factor = 500/700 = 71.4%)
|
||
- **WHEN** the sensor reports 710 (factor = 500/710 = 70.4%)
|
||
- **THEN** the change in factor is 1.0 percentage points, which is below the 5 pp threshold
|
||
- **AND** the integration SHALL NOT trigger a re-adaptation
|
||
|
||
#### Scenario: Listener is cleaned up on unload
|
||
|
||
- **GIVEN** the integration registered a state listener on the lux sensor
|
||
- **WHEN** the config entry is unloaded
|
||
- **THEN** the listener SHALL be removed
|
||
- **AND** subsequent sensor state changes SHALL NOT invoke the handler
|
||
|
||
### Requirement: `lux_reduce` is a pure function in `color_and_brightness.py`
|
||
|
||
The lux reduction logic SHALL be implemented as a standalone pure function `lux_reduce(curve_brightness, target_lux, current_lux, min_brightness)` in `color_and_brightness.py`. The function SHALL return a `float` (adjusted brightness) or `None` (turn off). It SHALL NOT access HA state, entity registries, or any global mutable state.
|
||
|
||
#### Scenario: Function returns None when below min_brightness
|
||
|
||
- **WHEN** `lux_reduce(80.0, 500, 10000, 5)` is called
|
||
- **THEN** the return value SHALL be `None` (80 × 0.05 = 4.0, below min 5)
|
||
|
||
#### Scenario: Function returns adjusted brightness when above min
|
||
|
||
- **WHEN** `lux_reduce(85.0, 500, 700, 5)` is called
|
||
- **THEN** the return value SHALL be approximately 60.7
|
||
|
||
#### Scenario: Function returns curve brightness when current ≤ target
|
||
|
||
- **WHEN** `lux_reduce(85.0, 500, 300, 5)` is called
|
||
- **THEN** the return value SHALL be 85.0
|
||
|
||
#### Scenario: Function handles zero and negative current_lux safely
|
||
|
||
- **WHEN** `lux_reduce(85.0, 500, 0, 5)` is called
|
||
- **THEN** the return value SHALL be 85.0 (treat zero/negative as "no data", pass through)
|