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

@@ -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;

View File

@@ -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)