Add mock LED device type for testing without hardware

Virtual device with configurable LED count, RGB/RGBW mode, and simulated
send latency. Includes full provider/client implementation, API schema
support, and frontend add/settings modal integration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 19:22:53 +03:00
parent dc12452bcd
commit a39dc1b06a
16 changed files with 291 additions and 9 deletions

View File

@@ -279,5 +279,8 @@ def _register_builtin_providers():
from wled_controller.core.devices.ambiled_provider import AmbiLEDDeviceProvider
register_provider(AmbiLEDDeviceProvider())
from wled_controller.core.devices.mock_provider import MockDeviceProvider
register_provider(MockDeviceProvider())
_register_builtin_providers()

View File

@@ -0,0 +1,73 @@
"""Mock LED client — simulates an LED strip with configurable latency for testing."""
import asyncio
from datetime import datetime
from typing import List, Optional, Tuple, Union
import numpy as np
from wled_controller.core.devices.led_client import DeviceHealth, LEDClient
from wled_controller.utils import get_logger
logger = get_logger(__name__)
class MockClient(LEDClient):
"""LED client that simulates an LED strip without real hardware.
Useful for load testing, development, and CI environments.
"""
def __init__(
self,
url: str = "",
led_count: int = 0,
send_latency_ms: int = 0,
rgbw: bool = False,
**kwargs,
):
self._led_count = led_count
self._latency = send_latency_ms / 1000.0 # convert to seconds
self._rgbw = rgbw
self._connected = False
async def connect(self) -> bool:
self._connected = True
logger.info(
f"Mock device connected ({self._led_count} LEDs, "
f"{'RGBW' if self._rgbw else 'RGB'}, "
f"{int(self._latency * 1000)}ms latency)"
)
return True
async def close(self) -> None:
self._connected = False
logger.info("Mock device disconnected")
@property
def is_connected(self) -> bool:
return self._connected
async def send_pixels(
self,
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
brightness: int = 255,
) -> bool:
if not self._connected:
return False
if self._latency > 0:
await asyncio.sleep(self._latency)
return True
@classmethod
async def check_health(
cls,
url: str,
http_client,
prev_health: Optional[DeviceHealth] = None,
) -> DeviceHealth:
return DeviceHealth(
online=True,
latency_ms=0.0,
last_checked=datetime.utcnow(),
)

View File

@@ -0,0 +1,43 @@
"""Mock device provider — virtual LED strip for testing."""
from datetime import datetime
from typing import List
from wled_controller.core.devices.led_client import (
DeviceHealth,
DiscoveredDevice,
LEDClient,
LEDDeviceProvider,
)
from wled_controller.core.devices.mock_client import MockClient
class MockDeviceProvider(LEDDeviceProvider):
"""Provider for virtual mock LED devices."""
@property
def device_type(self) -> str:
return "mock"
@property
def capabilities(self) -> set:
return {"manual_led_count", "power_control", "brightness_control"}
def create_client(self, url: str, **kwargs) -> LEDClient:
kwargs.pop("use_ddp", None)
return MockClient(url, **kwargs)
async def check_health(self, url: str, http_client, prev_health=None) -> DeviceHealth:
return DeviceHealth(online=True, latency_ms=0.0, last_checked=datetime.utcnow())
async def validate_device(self, url: str) -> dict:
return {}
async def discover(self, timeout: float = 3.0) -> List[DiscoveredDevice]:
return []
async def get_power(self, url: str, **kwargs) -> bool:
return True
async def set_power(self, url: str, on: bool, **kwargs) -> None:
pass

View File

@@ -129,6 +129,15 @@ class ProcessorManager:
ds = self._devices.get(device_id)
if ds is None:
return None
# Read mock-specific fields from persistent storage
send_latency_ms = 0
rgbw = False
if self._device_store:
dev = self._device_store.get_device(ds.device_id)
if dev:
send_latency_ms = getattr(dev, "send_latency_ms", 0)
rgbw = getattr(dev, "rgbw", False)
return DeviceInfo(
device_id=ds.device_id,
device_url=ds.device_url,
@@ -137,6 +146,8 @@ class ProcessorManager:
baud_rate=ds.baud_rate,
software_brightness=ds.software_brightness,
test_mode_active=ds.test_mode_active,
send_latency_ms=send_latency_ms,
rgbw=rgbw,
)
# ===== EVENT SYSTEM (state change notifications) =====

View File

@@ -69,6 +69,8 @@ class DeviceInfo:
baud_rate: Optional[int] = None
software_brightness: int = 255
test_mode_active: bool = False
send_latency_ms: int = 0
rgbw: bool = False
@dataclass

View File

@@ -86,6 +86,8 @@ class WledTargetProcessor(TargetProcessor):
device_info.device_type, device_info.device_url,
use_ddp=True, led_count=device_info.led_count,
baud_rate=device_info.baud_rate,
send_latency_ms=device_info.send_latency_ms,
rgbw=device_info.rgbw,
)
await self._led_client.connect()
logger.info(