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:
2026-03-08 21:10:32 +03:00
parent 80b48e3618
commit de04872fdc
16 changed files with 932 additions and 6 deletions

View File

@@ -19,6 +19,7 @@ from wled_controller.api.schemas.color_strip_sources import (
ColorStripSourceResponse,
ColorStripSourceUpdate,
CSSCalibrationTestRequest,
NotifyRequest,
)
from wled_controller.api.schemas.devices import (
Calibration as CalibrationSchema,
@@ -30,7 +31,7 @@ from wled_controller.core.capture.calibration import (
)
from wled_controller.core.capture.screen_capture import get_available_displays
from wled_controller.core.processing.processor_manager import ProcessorManager
from wled_controller.storage.color_strip_source import ApiInputColorStripSource, PictureColorStripSource
from wled_controller.storage.color_strip_source import ApiInputColorStripSource, NotificationColorStripSource, PictureColorStripSource
from wled_controller.storage.color_strip_store import ColorStripStore
from wled_controller.storage.picture_source import ProcessedPictureSource, ScreenCapturePictureSource
from wled_controller.storage.picture_source_store import PictureSourceStore
@@ -91,6 +92,13 @@ def _css_to_response(source, overlay_active: bool = False) -> ColorStripSourceRe
color_peak=getattr(source, "color_peak", None),
fallback_color=getattr(source, "fallback_color", None),
timeout=getattr(source, "timeout", None),
notification_effect=getattr(source, "notification_effect", None),
duration_ms=getattr(source, "duration_ms", None),
default_color=getattr(source, "default_color", None),
app_colors=getattr(source, "app_colors", None),
app_filter_mode=getattr(source, "app_filter_mode", None),
app_filter_list=getattr(source, "app_filter_list", None),
os_listener=getattr(source, "os_listener", None),
overlay_active=overlay_active,
created_at=source.created_at,
updated_at=source.updated_at,
@@ -175,6 +183,13 @@ async def create_color_strip_source(
fallback_color=data.fallback_color,
timeout=data.timeout,
clock_id=data.clock_id,
notification_effect=data.notification_effect,
duration_ms=data.duration_ms,
default_color=data.default_color,
app_colors=data.app_colors,
app_filter_mode=data.app_filter_mode,
app_filter_list=data.app_filter_list,
os_listener=data.os_listener,
)
return _css_to_response(source)
@@ -251,6 +266,13 @@ async def update_color_strip_source(
fallback_color=data.fallback_color,
timeout=data.timeout,
clock_id=data.clock_id,
notification_effect=data.notification_effect,
duration_ms=data.duration_ms,
default_color=data.default_color,
app_colors=data.app_colors,
app_filter_mode=data.app_filter_mode,
app_filter_list=data.app_filter_list,
os_listener=data.os_listener,
)
# Hot-reload running stream (no restart needed for in-place param changes)
@@ -489,6 +511,45 @@ async def push_colors(
}
@router.post("/api/v1/color-strip-sources/{source_id}/notify", tags=["Color Strip Sources"])
async def notify_source(
source_id: str,
_auth: AuthRequired,
body: NotifyRequest = None,
store: ColorStripStore = Depends(get_color_strip_store),
manager: ProcessorManager = Depends(get_processor_manager),
):
"""Trigger a notification on a notification color strip source.
Fires a one-shot visual effect (flash, pulse, sweep) on all running
stream instances for this source. Optionally specify an app name for
color lookup or a hex color override.
"""
try:
source = store.get_source(source_id)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
if not isinstance(source, NotificationColorStripSource):
raise HTTPException(status_code=400, detail="Source is not a notification type")
app_name = body.app if body else None
color_override = body.color if body else None
streams = manager._color_strip_stream_manager.get_streams_by_source_id(source_id)
accepted = 0
for stream in streams:
if hasattr(stream, "fire"):
if stream.fire(app_name=app_name, color_override=color_override):
accepted += 1
return {
"status": "ok",
"streams_notified": accepted,
"filtered": len(streams) - accepted,
}
@router.websocket("/api/v1/color-strip-sources/{source_id}/ws")
async def css_api_input_ws(
websocket: WebSocket,