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

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