/** * Automations — automation cards, editor, condition builder, process picker, scene selector. */ import { apiKey, _automationsLoading, set_automationsLoading, automationsCacheObj, scenePresetsCache, _cachedHASources } from '../core/state.ts'; import { fetchWithAuth, escapeHtml } from '../core/api.ts'; import { t } from '../core/i18n.ts'; import { showToast, showConfirm, setTabRefreshing } from '../core/ui.ts'; import { Modal } from '../core/modal.ts'; import { CardSection } from '../core/card-sections.ts'; import { updateTabBadge, updateSubTabHash } from './tabs.ts'; import { ICON_SETTINGS, ICON_START, ICON_PAUSE, ICON_CLOCK, ICON_AUTOMATION, ICON_HELP, ICON_OK, ICON_TIMER, ICON_MONITOR, ICON_RADIO, ICON_SCENE, ICON_CLONE, ICON_TRASH, ICON_CIRCLE_OFF, ICON_UNDO, ICON_WEB, ICON_SEARCH } from '../core/icons.ts'; import * as P from '../core/icon-paths.ts'; import { wrapCard } from '../core/card-colors.ts'; import { TagInput, renderTagChips } from '../core/tag-input.ts'; import { getBaseOrigin } from './settings.ts'; import { IconSelect } from '../core/icon-select.ts'; import { EntitySelect } from '../core/entity-palette.ts'; import { attachProcessPicker } from '../core/process-picker.ts'; import { TreeNav } from '../core/tree-nav.ts'; import { csScenes, createSceneCard, initScenePresetDelegation } from './scene-presets.ts'; import type { Automation } from '../types.ts'; let _automationTagsInput: any = null; // ── Auto-name ── let _autoNameManuallyEdited = false; function _autoGenerateAutomationName() { if (_autoNameManuallyEdited) return; if ((document.getElementById('automation-editor-id') as HTMLInputElement).value) return; const sceneSel = document.getElementById('automation-scene-id') as HTMLSelectElement | null; const sceneName = sceneSel?.selectedOptions[0]?.textContent?.trim() || ''; const logic = (document.getElementById('automation-editor-logic') as HTMLSelectElement).value; const condCount = document.querySelectorAll('#automation-conditions-list .condition-row').length; let name = ''; if (sceneName) name = sceneName; if (condCount > 0) { const logicLabel = logic === 'and' ? 'AND' : 'OR'; const suffix = `${condCount} ${logicLabel}`; name = name ? `${name} · ${suffix}` : suffix; } (document.getElementById('automation-editor-name') as HTMLInputElement).value = name || t('automations.add'); } class AutomationEditorModal extends Modal { constructor() { super('automation-editor-modal'); } onForceClose() { if (_automationTagsInput) { _automationTagsInput.destroy(); _automationTagsInput = null; } } snapshotValues() { return { name: (document.getElementById('automation-editor-name') as HTMLInputElement).value, enabled: (document.getElementById('automation-editor-enabled') as HTMLInputElement).checked.toString(), logic: (document.getElementById('automation-editor-logic') as HTMLSelectElement).value, conditions: JSON.stringify(getAutomationEditorConditions()), scenePresetId: (document.getElementById('automation-scene-id') as HTMLSelectElement).value, deactivationMode: (document.getElementById('automation-deactivation-mode') as HTMLSelectElement).value, deactivationScenePresetId: (document.getElementById('automation-fallback-scene-id') as HTMLSelectElement).value, tags: JSON.stringify(_automationTagsInput ? _automationTagsInput.getValue() : []), }; } } const automationModal = new AutomationEditorModal(); // ── Bulk action handlers ── async function _bulkEnableAutomations(ids: any) { const results = await Promise.allSettled(ids.map(id => fetchWithAuth(`/automations/${id}/enable`, { method: 'POST' }) )); const failed = results.filter(r => r.status === 'rejected' || ((r as any).value && !(r as any).value.ok)).length; if (failed) showToast(`${ids.length - failed}/${ids.length} enabled`, 'warning'); else showToast(t('automations.updated'), 'success'); automationsCacheObj.invalidate(); loadAutomations(); } async function _bulkDisableAutomations(ids: any) { const results = await Promise.allSettled(ids.map(id => fetchWithAuth(`/automations/${id}/disable`, { method: 'POST' }) )); const failed = results.filter(r => r.status === 'rejected' || ((r as any).value && !(r as any).value.ok)).length; if (failed) showToast(`${ids.length - failed}/${ids.length} disabled`, 'warning'); else showToast(t('automations.updated'), 'success'); automationsCacheObj.invalidate(); loadAutomations(); } async function _bulkDeleteAutomations(ids: any) { const results = await Promise.allSettled(ids.map(id => fetchWithAuth(`/automations/${id}`, { method: 'DELETE' }) )); const failed = results.filter(r => r.status === 'rejected' || ((r as any).value && !(r as any).value.ok)).length; if (failed) showToast(`${ids.length - failed}/${ids.length} deleted`, 'warning'); else showToast(t('automations.deleted'), 'success'); automationsCacheObj.invalidate(); loadAutomations(); } const csAutomations = new CardSection('automations', { titleKey: 'automations.title', gridClass: 'devices-grid', addCardOnclick: "openAutomationEditor()", keyAttr: 'data-automation-id', emptyKey: 'section.empty.automations', bulkActions: [ { key: 'enable', labelKey: 'bulk.enable', icon: ICON_OK, handler: _bulkEnableAutomations }, { key: 'disable', labelKey: 'bulk.disable', icon: ICON_CIRCLE_OFF, handler: _bulkDisableAutomations }, { key: 'delete', labelKey: 'bulk.delete', icon: ICON_TRASH, style: 'danger', confirm: 'bulk.confirm_delete', handler: _bulkDeleteAutomations }, ] } as any); // ── Tree navigation ── let _automationsTreeTriggered = false; const _automationsTree = new TreeNav('automations-tree-nav', { onSelect: (key: string) => { _automationsTreeTriggered = true; switchAutomationTab(key); _automationsTreeTriggered = false; } }); export function switchAutomationTab(tabKey: string) { document.querySelectorAll('.automation-sub-tab-panel').forEach(panel => (panel as HTMLElement).classList.toggle('active', panel.id === `automation-tab-${tabKey}`) ); localStorage.setItem('activeAutomationTab', tabKey); updateSubTabHash('automations', tabKey); if (!_automationsTreeTriggered) { _automationsTree.setActive(tabKey); } } /* ── Condition logic IconSelect ───────────────────────────────── */ const _icon = (d: any) => `${d}`; let _conditionLogicIconSelect: any = null; function _ensureConditionLogicIconSelect() { const sel = document.getElementById('automation-editor-logic'); if (!sel) return; const items = [ { value: 'or', icon: _icon(P.zap), label: t('automations.condition_logic.or'), desc: t('automations.condition_logic.or.desc') }, { value: 'and', icon: _icon(P.link), label: t('automations.condition_logic.and'), desc: t('automations.condition_logic.and.desc') }, ]; if (_conditionLogicIconSelect) { _conditionLogicIconSelect.updateItems(items); return; } _conditionLogicIconSelect = new IconSelect({ target: sel, items, columns: 2 } as any); } // Re-render automations when language changes (only if tab is active) document.addEventListener('languageChanged', () => { if (apiKey && (localStorage.getItem('activeTab') || 'dashboard') === 'automations') loadAutomations(); }); // React to real-time automation state changes from global events WS document.addEventListener('server:automation_state_changed', () => { if (apiKey && (localStorage.getItem('activeTab') || 'dashboard') === 'automations') { loadAutomations(); } }); export async function loadAutomations() { if (_automationsLoading) return; set_automationsLoading(true); const container = document.getElementById('automations-content'); if (!container) { set_automationsLoading(false); return; } if (!csAutomations.isMounted()) setTabRefreshing('automations-content', true); try { const [automations, scenes] = await Promise.all([ automationsCacheObj.fetch(), scenePresetsCache.fetch(), ]); const sceneMap = new Map(scenes.map(s => [s.id, s])); const activeCount = automations.filter(a => a.is_active).length; updateTabBadge('automations', activeCount); renderAutomations(automations, sceneMap); } catch (error: any) { if (error.isAuth) return; console.error('Failed to load automations:', error); container.innerHTML = `

