Add value sources for dynamic brightness control on LED targets
Introduces a new Value Source entity that produces a scalar float (0.0-1.0) for dynamic brightness modulation. Three subtypes: Static (constant), Animated (sine/triangle/square/sawtooth waveform), and Audio-reactive (RMS/peak/beat from mono audio source). Value sources can be optionally attached to LED targets to control brightness each frame. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
154
server/src/wled_controller/storage/value_source.py
Normal file
154
server/src/wled_controller/storage/value_source.py
Normal file
@@ -0,0 +1,154 @@
|
||||
"""Value source data model with inheritance-based source types.
|
||||
|
||||
A ValueSource produces a scalar float (0.0–1.0) that can drive target
|
||||
parameters like brightness. Three types:
|
||||
StaticValueSource — constant float value
|
||||
AnimatedValueSource — periodic waveform (sine, triangle, square, sawtooth)
|
||||
AudioValueSource — audio-reactive scalar (RMS, peak, beat detection)
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class ValueSource:
|
||||
"""Base class for value source configurations."""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
source_type: str # "static" | "animated" | "audio"
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
description: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Convert source to dictionary. Subclasses extend this."""
|
||||
return {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"source_type": self.source_type,
|
||||
"created_at": self.created_at.isoformat(),
|
||||
"updated_at": self.updated_at.isoformat(),
|
||||
"description": self.description,
|
||||
# Subclass fields default to None for forward compat
|
||||
"value": None,
|
||||
"waveform": None,
|
||||
"speed": None,
|
||||
"min_value": None,
|
||||
"max_value": None,
|
||||
"audio_source_id": None,
|
||||
"mode": None,
|
||||
"sensitivity": None,
|
||||
"smoothing": None,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data: dict) -> "ValueSource":
|
||||
"""Factory: dispatch to the correct subclass based on source_type."""
|
||||
source_type: str = data.get("source_type", "static") or "static"
|
||||
sid: str = data["id"]
|
||||
name: str = data["name"]
|
||||
description: str | None = data.get("description")
|
||||
|
||||
raw_created = data.get("created_at")
|
||||
created_at: datetime = (
|
||||
datetime.fromisoformat(raw_created)
|
||||
if isinstance(raw_created, str)
|
||||
else raw_created if isinstance(raw_created, datetime)
|
||||
else datetime.utcnow()
|
||||
)
|
||||
raw_updated = data.get("updated_at")
|
||||
updated_at: datetime = (
|
||||
datetime.fromisoformat(raw_updated)
|
||||
if isinstance(raw_updated, str)
|
||||
else raw_updated if isinstance(raw_updated, datetime)
|
||||
else datetime.utcnow()
|
||||
)
|
||||
|
||||
if source_type == "animated":
|
||||
return AnimatedValueSource(
|
||||
id=sid, name=name, source_type="animated",
|
||||
created_at=created_at, updated_at=updated_at, description=description,
|
||||
waveform=data.get("waveform") or "sine",
|
||||
speed=float(data.get("speed") or 10.0),
|
||||
min_value=float(data.get("min_value") or 0.0),
|
||||
max_value=float(data["max_value"]) if data.get("max_value") is not None else 1.0,
|
||||
)
|
||||
|
||||
if source_type == "audio":
|
||||
return AudioValueSource(
|
||||
id=sid, name=name, source_type="audio",
|
||||
created_at=created_at, updated_at=updated_at, description=description,
|
||||
audio_source_id=data.get("audio_source_id") or "",
|
||||
mode=data.get("mode") or "rms",
|
||||
sensitivity=float(data.get("sensitivity") or 1.0),
|
||||
smoothing=float(data.get("smoothing") or 0.3),
|
||||
)
|
||||
|
||||
# Default: "static" type
|
||||
return StaticValueSource(
|
||||
id=sid, name=name, source_type="static",
|
||||
created_at=created_at, updated_at=updated_at, description=description,
|
||||
value=float(data["value"]) if data.get("value") is not None else 1.0,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class StaticValueSource(ValueSource):
|
||||
"""Value source that outputs a constant float.
|
||||
|
||||
Useful as a simple per-target brightness override.
|
||||
"""
|
||||
|
||||
value: float = 1.0 # 0.0–1.0
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
d = super().to_dict()
|
||||
d["value"] = self.value
|
||||
return d
|
||||
|
||||
|
||||
@dataclass
|
||||
class AnimatedValueSource(ValueSource):
|
||||
"""Value source that cycles through a periodic waveform.
|
||||
|
||||
Produces a smooth animation between min_value and max_value
|
||||
at the configured speed (cycles per minute).
|
||||
"""
|
||||
|
||||
waveform: str = "sine" # sine | triangle | square | sawtooth
|
||||
speed: float = 10.0 # cycles per minute (1.0–120.0)
|
||||
min_value: float = 0.0 # minimum output (0.0–1.0)
|
||||
max_value: float = 1.0 # maximum output (0.0–1.0)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
d = super().to_dict()
|
||||
d["waveform"] = self.waveform
|
||||
d["speed"] = self.speed
|
||||
d["min_value"] = self.min_value
|
||||
d["max_value"] = self.max_value
|
||||
return d
|
||||
|
||||
|
||||
@dataclass
|
||||
class AudioValueSource(ValueSource):
|
||||
"""Value source driven by audio input.
|
||||
|
||||
Converts audio analysis (RMS level, peak, or beat detection)
|
||||
into a scalar value for brightness modulation.
|
||||
"""
|
||||
|
||||
audio_source_id: str = "" # references a MonoAudioSource
|
||||
mode: str = "rms" # rms | peak | beat
|
||||
sensitivity: float = 1.0 # gain multiplier (0.1–5.0)
|
||||
smoothing: float = 0.3 # temporal smoothing (0.0–1.0)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
d = super().to_dict()
|
||||
d["audio_source_id"] = self.audio_source_id
|
||||
d["mode"] = self.mode
|
||||
d["sensitivity"] = self.sensitivity
|
||||
d["smoothing"] = self.smoothing
|
||||
return d
|
||||
Reference in New Issue
Block a user