Add Daylight Cycle and Candlelight CSS source types

Full-stack implementation of two new color strip source types:
- Daylight: simulates day/night color cycle with real-time or speed-based mode, latitude support
- Candlelight: multi-candle fire simulation with Gaussian falloff, layered-sine flicker, warm color shift

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 11:07:30 +03:00
parent 954e37c2ca
commit 37c80f01af
15 changed files with 882 additions and 7 deletions

View File

@@ -13,6 +13,8 @@ Current types:
AudioColorStripSource — audio-reactive visualization (spectrum, beat pulse, VU meter)
ApiInputColorStripSource — receives raw LED colors from external clients via REST/WebSocket
NotificationColorStripSource — fires one-shot visual alerts (flash, pulse, sweep) via API
DaylightColorStripSource — simulates natural daylight color temperature over a 24-hour cycle
CandlelightColorStripSource — realistic per-LED candle flickering with warm glow
"""
from dataclasses import dataclass, field
@@ -93,6 +95,12 @@ class ColorStripSource:
"app_filter_mode": None,
"app_filter_list": None,
"os_listener": None,
# daylight-type fields
"speed": None,
"use_real_time": None,
"latitude": None,
# candlelight-type fields
"num_candles": None,
}
@staticmethod
@@ -244,6 +252,32 @@ class ColorStripSource:
os_listener=bool(data.get("os_listener", False)),
)
if source_type == "daylight":
return DaylightColorStripSource(
id=sid, name=name, source_type="daylight",
created_at=created_at, updated_at=updated_at, description=description,
clock_id=clock_id, 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),
)
if source_type == "candlelight":
raw_color = data.get("color")
color = (
raw_color if isinstance(raw_color, list) and len(raw_color) == 3
else [255, 147, 41]
)
return CandlelightColorStripSource(
id=sid, name=name, source_type="candlelight",
created_at=created_at, updated_at=updated_at, description=description,
clock_id=clock_id, tags=tags,
color=color,
intensity=float(data.get("intensity") or 1.0),
num_candles=int(data.get("num_candles") or 3),
speed=float(data.get("speed") or 1.0),
)
# Shared picture-type field extraction
_picture_kwargs = dict(
tags=tags,
@@ -567,3 +601,52 @@ class NotificationColorStripSource(ColorStripSource):
d["app_filter_list"] = list(self.app_filter_list)
d["os_listener"] = self.os_listener
return d
@dataclass
class DaylightColorStripSource(ColorStripSource):
"""Color strip source that simulates natural daylight over a 24-hour cycle.
All LEDs receive the same color at any point in time, smoothly
transitioning through dawn (warm orange), daylight (cool white),
sunset (warm red/orange), and night (dim blue).
LED count auto-sizes from the connected device.
When use_real_time is True, the current wall-clock hour determines
the color; speed is ignored. When False, speed controls how fast
a full 24-hour cycle plays (1.0 ≈ 4 minutes per full cycle).
"""
speed: float = 1.0 # cycle speed (ignored when use_real_time)
use_real_time: bool = False # use actual time of day
latitude: float = 50.0 # latitude for sunrise/sunset timing (-90..90)
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
return d
@dataclass
class CandlelightColorStripSource(ColorStripSource):
"""Color strip source that simulates realistic candle flickering.
Each LED or group of LEDs flickers independently with warm tones.
Uses layered noise for organic, non-repeating flicker patterns.
LED count auto-sizes from the connected device.
"""
color: list = field(default_factory=lambda: [255, 147, 41]) # warm candle base [R,G,B]
intensity: float = 1.0 # flicker intensity (0.12.0)
num_candles: int = 3 # number of independent candle sources
speed: float = 1.0 # flicker speed multiplier
def to_dict(self) -> dict:
d = super().to_dict()
d["color"] = list(self.color)
d["intensity"] = self.intensity
d["num_candles"] = self.num_candles
d["speed"] = self.speed
return d

View File

@@ -10,9 +10,11 @@ from wled_controller.storage.color_strip_source import (
AdvancedPictureColorStripSource,
ApiInputColorStripSource,
AudioColorStripSource,
CandlelightColorStripSource,
ColorCycleColorStripSource,
ColorStripSource,
CompositeColorStripSource,
DaylightColorStripSource,
EffectColorStripSource,
GradientColorStripSource,
MappedColorStripSource,
@@ -82,6 +84,12 @@ class ColorStripStore(BaseJsonStore[ColorStripSource]):
app_filter_mode: Optional[str] = None,
app_filter_list: Optional[list] = None,
os_listener: Optional[bool] = None,
# daylight-type fields
speed: Optional[float] = None,
use_real_time: Optional[bool] = None,
latitude: Optional[float] = None,
# candlelight-type fields
num_candles: Optional[int] = None,
tags: Optional[List[str]] = None,
) -> ColorStripSource:
"""Create a new color strip source.
@@ -235,6 +243,34 @@ class ColorStripStore(BaseJsonStore[ColorStripSource]):
app_filter_list=app_filter_list if isinstance(app_filter_list, list) else [],
os_listener=bool(os_listener) if os_listener is not None else False,
)
elif source_type == "daylight":
source = DaylightColorStripSource(
id=source_id,
name=name,
source_type="daylight",
created_at=now,
updated_at=now,
description=description,
clock_id=clock_id,
speed=float(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=float(latitude) if latitude is not None else 50.0,
)
elif source_type == "candlelight":
rgb = color if isinstance(color, list) and len(color) == 3 else [255, 147, 41]
source = CandlelightColorStripSource(
id=source_id,
name=name,
source_type="candlelight",
created_at=now,
updated_at=now,
description=description,
clock_id=clock_id,
color=rgb,
intensity=float(intensity) if intensity else 1.0,
num_candles=int(num_candles) if num_candles is not None else 3,
speed=float(speed) if speed is not None else 1.0,
)
elif source_type == "picture_advanced":
if calibration is None:
calibration = CalibrationConfig(mode="advanced")
@@ -326,6 +362,12 @@ class ColorStripStore(BaseJsonStore[ColorStripSource]):
app_filter_mode: Optional[str] = None,
app_filter_list: Optional[list] = None,
os_listener: Optional[bool] = None,
# daylight-type fields
speed: Optional[float] = None,
use_real_time: Optional[bool] = None,
latitude: Optional[float] = None,
# candlelight-type fields
num_candles: Optional[int] = None,
tags: Optional[List[str]] = None,
) -> ColorStripSource:
"""Update an existing color strip source.
@@ -452,6 +494,22 @@ class ColorStripStore(BaseJsonStore[ColorStripSource]):
source.app_filter_list = app_filter_list
if os_listener is not None:
source.os_listener = bool(os_listener)
elif isinstance(source, DaylightColorStripSource):
if speed is not None:
source.speed = float(speed)
if use_real_time is not None:
source.use_real_time = bool(use_real_time)
if latitude is not None:
source.latitude = float(latitude)
elif isinstance(source, CandlelightColorStripSource):
if color is not None and isinstance(color, list) and len(color) == 3:
source.color = color
if intensity is not None:
source.intensity = float(intensity)
if num_candles is not None:
source.num_candles = int(num_candles)
if speed is not None:
source.speed = float(speed)
source.updated_at = datetime.now(timezone.utc)
self._save()