feat: game integration system

Receive real-time events from games (CS2, Dota 2, LoL, etc.) and drive
LED effects through the existing color strip and value source pipelines.

Core:
- GameEventBus (thread-safe pub/sub) with standardized 23-type event vocabulary
- GameAdapter ABC + AdapterRegistry + MappingAdapter (YAML-driven)
- Built-in adapters: CS2 GSI, Dota 2 GSI, LoL Live Client, Generic Webhook
- Community YAML adapters: Minecraft, Valorant, Rocket League
- GameEventColorStripStream with 5 effects (flash/pulse/sweep/color_shift/breathing)
- GameEventValueSource with EMA smoothing and timeout
- 4 built-in effect presets (FPS Combat, MOBA Health, Racing, Generic Alert)
- Auto-setup for Valve GSI games (Steam path detection, cfg file writing)
- Demo capture engine exposed to non-demo mode

Frontend:
- Game tab in Streams tree navigation with integration cards
- Game integration editor modal with adapter picker, config fields, event mappings
- game_event source type in CSS and ValueSource editors
- Setup instructions overlay (markdown rendered)
- Live event monitor and connection test

API:
- Full CRUD for game integrations
- Event ingestion endpoint (adapter-level auth)
- Adapter metadata, presets, auto-setup, status/diagnostics endpoints
This commit is contained in:
2026-03-31 13:17:52 +03:00
parent b6713be390
commit 492bdb95e3
87 changed files with 12170 additions and 912 deletions
@@ -4,7 +4,7 @@ import uuid
from datetime import datetime, timezone
from typing import List, Optional
from wled_controller.storage.automation import Automation, Condition
from wled_controller.storage.automation import Automation, Rule
from wled_controller.storage.base_sqlite_store import BaseSqliteStore
from wled_controller.storage.database import Database
from wled_controller.utils import get_logger
@@ -28,13 +28,22 @@ class AutomationStore(BaseSqliteStore[Automation]):
self,
name: str,
enabled: bool = True,
condition_logic: str = "or",
conditions: Optional[List[Condition]] = None,
rule_logic: str = "or",
rules: Optional[List[Rule]] = None,
scene_preset_id: Optional[str] = None,
deactivation_mode: str = "none",
deactivation_scene_preset_id: Optional[str] = None,
tags: Optional[List[str]] = None,
# Legacy parameter aliases
condition_logic: Optional[str] = None,
conditions: Optional[List[Rule]] = None,
) -> Automation:
# Support legacy parameter names
if condition_logic is not None and rule_logic == "or":
rule_logic = condition_logic
if conditions is not None and rules is None:
rules = conditions
for a in self._items.values():
if a.name == name:
raise ValueError(f"Automation with name '{name}' already exists")
@@ -46,8 +55,8 @@ class AutomationStore(BaseSqliteStore[Automation]):
id=automation_id,
name=name,
enabled=enabled,
condition_logic=condition_logic,
conditions=conditions or [],
rule_logic=rule_logic,
rules=rules or [],
scene_preset_id=scene_preset_id,
deactivation_mode=deactivation_mode,
deactivation_scene_preset_id=deactivation_scene_preset_id,
@@ -66,13 +75,22 @@ class AutomationStore(BaseSqliteStore[Automation]):
automation_id: str,
name: Optional[str] = None,
enabled: Optional[bool] = None,
condition_logic: Optional[str] = None,
conditions: Optional[List[Condition]] = None,
rule_logic: Optional[str] = None,
rules: Optional[List[Rule]] = None,
scene_preset_id: str = "__unset__",
deactivation_mode: Optional[str] = None,
deactivation_scene_preset_id: str = "__unset__",
tags: Optional[List[str]] = None,
# Legacy parameter aliases
condition_logic: Optional[str] = None,
conditions: Optional[List[Rule]] = None,
) -> Automation:
# Support legacy parameter names
if condition_logic is not None and rule_logic is None:
rule_logic = condition_logic
if conditions is not None and rules is None:
rules = conditions
automation = self.get(automation_id)
if name is not None:
@@ -80,16 +98,18 @@ class AutomationStore(BaseSqliteStore[Automation]):
automation.name = name
if enabled is not None:
automation.enabled = enabled
if condition_logic is not None:
automation.condition_logic = condition_logic
if conditions is not None:
automation.conditions = conditions
if rule_logic is not None:
automation.rule_logic = rule_logic
if rules is not None:
automation.rules = rules
if scene_preset_id != "__unset__":
automation.scene_preset_id = None if scene_preset_id == "" else scene_preset_id
if deactivation_mode is not None:
automation.deactivation_mode = deactivation_mode
if deactivation_scene_preset_id != "__unset__":
automation.deactivation_scene_preset_id = None if deactivation_scene_preset_id == "" else deactivation_scene_preset_id
automation.deactivation_scene_preset_id = (
None if deactivation_scene_preset_id == "" else deactivation_scene_preset_id
)
if tags is not None:
automation.tags = tags