${escapeHtml(error.message)}

`; } finally { set_automationsLoading(false); setTabRefreshing('automations-content', false); } } function renderAutomations(automations: any, sceneMap: any) { const container = document.getElementById('automations-content'); const autoItems = csAutomations.applySortOrder(automations.map(a => ({ key: a.id, html: createAutomationCard(a, sceneMap) }))); const sceneItems = csScenes.applySortOrder(scenePresetsCache.data.map(s => ({ key: s.id, html: createSceneCard(s) }))); const activeTab = localStorage.getItem('activeAutomationTab') || 'automations'; const treeItems = [ { key: 'automations', icon: ICON_AUTOMATION, titleKey: 'automations.title', count: automations.length }, { key: 'scenes', icon: ICON_SCENE, titleKey: 'scenes.title', count: scenePresetsCache.data.length }, ]; if (csAutomations.isMounted()) { _automationsTree.updateCounts({ automations: automations.length, scenes: scenePresetsCache.data.length, }); csAutomations.reconcile(autoItems); csScenes.reconcile(sceneItems); } else { const panels = [ { key: 'automations', html: csAutomations.render(autoItems) }, { key: 'scenes', html: csScenes.render(sceneItems) }, ].map(p => `
${p.html}
`).join(''); container!.innerHTML = panels; CardSection.bindAll([csAutomations, csScenes]); // Event delegation for scene preset card actions initScenePresetDelegation(container!); _automationsTree.setExtraHtml(``); _automationsTree.update(treeItems, activeTab); _automationsTree.observeSections('automations-content', { 'automations': 'automations', 'scenes': 'scenes', }); } } type ConditionPillRenderer = (c: any) => string; const CONDITION_PILL_RENDERERS: Record = { always: (c) => `${ICON_OK} ${t('automations.condition.always')}`, startup: (c) => `${ICON_START} ${t('automations.condition.startup')}`, application: (c) => { const apps = (c.apps || []).join(', '); const matchLabel = t('automations.condition.application.match_type.' + (c.match_type || 'running')); return `${t('automations.condition.application')}: ${apps} (${matchLabel})`; }, time_of_day: (c) => `${ICON_CLOCK} ${t('automations.condition.time_of_day')}: ${c.start_time || '00:00'} – ${c.end_time || '23:59'}`, system_idle: (c) => { const mode = c.when_idle !== false ? t('automations.condition.system_idle.when_idle') : t('automations.condition.system_idle.when_active'); return `${ICON_TIMER} ${c.idle_minutes || 5}m (${mode})`; }, display_state: (c) => { const stateLabel = t('automations.condition.display_state.' + (c.state || 'on')); return `${ICON_MONITOR} ${t('automations.condition.display_state')}: ${stateLabel}`; }, mqtt: (c) => `${ICON_RADIO} ${t('automations.condition.mqtt')}: ${escapeHtml(c.topic || '')} = ${escapeHtml(c.payload || '*')}`, webhook: (c) => `${ICON_WEB} ${t('automations.condition.webhook')}`, home_assistant: (c) => `${_icon(P.home)} ${t('automations.condition.home_assistant')}: ${escapeHtml(c.entity_id || '')} = ${escapeHtml(c.state || '*')}`, }; function createAutomationCard(automation: Automation, sceneMap = new Map()) { const statusClass = !automation.enabled ? 'disabled' : automation.is_active ? 'active' : 'inactive'; const statusText = !automation.enabled ? t('automations.status.disabled') : automation.is_active ? t('automations.status.active') : t('automations.status.inactive'); let condPills = ''; if (automation.conditions.length === 0) { condPills = `${t('automations.conditions.empty')}`; } else { const parts = automation.conditions.map(c => { const renderer = CONDITION_PILL_RENDERERS[c.condition_type]; return renderer ? renderer(c) : `${c.condition_type}`; }); const logicLabel = automation.condition_logic === 'and' ? t('automations.logic.and') : t('automations.logic.or'); condPills = parts.join(`${logicLabel}`); } // Scene info const scene = automation.scene_preset_id ? sceneMap.get(automation.scene_preset_id) : null; const sceneName = scene ? escapeHtml(scene.name) : t('automations.scene.none_selected'); const sceneColor = scene ? scene.color || '#4fc3f7' : '#888'; // Deactivation mode label let deactivationMeta = ''; if (automation.deactivation_mode === 'revert') { deactivationMeta = `${ICON_UNDO} ${t('automations.deactivation_mode.revert')}`; } else if (automation.deactivation_mode === 'fallback_scene') { const fallback = automation.deactivation_scene_preset_id ? sceneMap.get(automation.deactivation_scene_preset_id) : null; if (fallback) { const fbColor = fallback.color || '#4fc3f7'; deactivationMeta = `${ICON_UNDO} ${escapeHtml(fallback.name)}`; } else { deactivationMeta = `${ICON_UNDO} ${t('automations.deactivation_mode.fallback_scene')}`; } } let lastActivityMeta = ''; if (automation.last_activated_at) { const ts = new Date(automation.last_activated_at); lastActivityMeta = `${ICON_CLOCK} ${ts.toLocaleString()}`; } return wrapCard({ dataAttr: 'data-automation-id', id: automation.id, classes: !automation.enabled ? 'automation-status-disabled' : '', removeOnclick: `deleteAutomation('${automation.id}', '${escapeHtml(automation.name)}')`, removeTitle: t('common.delete'), content: `
${escapeHtml(automation.name)} ${statusText}
${condPills} ${ICON_SCENE} ${sceneName} ${deactivationMeta}
${renderTagChips(automation.tags)}`, actions: ` `, }); } export async function openAutomationEditor(automationId?: any, cloneData?: any) { const modal = document.getElementById('automation-editor-modal'); const titleEl = document.getElementById('automation-editor-title'); const idInput = document.getElementById('automation-editor-id') as HTMLInputElement; const nameInput = document.getElementById('automation-editor-name') as HTMLInputElement; const enabledInput = document.getElementById('automation-editor-enabled') as HTMLInputElement; const logicSelect = document.getElementById('automation-editor-logic') as HTMLSelectElement; const condList = document.getElementById('automation-conditions-list'); const errorEl = document.getElementById('automation-editor-error') as HTMLElement; errorEl.style.display = 'none'; condList!.innerHTML = ''; _ensureConditionLogicIconSelect(); _ensureDeactivationModeIconSelect(); // Fetch scenes for selector try { await scenePresetsCache.fetch(); } catch { /* use cached */ } // Reset deactivation mode (document.getElementById('automation-deactivation-mode') as HTMLSelectElement).value = 'none'; if (_deactivationModeIconSelect) _deactivationModeIconSelect.setValue('none'); (document.getElementById('automation-fallback-scene-group') as HTMLElement).style.display = 'none'; let _editorTags: any[] = []; if (automationId) { titleEl!.innerHTML = `${ICON_AUTOMATION} ${t('automations.edit')}`; try { const resp = await fetchWithAuth(`/automations/${automationId}`); if (!resp.ok) throw new Error('Failed to load automation'); const automation = await resp.json(); idInput.value = automation.id; nameInput.value = automation.name; enabledInput.checked = automation.enabled; logicSelect.value = automation.condition_logic; if (_conditionLogicIconSelect) _conditionLogicIconSelect.setValue(automation.condition_logic); for (const c of automation.conditions) { addAutomationConditionRow(c); } // Scene selector _initSceneSelector('automation-scene-id', automation.scene_preset_id); // Deactivation mode const deactMode = automation.deactivation_mode || 'none'; (document.getElementById('automation-deactivation-mode') as HTMLSelectElement).value = deactMode; if (_deactivationModeIconSelect) _deactivationModeIconSelect.setValue(deactMode); _onDeactivationModeChange(); _initSceneSelector('automation-fallback-scene-id', automation.deactivation_scene_preset_id); _editorTags = automation.tags || []; } catch (e: any) { showToast(e.message, 'error'); return; } } else if (cloneData) { // Clone mode — create with prefilled data titleEl!.innerHTML = `${ICON_AUTOMATION} ${t('automations.add')}`; idInput.value = ''; nameInput.value = (cloneData.name || '') + ' (Copy)'; enabledInput.checked = cloneData.enabled !== false; logicSelect.value = cloneData.condition_logic || 'or'; if (_conditionLogicIconSelect) _conditionLogicIconSelect.setValue(cloneData.condition_logic || 'or'); // Clone conditions (strip webhook tokens — they must be unique) for (const c of (cloneData.conditions || [])) { const clonedCond = { ...c }; if (clonedCond.condition_type === 'webhook') delete clonedCond.token; addAutomationConditionRow(clonedCond); } _initSceneSelector('automation-scene-id', cloneData.scene_preset_id); const cloneDeactMode = cloneData.deactivation_mode || 'none'; (document.getElementById('automation-deactivation-mode') as HTMLSelectElement).value = cloneDeactMode; if (_deactivationModeIconSelect) _deactivationModeIconSelect.setValue(cloneDeactMode); _onDeactivationModeChange(); _initSceneSelector('automation-fallback-scene-id', cloneData.deactivation_scene_preset_id); _editorTags = cloneData.tags || []; } else { titleEl!.innerHTML = `${ICON_AUTOMATION} ${t('automations.add')}`; idInput.value = ''; nameInput.value = ''; enabledInput.checked = true; logicSelect.value = 'or'; if (_conditionLogicIconSelect) _conditionLogicIconSelect.setValue('or'); _initSceneSelector('automation-scene-id', null); _initSceneSelector('automation-fallback-scene-id', null); } // Wire up deactivation mode change (document.getElementById('automation-deactivation-mode') as HTMLSelectElement).onchange = _onDeactivationModeChange; // Auto-name wiring _autoNameManuallyEdited = !!(automationId || cloneData); nameInput.oninput = () => { _autoNameManuallyEdited = true; }; (window as any)._autoGenerateAutomationName = _autoGenerateAutomationName; if (!automationId && !cloneData) _autoGenerateAutomationName(); automationModal.open(); modal!.querySelectorAll('[data-i18n]').forEach(el => { el.textContent = t(el.getAttribute('data-i18n')!); }); modal!.querySelectorAll('[data-i18n-placeholder]').forEach(el => { (el as HTMLInputElement).placeholder = t(el.getAttribute('data-i18n-placeholder')!); }); // Tags if (_automationTagsInput) { _automationTagsInput.destroy(); _automationTagsInput = null; } _automationTagsInput = new TagInput(document.getElementById('automation-tags-container'), { placeholder: t('tags.placeholder') }); _automationTagsInput.setValue(_editorTags); automationModal.snapshot(); } function _onDeactivationModeChange() { const mode = (document.getElementById('automation-deactivation-mode') as HTMLSelectElement).value; (document.getElementById('automation-fallback-scene-group') as HTMLElement).style.display = mode === 'fallback_scene' ? '' : 'none'; } export async function closeAutomationEditorModal() { await automationModal.close(); } // ===== Scene selector (EntitySelect) ===== let _sceneEntitySelect: any = null; let _fallbackSceneEntitySelect: any = null; function _getSceneItems() { return (scenePresetsCache.data || []).map(s => ({ value: s.id, label: s.name, icon: ``, })); } function _initSceneSelector(selectId: any, selectedId: any) { const sel = document.getElementById(selectId) as HTMLSelectElement; // Populate ${CONDITION_TYPE_KEYS.map(k => ``).join('')}
`; const typeSelect = row.querySelector('.condition-type-select') as HTMLSelectElement; const container = row.querySelector('.condition-fields-container') as HTMLElement; // Attach IconSelect to the condition type dropdown const condIconSelect = new IconSelect({ target: typeSelect, items: _buildConditionTypeItems(), columns: 4, } as any); function renderFields(type: any, data: any) { if (type === 'always') { container.innerHTML = `${t('automations.condition.always.hint')}`; return; } if (type === 'startup') { container.innerHTML = `${t('automations.condition.startup.hint')}`; return; } if (type === 'time_of_day') { const startTime = data.start_time || '00:00'; const endTime = data.end_time || '23:59'; const [sh, sm] = startTime.split(':').map(Number); const [eh, em] = endTime.split(':').map(Number); const pad = (n: number) => String(n).padStart(2, '0'); container.innerHTML = `
${t('automations.condition.time_of_day.start_time')}
:
${t('automations.condition.time_of_day.end_time')}
:
${t('automations.condition.time_of_day.overnight_hint')}
`; _wireTimeRangePicker(container); return; } if (type === 'system_idle') { const idleMinutes = data.idle_minutes ?? 5; const whenIdle = data.when_idle ?? true; container.innerHTML = `
`; return; } if (type === 'display_state') { const dState = data.state || 'on'; container.innerHTML = `
`; return; } if (type === 'mqtt') { const topic = data.topic || ''; const payload = data.payload || ''; const matchMode = data.match_mode || 'exact'; container.innerHTML = `
`; return; } if (type === 'home_assistant') { const haSourceId = data.ha_source_id || ''; const entityId = data.entity_id || ''; const haState = data.state || ''; const matchMode = data.match_mode || 'exact'; // Build HA source options from cached data const haOptions = _cachedHASources.map((s: any) => `` ).join(''); container.innerHTML = `
${t('automations.condition.home_assistant.hint')}
`; return; } if (type === 'webhook') { if (data.token) { const webhookUrl = getBaseOrigin() + '/api/v1/webhooks/' + data.token; container.innerHTML = `
${t('automations.condition.webhook.hint')}
`; } else { container.innerHTML = `
${t('automations.condition.webhook.hint')}

