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:
@@ -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.1–2.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
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user