Add notification reactive color strip source with webhook trigger
New source_type "notification" fires one-shot visual effects (flash, pulse, sweep) triggered via POST webhook. Designed as a composite layer for overlay on persistent sources. Includes app color mapping, whitelist/blacklist filtering, and auto-sizing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ Current types:
|
||||
ColorCycleColorStripSource — smoothly cycles through a user-defined list of colors
|
||||
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
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
@@ -82,6 +83,13 @@ class ColorStripSource:
|
||||
"color_peak": None,
|
||||
"fallback_color": None,
|
||||
"timeout": None,
|
||||
"notification_effect": None,
|
||||
"duration_ms": None,
|
||||
"default_color": None,
|
||||
"app_colors": None,
|
||||
"app_filter_mode": None,
|
||||
"app_filter_list": None,
|
||||
"os_listener": None,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
@@ -218,6 +226,25 @@ class ColorStripSource:
|
||||
timeout=float(data.get("timeout") or 5.0),
|
||||
)
|
||||
|
||||
elif source_type == "notification":
|
||||
raw_app_colors = data.get("app_colors")
|
||||
app_colors = raw_app_colors if isinstance(raw_app_colors, dict) else {}
|
||||
raw_app_filter_list = data.get("app_filter_list")
|
||||
app_filter_list = raw_app_filter_list if isinstance(raw_app_filter_list, list) else []
|
||||
return NotificationColorStripSource(
|
||||
id=sid, name=name, source_type="notification",
|
||||
created_at=created_at, updated_at=updated_at, description=description,
|
||||
clock_id=clock_id,
|
||||
notification_effect=data.get("notification_effect") or "flash",
|
||||
duration_ms=int(data.get("duration_ms") or 1500),
|
||||
default_color=data.get("default_color") or "#FFFFFF",
|
||||
app_colors=app_colors,
|
||||
app_filter_mode=data.get("app_filter_mode") or "off",
|
||||
app_filter_list=app_filter_list,
|
||||
os_listener=bool(data.get("os_listener", False)),
|
||||
led_count=data.get("led_count") or 0,
|
||||
)
|
||||
|
||||
# Default: "picture" type
|
||||
return PictureColorStripSource(
|
||||
id=sid, name=name, source_type=source_type,
|
||||
@@ -469,3 +496,36 @@ class ApiInputColorStripSource(ColorStripSource):
|
||||
d["fallback_color"] = list(self.fallback_color)
|
||||
d["timeout"] = self.timeout
|
||||
return d
|
||||
|
||||
|
||||
@dataclass
|
||||
class NotificationColorStripSource(ColorStripSource):
|
||||
"""Color strip source that fires one-shot visual alerts (flash, pulse, sweep).
|
||||
|
||||
External clients trigger notifications via a REST webhook. The stream
|
||||
renders a brief animated effect and then returns to black. Each notification
|
||||
can carry an optional app name (for per-app color lookup) or an explicit
|
||||
color override.
|
||||
LED count auto-sizes from the connected device when led_count == 0.
|
||||
"""
|
||||
|
||||
notification_effect: str = "flash" # flash | pulse | sweep
|
||||
duration_ms: int = 1500 # effect duration in milliseconds
|
||||
default_color: str = "#FFFFFF" # hex color for notifications without app match
|
||||
app_colors: dict = field(default_factory=dict) # app name → hex color
|
||||
app_filter_mode: str = "off" # off | whitelist | blacklist
|
||||
app_filter_list: list = field(default_factory=list) # app names for filter
|
||||
os_listener: bool = False # whether to listen for OS notifications
|
||||
led_count: int = 0 # 0 = use device LED count
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
d = super().to_dict()
|
||||
d["notification_effect"] = self.notification_effect
|
||||
d["duration_ms"] = self.duration_ms
|
||||
d["default_color"] = self.default_color
|
||||
d["app_colors"] = dict(self.app_colors)
|
||||
d["app_filter_mode"] = self.app_filter_mode
|
||||
d["app_filter_list"] = list(self.app_filter_list)
|
||||
d["os_listener"] = self.os_listener
|
||||
d["led_count"] = self.led_count
|
||||
return d
|
||||
|
||||
@@ -16,6 +16,7 @@ from wled_controller.storage.color_strip_source import (
|
||||
EffectColorStripSource,
|
||||
GradientColorStripSource,
|
||||
MappedColorStripSource,
|
||||
NotificationColorStripSource,
|
||||
PictureColorStripSource,
|
||||
StaticColorStripSource,
|
||||
)
|
||||
@@ -120,6 +121,13 @@ class ColorStripStore:
|
||||
fallback_color: Optional[list] = None,
|
||||
timeout: Optional[float] = None,
|
||||
clock_id: Optional[str] = None,
|
||||
notification_effect: Optional[str] = None,
|
||||
duration_ms: Optional[int] = None,
|
||||
default_color: Optional[str] = None,
|
||||
app_colors: Optional[dict] = None,
|
||||
app_filter_mode: Optional[str] = None,
|
||||
app_filter_list: Optional[list] = None,
|
||||
os_listener: Optional[bool] = None,
|
||||
) -> ColorStripSource:
|
||||
"""Create a new color strip source.
|
||||
|
||||
@@ -259,6 +267,24 @@ class ColorStripStore:
|
||||
fallback_color=fb,
|
||||
timeout=float(timeout) if timeout is not None else 5.0,
|
||||
)
|
||||
elif source_type == "notification":
|
||||
source = NotificationColorStripSource(
|
||||
id=source_id,
|
||||
name=name,
|
||||
source_type="notification",
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
description=description,
|
||||
clock_id=clock_id,
|
||||
notification_effect=notification_effect or "flash",
|
||||
duration_ms=int(duration_ms) if duration_ms is not None else 1500,
|
||||
default_color=default_color or "#FFFFFF",
|
||||
app_colors=app_colors if isinstance(app_colors, dict) else {},
|
||||
app_filter_mode=app_filter_mode or "off",
|
||||
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,
|
||||
led_count=led_count,
|
||||
)
|
||||
else:
|
||||
if calibration is None:
|
||||
calibration = CalibrationConfig(layout="clockwise", start_position="bottom_left")
|
||||
@@ -321,6 +347,13 @@ class ColorStripStore:
|
||||
fallback_color: Optional[list] = None,
|
||||
timeout: Optional[float] = None,
|
||||
clock_id: Optional[str] = None,
|
||||
notification_effect: Optional[str] = None,
|
||||
duration_ms: Optional[int] = None,
|
||||
default_color: Optional[str] = None,
|
||||
app_colors: Optional[dict] = None,
|
||||
app_filter_mode: Optional[str] = None,
|
||||
app_filter_list: Optional[list] = None,
|
||||
os_listener: Optional[bool] = None,
|
||||
) -> ColorStripSource:
|
||||
"""Update an existing color strip source.
|
||||
|
||||
@@ -436,6 +469,23 @@ class ColorStripStore:
|
||||
source.timeout = float(timeout)
|
||||
if led_count is not None:
|
||||
source.led_count = led_count
|
||||
elif isinstance(source, NotificationColorStripSource):
|
||||
if notification_effect is not None:
|
||||
source.notification_effect = notification_effect
|
||||
if duration_ms is not None:
|
||||
source.duration_ms = int(duration_ms)
|
||||
if default_color is not None:
|
||||
source.default_color = default_color
|
||||
if app_colors is not None and isinstance(app_colors, dict):
|
||||
source.app_colors = app_colors
|
||||
if app_filter_mode is not None:
|
||||
source.app_filter_mode = app_filter_mode
|
||||
if app_filter_list is not None and isinstance(app_filter_list, list):
|
||||
source.app_filter_list = app_filter_list
|
||||
if os_listener is not None:
|
||||
source.os_listener = bool(os_listener)
|
||||
if led_count is not None:
|
||||
source.led_count = led_count
|
||||
|
||||
source.updated_at = datetime.utcnow()
|
||||
self._save()
|
||||
|
||||
Reference in New Issue
Block a user