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,7 +1,7 @@
"""Value stream — runtime scalar signal generators.
A ValueStream wraps a ValueSource config and computes a float (0.01.0)
on demand via ``get_value()``. Five concrete types:
on demand via ``get_value()``. Six concrete types:
StaticValueStream — returns a constant
AnimatedValueStream — evaluates a periodic waveform (sine/triangle/square/sawtooth)
@@ -9,6 +9,7 @@ on demand via ``get_value()``. Five concrete types:
sensitivity and temporal smoothing
TimeOfDayValueStream — interpolates brightness along a 24h schedule (adaptive_time)
SceneValueStream — derives brightness from a picture source's frame luminance (adaptive_scene)
DaylightValueStream — brightness derived from daylight cycle LUT (real-time or simulated)
ValueStreams are cheap (trivial math or single poll), so they compute inline
in the caller's processing loop — no background threads required.
@@ -541,6 +542,64 @@ class SceneValueStream(ValueStream):
self._live_stream = None
# ---------------------------------------------------------------------------
# Daylight
# ---------------------------------------------------------------------------
class DaylightValueStream(ValueStream):
"""Brightness derived from the daylight color cycle LUT.
Computes BT.601 luminance from the RGB sky color at the current
simulated (or real-time) minute of day, then maps to [min, max].
Cheap inline computation — no background thread needed.
"""
def __init__(
self,
speed: float = 1.0,
use_real_time: bool = False,
latitude: float = 50.0,
min_value: float = 0.0,
max_value: float = 1.0,
):
from wled_controller.core.processing.daylight_stream import _get_daylight_lut
self._lut = _get_daylight_lut()
self._speed = speed
self._use_real_time = use_real_time
self._latitude = latitude
self._min = min_value
self._max = max_value
self._start_time = time.perf_counter()
def get_value(self) -> float:
if self._use_real_time:
now = datetime.now()
minute_of_day = now.hour * 60 + now.minute + now.second / 60.0
else:
t_elapsed = time.perf_counter() - self._start_time
cycle_seconds = 240.0 / max(self._speed, 0.01)
phase = (t_elapsed % cycle_seconds) / cycle_seconds
minute_of_day = phase * 1440.0
idx = int(minute_of_day) % 1440
r, g, b = self._lut[idx]
# BT.601 luminance → 0..1
luminance = (0.299 * float(r) + 0.587 * float(g) + 0.114 * float(b)) / 255.0
return self._min + luminance * (self._max - self._min)
def update_source(self, source: "ValueSource") -> None:
from wled_controller.storage.value_source import DaylightValueSource
if isinstance(source, DaylightValueSource):
self._speed = source.speed
self._use_real_time = source.use_real_time
self._latitude = source.latitude
self._min = source.min_value
self._max = source.max_value
# ---------------------------------------------------------------------------
# Manager
# ---------------------------------------------------------------------------
@@ -635,6 +694,7 @@ class ValueStreamManager:
AdaptiveValueSource,
AnimatedValueSource,
AudioValueSource,
DaylightValueSource,
StaticValueSource,
)
@@ -663,6 +723,15 @@ class ValueStreamManager:
audio_template_store=self._audio_template_store,
)
if isinstance(source, DaylightValueSource):
return DaylightValueStream(
speed=source.speed,
use_real_time=source.use_real_time,
latitude=source.latitude,
min_value=source.min_value,
max_value=source.max_value,
)
if isinstance(source, AdaptiveValueSource):
if source.source_type == "adaptive_scene":
return SceneValueStream(