diff --git a/server/src/wled_controller/static/js/features/ha-light-targets.ts b/server/src/wled_controller/static/js/features/ha-light-targets.ts index e78ef8d..1e51bf3 100644 --- a/server/src/wled_controller/static/js/features/ha-light-targets.ts +++ b/server/src/wled_controller/static/js/features/ha-light-targets.ts @@ -7,7 +7,7 @@ import { fetchWithAuth, escapeHtml } from '../core/api.ts'; import { t } from '../core/i18n.ts'; import { Modal } from '../core/modal.ts'; import { showToast, showConfirm } from '../core/ui.ts'; -import { ICON_CLONE, ICON_EDIT, ICON_START, ICON_STOP } from '../core/icons.ts'; +import { ICON_CLONE, ICON_EDIT, ICON_START, ICON_STOP, ICON_TRASH } from '../core/icons.ts'; import * as P from '../core/icon-paths.ts'; import { EntitySelect } from '../core/entity-palette.ts'; import { wrapCard } from '../core/card-colors.ts'; @@ -22,7 +22,9 @@ const _icon = (d: string) => `${d}`; let _haLightTagsInput: TagInput | null = null; let _haSourceEntitySelect: EntitySelect | null = null; let _cssSourceEntitySelect: EntitySelect | null = null; +let _mappingEntitySelects: EntitySelect[] = []; let _editorCssSources: any[] = []; +let _cachedHAEntities: any[] = []; // fetched from selected HA source class HALightEditorModal extends Modal { constructor() { super('ha-light-editor-modal'); } @@ -31,6 +33,7 @@ class HALightEditorModal extends Modal { if (_haLightTagsInput) { _haLightTagsInput.destroy(); _haLightTagsInput = null; } if (_haSourceEntitySelect) { _haSourceEntitySelect.destroy(); _haSourceEntitySelect = null; } if (_cssSourceEntitySelect) { _cssSourceEntitySelect.destroy(); _cssSourceEntitySelect = null; } + _destroyMappingEntitySelects(); } snapshotValues() { @@ -48,12 +51,17 @@ class HALightEditorModal extends Modal { const haLightEditorModal = new HALightEditorModal(); +function _destroyMappingEntitySelects(): void { + for (const es of _mappingEntitySelects) es.destroy(); + _mappingEntitySelects = []; +} + function _getMappingsJSON(): string { const rows = document.querySelectorAll('#ha-light-mappings-list .ha-light-mapping-row'); const mappings: any[] = []; rows.forEach(row => { mappings.push({ - entity_id: (row.querySelector('.ha-mapping-entity') as HTMLInputElement).value.trim(), + entity_id: (row.querySelector('.ha-mapping-entity') as HTMLSelectElement).value.trim(), led_start: parseInt((row.querySelector('.ha-mapping-led-start') as HTMLInputElement).value) || 0, led_end: parseInt((row.querySelector('.ha-mapping-led-end') as HTMLInputElement).value) || -1, brightness_scale: parseFloat((row.querySelector('.ha-mapping-brightness') as HTMLInputElement).value) || 1.0, @@ -62,36 +70,134 @@ function _getMappingsJSON(): string { return JSON.stringify(mappings); } +function _getEntityItems() { + return _cachedHAEntities + .filter((e: any) => e.domain === 'light') + .map((e: any) => ({ + value: e.entity_id, + label: e.friendly_name || e.entity_id, + icon: _icon(P.lightbulb), + desc: e.state || '', + })); +} + +async function _fetchHAEntities(haSourceId: string): Promise { + if (!haSourceId) { _cachedHAEntities = []; return; } + try { + const resp = await fetchWithAuth(`/home-assistant/sources/${haSourceId}/entities`); + if (!resp.ok) { _cachedHAEntities = []; return; } + const data = await resp.json(); + _cachedHAEntities = data.entities || []; + } catch { + _cachedHAEntities = []; + } +} + +function _rebuildMappingEntityOptions(): void { + // Update all existing mapping entity +
+ ${_icon(P.lightbulb)} #${idx} +
-
-
- - +
+
+ +
-
- - -
-
- - +
+
+ + +
+
+ + +
+
+ + +
- `; list.appendChild(row); + + // Attach EntitySelect to the entity dropdown + const entitySelect = row.querySelector('.ha-mapping-entity') as HTMLSelectElement; + const es = new EntitySelect({ + target: entitySelect, + getItems: _getEntityItems, + placeholder: t('ha_light.mapping.search_entity'), + }); + _mappingEntitySelects.push(es); +} + +export function removeHALightMapping(btn: HTMLElement): void { + const row = btn.closest('.ha-light-mapping-row'); + if (!row) return; + // Find and destroy the EntitySelect for this row + const select = row.querySelector('.ha-mapping-entity') as HTMLSelectElement; + if (select) { + const idx = _mappingEntitySelects.findIndex(es => (es as any)._target === select || (es as any).target === select); + if (idx >= 0) { + _mappingEntitySelects[idx].destroy(); + _mappingEntitySelects.splice(idx, 1); + } + } + row.remove(); } // ── Show / Close ── @@ -124,6 +230,7 @@ export async function showHALightEditor(targetId: string | null = null, cloneDat ).join(''); // Clear mappings + _destroyMappingEntitySelects(); document.getElementById('ha-light-mappings-list')!.innerHTML = ''; let editData: any = null; @@ -153,7 +260,10 @@ export async function showHALightEditor(targetId: string | null = null, cloneDat document.getElementById('ha-light-editor-transition-display')!.textContent = (editData.ha_transition ?? 0.5).toFixed(1); (document.getElementById('ha-light-editor-description') as HTMLInputElement).value = editData.description || ''; - // Load mappings + // Fetch entities from the selected HA source before loading mappings + await _fetchHAEntities(editData.ha_source_id || ''); + + // Load mappings (with entity picker populated) const mappings = editData.ha_light_mappings || []; mappings.forEach((m: any) => addHALightMapping(m)); } else { @@ -163,6 +273,11 @@ export async function showHALightEditor(targetId: string | null = null, cloneDat (document.getElementById('ha-light-editor-transition') as HTMLInputElement).value = '0.5'; document.getElementById('ha-light-editor-transition-display')!.textContent = '0.5'; (document.getElementById('ha-light-editor-description') as HTMLInputElement).value = ''; + + // Fetch entities from the first HA source + const firstSource = haSources[0]?.id || ''; + await _fetchHAEntities(firstSource); + // Add one empty mapping by default addHALightMapping(); } @@ -176,6 +291,11 @@ export async function showHALightEditor(targetId: string | null = null, cloneDat desc: s.connected ? t('ha_source.connected') : t('ha_source.disconnected'), })), placeholder: t('palette.search'), + onChange: async (newSourceId: string) => { + // Refetch entities when HA source changes, then rebuild mapping selects + await _fetchHAEntities(newSourceId); + _rebuildMappingEntityOptions(); + }, }); if (_cssSourceEntitySelect) { _cssSourceEntitySelect.destroy(); _cssSourceEntitySelect = null; } @@ -373,3 +493,4 @@ window.saveHALightEditor = saveHALightEditor; window.editHALightTarget = editHALightTarget; window.cloneHALightTarget = cloneHALightTarget; window.addHALightMapping = addHALightMapping; +window.removeHALightMapping = removeHALightMapping; diff --git a/server/src/wled_controller/static/locales/en.json b/server/src/wled_controller/static/locales/en.json index e9de0d4..e3d0cc8 100644 --- a/server/src/wled_controller/static/locales/en.json +++ b/server/src/wled_controller/static/locales/en.json @@ -1842,6 +1842,8 @@ "ha_light.error.ha_source_required": "HA connection is required", "ha_light.created": "HA light target created", "ha_light.updated": "HA light target updated", + "ha_light.mapping.select_entity": "Select a light entity...", + "ha_light.mapping.search_entity": "Search light entities...", "section.empty.ha_light_targets": "No HA light targets yet. Click + to add one.", "automations.condition.home_assistant": "Home Assistant", "automations.condition.home_assistant.desc": "HA entity state",