mirror of
https://github.com/basnijholt/adaptive-lighting.git
synced 2026-09-16 08:44:03 +02:00
Archive add-lux-target: 26/26 + sync main specs
New lux-feedback spec, updated options-flow (7 sections, lux conditionals, live reading), updated output-sensors (conditional ambient_lux + lux_reduction).
This commit is contained in:
parent
11fe23253c
commit
c3b948874e
10 changed files with 277 additions and 14 deletions
144
openspec/specs/lux-feedback/spec.md
Normal file
144
openspec/specs/lux-feedback/spec.md
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
# 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)
|
||||
|
|
@ -5,13 +5,14 @@ TBD - created by archiving change cdit-config-redesign. Update Purpose after arc
|
|||
## Requirements
|
||||
### Requirement: Options dialog presents fields in named collapsible sections
|
||||
|
||||
The integration options dialog SHALL group its 18 configurable fields into five named sections plus a Diagnostics subsection, rendered using Home Assistant's `section()` schema helper. Section names and field membership SHALL match the layout below.
|
||||
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` |
|
||||
|
|
@ -19,8 +20,8 @@ The integration options dialog SHALL group its 18 configurable fields into five
|
|||
#### 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 six sections in the order: Targets, Daytime curve, Sun schedule, Light control, Advanced, Diagnostics
|
||||
- **AND** the Advanced and Diagnostics sections SHALL be rendered in their collapsed state
|
||||
- **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
|
||||
|
|
@ -35,6 +36,7 @@ Fields whose configuration is meaningful only under a specific value of another
|
|||
|
||||
The conditional pairs are:
|
||||
- `send_split_delay` is conditional on `separate_turn_on_commands` being `true`.
|
||||
- `target_lux` is conditional on `lux_sensor` being a non-empty string.
|
||||
|
||||
#### Scenario: send_split_delay hidden when transport mode disables it
|
||||
|
||||
|
|
@ -47,6 +49,18 @@ The conditional pairs are:
|
|||
- **THEN** the options dialog SHALL re-render with `send_split_delay` present 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_sensor` set to `""` (empty)
|
||||
- **THEN** the Ambient lux section SHALL show only the `lux_sensor` entity selector
|
||||
- **AND** `target_lux` SHALL NOT appear
|
||||
|
||||
#### Scenario: target_lux revealed when lux sensor is selected
|
||||
|
||||
- **WHEN** the user selects a `lux_sensor` entity and submits the form
|
||||
- **THEN** the options dialog SHALL re-render with `target_lux` present 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.
|
||||
|
|
@ -123,6 +137,8 @@ Every field in the options dialog SHALL be rendered using a class from `homeassi
|
|||
| 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
|
||||
|
||||
|
|
@ -141,6 +157,41 @@ Every field in the options dialog SHALL be rendered using a class from `homeassi
|
|||
- **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"` and `device_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_lux` SHALL 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_sensor` is set to `sensor.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_sensor` is 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_sensor` is 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.
|
||||
|
|
|
|||
|
|
@ -4,29 +4,44 @@
|
|||
Per-profile read-only `sensor` entities that expose the curve's current target brightness/color-temperature outputs and the actual solar elevation as graphable HA `MEASUREMENT`-class values. Sensors are pure readers of a runtime cache the master switch publishes to after each curve tick; they do not recompute the curve. The integration's existing master-switch attributes (`brightness_pct`, `color_temp_kelvin`, synthetic `sun_position` in [-1, +1]) are unaffected — the sensors are an additive, graphable surface over the same data plus `sun.sun.elevation`.
|
||||
|
||||
## Requirements
|
||||
### Requirement: Each AL profile exposes three output sensor entities
|
||||
### Requirement: Each AL profile exposes three output sensor entities plus two conditional lux sensors
|
||||
|
||||
For each Adaptive Lighting config entry, the integration SHALL create exactly three `sensor` entities exposing the curve's computed outputs. The entities SHALL be registered on the `sensor` platform during `async_setup_entry` and torn down during `async_unload_entry`. Each entity SHALL share the same device record (`(DOMAIN, entry.entry_id)`) as the profile's existing switches and number entities.
|
||||
For each Adaptive Lighting config entry, the integration SHALL create the three existing output sensor entities plus two additional lux-related sensors when a `lux_sensor` is configured. All sensors SHALL be registered on the `sensor` platform during `async_setup_entry` and torn down during `async_unload_entry`. Each entity SHALL share the same device record (`(DOMAIN, entry.entry_id)`) as the profile's existing switches and number entities.
|
||||
|
||||
| Output | `unique_id` suffix | `_attr_name` | `native_unit_of_measurement` | `state_class` | `device_class` | icon |
|
||||
| Output | `unique_id` suffix | `_attr_name` | `native_unit_of_measurement` | `state_class` | icon | Condition |
|
||||
|---|---|---|---|---|---|---|
|
||||
| Output brightness | `_output_brightness` | `"Output brightness"` | `"%"` | `MEASUREMENT` | (none) | `mdi:brightness-percent` |
|
||||
| Output color temperature | `_output_color_temp` | `"Output color temp"` | `"K"` | `MEASUREMENT` | (none) | `mdi:thermometer` |
|
||||
| Sun elevation | `_sun_elevation` | `"Sun elevation"` | `"°"` | `MEASUREMENT` | (none) | `mdi:weather-sunset` |
|
||||
| Output brightness | `_output_brightness` | `"Output brightness"` | `"%"` | `MEASUREMENT` | `mdi:brightness-percent` | always |
|
||||
| Output color temperature | `_output_color_temp` | `"Output color temp"` | `"K"` | `MEASUREMENT` | `mdi:thermometer` | always |
|
||||
| Sun elevation | `_sun_elevation` | `"Sun elevation"` | `"°"` | `MEASUREMENT` | `mdi:weather-sunset` | always |
|
||||
| Ambient lux | `_ambient_lux` | `"Ambient lux"` | `"lx"` | `MEASUREMENT` | `mdi:brightness-5` | `lux_sensor` configured |
|
||||
| Lux reduction | `_lux_reduction` | `"Lux reduction"` | `"%"` | `MEASUREMENT` | `mdi:chart-line-variant` | `lux_sensor` configured |
|
||||
|
||||
The full `unique_id` SHALL be `<entry.entry_id>_<suffix>`.
|
||||
|
||||
#### Scenario: A new config entry produces three sensor entities
|
||||
#### Scenario: Profile with lux sensor produces five sensor entities
|
||||
|
||||
- **WHEN** the user creates a new Adaptive Lighting config entry
|
||||
- **WHEN** the user creates an AL config entry with `lux_sensor` set to `sensor.office_illuminance`
|
||||
- **AND** `async_setup_entry` completes
|
||||
- **THEN** the entity registry SHALL contain five `sensor` entities owned by this entry
|
||||
- **AND** their unique_ids SHALL end with `_output_brightness`, `_output_color_temp`, `_sun_elevation`, `_ambient_lux`, and `_lux_reduction`
|
||||
|
||||
#### Scenario: Profile without lux sensor produces three sensor entities
|
||||
|
||||
- **WHEN** the user creates an AL config entry without a `lux_sensor` configured
|
||||
- **AND** `async_setup_entry` completes
|
||||
- **THEN** the entity registry SHALL contain three `sensor` entities owned by this entry
|
||||
- **AND** their unique_ids SHALL end with `_output_brightness`, `_output_color_temp`, and `_sun_elevation` respectively
|
||||
- **AND** all three sensors SHALL be attached to the same device as the profile's switches and number entities
|
||||
- **AND** the `_ambient_lux` and `_lux_reduction` entities SHALL NOT be created
|
||||
|
||||
#### Scenario: Removing lux sensor removes the two conditional sensors
|
||||
|
||||
- **GIVEN** an AL profile has `lux_sensor` configured and all five sensors exist
|
||||
- **WHEN** the user removes `lux_sensor` (sets to empty) and saves options
|
||||
- **THEN** on reload, the `_ambient_lux` and `_lux_reduction` entities SHALL be removed from the entity registry
|
||||
- **AND** only the three unconditional sensors SHALL remain
|
||||
|
||||
#### Scenario: Sensor metadata matches the design table
|
||||
|
||||
- **WHEN** any of the three sensors is inspected via the entity registry
|
||||
- **WHEN** any of the three unconditional sensors is inspected via the entity registry
|
||||
- **THEN** `output_brightness` SHALL declare `native_unit_of_measurement="%"`, `state_class=SensorStateClass.MEASUREMENT`, no `device_class`
|
||||
- **AND** `output_color_temp` SHALL declare `native_unit_of_measurement="K"`, `state_class=SensorStateClass.MEASUREMENT`, no `device_class`
|
||||
- **AND** `sun_elevation` SHALL declare `native_unit_of_measurement="°"`, `state_class=SensorStateClass.MEASUREMENT`, no `device_class`
|
||||
|
|
@ -141,3 +156,56 @@ The chosen role labels SHALL NOT collide with any existing entity's `_attr_name`
|
|||
- **THEN** no two entities SHALL share the same friendly name
|
||||
- **AND** specifically the adapt-brightness switch ("Dining MVP Brightness") and the output-brightness sensor ("Dining MVP Output brightness") SHALL be distinguishable strings
|
||||
- **AND** the output-color-temp sensor ("Dining MVP Output color temp") SHALL be distinguishable from the "Min color temp" and "Max color temp" number entities
|
||||
|
||||
### Requirement: Ambient lux sensor mirrors the configured lux sensor's reading
|
||||
|
||||
The `ambient_lux` output sensor SHALL read the configured `lux_sensor` entity's numeric state on each curve tick and publish the value to `hass.data[DOMAIN][entry.entry_id]["outputs"]["ambient_lux"]`. If the source sensor is unavailable or non-numeric, the value SHALL be `None` (rendered as `unknown` in HA).
|
||||
|
||||
#### Scenario: Ambient lux reflects source sensor
|
||||
|
||||
- **GIVEN** `lux_sensor` is `sensor.office_illuminance` with state `"340"`
|
||||
- **WHEN** a curve evaluation tick completes
|
||||
- **THEN** `outputs["ambient_lux"]` SHALL be `340.0`
|
||||
- **AND** the `ambient_lux` sensor entity state SHALL be `"340.0"`
|
||||
|
||||
#### Scenario: Source sensor unavailable results in unknown state
|
||||
|
||||
- **GIVEN** `lux_sensor` is configured but its state is `"unavailable"`
|
||||
- **WHEN** a curve evaluation tick completes
|
||||
- **THEN** `outputs["ambient_lux"]` SHALL be `None`
|
||||
- **AND** the `ambient_lux` sensor entity state SHALL be `unknown`
|
||||
|
||||
### Requirement: Lux reduction sensor exposes the applied factor as a percentage
|
||||
|
||||
The `lux_reduction` output sensor SHALL publish `factor × 100` to `hass.data[DOMAIN][entry.entry_id]["outputs"]["lux_reduction"]`, where `factor` is the value computed by the lux gate. A value of `100` means no reduction (curve passes through); `50` means brightness was halved. When the lux gate is inactive (sensor below target or unavailable), the value SHALL be `100`.
|
||||
|
||||
When the lights were turned off due to lux reduction (factor drove brightness below `min_brightness`), the value SHALL be `0`.
|
||||
|
||||
#### Scenario: Lux reduction shows the applied factor
|
||||
|
||||
- **GIVEN** `target_lux` is 500 and `lux_sensor` reads 700
|
||||
- **WHEN** a curve evaluation tick completes
|
||||
- **THEN** `outputs["lux_reduction"]` SHALL be `71` (rounded from 71.4)
|
||||
- **AND** the `lux_reduction` sensor entity state SHALL be `"71"`
|
||||
|
||||
#### Scenario: No reduction shows 100%
|
||||
|
||||
- **GIVEN** `target_lux` is 500 and `lux_sensor` reads 300
|
||||
- **WHEN** a curve evaluation tick completes
|
||||
- **THEN** `outputs["lux_reduction"]` SHALL be `100`
|
||||
|
||||
#### Scenario: Lights-off due to lux shows 0%
|
||||
|
||||
- **GIVEN** the lux gate drove brightness below `min_brightness`
|
||||
- **WHEN** a curve evaluation tick completes
|
||||
- **THEN** `outputs["lux_reduction"]` SHALL be `0`
|
||||
|
||||
### Requirement: Lux output sensors follow the same dispatcher pattern as existing sensors
|
||||
|
||||
The `ambient_lux` and `lux_reduction` sensors SHALL use the same `SIGNAL_OUTPUTS_UPDATED` dispatcher subscription as the three existing sensors. They SHALL NOT poll. `_attr_should_poll` SHALL be `False`. The dispatcher unsubscribe handle SHALL be tracked via `async_on_remove`.
|
||||
|
||||
#### Scenario: Lux sensors update on the same signal as existing sensors
|
||||
|
||||
- **GIVEN** the integration is loaded with `lux_sensor` configured
|
||||
- **WHEN** a curve evaluation completes and fires the dispatcher signal
|
||||
- **THEN** all five sensor entities SHALL execute their outputs-updated handler exactly once
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue