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
@@ -488,6 +488,9 @@ export function createHALightTargetCard(target: any, haSourceMap: Record<string,
<div class="metric-value" data-tm="ha-status">${state.ha_connected ? ICON_OK : ICON_WARNING}</div>
</div>
</div>
<div class="ha-light-swatches" data-ha-swatches="${target.id}">
${_renderEntitySwatches(state.entity_colors || {}, target.ha_light_mappings || [])}
</div>
` : ''}
</div>`,
actions: `
@@ -560,6 +563,78 @@ export function initHALightTargetDelegation(container: HTMLElement): void {
});
}
// ── Entity color swatches ──
function _renderEntitySwatches(entityColors: Record<string, any>, mappings: any[]): string {
if (!mappings.length) return '';
return mappings.map(m => {
const c = entityColors[m.entity_id];
const bg = c ? c.hex : '#333';
const label = m.entity_id.replace('light.', '');
return `<div class="ha-light-swatch" data-entity="${escapeHtml(m.entity_id)}">
<span class="swatch-color" style="background:${bg}"></span>
<span class="swatch-label">${escapeHtml(label)}</span>
</div>`;
}).join('');
}
// ── WebSocket color preview ──
const _haLightWS: Record<string, WebSocket> = {};
export function connectHALightWS(targetId: string): void {
if (_haLightWS[targetId]) return;
const loc = window.location;
const wsProto = loc.protocol === 'https:' ? 'wss:' : 'ws:';
const apiKey = (window as any).apiKey || localStorage.getItem('wled_api_key') || '';
const url = `${wsProto}//${loc.host}/api/v1/output-targets/${targetId}/ha-light/ws?token=${encodeURIComponent(apiKey)}`;
const ws = new WebSocket(url);
_haLightWS[targetId] = ws;
ws.onmessage = (ev) => {
try {
const data = JSON.parse(ev.data);
if (data.type === 'colors_update') {
_updateSwatchColors(targetId, data.colors);
}
} catch {}
};
ws.onclose = () => {
delete _haLightWS[targetId];
};
ws.onerror = () => {
delete _haLightWS[targetId];
};
}
export function disconnectHALightWS(targetId: string): void {
const ws = _haLightWS[targetId];
if (ws) {
ws.close();
delete _haLightWS[targetId];
}
}
export function disconnectAllHALightWS(): void {
for (const id of Object.keys(_haLightWS)) {
disconnectHALightWS(id);
}
}
function _updateSwatchColors(targetId: string, colors: Record<string, any>): void {
const container = document.querySelector(`[data-ha-swatches="${targetId}"]`);
if (!container) return;
for (const [entityId, c] of Object.entries(colors)) {
const swatch = container.querySelector(`[data-entity="${entityId}"] .swatch-color`) as HTMLElement | null;
if (swatch) {
swatch.style.background = (c as any).hex;
}
}
}
// ── Expose to global scope ──
window.showHALightEditor = showHALightEditor;