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:
@@ -44,6 +44,8 @@ def _device_to_response(device) -> DeviceResponse:
|
||||
enabled=device.enabled,
|
||||
baud_rate=device.baud_rate,
|
||||
auto_shutdown=device.auto_shutdown,
|
||||
send_latency_ms=device.send_latency_ms,
|
||||
rgbw=device.rgbw,
|
||||
capabilities=sorted(get_device_capabilities(device.device_type)),
|
||||
created_at=device.created_at,
|
||||
updated_at=device.updated_at,
|
||||
@@ -116,6 +118,8 @@ async def create_device(
|
||||
device_type=device_type,
|
||||
baud_rate=device_data.baud_rate,
|
||||
auto_shutdown=auto_shutdown,
|
||||
send_latency_ms=device_data.send_latency_ms or 0,
|
||||
rgbw=device_data.rgbw or False,
|
||||
)
|
||||
|
||||
# Register in processor manager for health monitoring
|
||||
@@ -242,6 +246,8 @@ async def update_device(
|
||||
led_count=update_data.led_count,
|
||||
baud_rate=update_data.baud_rate,
|
||||
auto_shutdown=update_data.auto_shutdown,
|
||||
send_latency_ms=update_data.send_latency_ms,
|
||||
rgbw=update_data.rgbw,
|
||||
)
|
||||
|
||||
# Sync connection info in processor manager
|
||||
|
||||
@@ -15,6 +15,8 @@ class DeviceCreate(BaseModel):
|
||||
led_count: Optional[int] = Field(None, ge=1, le=10000, description="Number of LEDs (required for adalight)")
|
||||
baud_rate: Optional[int] = Field(None, description="Serial baud rate (for adalight devices)")
|
||||
auto_shutdown: Optional[bool] = Field(default=None, description="Turn off device when server stops (defaults to true for adalight)")
|
||||
send_latency_ms: Optional[int] = Field(None, ge=0, le=5000, description="Simulated send latency in ms (mock devices)")
|
||||
rgbw: Optional[bool] = Field(None, description="RGBW mode (mock devices)")
|
||||
|
||||
|
||||
class DeviceUpdate(BaseModel):
|
||||
@@ -26,6 +28,8 @@ class DeviceUpdate(BaseModel):
|
||||
led_count: Optional[int] = Field(None, ge=1, le=10000, description="Number of LEDs (for devices with manual_led_count capability)")
|
||||
baud_rate: Optional[int] = Field(None, description="Serial baud rate (for adalight devices)")
|
||||
auto_shutdown: Optional[bool] = Field(None, description="Turn off device when server stops")
|
||||
send_latency_ms: Optional[int] = Field(None, ge=0, le=5000, description="Simulated send latency in ms (mock devices)")
|
||||
rgbw: Optional[bool] = Field(None, description="RGBW mode (mock devices)")
|
||||
|
||||
|
||||
class Calibration(BaseModel):
|
||||
@@ -93,6 +97,8 @@ class DeviceResponse(BaseModel):
|
||||
enabled: bool = Field(description="Whether device is enabled")
|
||||
baud_rate: Optional[int] = Field(None, description="Serial baud rate")
|
||||
auto_shutdown: bool = Field(default=False, description="Restore device to idle state when targets stop")
|
||||
send_latency_ms: int = Field(default=0, description="Simulated send latency in ms (mock devices)")
|
||||
rgbw: bool = Field(default=False, description="RGBW mode (mock devices)")
|
||||
capabilities: List[str] = Field(default_factory=list, description="Device type capabilities")
|
||||
created_at: datetime = Field(description="Creation timestamp")
|
||||
updated_at: datetime = Field(description="Last update timestamp")
|
||||
|
||||
@@ -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()
|
||||
|
||||
73
server/src/wled_controller/core/devices/mock_client.py
Normal file
73
server/src/wled_controller/core/devices/mock_client.py
Normal 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(),
|
||||
)
|
||||
43
server/src/wled_controller/core/devices/mock_provider.py
Normal file
43
server/src/wled_controller/core/devices/mock_provider.py
Normal 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
|
||||
@@ -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) =====
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -74,6 +74,10 @@ export function isSerialDevice(type) {
|
||||
return type === 'adalight' || type === 'ambiled';
|
||||
}
|
||||
|
||||
export function isMockDevice(type) {
|
||||
return type === 'mock';
|
||||
}
|
||||
|
||||
export function handle401Error() {
|
||||
if (!apiKey) return; // Already handled or no session
|
||||
localStorage.removeItem('wled_api_key');
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
_discoveryScanRunning, set_discoveryScanRunning,
|
||||
_discoveryCache, set_discoveryCache,
|
||||
} from '../core/state.js';
|
||||
import { API_BASE, fetchWithAuth, isSerialDevice, escapeHtml } from '../core/api.js';
|
||||
import { API_BASE, fetchWithAuth, isSerialDevice, isMockDevice, escapeHtml } from '../core/api.js';
|
||||
import { t } from '../core/i18n.js';
|
||||
import { showToast } from '../core/ui.js';
|
||||
import { Modal } from '../core/modal.js';
|
||||
@@ -23,6 +23,8 @@ class AddDeviceModal extends Modal {
|
||||
serialPort: document.getElementById('device-serial-port').value,
|
||||
ledCount: document.getElementById('device-led-count').value,
|
||||
baudRate: document.getElementById('device-baud-rate').value,
|
||||
ledType: document.getElementById('device-led-type')?.value || 'rgb',
|
||||
sendLatency: document.getElementById('device-send-latency')?.value || '0',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -37,16 +39,29 @@ export function onDeviceTypeChanged() {
|
||||
const serialSelect = document.getElementById('device-serial-port');
|
||||
const ledCountGroup = document.getElementById('device-led-count-group');
|
||||
const discoverySection = document.getElementById('discovery-section');
|
||||
|
||||
const baudRateGroup = document.getElementById('device-baud-rate-group');
|
||||
const ledTypeGroup = document.getElementById('device-led-type-group');
|
||||
const sendLatencyGroup = document.getElementById('device-send-latency-group');
|
||||
|
||||
if (isSerialDevice(deviceType)) {
|
||||
if (isMockDevice(deviceType)) {
|
||||
urlGroup.style.display = 'none';
|
||||
urlInput.removeAttribute('required');
|
||||
serialGroup.style.display = 'none';
|
||||
serialSelect.removeAttribute('required');
|
||||
ledCountGroup.style.display = '';
|
||||
baudRateGroup.style.display = 'none';
|
||||
if (ledTypeGroup) ledTypeGroup.style.display = '';
|
||||
if (sendLatencyGroup) sendLatencyGroup.style.display = '';
|
||||
if (discoverySection) discoverySection.style.display = 'none';
|
||||
} else if (isSerialDevice(deviceType)) {
|
||||
urlGroup.style.display = 'none';
|
||||
urlInput.removeAttribute('required');
|
||||
serialGroup.style.display = '';
|
||||
serialSelect.setAttribute('required', '');
|
||||
ledCountGroup.style.display = '';
|
||||
baudRateGroup.style.display = '';
|
||||
if (ledTypeGroup) ledTypeGroup.style.display = 'none';
|
||||
if (sendLatencyGroup) sendLatencyGroup.style.display = 'none';
|
||||
// Hide discovery list — serial port dropdown replaces it
|
||||
if (discoverySection) discoverySection.style.display = 'none';
|
||||
// Populate from cache or show placeholder (lazy-load on focus)
|
||||
@@ -68,6 +83,8 @@ export function onDeviceTypeChanged() {
|
||||
serialSelect.removeAttribute('required');
|
||||
ledCountGroup.style.display = 'none';
|
||||
baudRateGroup.style.display = 'none';
|
||||
if (ledTypeGroup) ledTypeGroup.style.display = 'none';
|
||||
if (sendLatencyGroup) sendLatencyGroup.style.display = 'none';
|
||||
// Show cached results or trigger scan for WLED
|
||||
if (deviceType in _discoveryCache) {
|
||||
_renderDiscoveryList();
|
||||
@@ -287,12 +304,19 @@ export async function handleAddDevice(event) {
|
||||
|
||||
const name = document.getElementById('device-name').value.trim();
|
||||
const deviceType = document.getElementById('device-type')?.value || 'wled';
|
||||
const url = isSerialDevice(deviceType)
|
||||
? document.getElementById('device-serial-port').value
|
||||
: document.getElementById('device-url').value.trim();
|
||||
const error = document.getElementById('add-device-error');
|
||||
|
||||
if (!name || !url) {
|
||||
let url;
|
||||
if (isMockDevice(deviceType)) {
|
||||
const ledCount = document.getElementById('device-led-count')?.value || '60';
|
||||
url = `mock://${ledCount}`;
|
||||
} else if (isSerialDevice(deviceType)) {
|
||||
url = document.getElementById('device-serial-port').value;
|
||||
} else {
|
||||
url = document.getElementById('device-url').value.trim();
|
||||
}
|
||||
|
||||
if (!name || (!isMockDevice(deviceType) && !url)) {
|
||||
error.textContent = 'Please fill in all fields';
|
||||
error.style.display = 'block';
|
||||
return;
|
||||
@@ -308,6 +332,12 @@ export async function handleAddDevice(event) {
|
||||
if (isSerialDevice(deviceType) && baudRateSelect && baudRateSelect.value) {
|
||||
body.baud_rate = parseInt(baudRateSelect.value, 10);
|
||||
}
|
||||
if (isMockDevice(deviceType)) {
|
||||
const sendLatency = document.getElementById('device-send-latency')?.value;
|
||||
if (sendLatency) body.send_latency_ms = parseInt(sendLatency, 10);
|
||||
const ledType = document.getElementById('device-led-type')?.value;
|
||||
body.rgbw = ledType === 'rgbw';
|
||||
}
|
||||
const lastTemplateId = localStorage.getItem('lastCaptureTemplateId');
|
||||
if (lastTemplateId) {
|
||||
body.capture_template_id = lastTemplateId;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import {
|
||||
_deviceBrightnessCache, updateDeviceBrightness,
|
||||
} from '../core/state.js';
|
||||
import { API_BASE, getHeaders, fetchWithAuth, escapeHtml, isSerialDevice } from '../core/api.js';
|
||||
import { API_BASE, getHeaders, fetchWithAuth, escapeHtml, isSerialDevice, isMockDevice } from '../core/api.js';
|
||||
import { t } from '../core/i18n.js';
|
||||
import { showToast, showConfirm } from '../core/ui.js';
|
||||
import { Modal } from '../core/modal.js';
|
||||
@@ -23,10 +23,16 @@ class DeviceSettingsModal extends Modal {
|
||||
state_check_interval: this.$('settings-health-interval').value,
|
||||
auto_shutdown: this.$('settings-auto-shutdown').checked,
|
||||
led_count: this.$('settings-led-count').value,
|
||||
led_type: document.getElementById('settings-led-type')?.value || 'rgb',
|
||||
send_latency: document.getElementById('settings-send-latency')?.value || '0',
|
||||
};
|
||||
}
|
||||
|
||||
_getUrl() {
|
||||
if (isMockDevice(this.deviceType)) {
|
||||
const ledCount = this.$('settings-led-count')?.value || '60';
|
||||
return `mock://${ledCount}`;
|
||||
}
|
||||
if (isSerialDevice(this.deviceType)) {
|
||||
return this.$('settings-serial-port').value;
|
||||
}
|
||||
@@ -166,9 +172,14 @@ export async function showSettings(deviceId) {
|
||||
document.getElementById('settings-device-name').value = device.name;
|
||||
document.getElementById('settings-health-interval').value = 30;
|
||||
|
||||
const isMock = isMockDevice(device.device_type);
|
||||
const urlGroup = document.getElementById('settings-url-group');
|
||||
const serialGroup = document.getElementById('settings-serial-port-group');
|
||||
if (isAdalight) {
|
||||
if (isMock) {
|
||||
urlGroup.style.display = 'none';
|
||||
document.getElementById('settings-device-url').removeAttribute('required');
|
||||
serialGroup.style.display = 'none';
|
||||
} else if (isAdalight) {
|
||||
urlGroup.style.display = 'none';
|
||||
document.getElementById('settings-device-url').removeAttribute('required');
|
||||
serialGroup.style.display = '';
|
||||
@@ -202,6 +213,23 @@ export async function showSettings(deviceId) {
|
||||
baudRateGroup.style.display = 'none';
|
||||
}
|
||||
|
||||
// Mock-specific fields
|
||||
const ledTypeGroup = document.getElementById('settings-led-type-group');
|
||||
const sendLatencyGroup = document.getElementById('settings-send-latency-group');
|
||||
if (isMock) {
|
||||
if (ledTypeGroup) {
|
||||
ledTypeGroup.style.display = '';
|
||||
document.getElementById('settings-led-type').value = device.rgbw ? 'rgbw' : 'rgb';
|
||||
}
|
||||
if (sendLatencyGroup) {
|
||||
sendLatencyGroup.style.display = '';
|
||||
document.getElementById('settings-send-latency').value = device.send_latency_ms || 0;
|
||||
}
|
||||
} else {
|
||||
if (ledTypeGroup) ledTypeGroup.style.display = 'none';
|
||||
if (sendLatencyGroup) sendLatencyGroup.style.display = 'none';
|
||||
}
|
||||
|
||||
document.getElementById('settings-auto-shutdown').checked = !!device.auto_shutdown;
|
||||
settingsModal.snapshot();
|
||||
settingsModal.open();
|
||||
@@ -241,6 +269,12 @@ export async function saveDeviceSettings() {
|
||||
const baudVal = document.getElementById('settings-baud-rate').value;
|
||||
if (baudVal) body.baud_rate = parseInt(baudVal, 10);
|
||||
}
|
||||
if (isMockDevice(settingsModal.deviceType)) {
|
||||
const sendLatency = document.getElementById('settings-send-latency')?.value;
|
||||
if (sendLatency !== undefined) body.send_latency_ms = parseInt(sendLatency, 10);
|
||||
const ledType = document.getElementById('settings-led-type')?.value;
|
||||
body.rgbw = ledType === 'rgbw';
|
||||
}
|
||||
const deviceResponse = await fetchWithAuth(`/devices/${deviceId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(body)
|
||||
|
||||
@@ -122,6 +122,10 @@
|
||||
"device.led_count_manual.hint": "Number of LEDs on the strip (must match your Arduino sketch)",
|
||||
"device.baud_rate": "Baud Rate:",
|
||||
"device.baud_rate.hint": "Serial communication speed. Higher = more FPS but requires matching Arduino sketch.",
|
||||
"device.led_type": "LED Type:",
|
||||
"device.led_type.hint": "RGB (3 channels) or RGBW (4 channels with dedicated white)",
|
||||
"device.send_latency": "Send Latency (ms):",
|
||||
"device.send_latency.hint": "Simulated network/serial delay per frame in milliseconds",
|
||||
"device.url.hint": "IP address or hostname of the device (e.g. http://192.168.1.100)",
|
||||
"device.name": "Device Name:",
|
||||
"device.name.placeholder": "Living Room TV",
|
||||
|
||||
@@ -122,6 +122,10 @@
|
||||
"device.led_count_manual.hint": "Количество светодиодов на ленте (должно совпадать с вашим скетчем Arduino)",
|
||||
"device.baud_rate": "Скорость порта:",
|
||||
"device.baud_rate.hint": "Скорость серийного соединения. Выше = больше FPS, но требует соответствия скетчу Arduino.",
|
||||
"device.led_type": "Тип LED:",
|
||||
"device.led_type.hint": "RGB (3 канала) или RGBW (4 канала с выделенным белым)",
|
||||
"device.send_latency": "Задержка отправки (мс):",
|
||||
"device.send_latency.hint": "Имитация сетевой/серийной задержки на кадр в миллисекундах",
|
||||
"device.url.hint": "IP адрес или имя хоста устройства (напр. http://192.168.1.100)",
|
||||
"device.name": "Имя Устройства:",
|
||||
"device.name.placeholder": "ТВ в Гостиной",
|
||||
|
||||
@@ -30,6 +30,8 @@ class Device:
|
||||
baud_rate: Optional[int] = None,
|
||||
software_brightness: int = 255,
|
||||
auto_shutdown: bool = False,
|
||||
send_latency_ms: int = 0,
|
||||
rgbw: bool = False,
|
||||
created_at: Optional[datetime] = None,
|
||||
updated_at: Optional[datetime] = None,
|
||||
):
|
||||
@@ -42,6 +44,8 @@ class Device:
|
||||
self.baud_rate = baud_rate
|
||||
self.software_brightness = software_brightness
|
||||
self.auto_shutdown = auto_shutdown
|
||||
self.send_latency_ms = send_latency_ms
|
||||
self.rgbw = rgbw
|
||||
self.created_at = created_at or datetime.utcnow()
|
||||
self.updated_at = updated_at or datetime.utcnow()
|
||||
# Preserved from old JSON for migration — not written back
|
||||
@@ -65,6 +69,10 @@ class Device:
|
||||
d["software_brightness"] = self.software_brightness
|
||||
if self.auto_shutdown:
|
||||
d["auto_shutdown"] = True
|
||||
if self.send_latency_ms:
|
||||
d["send_latency_ms"] = self.send_latency_ms
|
||||
if self.rgbw:
|
||||
d["rgbw"] = True
|
||||
return d
|
||||
|
||||
@classmethod
|
||||
@@ -84,6 +92,8 @@ class Device:
|
||||
baud_rate=data.get("baud_rate"),
|
||||
software_brightness=data.get("software_brightness", 255),
|
||||
auto_shutdown=data.get("auto_shutdown", False),
|
||||
send_latency_ms=data.get("send_latency_ms", 0),
|
||||
rgbw=data.get("rgbw", False),
|
||||
created_at=datetime.fromisoformat(data.get("created_at", datetime.utcnow().isoformat())),
|
||||
updated_at=datetime.fromisoformat(data.get("updated_at", datetime.utcnow().isoformat())),
|
||||
)
|
||||
@@ -180,6 +190,8 @@ class DeviceStore:
|
||||
device_type: str = "wled",
|
||||
baud_rate: Optional[int] = None,
|
||||
auto_shutdown: bool = False,
|
||||
send_latency_ms: int = 0,
|
||||
rgbw: bool = False,
|
||||
) -> Device:
|
||||
"""Create a new device."""
|
||||
device_id = f"device_{uuid.uuid4().hex[:8]}"
|
||||
@@ -192,6 +204,8 @@ class DeviceStore:
|
||||
device_type=device_type,
|
||||
baud_rate=baud_rate,
|
||||
auto_shutdown=auto_shutdown,
|
||||
send_latency_ms=send_latency_ms,
|
||||
rgbw=rgbw,
|
||||
)
|
||||
|
||||
self._devices[device_id] = device
|
||||
@@ -217,6 +231,8 @@ class DeviceStore:
|
||||
enabled: Optional[bool] = None,
|
||||
baud_rate: Optional[int] = None,
|
||||
auto_shutdown: Optional[bool] = None,
|
||||
send_latency_ms: Optional[int] = None,
|
||||
rgbw: Optional[bool] = None,
|
||||
) -> Device:
|
||||
"""Update device."""
|
||||
device = self._devices.get(device_id)
|
||||
@@ -235,6 +251,10 @@ class DeviceStore:
|
||||
device.baud_rate = baud_rate
|
||||
if auto_shutdown is not None:
|
||||
device.auto_shutdown = auto_shutdown
|
||||
if send_latency_ms is not None:
|
||||
device.send_latency_ms = send_latency_ms
|
||||
if rgbw is not None:
|
||||
device.rgbw = rgbw
|
||||
|
||||
device.updated_at = datetime.utcnow()
|
||||
self.save()
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
<option value="wled">WLED</option>
|
||||
<option value="adalight">Adalight</option>
|
||||
<option value="ambiled">AmbiLED</option>
|
||||
<option value="mock">Mock</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@@ -78,6 +79,25 @@
|
||||
</select>
|
||||
<small id="baud-fps-hint" class="fps-hint" style="display:none"></small>
|
||||
</div>
|
||||
<div class="form-group" id="device-led-type-group" style="display: none;">
|
||||
<div class="label-row">
|
||||
<label for="device-led-type" data-i18n="device.led_type">LED Type:</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="device.led_type.hint">RGB (3 channels) or RGBW (4 channels with dedicated white)</small>
|
||||
<select id="device-led-type">
|
||||
<option value="rgb">RGB</option>
|
||||
<option value="rgbw">RGBW</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" id="device-send-latency-group" style="display: none;">
|
||||
<div class="label-row">
|
||||
<label for="device-send-latency" data-i18n="device.send_latency">Send Latency (ms):</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="device.send_latency.hint">Simulated network/serial delay per frame in milliseconds</small>
|
||||
<input type="number" id="device-send-latency" min="0" max="5000" value="0">
|
||||
</div>
|
||||
<div id="add-device-error" class="error-message" style="display: none;"></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -58,6 +58,26 @@
|
||||
<small id="settings-baud-fps-hint" class="fps-hint" style="display:none"></small>
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="settings-led-type-group" style="display: none;">
|
||||
<div class="label-row">
|
||||
<label for="settings-led-type" data-i18n="device.led_type">LED Type:</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="device.led_type.hint">RGB (3 channels) or RGBW (4 channels with dedicated white)</small>
|
||||
<select id="settings-led-type">
|
||||
<option value="rgb">RGB</option>
|
||||
<option value="rgbw">RGBW</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" id="settings-send-latency-group" style="display: none;">
|
||||
<div class="label-row">
|
||||
<label for="settings-send-latency" data-i18n="device.send_latency">Send Latency (ms):</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="device.send_latency.hint">Simulated network/serial delay per frame in milliseconds</small>
|
||||
<input type="number" id="settings-send-latency" min="0" max="5000" value="0">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
<label for="settings-health-interval" data-i18n="settings.health_interval">Health Check Interval (s):</label>
|
||||
|
||||
Reference in New Issue
Block a user