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:
@@ -1,13 +1,14 @@
|
||||
"""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. 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
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user