${t('automations.condition.webhook.save_first')}

`; } return; } const appsValue = (data.apps || []).join('\n'); const matchType = data.match_type || 'running'; container.innerHTML = `
`; const textarea = container.querySelector('.condition-apps') as HTMLTextAreaElement; attachProcessPicker(container, textarea); // Attach IconSelect to match type const matchSel = container.querySelector('.condition-match-type'); if (matchSel) { new IconSelect({ target: matchSel, items: _buildMatchTypeItems(), columns: 2, } as any); } } renderFields(condType, condition); typeSelect.addEventListener('change', () => { renderFields(typeSelect.value, {}); }); list!.appendChild(row); } function getAutomationEditorConditions() { const rows = document.querySelectorAll('#automation-conditions-list .automation-condition-row'); const conditions: any[] = []; rows.forEach(row => { const typeSelect = row.querySelector('.condition-type-select') as HTMLSelectElement; const condType = typeSelect ? typeSelect.value : 'application'; if (condType === 'always') { conditions.push({ condition_type: 'always' }); } else if (condType === 'startup') { conditions.push({ condition_type: 'startup' }); } else if (condType === 'time_of_day') { conditions.push({ condition_type: 'time_of_day', start_time: (row.querySelector('.condition-start-time') as HTMLInputElement).value || '00:00', end_time: (row.querySelector('.condition-end-time') as HTMLInputElement).value || '23:59', }); } else if (condType === 'system_idle') { conditions.push({ condition_type: 'system_idle', idle_minutes: parseInt((row.querySelector('.condition-idle-minutes') as HTMLInputElement).value, 10) || 5, when_idle: (row.querySelector('.condition-when-idle') as HTMLSelectElement).value === 'true', }); } else if (condType === 'display_state') { conditions.push({ condition_type: 'display_state', state: (row.querySelector('.condition-display-state') as HTMLSelectElement).value || 'on', }); } else if (condType === 'mqtt') { conditions.push({ condition_type: 'mqtt', topic: (row.querySelector('.condition-mqtt-topic') as HTMLInputElement).value.trim(), payload: (row.querySelector('.condition-mqtt-payload') as HTMLInputElement).value, match_mode: (row.querySelector('.condition-mqtt-match-mode') as HTMLSelectElement).value || 'exact', }); } else if (condType === 'webhook') { const tokenInput = row.querySelector('.condition-webhook-token') as HTMLInputElement; const cond: any = { condition_type: 'webhook' }; if (tokenInput && tokenInput.value) cond.token = tokenInput.value; conditions.push(cond); } else if (condType === 'home_assistant') { conditions.push({ condition_type: 'home_assistant', ha_source_id: (row.querySelector('.condition-ha-source-id') as HTMLSelectElement).value, entity_id: (row.querySelector('.condition-ha-entity-id') as HTMLInputElement).value.trim(), state: (row.querySelector('.condition-ha-state') as HTMLInputElement).value, match_mode: (row.querySelector('.condition-ha-match-mode') as HTMLSelectElement).value || 'exact', }); } else { const matchType = (row.querySelector('.condition-match-type') as HTMLSelectElement).value; const appsText = (row.querySelector('.condition-apps') as HTMLTextAreaElement).value.trim(); const apps = appsText ? appsText.split('\n').map(a => a.trim()).filter(Boolean) : []; conditions.push({ condition_type: 'application', apps, match_type: matchType }); } }); return conditions; } export async function saveAutomationEditor() { const idInput = document.getElementById('automation-editor-id') as HTMLInputElement; const nameInput = document.getElementById('automation-editor-name') as HTMLInputElement; const enabledInput = document.getElementById('automation-editor-enabled') as HTMLInputElement; const logicSelect = document.getElementById('automation-editor-logic') as HTMLSelectElement; const name = nameInput.value.trim(); if (!name) { automationModal.showError(t('automations.error.name_required')); return; } const body = { name, enabled: enabledInput.checked, condition_logic: logicSelect.value, conditions: getAutomationEditorConditions(), scene_preset_id: (document.getElementById('automation-scene-id') as HTMLSelectElement).value || null, deactivation_mode: (document.getElementById('automation-deactivation-mode') as HTMLSelectElement).value, deactivation_scene_preset_id: (document.getElementById('automation-fallback-scene-id') as HTMLSelectElement).value || null, tags: _automationTagsInput ? _automationTagsInput.getValue() : [], }; const automationId = idInput.value; const isEdit = !!automationId; try { const url = isEdit ? `/automations/${automationId}` : '/automations'; const resp = await fetchWithAuth(url, { method: isEdit ? 'PUT' : 'POST', body: JSON.stringify(body), }); if (!resp.ok) { const err = await resp.json().catch(() => ({})); throw new Error(err.detail || 'Failed to save automation'); } automationModal.forceClose(); showToast(isEdit ? t('automations.updated') : t('automations.created'), 'success'); automationsCacheObj.invalidate(); loadAutomations(); } catch (e: any) { if (e.isAuth) return; automationModal.showError(e.message); } } export async function toggleAutomationEnabled(automationId: any, enable: any) { try { const action = enable ? 'enable' : 'disable'; const resp = await fetchWithAuth(`/automations/${automationId}/${action}`, { method: 'POST', }); if (!resp.ok) { const err = await resp.json().catch(() => ({})); throw new Error(err.detail || `Failed to ${action} automation`); } automationsCacheObj.invalidate(); loadAutomations(); } catch (e: any) { if (e.isAuth) return; showToast(e.message, 'error'); } } export function copyWebhookUrl(btn: any) { const input = btn.closest('.webhook-url-row').querySelector('.condition-webhook-url') as HTMLInputElement; if (!input || !input.value) return; const onCopied = () => { const orig = btn.textContent; btn.textContent = t('automations.condition.webhook.copied'); setTimeout(() => { btn.textContent = orig; }, 1500); }; if (navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(input.value).then(onCopied); } else { input.select(); document.execCommand('copy'); onCopied(); } } export async function cloneAutomation(automationId: any) { try { const resp = await fetchWithAuth(`/automations/${automationId}`); if (!resp.ok) throw new Error('Failed to load automation'); const automation = await resp.json(); openAutomationEditor(null, automation); } catch (e: any) { if (e.isAuth) return; showToast(t('automations.error.clone_failed'), 'error'); } } export async function deleteAutomation(automationId: any, automationName: any) { const msg = t('automations.delete.confirm').replace('{name}', automationName); const confirmed = await showConfirm(msg); if (!confirmed) return; try { const resp = await fetchWithAuth(`/automations/${automationId}`, { method: 'DELETE', }); if (!resp.ok) { const err = await resp.json().catch(() => ({})); throw new Error(err.detail || 'Failed to delete automation'); } showToast(t('automations.deleted'), 'success'); automationsCacheObj.invalidate(); loadAutomations(); } catch (e: any) { if (e.isAuth) return; showToast(e.message, 'error'); } }