Add demo mode: virtual hardware sandbox for testing without real devices

Demo mode provides a complete sandbox environment with:
- Virtual capture engine (radial rainbow test pattern on 3 displays)
- Virtual audio engine (synthetic music-like audio on 2 devices)
- Virtual LED device provider (strip/60, matrix/256, ring/24 LEDs)
- Isolated data directory (data/demo/) with auto-seeded sample entities
- Dedicated config (config/demo_config.yaml) with pre-configured API key
- Frontend indicator (DEMO badge + dismissible banner)
- Engine filtering (only demo engines visible in demo mode)
- Separate entry point: python -m wled_controller.demo (port 8081)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-20 16:17:14 +03:00
parent 81b275979b
commit 2240471b67
36 changed files with 1548 additions and 282 deletions
@@ -0,0 +1,93 @@
"""Demo device provider — virtual LED devices for demo mode."""
from datetime import datetime, timezone
from typing import List
from wled_controller.config import is_demo_mode
from wled_controller.core.devices.led_client import (
DeviceHealth,
DiscoveredDevice,
LEDClient,
LEDDeviceProvider,
)
from wled_controller.core.devices.mock_client import MockClient
# Pre-defined virtual devices: (name, led_count, ip, width, height)
_DEMO_DEVICES = [
("Demo LED Strip", 60, "demo-strip", None, None),
("Demo LED Matrix", 256, "demo-matrix", 16, 16),
("Demo LED Ring", 24, "demo-ring", None, None),
]
class DemoDeviceProvider(LEDDeviceProvider):
"""Provider for virtual demo LED devices.
Exposes three discoverable virtual devices when demo mode is active.
Uses MockClient for actual LED output (pixels are silently discarded).
"""
@property
def device_type(self) -> str:
return "demo"
@property
def capabilities(self) -> set:
return {"manual_led_count", "power_control", "brightness_control", "static_color"}
def create_client(self, url: str, **kwargs) -> LEDClient:
return MockClient(
url,
led_count=kwargs.get("led_count", 0),
send_latency_ms=kwargs.get("send_latency_ms", 0),
)
async def check_health(self, url: str, http_client, prev_health=None) -> DeviceHealth:
# Simulate ~2ms latency for realistic appearance
return DeviceHealth(
online=True,
latency_ms=2.0,
last_checked=datetime.now(timezone.utc),
device_name=url,
device_version="demo",
)
async def validate_device(self, url: str) -> dict:
# Look up configured LED count from demo devices
for name, led_count, ip, _w, _h in _DEMO_DEVICES:
if url == f"demo://{ip}":
return {"led_count": led_count}
# Fallback for unknown demo URLs
return {"led_count": 60}
async def discover(self, timeout: float = 3.0) -> List[DiscoveredDevice]:
if not is_demo_mode():
return []
return [
DiscoveredDevice(
name=name,
url=f"demo://{ip}",
device_type="demo",
ip=ip,
mac=f"DE:MO:00:00:00:{i:02X}",
led_count=led_count,
version="demo",
)
for i, (name, led_count, ip, _w, _h) in enumerate(_DEMO_DEVICES)
]
async def get_power(self, url: str, **kwargs) -> bool:
return True
async def set_power(self, url: str, on: bool, **kwargs) -> None:
pass
async def get_brightness(self, url: str) -> int:
return 255
async def set_brightness(self, url: str, brightness: int) -> None:
pass
async def set_color(self, url: str, color, **kwargs) -> None:
pass
@@ -317,5 +317,8 @@ def _register_builtin_providers():
from wled_controller.core.devices.gamesense_provider import GameSenseDeviceProvider
register_provider(GameSenseDeviceProvider())
from wled_controller.core.devices.demo_provider import DemoDeviceProvider
register_provider(DemoDeviceProvider())
_register_builtin_providers()