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