Add WebSocket device type, capability-driven settings, hide filter on collapse
- New WS device type: broadcaster singleton + LEDClient that sends binary
frames to connected WebSocket clients during processing
- FastAPI WS endpoint at /api/v1/devices/{device_id}/ws with token auth
- Frontend: add/edit WS devices, connection URL with copy button in settings
- Add health_check and auto_restore capabilities to WLED and Serial providers;
hide health interval and auto-restore toggle for devices without them
- Skip health check loop for virtual devices (Mock, MQTT, WS) — set always-online
- Copy buttons and labels for API CSS push endpoints (REST POST / WebSocket)
- Hide mock:// and ws:// URLs in target device dropdown
- Hide filter textbox when card section is collapsed (cs-collapsed CSS class)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1080,12 +1080,26 @@ function _showApiInputEndpoints(cssId) {
|
||||
const base = `${window.location.origin}/api/v1`;
|
||||
const wsProto = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsBase = `${wsProto}//${window.location.host}/api/v1`;
|
||||
const restUrl = `${base}/color-strip-sources/${cssId}/colors`;
|
||||
const apiKey = localStorage.getItem('wled_api_key') || '';
|
||||
const wsUrl = `${wsBase}/color-strip-sources/${cssId}/ws?token=${encodeURIComponent(apiKey)}`;
|
||||
el.innerHTML = `
|
||||
<div style="margin-bottom:4px"><strong>REST POST:</strong><br>${base}/color-strip-sources/${cssId}/colors</div>
|
||||
<div><strong>WebSocket:</strong><br>${wsBase}/color-strip-sources/${cssId}/ws?token=<api_key></div>
|
||||
<small class="endpoint-label">REST POST</small>
|
||||
<div class="ws-url-row" style="margin-bottom:6px"><input type="text" value="${restUrl}" readonly style="font-size:0.85em"><button type="button" class="btn btn-sm btn-secondary" onclick="copyEndpointUrl(this)" title="Copy">📋</button></div>
|
||||
<small class="endpoint-label">WebSocket</small>
|
||||
<div class="ws-url-row"><input type="text" value="${wsUrl}" readonly style="font-size:0.85em"><button type="button" class="btn btn-sm btn-secondary" onclick="copyEndpointUrl(this)" title="Copy">📋</button></div>
|
||||
`;
|
||||
}
|
||||
|
||||
export function copyEndpointUrl(btn) {
|
||||
const input = btn.parentElement.querySelector('input');
|
||||
if (input && input.value) {
|
||||
navigator.clipboard.writeText(input.value).then(() => {
|
||||
showToast(t('settings.copied') || 'Copied!', 'success');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Clone ────────────────────────────────────────────────────── */
|
||||
|
||||
export async function cloneColorStrip(cssId) {
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
_discoveryScanRunning, set_discoveryScanRunning,
|
||||
_discoveryCache, set_discoveryCache,
|
||||
} from '../core/state.js';
|
||||
import { API_BASE, fetchWithAuth, isSerialDevice, isMockDevice, isMqttDevice, escapeHtml } from '../core/api.js';
|
||||
import { API_BASE, fetchWithAuth, isSerialDevice, isMockDevice, isMqttDevice, isWsDevice, escapeHtml } from '../core/api.js';
|
||||
import { t } from '../core/i18n.js';
|
||||
import { showToast } from '../core/ui.js';
|
||||
import { Modal } from '../core/modal.js';
|
||||
@@ -76,6 +76,17 @@ export function onDeviceTypeChanged() {
|
||||
if (sendLatencyGroup) sendLatencyGroup.style.display = '';
|
||||
if (discoverySection) discoverySection.style.display = 'none';
|
||||
if (scanBtn) scanBtn.style.display = 'none';
|
||||
} else if (isWsDevice(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 = 'none';
|
||||
if (sendLatencyGroup) sendLatencyGroup.style.display = 'none';
|
||||
if (discoverySection) discoverySection.style.display = 'none';
|
||||
if (scanBtn) scanBtn.style.display = 'none';
|
||||
} else if (isSerialDevice(deviceType)) {
|
||||
urlGroup.style.display = 'none';
|
||||
urlInput.removeAttribute('required');
|
||||
@@ -338,6 +349,8 @@ export async function handleAddDevice(event) {
|
||||
let url;
|
||||
if (isMockDevice(deviceType)) {
|
||||
url = 'mock://';
|
||||
} else if (isWsDevice(deviceType)) {
|
||||
url = 'ws://';
|
||||
} else if (isSerialDevice(deviceType)) {
|
||||
url = document.getElementById('device-serial-port').value;
|
||||
} else {
|
||||
@@ -349,7 +362,7 @@ export async function handleAddDevice(event) {
|
||||
url = 'mqtt://' + url;
|
||||
}
|
||||
|
||||
if (!name || (!isMockDevice(deviceType) && !url)) {
|
||||
if (!name || (!isMockDevice(deviceType) && !isWsDevice(deviceType) && !url)) {
|
||||
error.textContent = t('device_discovery.error.fill_all_fields');
|
||||
error.style.display = 'block';
|
||||
return;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import {
|
||||
_deviceBrightnessCache, updateDeviceBrightness,
|
||||
} from '../core/state.js';
|
||||
import { API_BASE, getHeaders, fetchWithAuth, escapeHtml, isSerialDevice, isMockDevice, isMqttDevice } from '../core/api.js';
|
||||
import { API_BASE, getHeaders, fetchWithAuth, escapeHtml, isSerialDevice, isMockDevice, isMqttDevice, isWsDevice } from '../core/api.js';
|
||||
import { t } from '../core/i18n.js';
|
||||
import { showToast, showConfirm } from '../core/ui.js';
|
||||
import { Modal } from '../core/modal.js';
|
||||
@@ -34,6 +34,10 @@ class DeviceSettingsModal extends Modal {
|
||||
const deviceId = this.$('settings-device-id')?.value || '';
|
||||
return `mock://${deviceId}`;
|
||||
}
|
||||
if (isWsDevice(this.deviceType)) {
|
||||
const deviceId = this.$('settings-device-id')?.value || '';
|
||||
return `ws://${deviceId}`;
|
||||
}
|
||||
if (isSerialDevice(this.deviceType)) {
|
||||
return this.$('settings-serial-port').value;
|
||||
}
|
||||
@@ -83,7 +87,7 @@ export function createDeviceCard(device) {
|
||||
<div class="card-title">
|
||||
<span class="health-dot ${healthClass}" title="${healthTitle}"></span>
|
||||
${device.name || device.id}
|
||||
${device.url && device.url.startsWith('http') ? `<a class="device-url-badge" href="${device.url}" target="_blank" rel="noopener" title="${t('device.button.webui')}"><span class="device-url-text">${escapeHtml(device.url.replace(/^https?:\/\//, ''))}</span><span class="device-url-icon">${ICON_WEB}</span></a>` : (device.url && !device.url.startsWith('mock://') && !device.url.startsWith('http') ? `<span class="device-url-badge"><span class="device-url-text">${escapeHtml(device.url)}</span></span>` : '')}
|
||||
${device.url && device.url.startsWith('http') ? `<a class="device-url-badge" href="${device.url}" target="_blank" rel="noopener" title="${t('device.button.webui')}"><span class="device-url-text">${escapeHtml(device.url.replace(/^https?:\/\//, ''))}</span><span class="device-url-icon">${ICON_WEB}</span></a>` : (device.url && !device.url.startsWith('mock://') && !device.url.startsWith('ws://') && !device.url.startsWith('http') ? `<span class="device-url-badge"><span class="device-url-text">${escapeHtml(device.url)}</span></span>` : '')}
|
||||
${healthLabel}
|
||||
</div>
|
||||
</div>
|
||||
@@ -174,13 +178,14 @@ export async function showSettings(deviceId) {
|
||||
document.getElementById('settings-health-interval').value = device.state_check_interval ?? 30;
|
||||
|
||||
const isMock = isMockDevice(device.device_type);
|
||||
const isWs = isWsDevice(device.device_type);
|
||||
const isMqtt = isMqttDevice(device.device_type);
|
||||
const urlGroup = document.getElementById('settings-url-group');
|
||||
const serialGroup = document.getElementById('settings-serial-port-group');
|
||||
const urlLabel = urlGroup.querySelector('label[for="settings-device-url"]');
|
||||
const urlHint = urlGroup.querySelector('.input-hint');
|
||||
const urlInput = document.getElementById('settings-device-url');
|
||||
if (isMock) {
|
||||
if (isMock || isWs) {
|
||||
urlGroup.style.display = 'none';
|
||||
urlInput.removeAttribute('required');
|
||||
serialGroup.style.display = 'none';
|
||||
@@ -245,6 +250,31 @@ export async function showSettings(deviceId) {
|
||||
if (sendLatencyGroup) sendLatencyGroup.style.display = 'none';
|
||||
}
|
||||
|
||||
// WS connection URL
|
||||
const wsUrlGroup = document.getElementById('settings-ws-url-group');
|
||||
if (wsUrlGroup) {
|
||||
if (isWs) {
|
||||
const wsProto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const apiKey = localStorage.getItem('wled_api_key') || '';
|
||||
const wsUrl = `${wsProto}//${location.host}/api/v1/devices/${device.id}/ws?token=${encodeURIComponent(apiKey)}`;
|
||||
document.getElementById('settings-ws-url').value = wsUrl;
|
||||
wsUrlGroup.style.display = '';
|
||||
} else {
|
||||
wsUrlGroup.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Hide health check for devices without health_check capability
|
||||
const healthIntervalGroup = document.getElementById('settings-health-interval-group');
|
||||
if (healthIntervalGroup) {
|
||||
healthIntervalGroup.style.display = caps.includes('health_check') ? '' : 'none';
|
||||
}
|
||||
|
||||
// Hide auto-restore for devices without auto_restore capability
|
||||
const autoShutdownGroup = document.getElementById('settings-auto-shutdown-group');
|
||||
if (autoShutdownGroup) {
|
||||
autoShutdownGroup.style.display = caps.includes('auto_restore') ? '' : 'none';
|
||||
}
|
||||
document.getElementById('settings-auto-shutdown').checked = !!device.auto_shutdown;
|
||||
settingsModal.snapshot();
|
||||
settingsModal.open();
|
||||
@@ -442,6 +472,15 @@ async function _populateSettingsSerialPorts(currentUrl) {
|
||||
}
|
||||
}
|
||||
|
||||
export function copyWsUrl() {
|
||||
const input = document.getElementById('settings-ws-url');
|
||||
if (input && input.value) {
|
||||
navigator.clipboard.writeText(input.value).then(() => {
|
||||
showToast(t('settings.copied') || 'Copied!', 'success');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadDevices() {
|
||||
await window.loadTargetsTab();
|
||||
}
|
||||
|
||||
@@ -259,7 +259,7 @@ export async function showTargetEditor(targetId = null, cloneData = null) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = d.id;
|
||||
opt.dataset.name = d.name;
|
||||
const shortUrl = d.url ? d.url.replace(/^https?:\/\//, '') : '';
|
||||
const shortUrl = d.url && d.url.startsWith('http') ? d.url.replace(/^https?:\/\//, '') : '';
|
||||
const devType = (d.device_type || 'wled').toUpperCase();
|
||||
opt.textContent = `${d.name} [${devType}]${shortUrl ? ' (' + shortUrl + ')' : ''}`;
|
||||
deviceSelect.appendChild(opt);
|
||||
|
||||
Reference in New Issue
Block a user