function renderAll() { renderSummary(); renderHouseClimate(); renderGroups(); renderControlPlan(); renderSimulationPage(); renderDevices(); renderZones(); renderSchedules(); renderAutomations(); renderAccessTokens(); fillSelects(); renderSettings(); renderNightSettings(); renderHomeAssistantSettings(); renderSimulationModeBanner(); renderDebugOverlay(); } function renderSummary() { const temperatures = app.devices.map(d => d.current_temperature).filter(Number.isFinite); const average = temperatures.length ? temperatures.reduce((a,b) => a+b, 0) / temperatures.length : null; const online = app.devices.filter(d => d.online).length; const active = app.devices.filter(d => d.power).length; const demand = app.zones.filter(z => z.enabled && z.demand).length; const toolbarStatus = $('#toolbarRuntimeStatus'); if (toolbarStatus) toolbarStatus.textContent = tr('dashboard.toolbarWorking', {active, total:app.devices.length}); $('#heroTemperature').innerHTML = `${average === null ? '--' : average.toFixed(1)}°C`; const activeSuffix = active ? tr('dashboard.summaryActive', {count: active}) : ''; $('#summaryText').textContent = app.devices.length ? tr('dashboard.summary', {online, total: app.devices.length, active: activeSuffix}) : tr('dashboard.empty'); $('#metrics').innerHTML = [ [tr('dashboard.metricOnline'), `${online}/${app.devices.length}`], [tr('dashboard.metricActive'), active], [tr('dashboard.metricDemand'), demand], ].map(([label,value]) => `
${esc(label)}${esc(value)}
`).join(''); renderSystemInfo(); } function renderHouseClimate() { const node = $('#houseClimate'); if (!node || !app.settings) return; const mode = app.settings.house_mode || 'cool'; const zonePresets = (app.zones || []).map(zone => zone.manual_preset || 'auto'); const preset = zonePresets.length && zonePresets.every(value => value === zonePresets[0]) ? zonePresets[0] : null; const outdoor = app.outdoorTemperature == null ? tr('common.unavailable') : fmtTemp(app.outdoorTemperature); const powerEnabled = app.settings.house_power_enabled !== false; const powerOn = $('#housePowerOn'), powerOff = $('#housePowerOff'); if (powerOn) { powerOn.classList.toggle('active', powerEnabled); powerOn.setAttribute('aria-pressed', String(powerEnabled)); } if (powerOff) { powerOff.classList.toggle('active', !powerEnabled); powerOff.setAttribute('aria-pressed', String(!powerEnabled)); } node.innerHTML = `
${esc(tr('house.seasonMode'))}

${esc(tr('house.smartThermostat'))}

${esc(tr('house.setpointStrategy'))}

${esc(tr('house.outdoor'))}${esc(outdoor)}
${['auto','comfort','sleep','away'].map(p=>``).join('')}
`; } function planEventMarkup(event) { const when = event?.at ? new Date(event.at).toLocaleString(locale(), {weekday:'short', hour:'2-digit', minute:'2-digit'}) : '—'; const target = event?.target_temperature == null ? '' : ` · ${fmtTemp(event.target_temperature)}`; return `
  • ${esc(event?.label || event?.kind || tr('plan.event'))}${esc(target)}
  • `; } function automationTriggerLabel(item) { if (item.trigger_kind === 'time') return tr('automations.triggerAt', {time: item.at_time || '—'}); const key = item.trigger_kind === 'temperature_above' ? 'automations.triggerAbove' : 'automations.triggerBelow'; return tr(key, {temperature: fmtTemp(item.threshold)}); } function planZoneEffectiveEnabled(zone) { if (!zone) return undefined; return zone.effective_enabled ?? zone.enabled ?? false; } function renderControlPlan() { const host = $('#controlPlan'); if (!host) return; const plan = app.controlPlan; if (!plan) { host.innerHTML = `
    ${esc(tr('plan.loading'))}
    `; return; } const allZones = plan.zones || []; const houseEvents = (plan.next_events || []).slice(0, 3); const house = `
    ${esc(tr('plan.house'))}

    ${esc(houseModeLabel(plan.house_mode || 'off'))}

    ${esc((plan.control_strategy || 'setpoint')==='setpoint'?tr('plan.strategySetpoint'):(plan.control_strategy || 'setpoint'))}

    ${esc(tr('plan.houseSummary', {zones:allZones.filter(planZoneEffectiveEnabled).length, demand:allZones.filter(zone=>planZoneEffectiveEnabled(zone) && zone.demand).length}))}

    `; const groupCards = (app.groups || []).map(group => { const memberIds = new Set(group.zone_ids || []); const members = allZones.filter(zone => memberIds.has(zone.zone_id)); const state = groupState(group); const powerEnabled = group.power_enabled !== false; const demand = members.filter(zone => planZoneEffectiveEnabled(zone) && zone.demand).length; const mode = state.mode === 'house' ? tr('groups.followHouse') : state.mode === 'mixed' ? tr('groups.mixed') : modeLabel(state.mode); const preset = state.preset === 'mixed' ? tr('groups.mixed') : zonePresetLabel(state.preset); const events = members.flatMap(zone => (zone.next_events || []).map(event => ({...event, label:`${zone.zone_name}: ${event.label}`}))) .sort((a,b) => new Date(a.at) - new Date(b.at)).slice(0, 2); return `
    ${esc(tr('groups.group'))}

    ${esc(group.name)}

    ${esc(powerEnabled ? tr('common.on') : tr('common.off'))}
    ${esc(mode)}·${esc(preset)}

    ${esc(tr('plan.groupSummary', {zones:members.length, demand}))}

    `; }).join(''); const zoneGroups = new Map(); (app.groups || []).forEach(group => (group.zone_ids || []).forEach(zoneId => { const names = zoneGroups.get(zoneId) || []; names.push(group.name); zoneGroups.set(zoneId, names); })); const zones = allZones.map(zone => { const events = (zone.next_events || []).slice(0, 2); const target = zone.target_temperature == null ? '--' : Number(zone.target_temperature).toFixed(1); const groupNames = zoneGroups.get(zone.zone_id) || []; const scope = groupNames.length ? `${tr('groups.group')}: ${groupNames.join(' · ')}` : (zone.device_name || tr('common.noDevice')); const planState = zoneRuntimeStatusLabel(zone, zone.mode || 'off'); return `
    ${esc(scope)}

    ${esc(zone.zone_name)}

    ${esc(planState)}
    ${fmtTemp(zone.current_temperature)}${esc(target)}°C

    ${esc(houseModeLabel(zone.mode || 'off'))} · ${esc(zonePresetLabel(zone.preset))}${zone.current_schedule_name ? ` · ${esc(zone.current_schedule_name)}` : ''}

    `; }).join(''); const rules = (plan.rules || []).filter(rule => rule.enabled); const ruleCard = rules.length ? `
    ${esc(tr('plan.rules'))}

    ${esc(tr('plan.ruleCount', {count:rules.length}))}

    ` : ''; host.innerHTML = house + groupCards + zones + ruleCard; } async function loadControlPlan() { try { app.controlPlan = await api('/api/control-plan'); renderControlPlan(); renderSimulationPage(); } catch (error) { console.warn('Unable to load control plan:', error); } } function simulationTime(value, options = {}) { if (!value) return '—'; const date = new Date(value); return date.toLocaleString(locale(), {weekday: options.withDay === false ? undefined : 'short', hour:'2-digit', minute:'2-digit'}).replace(',', ''); } function simulationNumeric(value) { const n = Number(value); return Number.isFinite(n) ? n : null; } function simulationOutdoorAssist(mode, outdoor, room, target) { if (outdoor == null || room == null || target == null) return 0; const roomError = Math.abs(room - target); const weather = mode === 'heat' ? clamp((5 - outdoor) / 15, 0, 1) : clamp((outdoor - 30) / 10, 0, 1); return clamp(weather * clamp(roomError, 0, 2) * 0.5, 0, 1); } function simulationRoundDeviceSetpoint(mode, demand, value) { const safe = clamp(Number(value) || 0, 16, 30); if (mode === 'heat' && demand) return Math.ceil(safe); if (mode === 'heat' && !demand) return Math.floor(safe); if (mode !== 'heat' && demand) return Math.floor(safe); return Math.ceil(safe); } function simulationSmartFanSpeed(mode, room, target, outdoor, demand) { if (!demand) return 1; const error = Math.abs(room - target); const extremeWeather = mode === 'heat' ? outdoor != null && outdoor <= 0 : outdoor != null && outdoor >= 32; if (error >= 2 || extremeWeather) return 3; if (error >= 1) return 2; return 0; } function simulationStatusInfo(status) { const map = { disabled: {label: tr('common.disabled'), badge: ''}, off: {label: tr('house.noControl'), badge: ''}, waiting: {label: tr('simulation.waitingForData'), badge: ''}, demand: {label: tr('simulation.stateDemand'), badge: 'active'}, satisfied: {label: tr('simulation.stateSatisfied'), badge: ''}, }; return map[status] || {label: status, badge: ''}; } function buildZoneSimulation(zone, planZone) { const device = app.devices.find(item => item.id === (planZone?.device_id || zone?.device_id)); const mode = planZone?.mode || zone?.effective_mode || zone?.mode || app.settings?.house_mode || 'off'; const current = simulationNumeric(planZone?.current_temperature ?? zone?.current_temperature ?? zone?.device_temperature ?? device?.current_temperature); const target = mode === 'off' ? null : simulationNumeric(planZone?.target_temperature ?? zone?.effective_setpoint ?? zone?.manual_setpoint ?? zone?.setpoint); const outdoor = simulationNumeric(app.outdoorTemperature ?? app.controlPlan?.outdoor_temperature); const hysteresis = Math.max(simulationNumeric(zone?.hysteresis) || 0.4, 0.1); const half = hysteresis / 2; let status = 'waiting'; let demand = false; if (!zone?.enabled || planZoneEffectiveEnabled(planZone) === false) status = 'disabled'; else if (mode === 'off') status = 'off'; else if (current == null || target == null) status = 'waiting'; else { if (mode === 'heat') { if (current <= target - half) demand = true; else if (current >= target + half) demand = false; else demand = !!zone?.demand; } else { if (current >= target + half) demand = true; else if (current <= target - half) demand = false; else demand = !!zone?.demand; } status = demand ? 'demand' : 'satisfied'; } const standbyOffset = Math.max(simulationNumeric(zone?.standby_offset_c) || 0.5, 0.5); const assist = current == null || target == null ? 0 : simulationOutdoorAssist(mode, outdoor, current, target); const activeTarget = current == null || target == null ? null : (mode === 'heat' ? target + assist : target - assist); const standbyTarget = target == null ? null : (mode === 'heat' ? target - standbyOffset : target + standbyOffset); const desiredDeviceTarget = current == null || target == null ? null : simulationRoundDeviceSetpoint(mode, demand, demand ? activeTarget : standbyTarget); const nightActive = !!app.controlPlan?.night_mode_active; const nightMaxFan = clamp(Number(app.controlPlan?.night_mode_max_fan_speed || 1), 1, 5); let fanSpeed = simulationNumeric(device?.fan_speed); if (mode !== 'off') { fanSpeed = zone?.smart_fan && current != null && target != null ? simulationSmartFanSpeed(mode, current, target, outdoor, demand) : fanSpeed; if (nightActive) { if (zone?.smart_fan) fanSpeed = fanSpeed === 0 ? 1 : Math.min(fanSpeed ?? nightMaxFan, nightMaxFan); else if (fanSpeed === 0 || fanSpeed == null || fanSpeed > nightMaxFan) fanSpeed = nightMaxFan; } } const quiet = mode === 'off' ? tr('simulation.manual') : nightActive && app.settings?.night_mode?.force_quiet ? tr('simulation.quietIfSupported') : (zone?.smart_fan ? (!demand ? tr('simulation.quietIfSupported') : tr('common.off')) : tr('simulation.manual')); const nativeSleep = mode !== 'off' && nightActive && app.settings?.night_mode?.use_native_sleep && device?.supports_sleep === true; const reasoning = !zone?.enabled || planZoneEffectiveEnabled(planZone) === false ? tr('simulation.reasonDisabled') : mode === 'off' ? tr('simulation.reasonHouseOff') : current == null || target == null ? tr('simulation.reasonWaiting') : demand ? tr('simulation.reasonDemand', {mode: modeLabel(mode), target: target.toFixed(1), hysteresis: hysteresis.toFixed(1)}) : tr('simulation.reasonSatisfied', {target: target.toFixed(1), standby: standbyTarget.toFixed(1)}); return {device, mode, current, target, demand, status, outdoor, hysteresis, assist, activeTarget, standbyTarget, desiredDeviceTarget, fanSpeed, quiet, nativeSleep, reasoning}; } function simulationRuleAction(rule) { const bits = []; if (rule.action?.power != null) bits.push(`${tr('common.power')}: ${rule.action.power ? tr('common.on') : tr('common.off')}`); if (rule.action?.mode) bits.push(`${tr('common.mode')}: ${rule.action_group_id && rule.action.mode === 'auto' ? tr('groups.followHouse') : modeLabel(rule.action.mode)}`); if (rule.action?.target_temperature != null) bits.push(`${tr('common.temperature')}: ${fmtTemp(rule.action.target_temperature)}`); if (rule.action_preset) bits.push(`${tr('groups.profile')}: ${zonePresetLabel(rule.action_preset)}`); return bits.length ? bits.join(' · ') : tr('simulation.noActionPreview'); } function simulationGroupsForZone(zoneId) { return app.groups.filter(group => (group.zone_ids || []).includes(zoneId)); } function renderSimulationScopeControls(plan) { const scope = $('#simulationScope'); const target = $('#simulationTarget'); if (!scope || !target) return; if (!['units','groups'].includes(app.simulationScope)) app.simulationScope = 'units'; scope.value = app.simulationScope; const zones = plan?.zones || []; const options = app.simulationScope === 'groups' ? [{id:'all', name:tr('simulation.allGroups')}, ...app.groups.map(group => ({id:group.id, name:group.name}))] : [{id:'all', name:tr('simulation.allUnits')}, ...zones.map(zone => ({id:zone.zone_id, name:`${zone.zone_name} · ${zone.device_name || tr('common.device')}`}))]; if (!options.some(option => option.id === app.simulationTarget)) app.simulationTarget = 'all'; target.innerHTML = options.map(option => ``).join(''); target.value = app.simulationTarget; } function simulationFilteredZones(plan) { const zones = plan?.zones || []; if (app.simulationScope === 'groups') { if (app.simulationTarget === 'all') { const grouped = new Set(app.groups.flatMap(group => group.zone_ids || [])); return zones.filter(zone => grouped.has(zone.zone_id)); } const group = app.groups.find(item => item.id === app.simulationTarget); const wanted = new Set(group?.zone_ids || []); return zones.filter(zone => wanted.has(zone.zone_id)); } return app.simulationTarget === 'all' ? zones : zones.filter(zone => zone.zone_id === app.simulationTarget); } function simulationRuleVisible(rule, zones) { if (app.simulationTarget === 'all') return true; const zoneIds = new Set(zones.map(zone => zone.zone_id)); const deviceIds = new Set(zones.map(zone => zone.device_id).filter(Boolean)); if (app.simulationScope === 'groups') { if (rule.action_group_id === app.simulationTarget) return true; return deviceIds.has(rule.trigger_device_id) || deviceIds.has(rule.action_device_id); } const directZone = [...zoneIds][0]; if (simulationGroupsForZone(directZone).some(group => group.id === rule.action_group_id)) return true; return deviceIds.has(rule.trigger_device_id) || deviceIds.has(rule.action_device_id); } function simulationLaneContext(planZone) { if (app.simulationScope !== 'groups') return planZone.device_name || ''; const groups = simulationGroupsForZone(planZone.zone_id); return groups.length ? groups.map(group => group.name).join(' · ') : (planZone.device_name || ''); } function updateSimulationUrl() { if (app.currentView !== 'simulation') return; const params = new URLSearchParams(); if (app.standaloneSimulation) params.set('standalone', '1'); if (app.simulationScope !== 'units') params.set('scope', app.simulationScope); if (app.simulationTarget !== 'all') params.set('target', app.simulationTarget); const query = params.toString(); updateBrowserUrl(`/simulation${query ? `?${query}` : ''}`, true); } function renderSimulationPage() { const summaryHost = $('#simulationSummary'); const timelineHost = $('#simulationTimeline'); const boardHost = $('#simulationFlowBoard'); const rulesHost = $('#simulationRules'); if (!summaryHost || !timelineHost || !boardHost || !rulesHost) return; const plan = app.controlPlan; if (!plan) { summaryHost.innerHTML = `
    ${esc(tr('plan.loading'))}
    `; timelineHost.innerHTML = ''; boardHost.innerHTML = ''; rulesHost.innerHTML = ''; return; } renderSimulationScopeControls(plan); const zones = simulationFilteredZones(plan); const enabledZones = zones.filter(zone => zone.enabled); const demandingZones = enabledZones.filter(zone => zone.demand); const nightOn = !!plan.night_mode_active; summaryHost.innerHTML = [ {label: tr('simulation.houseMode'), value: houseModeLabel(plan.house_mode || 'off'), note: tr('simulation.strategy', {strategy:(plan.control_strategy || 'setpoint')==='setpoint'?tr('plan.strategySetpoint'):(plan.control_strategy || 'setpoint')})}, {label: tr('settings.nightMode'), value: nightOn ? tr('common.active') : (app.settings?.night_mode?.enabled ? tr('simulation.scheduled') : tr('common.disabled')), note: `${plan.night_mode_start || '22:00'} → ${plan.night_mode_end || '06:00'} · ${tr('simulation.fanMax')} ${fanLabel(plan.night_mode_max_fan_speed || 1)}`}, {label: tr('simulation.outdoor'), value: plan.outdoor_temperature == null ? '—' : fmtTemp(plan.outdoor_temperature), note: tr('simulation.generatedAt', {time: dateTime(plan.generated_at)})}, {label: tr('simulation.activeZones'), value: String(enabledZones.length), note: `${tr('simulation.requestingZones', {count: demandingZones.length})} · ${tr('simulation.rulesLabel')}: ${(plan.rules || []).filter(rule => rule.enabled && simulationRuleVisible(rule, zones)).length}`}, ].map(card => `
    ${esc(card.label)}${esc(card.value)}${esc(card.note)}
    `).join(''); const nodeW = 190, nodeH = 106, rowH = 190, topY = 155; const x = {sensor:45, thermostat:305, decision:565, unit:825, event:1085}; const boardW = 1325, boardH = Math.max(390, topY + zones.length * rowH + 35); const nodes = []; const links = []; const pathBetween = (ax, ay, bx, by, cls='') => { const bend = Math.max(36, (bx - ax) * .45); return ``; }; const verticalLink = (ax, ay, bx, by, cls='bus') => ``; const node = ({left, top, kind, eyebrow, title, value, meta='', badge='', badgeClass=''}) => `
    ${esc(eyebrow)}${badge ? `${esc(badge)}` : ''}

    ${esc(title)}

    ${esc(value)}${meta ? `

    ${esc(meta)}

    ` : ''}
    `; const globalHouseLeft = 305, globalNightLeft = 565, globalTop = 28; nodes.push(node({left:globalHouseLeft,top:globalTop,kind:'logic global',eyebrow:tr('simulation.globalInput'),title:tr('simulation.houseMode'),value:houseModeLabel(plan.house_mode || 'off'),meta:tr('simulation.strategy',{strategy:(plan.control_strategy || 'setpoint')==='setpoint'?tr('plan.strategySetpoint'):(plan.control_strategy || 'setpoint')}),badge:tr('simulation.house')})); nodes.push(node({left:globalNightLeft,top:globalTop,kind:`logic global ${nightOn?'night-active':''}`,eyebrow:tr('simulation.globalInput'),title:tr('settings.nightMode'),value:nightOn?tr('common.active'):(app.settings?.night_mode?.enabled?tr('simulation.scheduled'):tr('common.disabled')),meta:`${plan.night_mode_start || '22:00'} → ${plan.night_mode_end || '06:00'} · ${fanLabel(plan.night_mode_max_fan_speed || 1)}`,badge:nightOn?'☾':'○',badgeClass:nightOn?'active':''})); zones.forEach((planZone, index) => { const zone = app.zones.find(item => item.id === planZone.zone_id); const sim = buildZoneSimulation(zone, planZone); const status = simulationStatusInfo(sim.status); const y = topY + index * rowH; const mid = y + nodeH / 2; const next = (planZone.next_events || [])[0]; const source = zoneControlSourceLabel(planZone.control_source); const sensorName = planZone.control_source === 'external' || planZone.control_source === 'combined' ? haSensorLabel(zone?.ha_entity_id || source) : (planZone.device_name || tr('common.device')); const fanText = sim.fanSpeed == null ? '—' : fanLabel(sim.fanSpeed); const quietText = sim.quiet; const command = sim.desiredDeviceTarget == null ? tr('simulation.noCommand') : `${fmtTemp(sim.desiredDeviceTarget)} · ${fanText}`; const eventValue = next ? simulationTime(next.at) : tr('simulation.noEventShort'); const eventMeta = next ? `${next.label}${next.target_temperature == null ? '' : ` · ${fmtTemp(next.target_temperature)}`}` : tr('plan.noEvents'); nodes.push(`
    ${esc(planZone.zone_name)}${esc(simulationLaneContext(planZone))}
    `); nodes.push(node({left:x.sensor,top:y,kind:'input',eyebrow:tr('simulation.stepRoom'),title:sensorName,value:fmtTemp(sim.current),meta:source,badge:'●',badgeClass:sim.current == null?'':'active'})); nodes.push(node({left:x.thermostat,top:y,kind:'logic',eyebrow:tr('simulation.thermostat'),title:planZone.zone_name,value:sim.target == null?'—':fmtTemp(sim.target),meta:`${zonePresetLabel(planZone.preset)} · ${tr('simulation.hysteresis',{value:sim.hysteresis.toFixed(1)})}`,badge:houseModeLabel(sim.mode)})); nodes.push(node({left:x.decision,top:y,kind:`logic decision ${sim.demand?'demand':'satisfied'}`,eyebrow:tr('simulation.stepDecision'),title:status.label,value:sim.demand?tr('simulation.callForComfort'):tr('simulation.standby'),meta:sim.reasoning,badge:sim.demand?'▶':'✓',badgeClass:status.badge})); const sleepMeta = sim.nativeSleep ? ` · ${tr('devices.sleep')}: ${tr('common.on')}` : ''; nodes.push(node({left:x.unit,top:y,kind:'action',eyebrow:tr('simulation.stepCommand'),title:planZone.device_name || tr('common.device'),value:command,meta:`${tr('devices.quiet')}: ${quietText}${sleepMeta}`,badge:sim.mode==='off'?tr('simulation.manual'):(sim.demand?tr('simulation.running'):tr('simulation.idle')),badgeClass:sim.demand?'active':''})); nodes.push(node({left:x.event,top:y,kind:'event',eyebrow:tr('simulation.nextEvent'),title:eventValue,value:next?.preset ? zonePresetLabel(next.preset) : tr('simulation.schedule'),meta:eventMeta,badge:'›'})); links.push(pathBetween(x.sensor+nodeW,mid,x.thermostat,mid,'input-link')); links.push(pathBetween(x.thermostat+nodeW,mid,x.decision,mid,'logic-link')); links.push(pathBetween(x.decision+nodeW,mid,x.unit,mid,sim.demand?'active-link':'logic-link')); links.push(pathBetween(x.unit+nodeW,mid,x.event,mid,'action-link')); links.push(verticalLink(globalHouseLeft+nodeW/2,globalTop+nodeH,x.thermostat+nodeW/2,y,'bus')); if (app.settings?.night_mode?.enabled) links.push(verticalLink(globalNightLeft+nodeW/2,globalTop+nodeH,x.decision+nodeW/2,y,nightOn?'night-link':'bus')); }); boardHost.style.width = `${boardW}px`; boardHost.style.height = `${boardH}px`; boardHost.innerHTML = `${nodes.join('')}`; const timelineItems = []; if (app.simulationTarget === 'all') (plan.next_events || []).forEach(event => timelineItems.push({scope: tr('simulation.house'), event})); zones.forEach(zone => (zone.next_events || []).forEach(event => timelineItems.push({scope: zone.zone_name, event, device: zone.device_name}))); timelineItems.sort((a, b) => new Date(a.event.at) - new Date(b.event.at)); const unique = [], seen = new Set(); for (const item of timelineItems) { const key = `${item.scope}|${item.event.at}|${item.event.label}`; if (seen.has(key)) continue; seen.add(key); unique.push(item); if (unique.length >= 16) break; } timelineHost.innerHTML = unique.length ? unique.map(item => { const target = item.event.target_temperature == null ? '' : ` · ${fmtTemp(item.event.target_temperature)}`; return `
    ${esc(item.scope)}

    ${esc(item.event.label)}${esc(target)}${item.device ? ` · ${esc(item.device)}` : ''}

    `; }).join('') : `
    ${esc(tr('plan.noEvents'))}
    `; const activeRules = (plan.rules || []).filter(rule => rule.enabled && simulationRuleVisible(rule, zones)); rulesHost.innerHTML = activeRules.length ? activeRules.map(rule => `
    ${esc(tr('common.automation'))}

    ${esc(rule.name)}

    ${esc(automationTriggerLabel(rule))}

    ${esc(tr('simulation.ruleOnDevice', {device: rule.action_device_name || tr('common.noDevice')}))}

    ${esc(simulationRuleAction(rule))}
    ${esc(tr('automations.last'))}: ${esc(dateTime(rule.last_fired_at))}${esc(tr('simulation.nextReady'))}: ${esc(dateTime(rule.next_ready_at))}
    `).join('') : `
    ${esc(tr('simulation.noRules'))}
    `; } function scheduleControlPlanLoad() { clearTimeout(app.controlPlanTimer); app.controlPlanTimer = setTimeout(loadControlPlan, 180); }