Files
wled-screen-controller-mixed/server/src/wled_controller/storage/value_source.py
alexei.dolgolyov 73947eb6cb
Some checks failed
Lint & Test / test (push) Failing after 30s
refactor: replace type-dispatch if/elif chains with registry patterns and handler maps
Backend: add registry dicts (_CONDITION_MAP, _VALUE_SOURCE_MAP, _PICTURE_SOURCE_MAP)
and per-subclass from_dict() methods to eliminate ~300 lines of if/elif in factory
functions. Convert automation engine dispatch (condition eval, match_mode, match_type,
deactivation_mode) to dict-based lookup.

Frontend: extract CSS_CARD_RENDERERS, CSS_SECTION_MAP, CSS_TYPE_SETUP,
CONDITION_PILL_RENDERERS, and PICTURE_SOURCE_CARD_RENDERERS handler maps to replace
scattered type-check chains in color-strips.ts, automations.ts, and streams.ts.
2026-03-24 14:51:27 +03:00

286 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Value source data model with inheritance-based source types.
A ValueSource produces a scalar float (0.01.0) that can drive target
parameters like brightness. Six types:
StaticValueSource — constant float value
AnimatedValueSource — periodic waveform (sine, triangle, square, sawtooth)
AudioValueSource — audio-reactive scalar (RMS, peak, beat detection)
AdaptiveValueSource — adapts to external conditions:
adaptive_time — interpolates brightness along a 24-hour schedule
adaptive_scene — derives brightness from a picture source's frame luminance
DaylightValueSource — brightness based on simulated or real-time daylight cycle
"""
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Dict, List, Optional, Type
@dataclass
class ValueSource:
"""Base class for value source configurations."""
id: str
name: str
source_type: str # "static" | "animated" | "audio" | "adaptive_time" | "adaptive_scene" | "daylight"
created_at: datetime
updated_at: datetime
description: Optional[str] = None
tags: List[str] = field(default_factory=list)
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,
"tags": self.tags,
# 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,
"auto_gain": None,
"schedule": None,
"picture_source_id": None,
"scene_behavior": None,
"use_real_time": None,
"latitude": None,
}
@staticmethod
def from_dict(data: dict) -> "ValueSource":
"""Factory: dispatch to the correct subclass based on source_type."""
source_type = data.get("source_type", "static") or "static"
subcls = _VALUE_SOURCE_MAP.get(source_type, StaticValueSource)
return subcls.from_dict(data)
def _parse_common_fields(data: dict) -> dict:
"""Extract common fields shared by all value source types."""
raw_created = data.get("created_at")
created_at = (
datetime.fromisoformat(raw_created)
if isinstance(raw_created, str)
else raw_created if isinstance(raw_created, datetime)
else datetime.now(timezone.utc)
)
raw_updated = data.get("updated_at")
updated_at = (
datetime.fromisoformat(raw_updated)
if isinstance(raw_updated, str)
else raw_updated if isinstance(raw_updated, datetime)
else datetime.now(timezone.utc)
)
return dict(
id=data["id"],
name=data["name"],
description=data.get("description"),
tags=data.get("tags", []),
created_at=created_at,
updated_at=updated_at,
)
@dataclass
class StaticValueSource(ValueSource):
"""Value source that outputs a constant float.
Useful as a simple per-target brightness override.
"""
value: float = 1.0 # 0.01.0
def to_dict(self) -> dict:
d = super().to_dict()
d["value"] = self.value
return d
@classmethod
def from_dict(cls, data: dict) -> "StaticValueSource":
common = _parse_common_fields(data)
return cls(
**common,
source_type="static",
value=float(data["value"]) if data.get("value") is not None else 1.0,
)
@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.0120.0)
min_value: float = 0.0 # minimum output (0.01.0)
max_value: float = 1.0 # maximum output (0.01.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
@classmethod
def from_dict(cls, data: dict) -> "AnimatedValueSource":
common = _parse_common_fields(data)
return cls(
**common,
source_type="animated",
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,
)
@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 an audio source (mono or multichannel)
mode: str = "rms" # rms | peak | beat
sensitivity: float = 1.0 # gain multiplier (0.120.0)
smoothing: float = 0.3 # temporal smoothing (0.01.0)
min_value: float = 0.0 # minimum output (0.01.0)
max_value: float = 1.0 # maximum output (0.01.0)
auto_gain: bool = False # auto-normalize audio levels to full range
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
d["min_value"] = self.min_value
d["max_value"] = self.max_value
d["auto_gain"] = self.auto_gain
return d
@classmethod
def from_dict(cls, data: dict) -> "AudioValueSource":
common = _parse_common_fields(data)
return cls(
**common,
source_type="audio",
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),
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,
auto_gain=bool(data.get("auto_gain", False)),
)
@dataclass
class AdaptiveValueSource(ValueSource):
"""Value source that adapts to external conditions.
source_type determines the sub-mode:
adaptive_time — interpolates brightness along a 24-hour schedule
adaptive_scene — derives brightness from a picture source's frame luminance
"""
schedule: List[dict] = field(default_factory=list) # [{time: "HH:MM", value: 0.0-1.0}]
picture_source_id: str = "" # for scene mode
scene_behavior: str = "complement" # "complement" | "match"
sensitivity: float = 1.0 # gain multiplier (0.1-5.0)
smoothing: float = 0.3 # temporal smoothing (0.0-1.0)
min_value: float = 0.0 # output range min
max_value: float = 1.0 # output range max
def to_dict(self) -> dict:
d = super().to_dict()
d["schedule"] = self.schedule
d["picture_source_id"] = self.picture_source_id
d["scene_behavior"] = self.scene_behavior
d["sensitivity"] = self.sensitivity
d["smoothing"] = self.smoothing
d["min_value"] = self.min_value
d["max_value"] = self.max_value
return d
@classmethod
def from_dict(cls, data: dict) -> "AdaptiveValueSource":
common = _parse_common_fields(data)
source_type = data.get("source_type", "adaptive_time")
return cls(
**common,
source_type=source_type,
schedule=data.get("schedule") or [],
picture_source_id=data.get("picture_source_id") or "",
scene_behavior=data.get("scene_behavior") or "complement",
sensitivity=float(data.get("sensitivity") or 1.0),
smoothing=float(data.get("smoothing") or 0.3),
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,
)
@dataclass
class DaylightValueSource(ValueSource):
"""Value source that outputs brightness based on a daylight cycle.
Uses the same daylight LUT as the CSS daylight stream to derive a
scalar brightness from the simulated (or real-time) sky color luminance.
"""
speed: float = 1.0 # simulation speed (ignored when use_real_time)
use_real_time: bool = False # use wall clock instead of simulation
latitude: float = 50.0 # affects sunrise/sunset in real-time mode
min_value: float = 0.0 # output range min
max_value: float = 1.0 # output range max
def to_dict(self) -> dict:
d = super().to_dict()
d["speed"] = self.speed
d["use_real_time"] = self.use_real_time
d["latitude"] = self.latitude
d["min_value"] = self.min_value
d["max_value"] = self.max_value
return d
@classmethod
def from_dict(cls, data: dict) -> "DaylightValueSource":
common = _parse_common_fields(data)
return cls(
**common,
source_type="daylight",
speed=float(data.get("speed") or 1.0),
use_real_time=bool(data.get("use_real_time", False)),
latitude=float(data.get("latitude") or 50.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,
)
# -- Source type registry --
# Maps source_type string to its subclass for factory dispatch.
_VALUE_SOURCE_MAP: Dict[str, Type[ValueSource]] = {
"static": StaticValueSource,
"animated": AnimatedValueSource,
"audio": AudioValueSource,
"adaptive_time": AdaptiveValueSource,
"adaptive_scene": AdaptiveValueSource,
"daylight": DaylightValueSource,
}