Add Daylight Cycle value source type

New value source that outputs brightness (0-1) based on the daylight
color LUT, computing BT.601 luminance from the simulated sky color.
Supports real-time wall-clock mode or configurable simulation speed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 11:27:36 +03:00
parent 73562cd525
commit ee40d99067
13 changed files with 271 additions and 12 deletions

View File

@@ -1,13 +1,14 @@
"""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. Five types:
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
@@ -21,7 +22,7 @@ class ValueSource:
id: str
name: str
source_type: str # "static" | "animated" | "audio" | "adaptive_time" | "adaptive_scene"
source_type: str # "static" | "animated" | "audio" | "adaptive_time" | "adaptive_scene" | "daylight"
created_at: datetime
updated_at: datetime
description: Optional[str] = None
@@ -51,6 +52,8 @@ class ValueSource:
"schedule": None,
"picture_source_id": None,
"scene_behavior": None,
"use_real_time": None,
"latitude": None,
}
@staticmethod
@@ -121,6 +124,17 @@ class ValueSource:
max_value=float(data["max_value"]) if data.get("max_value") is not None else 1.0,
)
if source_type == "daylight":
return DaylightValueSource(
id=sid, name=name, source_type="daylight",
created_at=created_at, updated_at=updated_at, description=description, tags=tags,
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,
)
# Default: "static" type
return StaticValueSource(
id=sid, name=name, source_type="static",
@@ -221,3 +235,27 @@ class AdaptiveValueSource(ValueSource):
d["min_value"] = self.min_value
d["max_value"] = self.max_value
return d
@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

View File

@@ -9,6 +9,7 @@ from wled_controller.storage.value_source import (
AdaptiveValueSource,
AnimatedValueSource,
AudioValueSource,
DaylightValueSource,
StaticValueSource,
ValueSource,
)
@@ -51,9 +52,11 @@ class ValueSourceStore(BaseJsonStore[ValueSource]):
picture_source_id: Optional[str] = None,
scene_behavior: Optional[str] = None,
auto_gain: Optional[bool] = None,
use_real_time: Optional[bool] = None,
latitude: Optional[float] = None,
tags: Optional[List[str]] = None,
) -> ValueSource:
if source_type not in ("static", "animated", "audio", "adaptive_time", "adaptive_scene"):
if source_type not in ("static", "animated", "audio", "adaptive_time", "adaptive_scene", "daylight"):
raise ValueError(f"Invalid source type: {source_type}")
self._check_name_unique(name)
@@ -112,6 +115,16 @@ class ValueSourceStore(BaseJsonStore[ValueSource]):
min_value=min_value if min_value is not None else 0.0,
max_value=max_value if max_value is not None else 1.0,
)
elif source_type == "daylight":
source = DaylightValueSource(
id=sid, name=name, source_type="daylight",
created_at=now, updated_at=now, description=description, tags=common_tags,
speed=speed if speed is not None else 1.0,
use_real_time=bool(use_real_time) if use_real_time is not None else False,
latitude=latitude if latitude is not None else 50.0,
min_value=min_value if min_value is not None else 0.0,
max_value=max_value if max_value is not None else 1.0,
)
self._items[sid] = source
self._save()
@@ -137,6 +150,8 @@ class ValueSourceStore(BaseJsonStore[ValueSource]):
picture_source_id: Optional[str] = None,
scene_behavior: Optional[str] = None,
auto_gain: Optional[bool] = None,
use_real_time: Optional[bool] = None,
latitude: Optional[float] = None,
tags: Optional[List[str]] = None,
) -> ValueSource:
source = self.get(source_id)
@@ -194,6 +209,17 @@ class ValueSourceStore(BaseJsonStore[ValueSource]):
source.min_value = min_value
if max_value is not None:
source.max_value = max_value
elif isinstance(source, DaylightValueSource):
if speed is not None:
source.speed = speed
if use_real_time is not None:
source.use_real_time = use_real_time
if latitude is not None:
source.latitude = latitude
if min_value is not None:
source.min_value = min_value
if max_value is not None:
source.max_value = max_value
source.updated_at = datetime.now(timezone.utc)
self._save()