feat: BindableFloat — universal value source binding for all scalar properties
Lint & Test / test (push) Successful in 1m20s
Lint & Test / test (push) Successful in 1m20s
Introduce BindableFloat abstraction that allows any numeric property to be
either a static value or dynamically driven by a ValueSource. Backward-compatible
serialization: plain float when unbound, {value, source_id} dict when bound.
Backend:
- storage/bindable.py — BindableFloat dataclass + bfloat() helper
- 25+ scalar properties converted across all entity types
- Runtime VS acquisition in ColorStripStreamManager for CSS bindings
- All stream hot loops use self.resolve() for live values
- KeyColorsColorStripStream now inherits ColorStripStream
Frontend:
- BindableScalarWidget (slider + VS picker toggle) for all editors
- TypeScript BindableFloat type + helpers
- Graph editor edges for all bindable properties
- Audio source channel IconSelect grid
Fixes: daylight longitude, candlelight wind_strength/candle_type from_dict
This commit is contained in:
@@ -19,6 +19,7 @@ from wled_controller.api.schemas.output_targets import (
|
||||
)
|
||||
from wled_controller.core.processing.processor_manager import ProcessorManager
|
||||
from wled_controller.storage import DeviceStore
|
||||
from wled_controller.storage.bindable import BindableFloat
|
||||
from wled_controller.storage.wled_output_target import WledOutputTarget
|
||||
from wled_controller.storage.ha_light_output_target import (
|
||||
HALightMapping,
|
||||
@@ -43,11 +44,11 @@ def _target_to_response(target) -> OutputTargetResponse:
|
||||
target_type=target.target_type,
|
||||
device_id=target.device_id,
|
||||
color_strip_source_id=target.color_strip_source_id,
|
||||
brightness_value_source_id=target.brightness_value_source_id or "",
|
||||
fps=target.fps,
|
||||
brightness=target.brightness.to_dict(),
|
||||
fps=target.fps.to_dict(),
|
||||
keepalive_interval=target.keepalive_interval,
|
||||
state_check_interval=target.state_check_interval,
|
||||
min_brightness_threshold=target.min_brightness_threshold,
|
||||
min_brightness_threshold=target.min_brightness_threshold.to_dict(),
|
||||
adaptive_fps=target.adaptive_fps,
|
||||
protocol=target.protocol,
|
||||
description=target.description,
|
||||
@@ -62,20 +63,20 @@ def _target_to_response(target) -> OutputTargetResponse:
|
||||
target_type=target.target_type,
|
||||
ha_source_id=target.ha_source_id,
|
||||
color_strip_source_id=target.color_strip_source_id,
|
||||
brightness_value_source_id=target.brightness_value_source_id or "",
|
||||
brightness=target.brightness.to_dict(),
|
||||
ha_light_mappings=[
|
||||
HALightMappingSchema(
|
||||
entity_id=m.entity_id,
|
||||
led_start=m.led_start,
|
||||
led_end=m.led_end,
|
||||
brightness_scale=m.brightness_scale,
|
||||
brightness_scale=m.brightness_scale.to_dict(),
|
||||
)
|
||||
for m in target.light_mappings
|
||||
],
|
||||
update_rate=target.update_rate,
|
||||
ha_transition=target.transition,
|
||||
color_tolerance=target.color_tolerance,
|
||||
min_brightness_threshold=target.min_brightness_threshold,
|
||||
update_rate=target.update_rate.to_dict(),
|
||||
transition=target.transition.to_dict(),
|
||||
color_tolerance=target.color_tolerance.to_dict(),
|
||||
min_brightness_threshold=target.min_brightness_threshold.to_dict(),
|
||||
description=target.description,
|
||||
tags=target.tags,
|
||||
created_at=target.created_at,
|
||||
@@ -121,7 +122,7 @@ async def create_target(
|
||||
entity_id=m.entity_id,
|
||||
led_start=m.led_start,
|
||||
led_end=m.led_end,
|
||||
brightness_scale=m.brightness_scale,
|
||||
brightness_scale=BindableFloat.from_raw(m.brightness_scale, default=1.0),
|
||||
)
|
||||
for m in data.ha_light_mappings
|
||||
]
|
||||
@@ -135,7 +136,7 @@ async def create_target(
|
||||
target_type=data.target_type,
|
||||
device_id=data.device_id,
|
||||
color_strip_source_id=data.color_strip_source_id,
|
||||
brightness_value_source_id=data.brightness_value_source_id,
|
||||
brightness=data.brightness,
|
||||
fps=data.fps,
|
||||
keepalive_interval=data.keepalive_interval,
|
||||
state_check_interval=data.state_check_interval,
|
||||
@@ -245,7 +246,7 @@ async def update_target(
|
||||
entity_id=m.entity_id,
|
||||
led_start=m.led_start,
|
||||
led_end=m.led_end,
|
||||
brightness_scale=m.brightness_scale,
|
||||
brightness_scale=BindableFloat.from_raw(m.brightness_scale, default=1.0),
|
||||
)
|
||||
for m in data.ha_light_mappings
|
||||
]
|
||||
@@ -256,7 +257,7 @@ async def update_target(
|
||||
name=data.name,
|
||||
device_id=data.device_id,
|
||||
color_strip_source_id=data.color_strip_source_id,
|
||||
brightness_value_source_id=data.brightness_value_source_id,
|
||||
brightness=data.brightness,
|
||||
fps=data.fps,
|
||||
keepalive_interval=data.keepalive_interval,
|
||||
state_check_interval=data.state_check_interval,
|
||||
@@ -287,9 +288,10 @@ async def update_target(
|
||||
or data.transition is not None
|
||||
or data.color_tolerance is not None
|
||||
or data.ha_light_mappings is not None
|
||||
or data.brightness is not None
|
||||
),
|
||||
css_changed=data.color_strip_source_id is not None,
|
||||
brightness_vs_changed=data.brightness_value_source_id is not None,
|
||||
brightness_changed=data.brightness is not None,
|
||||
)
|
||||
except ValueError as e:
|
||||
logger.debug("Processor config update skipped for target %s: %s", target_id, e)
|
||||
|
||||
@@ -1,12 +1,26 @@
|
||||
"""Output target schemas (CRUD, processing state, metrics)."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Dict, Optional, List
|
||||
from typing import Any, Dict, Optional, List, Union
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
DEFAULT_STATE_CHECK_INTERVAL = 30 # seconds between health checks
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# BindableFloat — accepts plain number OR {value, source_id} dict
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
BindableFloatInput = Union[float, int, Dict[str, Any]]
|
||||
"""API input type: a plain number (static) or {"value": float, "source_id": str}."""
|
||||
|
||||
|
||||
class BindableFloatSchema(BaseModel):
|
||||
"""Response schema for a bindable scalar property."""
|
||||
|
||||
value: float = Field(description="Static value (used when source_id is empty)")
|
||||
source_id: str = Field(default="", description="Value source ID (empty = static)")
|
||||
|
||||
|
||||
class KeyColorRectangleSchema(BaseModel):
|
||||
"""A named rectangle for key color extraction (relative coords 0.0-1.0)."""
|
||||
@@ -24,8 +38,8 @@ class HALightMappingSchema(BaseModel):
|
||||
entity_id: str = Field(description="HA light entity ID (e.g. 'light.living_room')")
|
||||
led_start: int = Field(default=0, ge=0, description="Start LED index (0-based)")
|
||||
led_end: int = Field(default=-1, description="End LED index (-1 = last)")
|
||||
brightness_scale: float = Field(
|
||||
default=1.0, ge=0.0, le=1.0, description="Brightness multiplier"
|
||||
brightness_scale: Optional[BindableFloatInput] = Field(
|
||||
default=1.0, description="Brightness multiplier (bindable)"
|
||||
)
|
||||
|
||||
|
||||
@@ -37,8 +51,12 @@ class OutputTargetCreate(BaseModel):
|
||||
# LED target fields
|
||||
device_id: str = Field(default="", description="LED device ID")
|
||||
color_strip_source_id: str = Field(default="", description="Color strip source ID")
|
||||
brightness_value_source_id: str = Field(default="", description="Brightness value source ID")
|
||||
fps: int = Field(default=30, ge=1, le=90, description="Target send FPS (1-90)")
|
||||
brightness: Optional[BindableFloatInput] = Field(
|
||||
default=1.0, description="Brightness (bindable)"
|
||||
)
|
||||
fps: Optional[BindableFloatInput] = Field(
|
||||
default=30, description="Target send FPS (bindable, 1-90)"
|
||||
)
|
||||
keepalive_interval: float = Field(
|
||||
default=1.0,
|
||||
description="Keepalive send interval when screen is static (0.5-5.0s)",
|
||||
@@ -51,11 +69,9 @@ class OutputTargetCreate(BaseModel):
|
||||
ge=5,
|
||||
le=600,
|
||||
)
|
||||
min_brightness_threshold: int = Field(
|
||||
min_brightness_threshold: Optional[BindableFloatInput] = Field(
|
||||
default=0,
|
||||
ge=0,
|
||||
le=254,
|
||||
description="Min brightness threshold (0=disabled); below this → off",
|
||||
description="Min brightness threshold (bindable, 0=disabled); below this → off",
|
||||
)
|
||||
adaptive_fps: bool = Field(
|
||||
default=False, description="Auto-reduce FPS when device is unresponsive"
|
||||
@@ -72,17 +88,15 @@ class OutputTargetCreate(BaseModel):
|
||||
ha_light_mappings: Optional[List[HALightMappingSchema]] = Field(
|
||||
None, description="LED-to-light mappings (for ha_light targets)"
|
||||
)
|
||||
update_rate: float = Field(
|
||||
default=2.0, ge=0.5, le=5.0, description="Service call rate in Hz (for ha_light targets)"
|
||||
update_rate: Optional[BindableFloatInput] = Field(
|
||||
default=2.0, description="Service call rate in Hz (bindable, for ha_light targets)"
|
||||
)
|
||||
transition: float = Field(
|
||||
default=0.5, ge=0.0, le=10.0, description="HA transition seconds (for ha_light targets)"
|
||||
transition: Optional[BindableFloatInput] = Field(
|
||||
default=0.5, description="HA transition seconds (bindable, for ha_light targets)"
|
||||
)
|
||||
color_tolerance: int = Field(
|
||||
color_tolerance: Optional[BindableFloatInput] = Field(
|
||||
default=5,
|
||||
ge=0,
|
||||
le=50,
|
||||
description="Skip service call if RGB delta < this (for ha_light targets)",
|
||||
description="RGB delta tolerance (bindable, for ha_light targets)",
|
||||
)
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
@@ -95,18 +109,16 @@ class OutputTargetUpdate(BaseModel):
|
||||
# LED target fields
|
||||
device_id: Optional[str] = Field(None, description="LED device ID")
|
||||
color_strip_source_id: Optional[str] = Field(None, description="Color strip source ID")
|
||||
brightness_value_source_id: Optional[str] = Field(
|
||||
None, description="Brightness value source ID"
|
||||
)
|
||||
fps: Optional[int] = Field(None, ge=1, le=90, description="Target send FPS (1-90)")
|
||||
brightness: Optional[BindableFloatInput] = Field(None, description="Brightness (bindable)")
|
||||
fps: Optional[BindableFloatInput] = Field(None, description="Target send FPS (bindable, 1-90)")
|
||||
keepalive_interval: Optional[float] = Field(
|
||||
None, description="Keepalive interval (0.5-5.0s)", ge=0.5, le=5.0
|
||||
)
|
||||
state_check_interval: Optional[int] = Field(
|
||||
None, description="Health check interval (5-600s)", ge=5, le=600
|
||||
)
|
||||
min_brightness_threshold: Optional[int] = Field(
|
||||
None, ge=0, le=254, description="Min brightness threshold (0=disabled); below this → off"
|
||||
min_brightness_threshold: Optional[BindableFloatInput] = Field(
|
||||
None, description="Min brightness threshold (bindable, 0=disabled)"
|
||||
)
|
||||
adaptive_fps: Optional[bool] = Field(
|
||||
None, description="Auto-reduce FPS when device is unresponsive"
|
||||
@@ -121,14 +133,14 @@ class OutputTargetUpdate(BaseModel):
|
||||
ha_light_mappings: Optional[List[HALightMappingSchema]] = Field(
|
||||
None, description="LED-to-light mappings (for ha_light targets)"
|
||||
)
|
||||
update_rate: Optional[float] = Field(
|
||||
None, ge=0.5, le=5.0, description="Service call rate Hz (for ha_light targets)"
|
||||
update_rate: Optional[BindableFloatInput] = Field(
|
||||
None, description="Service call rate Hz (bindable, for ha_light targets)"
|
||||
)
|
||||
transition: Optional[float] = Field(
|
||||
None, ge=0.0, le=10.0, description="HA transition seconds (for ha_light targets)"
|
||||
transition: Optional[BindableFloatInput] = Field(
|
||||
None, description="HA transition seconds (bindable, for ha_light targets)"
|
||||
)
|
||||
color_tolerance: Optional[int] = Field(
|
||||
None, ge=0, le=50, description="RGB delta tolerance (for ha_light targets)"
|
||||
color_tolerance: Optional[BindableFloatInput] = Field(
|
||||
None, description="RGB delta tolerance (bindable, for ha_light targets)"
|
||||
)
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
tags: Optional[List[str]] = None
|
||||
@@ -143,14 +155,14 @@ class OutputTargetResponse(BaseModel):
|
||||
# LED target fields
|
||||
device_id: str = Field(default="", description="LED device ID")
|
||||
color_strip_source_id: str = Field(default="", description="Color strip source ID")
|
||||
brightness_value_source_id: str = Field(default="", description="Brightness value source ID")
|
||||
fps: Optional[int] = Field(None, description="Target send FPS")
|
||||
brightness: Optional[BindableFloatInput] = Field(None, description="Brightness (bindable)")
|
||||
fps: Optional[BindableFloatInput] = Field(None, description="Target send FPS (bindable)")
|
||||
keepalive_interval: float = Field(default=1.0, description="Keepalive interval (s)")
|
||||
state_check_interval: int = Field(
|
||||
default=DEFAULT_STATE_CHECK_INTERVAL, description="Health check interval (s)"
|
||||
)
|
||||
min_brightness_threshold: int = Field(
|
||||
default=0, description="Min brightness threshold (0=disabled)"
|
||||
min_brightness_threshold: Optional[BindableFloatInput] = Field(
|
||||
default=0, description="Min brightness threshold (bindable, 0=disabled)"
|
||||
)
|
||||
adaptive_fps: bool = Field(
|
||||
default=False, description="Auto-reduce FPS when device is unresponsive"
|
||||
@@ -161,8 +173,12 @@ class OutputTargetResponse(BaseModel):
|
||||
ha_light_mappings: Optional[List[HALightMappingSchema]] = Field(
|
||||
None, description="LED-to-light mappings (ha_light)"
|
||||
)
|
||||
update_rate: Optional[float] = Field(None, description="Service call rate Hz (ha_light)")
|
||||
ha_transition: Optional[float] = Field(None, description="HA transition seconds (ha_light)")
|
||||
update_rate: Optional[BindableFloatInput] = Field(
|
||||
None, description="Service call rate Hz (bindable, ha_light)"
|
||||
)
|
||||
transition: Optional[BindableFloatInput] = Field(
|
||||
None, description="HA transition seconds (bindable, ha_light)"
|
||||
)
|
||||
color_tolerance: Optional[int] = Field(None, description="RGB delta tolerance (ha_light)")
|
||||
description: Optional[str] = Field(None, description="Description")
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
|
||||
Reference in New Issue
Block a user