feat: new value source types (HA entity, gradient map, strip extract) + UI fixes
Lint & Test / test (push) Successful in 1m27s

New value source types:
- ha_entity: reads numeric values from HA entity state/attribute, normalizes
  via min/max range, applies EMA smoothing. EntitySelect for HA connection
  and entity selection with live entity list fetching.
- gradient_map: maps a float value source (0-1) through a gradient entity.
  EntitySelect for both input source and gradient with inline previews.
- css_extract: extracts single color by averaging LED range from a color
  strip source. EntitySelect for source selection.

Value source type picker:
- Filter tabs (All / Numeric / Color) above the icon grid
- showTypePicker extended with filterTabs + onFilterChange support

Palette selectors converted to EntitySelect:
- Effect palette, gradient preset, and audio palette selectors now use
  command-palette style EntitySelect with gradient strip previews

Tab indicator fixes:
- Icon now updates on tab switch (was passing no args to updateTabIndicator)
- Visible with any background effect active, not just Noise Field
- Noise Field is the default background effect for new users

Dashboard section collapse fix:
- Split header into clickable toggle (chevron+label) and non-clickable
  actions area — buttons no longer trigger collapse/expand

Discriminated union fix (422 errors):
- source_type/target_type now always included in update payloads for:
  CSS editor, LED target, HA light target, simple calibration,
  advanced calibration
