Add static color support, HAOS light entity, and real-time profile updates
- Add static_color capability to WLED and serial providers with native set_color() dispatch (WLED uses JSON API, serial uses idle client) - Encapsulate device-specific logic in providers instead of device_type checks in ProcessorManager and API routes - Add HAOS light entity for devices with brightness_control + static_color (Adalight/AmbiLED get light entity, WLED keeps number entity) - Fix serial device brightness and turn-off: pass software_brightness through provider chain, clear device on color=null, re-send static color after brightness change - Add global events WebSocket (events-ws.js) replacing per-tab WS, enabling real-time profile state updates on both dashboard and profiles tabs - Fix profile activation: mark active when all targets already running, add asyncio.Lock to prevent concurrent evaluation races, skip process enumeration when no profile has conditions, trigger immediate evaluation on enable/create/update for instant target startup - Add reliable server restart script (restart.ps1) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -35,10 +35,11 @@ import {
|
||||
updateSettingsBaudFpsHint,
|
||||
} from './features/devices.js';
|
||||
import {
|
||||
loadDashboard, startDashboardWS, stopDashboardWS, stopUptimeTimer,
|
||||
loadDashboard, stopUptimeTimer,
|
||||
dashboardToggleProfile, dashboardStartTarget, dashboardStopTarget, dashboardStopAll,
|
||||
toggleDashboardSection, changeDashboardPollInterval,
|
||||
} from './features/dashboard.js';
|
||||
import { startEventsWS, stopEventsWS } from './core/events-ws.js';
|
||||
import {
|
||||
startPerfPolling, stopPerfPolling,
|
||||
} from './features/perf-charts.js';
|
||||
@@ -150,8 +151,6 @@ Object.assign(window, {
|
||||
|
||||
// dashboard
|
||||
loadDashboard,
|
||||
startDashboardWS,
|
||||
stopDashboardWS,
|
||||
dashboardToggleProfile,
|
||||
dashboardStartTarget,
|
||||
dashboardStopTarget,
|
||||
@@ -301,6 +300,7 @@ window.addEventListener('beforeunload', () => {
|
||||
if (refreshInterval) {
|
||||
clearInterval(refreshInterval);
|
||||
}
|
||||
stopEventsWS();
|
||||
disconnectAllKCWebSockets();
|
||||
});
|
||||
|
||||
@@ -337,6 +337,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
loadDisplays();
|
||||
loadTargetsTab();
|
||||
|
||||
// Start auto-refresh
|
||||
// Start global events WebSocket and auto-refresh
|
||||
startEventsWS();
|
||||
startAutoRefresh();
|
||||
});
|
||||
|
||||
48
server/src/wled_controller/static/js/core/events-ws.js
Normal file
48
server/src/wled_controller/static/js/core/events-ws.js
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Global events WebSocket — stays connected while logged in,
|
||||
* dispatches DOM custom events that feature modules can listen to.
|
||||
*
|
||||
* Events dispatched: server:state_change, server:profile_state_changed
|
||||
*/
|
||||
|
||||
import { apiKey } from './state.js';
|
||||
|
||||
let _ws = null;
|
||||
let _reconnectTimer = null;
|
||||
|
||||
export function startEventsWS() {
|
||||
stopEventsWS();
|
||||
if (!apiKey) return;
|
||||
|
||||
const wsProto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const url = `${wsProto}//${location.host}/api/v1/events/ws?token=${apiKey}`;
|
||||
|
||||
try {
|
||||
_ws = new WebSocket(url);
|
||||
_ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
document.dispatchEvent(new CustomEvent(`server:${data.type}`, { detail: data }));
|
||||
} catch {}
|
||||
};
|
||||
_ws.onclose = () => {
|
||||
_ws = null;
|
||||
_reconnectTimer = setTimeout(startEventsWS, 3000);
|
||||
};
|
||||
_ws.onerror = () => {};
|
||||
} catch {
|
||||
_ws = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function stopEventsWS() {
|
||||
if (_reconnectTimer) {
|
||||
clearTimeout(_reconnectTimer);
|
||||
_reconnectTimer = null;
|
||||
}
|
||||
if (_ws) {
|
||||
_ws.onclose = null;
|
||||
_ws.close();
|
||||
_ws = null;
|
||||
}
|
||||
}
|
||||
@@ -18,9 +18,6 @@ export function setKcTestAutoRefresh(v) { kcTestAutoRefresh = v; }
|
||||
export let kcTestTargetId = null;
|
||||
export function setKcTestTargetId(v) { kcTestTargetId = v; }
|
||||
|
||||
export let _dashboardWS = null;
|
||||
export function set_dashboardWS(v) { _dashboardWS = v; }
|
||||
|
||||
export let _cachedDisplays = null;
|
||||
export function set_cachedDisplays(v) { _cachedDisplays = v; }
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Dashboard — real-time target status overview.
|
||||
*/
|
||||
|
||||
import { apiKey, _dashboardWS, set_dashboardWS, _dashboardLoading, set_dashboardLoading, dashboardPollInterval, setDashboardPollInterval } from '../core/state.js';
|
||||
import { apiKey, _dashboardLoading, set_dashboardLoading, dashboardPollInterval, setDashboardPollInterval } from '../core/state.js';
|
||||
import { API_BASE, getHeaders, fetchWithAuth } from '../core/api.js';
|
||||
import { t } from '../core/i18n.js';
|
||||
import { escapeHtml, handle401Error } from '../core/api.js';
|
||||
@@ -239,7 +239,7 @@ function formatUptime(seconds) {
|
||||
return `${s}s`;
|
||||
}
|
||||
|
||||
export async function loadDashboard() {
|
||||
export async function loadDashboard(forceFullRender = false) {
|
||||
if (_dashboardLoading) return;
|
||||
set_dashboardLoading(true);
|
||||
const container = document.getElementById('dashboard-content');
|
||||
@@ -289,7 +289,7 @@ export async function loadDashboard() {
|
||||
const newRunningIds = running.map(t => t.id).sort().join(',');
|
||||
const prevRunningIds = [..._lastRunningIds].sort().join(',');
|
||||
const hasExistingDom = !!container.querySelector('.dashboard-perf-persistent');
|
||||
if (hasExistingDom && newRunningIds === prevRunningIds && newRunningIds !== '') {
|
||||
if (!forceFullRender && hasExistingDom && newRunningIds === prevRunningIds && newRunningIds !== '') {
|
||||
_updateRunningMetrics(running);
|
||||
set_dashboardLoading(false);
|
||||
return;
|
||||
@@ -567,32 +567,23 @@ export async function dashboardStopAll() {
|
||||
}
|
||||
}
|
||||
|
||||
export function startDashboardWS() {
|
||||
stopDashboardWS();
|
||||
if (!apiKey) return;
|
||||
const wsProto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const url = `${wsProto}//${location.host}/api/v1/events/ws?token=${apiKey}`;
|
||||
try {
|
||||
set_dashboardWS(new WebSocket(url));
|
||||
_dashboardWS.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.type === 'state_change' || data.type === 'profile_state_changed') {
|
||||
loadDashboard();
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
_dashboardWS.onclose = () => { set_dashboardWS(null); };
|
||||
_dashboardWS.onerror = () => { set_dashboardWS(null); };
|
||||
} catch {
|
||||
set_dashboardWS(null);
|
||||
}
|
||||
}
|
||||
|
||||
export function stopUptimeTimer() {
|
||||
_stopUptimeTimer();
|
||||
}
|
||||
|
||||
// React to global server events when dashboard tab is active
|
||||
function _isDashboardActive() {
|
||||
return (localStorage.getItem('activeTab') || 'dashboard') === 'dashboard';
|
||||
}
|
||||
|
||||
document.addEventListener('server:state_change', () => {
|
||||
if (_isDashboardActive()) loadDashboard();
|
||||
});
|
||||
|
||||
document.addEventListener('server:profile_state_changed', () => {
|
||||
if (_isDashboardActive()) loadDashboard(true);
|
||||
});
|
||||
|
||||
// Re-render dashboard when language changes
|
||||
document.addEventListener('languageChanged', () => {
|
||||
if (!apiKey) return;
|
||||
@@ -601,10 +592,3 @@ document.addEventListener('languageChanged', () => {
|
||||
if (perfEl) perfEl.remove();
|
||||
loadDashboard();
|
||||
});
|
||||
|
||||
export function stopDashboardWS() {
|
||||
if (_dashboardWS) {
|
||||
_dashboardWS.close();
|
||||
set_dashboardWS(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,13 @@ const profileModal = new Modal('profile-editor-modal');
|
||||
// Re-render profiles when language changes
|
||||
document.addEventListener('languageChanged', () => { if (apiKey) loadProfiles(); });
|
||||
|
||||
// React to real-time profile state changes from global events WS
|
||||
document.addEventListener('server:profile_state_changed', () => {
|
||||
if (apiKey && (localStorage.getItem('activeTab') || 'dashboard') === 'profiles') {
|
||||
loadProfiles();
|
||||
}
|
||||
});
|
||||
|
||||
export async function loadProfiles() {
|
||||
const container = document.getElementById('profiles-content');
|
||||
if (!container) return;
|
||||
|
||||
@@ -11,9 +11,7 @@ export function switchTab(name) {
|
||||
if (name === 'dashboard') {
|
||||
// Use window.* to avoid circular imports with feature modules
|
||||
if (apiKey && typeof window.loadDashboard === 'function') window.loadDashboard();
|
||||
if (apiKey && typeof window.startDashboardWS === 'function') window.startDashboardWS();
|
||||
} else {
|
||||
if (typeof window.stopDashboardWS === 'function') window.stopDashboardWS();
|
||||
if (typeof window.stopPerfPolling === 'function') window.stopPerfPolling();
|
||||
if (typeof window.stopUptimeTimer === 'function') window.stopUptimeTimer();
|
||||
if (!apiKey) return;
|
||||
|
||||
Reference in New Issue
Block a user