feat: HA light target live color preview — per-entity swatches via WebSocket
Lint & Test / test (push) Successful in 1m24s

- Cache per-entity colors in HALightTargetProcessor._update_lights()
- Broadcast colors_update to WS clients at target's update_rate
- WS endpoint: /api/v1/output-targets/{target_id}/ha-light/ws
- Frontend: connect WS when target runs, update swatch colors live
- Card shows colored boxes per mapped entity with entity name labels
This commit is contained in:
2026-03-28 18:28:16 +03:00
parent 381ee75371
commit 40751fecb7
31 changed files with 6245 additions and 8351 deletions
@@ -18,7 +18,7 @@ import { showToast, showConfirm, formatUptime, formatCompact, setTabRefreshing,
import { Modal } from '../core/modal.ts';
import { createDeviceCard, attachDeviceListeners, fetchDeviceBrightness, enrichOpenrgbZoneBadges, _computeMaxFps, getZoneCountCache, formatRelativeTime } from './devices.ts';
import { _splitOpenrgbZone } from './device-discovery.ts';
import { createHALightTargetCard, initHALightTargetDelegation, patchHALightTargetMetrics } from './ha-light-targets.ts';
import { createHALightTargetCard, initHALightTargetDelegation, patchHALightTargetMetrics, connectHALightWS, disconnectHALightWS } from './ha-light-targets.ts';
import {
getValueSourceIcon, getTargetTypeIcon, getDeviceTypeIcon, getColorStripIcon,
ICON_CLONE, ICON_EDIT, ICON_START, ICON_STOP,
@@ -616,21 +616,12 @@ export async function loadTargetsTab() {
const devicesWithState = devices.map(d => ({ ...d, state: allDeviceStates[d.id] || {} }));
// Enrich targets with state/metrics; fetch colors only for running KC targets
const targetsWithState = await Promise.all(
targets.map(async (target) => {
const state = allTargetStates[target.id] || {};
const metrics = allTargetMetrics[target.id] || {};
let latestColors = null;
if (target.target_type === 'key_colors' && state.processing) {
try {
const colorsResp = await fetch(`${API_BASE}/output-targets/${target.id}/colors`, { headers: getHeaders() });
if (colorsResp.ok) latestColors = await colorsResp.json();
} catch {}
}
return { ...target, state, metrics, latestColors };
})
);
// Enrich targets with state/metrics
const targetsWithState = targets.map((target) => {
const state = allTargetStates[target.id] || {};
const metrics = allTargetMetrics[target.id] || {};
return { ...target, state, metrics };
});
// Build device map for target name resolution
const deviceMap = {};
@@ -769,6 +760,15 @@ export async function loadTargetsTab() {
if (!processingLedIds.has(id)) disconnectLedPreviewWS(id);
});
// Manage HA light color preview WebSockets
haLightTargets.forEach(target => {
if (target.state && target.state.processing) {
connectHALightWS(target.id);
} else {
disconnectHALightWS(target.id);
}
});
// FPS charts: only destroy charts for replaced/removed cards (or all on first render)
if (changedTargetIds) {
// Incremental: destroy only charts whose cards were replaced or removed