function deviceFeaturePanel(device) { const features = [ ['light', 'supports_light', tr('devices.light')], ['xfan', 'supports_xfan', tr('devices.xfan')], ['health', 'supports_health', tr('devices.health')], ['air', 'supports_air', tr('devices.air')], ['sleep', 'supports_sleep', tr('devices.sleep')], ].filter(([, support]) => device[support] === true); if (!features.length) return `
${esc(tr('devices.features'))}${esc(tr('devices.noExtraFeatures'))}
`; return `
${esc(tr('devices.features'))}
${features.map(([field,,label]) => ``).join('')}
`; } function deviceZoneDisabled(deviceId) { return app.zones.some(zone => zone.device_id === deviceId && zone.enabled === false); } function zoneForDevice(deviceId) { return app.zones.find(zone => zone.device_id === deviceId) || null; } function deviceCard(device, detailed = false) { const modes = ['auto','cool','dry','fan','heat']; const fans = [0,1,3,5]; const hasResponseTime = device.response_time_ms !== null && device.response_time_ms !== undefined && Number.isFinite(Number(device.response_time_ms)); const responseTime = hasResponseTime ? `${Math.max(0, Math.round(Number(device.response_time_ms)))} ms` : '— ms'; const protocol = device.simulated ? tr('devices.simulator') : device.protocol_version === 2 ? 'V2 GCM' : device.protocol_version === 1 ? 'V1 ECB' : tr('devices.protocolAuto'); const networkInfo = `
${esc(device.ip)} · ${esc(responseTime)} · ${esc(protocol)}${device.last_error ? `${esc(device.last_error)}` : ''}
`; const managedZone = app.zones.find(zone => zone.device_id === device.id && zone.compressor_pending_action) || zoneForDevice(device.id); const zoneLocked = !detailed && deviceZoneDisabled(device.id); const locked = zoneLocked ? ' disabled' : ''; const quickClass = detailed ? '' : ' quick-control-card quick-device-control'; const quickRow = detailed ? '' : ' quick-control-row'; return `

${esc(device.name)}

${esc(tr(device.online ? 'status.online' : 'status.offline'))} · ${esc(device.model || device.mac)}

${Number(device.target_temperature).toFixed(1)}°C
${esc(tr('devices.currentTemperature'))}: ${fmtTemp(device.current_temperature)}${device.outdoor_temperature == null ? '' : ` · ${esc(tr('devices.outdoor'))} ${fmtTemp(device.outdoor_temperature)}`}
${compressorQueuePanel(managedZone)} ${zoneLocked ? `
${esc(tr('devices.disabledZoneTechnicalOnly'))}
` : ''}
${modes.map(mode => ``).join('')}
${fans.map(fan => ``).join('')}
${device.supports_quiet === false ? '' : ``} ${device.supports_turbo === false ? '' : ``}
${detailed ? deviceFeaturePanel(device) : ''} ${detailed ? `` : ''}
`; } function renderDevices() { const empty = `
${esc(tr('devices.emptyTitle'))}${esc(tr('devices.emptyText'))}
`; $('#dashboardDevices').innerHTML = app.devices.length ? app.devices.map(d => deviceCard(d, false)).join('') : empty; $('#deviceList').innerHTML = app.devices.length ? app.devices.map(d => deviceCard(d, true)).join('') : empty; const dashboardCount = $('#dashboardDeviceCount'); if (dashboardCount) dashboardCount.textContent = String(app.devices.length); } function zoneStrategyLabel(zone) { if (zone.sensor_source === 'combined') return tr('zones.combinedSource'); if (zone.sensor_source === 'home_assistant') return tr('zones.externalSource'); return tr('zones.greeSource'); } function zoneControlSourceLabel(source) { return ({ device: tr('zones.sourceDevice'), external: tr('zones.sourceExternal'), combined: tr('zones.sourceCombined'), device_fallback: tr('zones.sourceFallback'), device_discrepancy_fallback: tr('zones.sourceDiscrepancy'), unavailable: tr('zones.sourceUnavailable'), })[source] || source || tr('zones.sourceUnavailable'); } function zonePresetLabel(preset) { const key = `preset.${preset || 'comfort'}`; return tr(key) === key ? (preset || 'comfort') : tr(key); } function groupControlHue(groupId) { let hash=0; for(const char of String(groupId || 'group')) hash=((hash*31)+char.charCodeAt(0))>>>0; return (hash % 300) + 20; } function groupControlStyle(group) { return group ? ` style="--group-control-color:hsl(${groupControlHue(group.id)} 72% 48%)"` : ''; } function groupControlForZone(zone) { if(app.settings?.house_power_enabled === false || zone?.device_manual_override || zone?.local_thermostat_power != null) return null; const source=String(zone?.control_source || ''); if(!source.startsWith('group:')) return null; const groupId=source.slice(6); return app.groups.find(group => group.id===groupId && group.power_enabled !== false && (group.zone_ids || []).includes(zone.id)) || null; } function zoneLockoutActive(zone) { const until=zone?.lockout_until ? new Date(zone.lockout_until).getTime() : NaN; return Number.isFinite(until) && until>Date.now(); } function compressorPendingDescription(zone) { const action=String(zone?.compressor_pending_action || ''); if(!action) return ''; const [kind, mode, target] = action.split(':'); const targetValue = Number(target); if(kind==='mode_change') return tr('zones.queuedModeChange', {mode:modeLabel(mode), temperature:Number.isFinite(targetValue)?targetValue.toFixed(1):'—'}); if(kind==='power_on') return tr('zones.queuedPowerOn', {mode:modeLabel(mode), temperature:Number.isFinite(targetValue)?targetValue.toFixed(1):'—'}); return tr('zones.queuedAction'); } function compressorQueuePanel(zone) { if(!zone?.compressor_pending_action) return ''; const until=zone.compressor_pending_until || zone.lockout_until; const untilMs=until ? new Date(until).getTime() : NaN; const when=Number.isFinite(untilMs) && untilMs>Date.now() ? dateTime(until) : tr('zones.afterProtection'); return `
${esc(tr('zones.compressorQueueTitle'))}${esc(compressorPendingDescription(zone))}${esc(tr('zones.compressorQueueUntil', {time:when}))}
`; } function zoneControlOwnerLabel(zone) { const owner=zone.control_owner || (zone.device_manual_override ? 'direct_manual' : (zone.local_thermostat_power != null ? 'local_thermostat' : 'automation')); const source=zone.control_source || ''; if(owner==='global_off') return tr('zones.ownerGlobalOff'); if(owner==='direct_manual') { if(source==='home_assistant_direct') return tr('zones.ownerHaDirect'); if(source==='web_direct') return tr('zones.ownerWebDirect'); return tr('zones.ownerExternal'); } if(owner==='local_thermostat') return tr('zones.ownerLocalThermostat'); if(source.startsWith('group:')) return tr('zones.ownerGroup'); return tr('zones.ownerAutomation'); } function zoneControlOwnerMeta(zone) { const parts=[]; const controlGroup=groupControlForZone(zone); if(controlGroup) parts.push(`${tr('groups.group')}: ${controlGroup.name}`); if(zone.control_since) parts.push(`${tr('zones.ownerSince')} ${dateTime(zone.control_since)}`); if(zone.control_resume_at) parts.push(`${tr('zones.ownerResume')} ${dateTime(zone.control_resume_at)}`); if(zone.lockout_until && new Date(zone.lockout_until)>new Date()) parts.push(`${tr('zones.lockoutUntil')} ${dateTime(zone.lockout_until)}`); return parts.join(' · '); } function zoneRuntimeStatusLabel(zone, effectiveMode, device = null) { if (zone.device_manual_override === true) return tr('zones.manualDeviceControl'); if (!zone.enabled) return tr('common.disabled'); if (zone.local_thermostat_power === false) return tr('zones.localThermostatOff'); if ((effectiveMode || 'off') === 'off') return tr('zones.waiting'); if (device?.enabled === false) return tr('zones.waitingDeviceDisabled'); if (device && (!device.online || Number(device.communication_failures || 0) > 0)) return tr('zones.waitingOffline'); if (zone.current_temperature == null) return tr('zones.noMeasurement'); if (zone.compressor_pending_action && zoneLockoutActive(zone)) return tr('zones.waitingLockout'); if (zone.compressor_cancelled_action && device && (!device.power || (['heat','cool'].includes(effectiveMode) && device.mode !== effectiveMode))) return tr('zones.queueCancelledStatus'); if (zone.demand && device && !device.power) return tr('zones.waitingStart'); if (zone.demand && device?.power && ['heat','cool'].includes(effectiveMode) && device.mode !== effectiveMode) return tr('zones.waitingMode'); return zone.demand ? tr('zones.runningDemand') : tr('zones.satisfied'); } function zoneCard(zone, detailed = true) { const device = app.devices.find(d => d.id === zone.device_id); const state = zone.enabled ? tr('common.active') : tr('common.disabled'); const sensorDetails = zone.sensor_source === 'device' ? `${tr('zones.greeTemp')}: ${fmtTemp(zone.device_temperature ?? device?.current_temperature)}` : `${tr('zones.greeTemp')}: ${fmtTemp(zone.device_temperature)} · ${tr('zones.externalTemp')}: ${fmtTemp(zone.external_temperature)} · ${tr('zones.usedSource')}: ${zoneControlSourceLabel(zone.control_temperature_source)}`; const roomTemperature = zone.current_temperature ?? zone.device_temperature ?? device?.current_temperature; const target = Number(zone.effective_setpoint ?? zone.setpoint); const manual = zone.manual_preset || 'auto'; const displayPreset = zone.manual_preset || zone.active_preset || 'comfort'; const mode = zone.inherit_house_mode ? 'house' : zone.mode; const effectiveMode = zone.effective_mode || (zone.inherit_house_mode ? (app.settings?.house_mode || 'off') : zone.mode) || 'off'; const globalModeAvailable = ['heat', 'cool'].includes(app.settings?.house_mode); const globalModeDisabled = globalModeAvailable ? '' : ` disabled aria-disabled="true" title="${esc(tr('zones.globalModeUnavailable'))}"`; const hasManualOverride = zone.manual_preset != null || zone.manual_setpoint != null; const deviceManualOverride = zone.device_manual_override === true; const controlGroup = groupControlForZone(zone); const deviceUnavailable = !!device && (device.enabled === false || !device.online || Number(device.communication_failures || 0) > 0); const lockoutWaiting = !!zone.compressor_pending_action && zoneLockoutActive(zone); const startWaiting = zone.demand && !!device && !deviceUnavailable && !device.power; const modeWaiting = zone.demand && !!device?.power && !deviceUnavailable && ['heat','cool'].includes(effectiveMode) && device.mode !== effectiveMode; const visualGroup = controlGroup; const waitingDemand = lockoutWaiting || startWaiting || modeWaiting || (zone.demand && deviceUnavailable); const workingDemand = zone.demand && !!device?.power && !deviceUnavailable && !waitingDemand; const localThermostatPower = zone.local_thermostat_power; const localThermostatResumeAt = zone.local_thermostat_resume_at || null; const temporarySession = zone.temporary_quick_thermostat || null; const temporaryStatus = temporarySession ? temporarySessionStatus(zone) : null; const localThermostatOverride = localThermostatPower === true || localThermostatPower === false; const override = deviceManualOverride ? (zone.device_manual_override_until ? tr('zones.manualDeviceUntil', {time:new Date(zone.device_manual_override_until).toLocaleTimeString(locale(), {hour:'2-digit',minute:'2-digit'})}) : tr('zones.manualDeviceNoBoundary')) : temporarySession ? tr('zones.temporaryOverride', {time:temporaryStatus?.countdown || '—'}) : localThermostatOverride ? tr(localThermostatPower ? 'zones.localThermostatOn' : 'zones.localThermostatOff') : (zone.manual_override_until ? `${tr('zones.overrideUntil')} ${new Date(zone.manual_override_until).toLocaleTimeString(locale(), {hour:'2-digit',minute:'2-digit'})}` : (hasManualOverride ? tr('zones.manualNoBoundary') : tr('zones.scheduleControl'))); const manualTakeover = deviceManualOverride ? `
${esc(tr('zones.manualDeviceControl'))}

${esc(tr('zones.manualDeviceDescription'))}

` : localThermostatOverride && !temporarySession ? `
${esc(tr('zones.localThermostatControl'))}${localThermostatPower === false && localThermostatResumeAt ? `

${esc(tr('zones.localThermostatOffDescriptionTimed', {time:formatCountdown(localResumeSeconds(localThermostatResumeAt))}))}

` : `

${esc(tr(localThermostatPower ? 'zones.localThermostatOnDescription' : 'zones.localThermostatOffDescription'))}

`}
` : ''; const localPowerDisabled = !device || device.enabled === false ? ' disabled' : ''; const localRequestedOn = localThermostatPower === true ? true : (localThermostatPower === false ? false : !!device?.power); const localThermostatPowerControl = ``; const temporaryThermostatControl = temporarySession ? `` : ``; if (detailed) { const groupNames = (app.groups || []).filter(group => (group.zone_ids || []).includes(zone.id)).map(group => group.name); const groupText = groupNames.length ? groupNames.join(' · ') : tr('zones.noGroup'); const policy = zone.inherit_house_mode ? tr('zones.followHouse') : tr(zone.mode === 'heat' ? 'zones.heatOnly' : 'zones.coolOnly'); return `
${esc(tr('zones.configuration'))}

${esc(zone.name)}

${esc(device?.name || tr('common.noDevice'))} · ${esc(tr('groups.group'))}: ${esc(groupText)}

${esc(tr('zones.currentStatus'))}
${fmtTemp(roomTemperature)}${Number.isFinite(target) ? `${target.toFixed(1)}°C` : '—'}
${esc(zoneRuntimeStatusLabel(zone, effectiveMode, device))}${esc(houseModeLabel(effectiveMode))} · ${esc(zonePresetLabel(displayPreset))}${esc(override)}${esc(tr('zones.controlOwner'))}: ${esc(zoneControlOwnerLabel(zone))}${zoneControlOwnerMeta(zone) ? ` · ${esc(zoneControlOwnerMeta(zone))}` : ''}
${esc(tr('zones.modePolicy'))}${esc(policy)}
${esc(tr('zones.source'))}${esc(zoneStrategyLabel(zone))}
${esc(tr('zones.hysteresis'))}${zone.separate_hysteresis ? `${esc(tr('mode.cool'))} ${Number(zone.cool_hysteresis ?? zone.hysteresis ?? 0.6).toFixed(1)}°C · ${esc(tr('mode.heat'))} ${Number(zone.heat_hysteresis ?? zone.hysteresis ?? 0.6).toFixed(1)}°C` : `${Number(zone.hysteresis ?? 0.6).toFixed(1)}°C`}
${esc(tr('zones.smartFan'))}${esc(tr(zone.smart_fan === false ? 'common.off' : 'common.on'))}
${esc(sensorDetails)}
${compressorQueuePanel(zone)} ${manualTakeover}
`; } const runtimeStatus = zoneRuntimeStatusLabel(zone, effectiveMode, device); return `

${esc(zone.name)}

${esc(device?.name || tr('common.noDevice'))} · ${esc(zonePresetLabel(displayPreset))}

${localThermostatPowerControl}${temporaryThermostatControl}
${esc(tr('zones.measurement'))}${fmtTemp(roomTemperature)}
${Number.isFinite(target)?target.toFixed(1):'--'}°C
${esc(tr('zones.deviceTarget'))}${fmtTemp(zone.device_setpoint)}
${['auto','comfort','sleep','away'].map(preset=>``).join('')}
${esc(runtimeStatus)}${controlGroup ? ` ${esc(controlGroup.name)}` : ''}${esc(override)}
${esc(tr('zones.controlOwner'))}: ${esc(zoneControlOwnerLabel(zone))}${esc(zoneControlOwnerMeta(zone))}
${compressorQueuePanel(zone)} ${manualTakeover}
`; } function renderZones() { const empty = `
${esc(tr('zones.emptyTitle'))}${esc(tr('zones.emptyText'))}
`; $('#zoneList').innerHTML = app.zones.length ? app.zones.map(zone => zoneCard(zone, true)).join('') : empty; const dashboard = $('#dashboardZones'); if (dashboard) dashboard.innerHTML = app.zones.length ? app.zones.map(zone => zoneCard(zone, false)).join('') : empty; const dashboardCount = $('#dashboardZoneCount'); if (dashboardCount) dashboardCount.textContent = String(app.zones.length); const pendingCount = app.zones.filter(zone => !!zone.compressor_pending_action).length; $$('[data-compressor-cancel-all]').forEach(cancelAll => { cancelAll.hidden = pendingCount === 0; cancelAll.textContent = pendingCount ? `${tr('zones.cancelAllQueued')} (${pendingCount})` : tr('zones.cancelAllQueued'); }); } function groupZones(group) { const wanted = new Set(group?.zone_ids || []); return app.zones.filter(zone => wanted.has(zone.id)); } function groupState(group) { const zones = groupZones(group); const modes = [...new Set(zones.map(zone => zone.inherit_house_mode ? 'house' : (zone.mode || 'cool')))]; const presets = [...new Set(zones.map(zone => zone.manual_preset || 'auto'))]; const customTargets = zones .filter(zone => (zone.manual_preset || 'auto') === 'custom') .map(zone => Number(zone.manual_setpoint ?? zone.effective_setpoint ?? zone.setpoint)) .filter(Number.isFinite); const customTemperature = customTargets.length === zones.length && customTargets.length && customTargets.every(value => Math.abs(value - customTargets[0]) < 0.05) ? customTargets[0] : null; return { zones, mode: modes.length === 1 ? modes[0] : (modes.length ? 'mixed' : 'house'), preset: presets.length === 1 ? presets[0] : (presets.length ? 'mixed' : 'auto'), customTemperature, }; } function groupCard(group, detailed = false) { const state = groupState(group); const memberNames = state.zones.map(zone => zone.name); const memberText = memberNames.length ? memberNames.join(' · ') : tr('groups.noMembers'); const powerEnabled = group.power_enabled !== false; const masterOff = app.settings?.house_power_enabled === false; const modeLabelForGroup = value => value === 'house' ? tr('groups.followHouse') : (value === 'mixed' ? tr('groups.mixed') : modeLabel(value)); const presetLabelForGroup = value => value === 'mixed' ? tr('groups.mixed') : zonePresetLabel(value); const fallbackTarget = Number(state.zones[0]?.manual_setpoint ?? state.zones[0]?.effective_setpoint ?? state.zones[0]?.setpoint ?? 23); const currentCustomTarget = Number.isFinite(state.customTemperature) ? state.customTemperature : (Number.isFinite(fallbackTarget) ? fallbackTarget : 23); const customDraft = app.groupCustomDrafts?.[group.id]; const draftTarget = Number(customDraft?.value); const customTarget = Number.isFinite(draftTarget) ? draftTarget : currentCustomTarget; const customEditorVisible = powerEnabled && (state.preset === 'custom' || customDraft?.open === true); const customDraftPending = powerEnabled && customDraft?.open === true && state.preset !== 'custom'; const groupIsControlSource = powerEnabled && state.zones.some(zone => String(zone.control_source || '') === `group:${group.id}` && !zone.device_manual_override && zone.local_thermostat_power == null); const climateControlsDisabled = powerEnabled ? '' : ` disabled aria-disabled="true" title="${esc(tr('groups.enableControlFirst'))}"`; const groupStatusBadge = groupIsControlSource ? `${esc(tr('groups.activeControl'))}` : (!powerEnabled ? `${esc(tr('groups.gateOff'))}` : ''); return `
${esc(tr('groups.group'))}

${esc(group.name)}

${esc(memberText)}

${groupStatusBadge}
${detailed ? `
${esc(tr('groups.mode'))}${esc(modeLabelForGroup(state.mode))}
${esc(tr('groups.profile'))}${esc(presetLabelForGroup(state.preset))}
${esc(tr('groups.members'))}${state.zones.length}
` : ''}
${esc(tr('groups.controlToggle'))}
${esc(tr('groups.mode'))}
${['house','heat','cool'].map(mode => ``).join('')}
${esc(tr('groups.profile'))}
${['auto','comfort','sleep','away'].map(preset => ``).join('')}
${customDraftPending ? `${esc(tr('groups.customDraftPending'))}` : ''}
${masterOff ? `

${esc(tr('groups.masterOff'))}

` : ''} ${detailed ? `` : ''}
`; } function renderGroups() { const empty = `
${esc(tr('groups.emptyTitle'))}${esc(tr('groups.emptyText'))}
`; const dashboard = $('#dashboardGroups'); if (dashboard) dashboard.innerHTML = app.groups.length ? app.groups.map(group => groupCard(group, false)).join('') : empty; const dashboardCount = $('#dashboardGroupCount'); if (dashboardCount) dashboardCount.textContent = String(app.groups.length); const list = $('#groupList'); if (list) list.innerHTML = app.groups.length ? app.groups.map(group => groupCard(group, true)).join('') : empty; } async function sendGroupControl(id, patch) { try { await enqueueClimateControlTask(async () => { const result = await api(`/api/groups/${encodeURIComponent(id)}/control`, {method:'POST', body:patch}); if (result.group) { const index = app.groups.findIndex(group => group.id === result.group.id); if (index >= 0) app.groups[index] = result.group; else app.groups.push(result.group); } (result.zones || []).forEach(zone => { const index = app.zones.findIndex(item => item.id === zone.id); if (index >= 0) app.zones[index] = zone; else app.zones.push(zone); }); (result.devices || []).forEach(updateDevice); if (typeof result.master_power_enabled === 'boolean' && app.settings) app.settings.house_power_enabled = result.master_power_enabled; if (patch.preset === 'custom' && patch.setpoint != null) delete app.groupCustomDrafts[id]; renderAll(); scheduleControlPlanLoad(); const failed = Array.isArray(result.failed) ? result.failed.length : 0; if (failed) toast(tr('groups.partial', {count: failed}), true); else toast(tr('groups.controlUpdated')); return result; }); } catch (error) { await loadBootstrap(); toast(error.message, true); } } function renderGroupZoneChoices(selectedIds = null) { const host = $('#groupZoneChoices'); if (!host) return; const selected = new Set(selectedIds || []); host.innerHTML = app.zones.length ? app.zones.map(zone => { const device = app.devices.find(item => item.id === zone.device_id); return ``; }).join('') : `
${esc(tr('groups.noZones'))}
`; } function populateGroup(id) { const item = app.groups.find(group => group.id === id); if (!item) return; const form = $('#groupForm'); form.reset(); form.elements.id.value = item.id; form.elements.name.value = item.name; renderGroupZoneChoices(item.zone_ids || []); openDialog('groupDialog'); } function renderSchedules() { const dayNames = Array.from({length:7}, (_, index) => tr(`day.${index + 1}`)); $('#scheduleList').innerHTML = app.schedules.length ? app.schedules.map(item => { const zone = app.zones.find(z => z.id === item.zone_id); const days = item.weekdays.map(day => dayNames[day-1]).join(', '); return `

${esc(item.name)}

${esc(zone?.name || tr('common.noZone'))} · ${esc(days)}

${esc(item.enabled ? tr('common.enabled') : tr('common.disabled'))}
${esc(tr('common.from'))}${esc(item.start_time)}
${esc(tr('common.to'))}${esc(item.end_time)}
${esc(tr('schedules.profile'))}${esc(item.preset === 'custom' ? fmtTemp(item.setpoint) : zonePresetLabel(item.preset))}
`; }).join('') : `
${esc(tr('schedules.emptyTitle'))}${esc(tr('schedules.emptyText'))}
`; } function renderAutomations() { $('#automationList').innerHTML = app.automations.length ? app.automations.map(item => { const actionGroup = item.action_group_id ? app.groups.find(group => group.id === item.action_group_id) : null; const actionDevice = app.devices.find(device => device.id === item.action_device_id); const targetName = actionGroup?.name || actionDevice?.name || tr('common.noDevice'); const mode = item.action.mode === 'auto' && actionGroup ? tr('groups.followHouse') : (item.action.mode ? modeLabel(item.action.mode) : '—'); const preset = item.action_preset ? zonePresetLabel(item.action_preset) : '—'; return `

${esc(item.name)}

${esc(tr('automations.triggerSummary', {trigger: automationTriggerLabel(item), device: targetName}))}

${esc(item.enabled ? tr('common.active') : tr('common.disabled'))}
${esc(tr('common.power'))}${item.action.power == null ? '—' : item.action.power ? tr('common.on') : tr('common.off')}
${esc(tr('common.mode'))}${esc(mode)}
${esc(tr('groups.profile'))}${esc(preset)}
${esc(tr('automations.last'))}${item.last_fired_at ? new Date(item.last_fired_at).toLocaleTimeString(locale(),{hour:'2-digit',minute:'2-digit'}) : '—'}
`; }).join('') : `
${esc(tr('automations.emptyTitle'))}${esc(tr('automations.emptyText'))}
`; } function fillSelects() { const deviceOptions = app.devices.map(d => ``).join(''); const zoneOptions = app.zones.map(z => ``).join(''); const groupOptions = app.groups.map(g => ``).join(''); ['#zoneForm [name=device_id]', '#automationForm [name=trigger_device_id]', '#automationForm [name=action_device_id]'].forEach(selector => { const select = $(selector); if (!select) return; const current = select.value; select.innerHTML = deviceOptions; if ([...select.options].some(o => o.value === current)) select.value = current; }); const automationGroupSelect = $('#automationForm [name=action_group_id]'); if (automationGroupSelect) { const currentGroup = automationGroupSelect.value; automationGroupSelect.innerHTML = groupOptions; if ([...automationGroupSelect.options].some(o => o.value === currentGroup)) automationGroupSelect.value = currentGroup; } const zoneSelect = $('#scheduleForm [name=zone_id]'); if (zoneSelect) { const currentZone = zoneSelect.value; zoneSelect.innerHTML = zoneOptions; if ([...zoneSelect.options].some(o => o.value === currentZone)) zoneSelect.value = currentZone; } const presetZone = $('#schedulePresetZone'); if (presetZone) { const currentZone = presetZone.value; presetZone.innerHTML = zoneOptions; if ([...presetZone.options].some(o => o.value === currentZone)) presetZone.value = currentZone; } const copyZone=$('#copyZoneSource'); if(copyZone){ const current=copyZone.value; const editingZoneId=$('#zoneForm')?.dataset.editingZoneId || ''; const sources=app.zones.filter(zone=>zone.id!==editingZoneId); copyZone.innerHTML=``+sources.map(z=>``).join(''); copyZone.value=[...copyZone.options].some(option=>option.value===current)?current:''; } }