This commit is contained in:
2026-03-29 20:38:22 +03:00
parent ea812bb4d5
commit 384362ccf1
61 changed files with 5367 additions and 1620 deletions
+222 -29
View File
@@ -1,38 +1,231 @@
# BindableFloat — Universal Value Source Binding
# New Value Source Types + Filter Support
## ALL PHASES COMPLETE
## Feature 1: HA Value Source (`ha_entity`)
### Phase 1: Core Infrastructure
A value source that reads numeric values from a Home Assistant entity's state or attribute. Allows binding any scalar property in the system to a live HA sensor/entity value.
- [x] `storage/bindable.py` — BindableFloat dataclass + `bfloat()` extraction helper
- [x] WledOutputTarget, HALightOutputTarget, HALightMapping — brightness/transition
- [x] All 15 CSS source types — smoothing, sensitivity, intensity, scale, speed, etc.
- [x] API schemas + routes updated
- [x] output_target_store create/update
- [x] processor_manager add_target / add_ha_light_target
### Configuration
- `ha_source_id: str` — HA connection entity (EntitySelect picker)
- `entity_id: str` — HA entity (EntitySelect picker, populated from `/api/v1/home-assistant/sources/{id}/entities`)
- `attribute: str` — optional attribute name (text input or dropdown populated from entity attributes)
- `min_ha_value: float` — raw HA value corresponding to output 0.0
- `max_ha_value: float` — raw HA value corresponding to output 1.0
- `smoothing: float` — EMA smoothing factor (0..1)
- `return_type: "float"` — always float
### Phase 2: Runtime Resolution
### Backend
- [x] WledTargetProcessor — BindableFloat brightness, acquire/release value streams
- [x] HALightTargetProcessor — BindableFloat brightness + transition
- [x] All CSS streams use `bfloat()` to extract static values from BindableFloat properties
- [x] scene_activator — brightness_changed flag
- [x] ColorStripStream base class — `resolve()`, `set_value_stream()`, `remove_value_stream()`
- [x] ColorStripStreamManager — `_bind_value_streams()` / `_release_value_streams()` on acquire/release
- [x] All stream hot loops call `self.resolve(prop, static)` for dynamic runtime binding
- [x] KeyColorsColorStripStream — fixed to inherit from ColorStripStream
- [ ] **Storage model**`HAEntityValueSource` subclass in `storage/value_source.py`
- Fields: `ha_source_id`, `entity_id`, `attribute`, `min_ha_value`, `max_ha_value`, `smoothing`
- Register in `_VALUE_SOURCE_MAP` as `"ha_entity"`
- `to_dict()` / `from_dict()` / `_parse_common_fields()`
### Phase 3: Frontend
- [ ] **Store** — add `"ha_entity"` case in `ValueSourceStore.create_source()` and `update_source()`
- Validate: `ha_source_id` must be non-empty, `entity_id` must be non-empty
- [x] TypeScript BindableFloat type + `bindableValue()` / `bindableSourceId()` helpers
- [x] targets.ts, ha-light-targets.ts, color-strips.ts — save/load/display
- [x] Graph connections — value source edges for ALL bindable CSS properties
- [x] Graph layout — edge creation for CSS + target bindable properties
- [x] custom_components/select.py — HA integration backward compat
- [ ] **API schemas**`HAEntityValueSourceCreate`, `HAEntityValueSourceResponse` in `api/schemas/value_sources.py`
- Add to `ValueSourceCreate` / `ValueSourceResponse` discriminated unions
- Fields: `ha_source_id`, `entity_id`, `attribute` (optional), `min_ha_value`, `max_ha_value`, `smoothing`
### Phase 4: BindableScalarWidget
- [ ] **API routes** — add `HAEntityValueSource` → response builder in `_RESPONSE_MAP`
- [x] `core/bindable-scalar.ts` — reusable widget (slider + VS picker toggle)
- [x] CSS styles (`.bindable-toggle`, `.bindable-slider-row`, `.bindable-vs-row`)
- [x] All 11 CSS editor sliders converted (smoothing, sensitivity, intensity, scale, speed, wind, temp_influence, timeout)
- [x] HTML templates updated with container divs
- [ ] **Stream**`HAEntityValueStream` in `core/processing/value_stream.py`
- `start()`: acquire HA runtime via `ha_manager.acquire(ha_source_id)`
- `get_value()`: read `ha_manager.get_state(ha_source_id, entity_id)` → extract state or attribute → clamp/normalize to [0,1] via min/max range → apply EMA smoothing
- `stop()`: release HA runtime
- `update_source()`: hot-update parameters
- Add to `ValueStreamManager._create_stream()`
### Frontend
- [ ] **TypeScript type**`HAEntityValueSource` interface in `types.ts`
- `source_type: 'ha_entity'`, `return_type: 'float'`
- Fields: `ha_source_id`, `entity_id`, `attribute`, `min_ha_value`, `max_ha_value`, `smoothing`
- Add to `ValueSourceType` union and `ValueSource` union
- [ ] **Icon** — add `ha_entity: _svg(P.home)` to `_valueSourceTypeIcons` in `icons.ts`
- [ ] **i18n** — add keys in `en.json`:
- `value_source.type.ha_entity`: "Home Assistant Entity"
- `value_source.type.ha_entity.desc`: "Reads value from a Home Assistant sensor or entity attribute"
- `value_source.ha_source`: "HA Connection:"
- `value_source.entity_id`: "Entity:"
- `value_source.attribute`: "Attribute (optional):"
- `value_source.min_ha_value`: "Min HA Value:"
- `value_source.max_ha_value`: "Max HA Value:"
- [ ] **Editor modal** — add `ha_entity` section to `value-source-editor.html`
- HA connection selector (EntitySelect from HA sources cache)
- Entity selector (EntitySelect populated from HA entities endpoint)
- Attribute text input (optional)
- Min/Max HA value range inputs
- Smoothing slider
- [ ] **Editor logic** — add `ha_entity` handler in `value-sources.ts`
- `onValueSourceTypeChange()`: show/hide ha_entity section
- `_typeHandlers['ha_entity']`: load/reset/getPayload
- EntitySelect for HA source + EntitySelect for entity (refreshes when HA source changes)
- Auto-name: "{entity_friendly_name}" or "{entity_id}"
- [ ] **Card renderer** — show HA source link + entity ID + attribute (if set) + range
- [ ] **VS_TYPE_KEYS** — add `'ha_entity'` to the array
---
## Feature 2: Lerp Color Value Source (`gradient_map`)
A color value source that maps a numeric value source's output through a color gradient. Given a float value source (0..1), interpolates the color at that position in a user-defined gradient.
### Configuration
- `value_source_id: str` — reference to a float-returning value source (EntitySelect)
- `stops: List[ColorStop]` — gradient color stops `[{position: float, color: [R,G,B]}]` (reuse existing `ColorStop` model from color strip sources)
- `easing: str` — interpolation mode: "linear", "step" (reuse existing easing modes)
- `return_type: "color"` — always color
### Backend
- [ ] **Storage model**`GradientMapValueSource` subclass in `storage/value_source.py`
- Fields: `value_source_id`, `stops` (list of dicts with `position` + `color`), `easing`
- Register in `_VALUE_SOURCE_MAP` as `"gradient_map"`
- [ ] **Store** — add `"gradient_map"` case in `create_source()` / `update_source()`
- Validate: at least 2 stops, `value_source_id` non-empty
- [ ] **API schemas**`GradientMapValueSourceCreate`, `GradientMapValueSourceResponse`
- Reuse `ColorStop` schema from color_strip_sources schemas (or define minimal version)
- Add to discriminated unions
- [ ] **API routes** — add to `_RESPONSE_MAP`
- [ ] **Stream**`GradientMapValueStream` in `value_stream.py`
- `start()`: acquire the referenced value stream via `ValueStreamManager.acquire(value_source_id)`
- `get_value()`: return BT.601 luminance of current color
- `get_color()`: call `inner_stream.get_value()` → interpolate through gradient stops → return RGB tuple
- Reuse `_compute_gradient_colors()` logic from color_strip_stream.py (or a shared helper for single-point interpolation)
- `stop()`: release inner value stream
- `update_source()`: hot-update stops/easing, re-acquire if value_source_id changed
### Frontend
- [ ] **TypeScript type**`GradientMapValueSource` interface
- `source_type: 'gradient_map'`, `return_type: 'color'`
- Fields: `value_source_id`, `stops: ColorStop[]`, `easing`
- [ ] **Icon** — add `gradient_map: _svg(P.rainbow)` to `_valueSourceTypeIcons`
- [ ] **i18n** — add keys:
- `value_source.type.gradient_map`: "Gradient Map"
- `value_source.type.gradient_map.desc`: "Maps a numeric value through a color gradient"
- `value_source.input_source`: "Input Value Source:"
- `value_source.gradient_stops`: "Gradient:"
- `value_source.easing`: "Interpolation:"
- [ ] **Editor modal** — add `gradient_map` section
- Value source selector (EntitySelect from float value sources)
- Gradient stop editor (reuse gradient stop UI from CSS editor if possible, or build minimal version: list of position + color picker rows)
- Easing selector (IconSelect: linear, step)
- Live gradient preview bar (CSS linear-gradient from stops)
- [ ] **Editor logic**`_typeHandlers['gradient_map']`: load/reset/getPayload
- [ ] **Card renderer** — CSS gradient preview bar + input source link + stop count
- [ ] **VS_TYPE_KEYS** — add `'gradient_map'`
---
## Feature 3: CSS Extraction Color Value Source (`css_extract`)
A color value source that extracts a single color from a color strip source by averaging a range of LEDs. Useful for deriving a single color signal from an existing color strip.
### Configuration
- `color_strip_source_id: str` — reference to a color strip source (EntitySelect)
- `led_start: int` — start of LED range (0-based, optional, default 0)
- `led_end: int` — end of LED range (exclusive, optional, default -1 = whole strip)
- `return_type: "color"` — always color
### Backend
- [ ] **Storage model**`CSSExtractValueSource` subclass in `storage/value_source.py`
- Fields: `color_strip_source_id`, `led_start`, `led_end`
- Register as `"css_extract"`
- [ ] **Store** — add `"css_extract"` case in `create_source()` / `update_source()`
- Validate: `color_strip_source_id` non-empty
- [ ] **API schemas**`CSSExtractValueSourceCreate`, `CSSExtractValueSourceResponse`
- Add to discriminated unions
- [ ] **API routes** — add to `_RESPONSE_MAP`
- [ ] **Stream**`CSSExtractValueStream` in `value_stream.py`
- `start()`: acquire color strip stream via `ColorStripStreamManager.acquire(color_strip_source_id, led_count=needed)`
- `get_color()`: read strip colors → average the specified LED range → return single RGB tuple
- `get_value()`: BT.601 luminance of extracted color
- `stop()`: release color strip stream
- `update_source()`: hot-update range, re-acquire if source changed
- **Note**: Needs access to `ColorStripStreamManager` — may need to inject it into `ValueStreamManager` or pass via constructor
### Frontend
- [ ] **TypeScript type**`CSSExtractValueSource` interface
- `source_type: 'css_extract'`, `return_type: 'color'`
- Fields: `color_strip_source_id`, `led_start`, `led_end`
- [ ] **Icon** — add `css_extract: _svg(P.eyedropper)` (or `P.pipette` if available, else `P.palette`)
- [ ] **i18n** — add keys:
- `value_source.type.css_extract`: "Strip Color Extract"
- `value_source.type.css_extract.desc`: "Extracts a single color from a color strip source"
- `value_source.color_strip_source`: "Color Strip Source:"
- `value_source.led_start`: "LED Start:"
- `value_source.led_end`: "LED End (-1 = all):"
- [ ] **Editor modal** — add `css_extract` section
- Color strip source selector (EntitySelect from color strip sources cache)
- LED start/end numeric inputs
- Optional: live color preview swatch
- [ ] **Editor logic**`_typeHandlers['css_extract']`: load/reset/getPayload
- [ ] **Card renderer** — color strip source link + LED range badge
- [ ] **VS_TYPE_KEYS** — add `'css_extract'`
---
## Feature 4: Value Source Type Filter in Icon Grid
Add a filter/category system to the value source type IconSelect so users can filter by return type or category.
### Implementation
- [ ] **Add filter tabs** above the value source type icon grid in the editor modal
- "All" (default) — show all types
- "Float" — show float-returning types: static, animated, audio, adaptive_time, adaptive_scene, daylight, ha_entity
- "Color" — show color-returning types: static_color, animated_color, adaptive_time_color, gradient_map, css_extract
- [ ] **IconSelect enhancement** — either:
- Option A: Add `groups` support to IconSelect (items grouped by category with filter tabs)
- Option B: Filter `VS_TYPE_KEYS` before building items, with toggle buttons above the grid
- Decision: Option B is simpler and follows existing patterns — add filter buttons that rebuild the icon grid
- [ ] **i18n** — add keys:
- `value_source.filter.all`: "All"
- `value_source.filter.float`: "Float"
- `value_source.filter.color`: "Color"
---
## Implementation Order
1. **Feature 4** (filter) — smallest, unblocks better UX for the growing type list
2. **Feature 1** (ha_entity) — standalone float type, no cross-dependencies
3. **Feature 3** (css_extract) — needs ColorStripStreamManager injection
4. **Feature 2** (gradient_map) — needs float VS reference + gradient UI
## Cross-Cutting Concerns
- All new types need entries in `_VALUE_SOURCE_MAP` (backend) and `VS_TYPE_KEYS` (frontend)
- All new types need `_RESPONSE_MAP` entries in routes
- All new types need `ValueStreamManager._create_stream()` factory case
- All new types need icon in `_valueSourceTypeIcons`
- All new types need i18n keys in `en.json` (and `ru.json`, `zh.json` — can defer translations)
- `ValueSourceStore` referential integrity check on delete should verify new references (ha_entity → ha_source, gradient_map → value_source, css_extract → color_strip_source)
- Graph editor: new edge types for ha_entity → HA source node, gradient_map → value source node, css_extract → color strip node