This commit is contained in:
Mateusz Gruszczyński
2026-09-02 09:17:56 +02:00
parent 43441d2fb7
commit 87b68187e7
13 changed files with 119 additions and 58 deletions
+18 -5
View File
@@ -78,15 +78,17 @@ function planZoneEffectiveEnabled(zone) {
function renderControlPlan() {
const host = $('#controlPlan'); if (!host) return;
const section = $('#controlPlanSection');
const plan = app.controlPlan;
if (!plan) {
if (section) section.hidden = false;
host.innerHTML = `<div class="panel plan-loading">${esc(tr('plan.loading'))}</div>`;
return;
}
const allZones = plan.zones || [];
const houseEvents = (plan.next_events || []).slice(0, 3);
const house = `<article class="panel plan-card plan-house"><div class="plan-card-head"><div><span class="eyebrow">${esc(tr('plan.house'))}</span><h3>${esc(houseModeLabel(plan.house_mode || 'off'))}</h3></div><span class="badge active">${esc((plan.control_strategy || 'setpoint') === 'setpoint' ? tr('plan.strategySetpoint') : (plan.control_strategy || 'setpoint'))}</span></div><p>${esc(tr('plan.houseSummary', { zones: allZones.filter(planZoneEffectiveEnabled).length, demand: allZones.filter(zone => planZoneEffectiveEnabled(zone) && zone.demand).length }))}</p><ul class="plan-events">${houseEvents.length ? houseEvents.map(planEventMarkup).join('') : `<li class="muted">${esc(tr('plan.noEvents'))}</li>`}</ul></article>`;
const house = houseEvents.length ? `<article class="panel plan-card plan-house"><div class="plan-card-head"><div><span class="eyebrow">${esc(tr('plan.house'))}</span><h3>${esc(houseModeLabel(plan.house_mode || 'off'))}</h3></div><span class="badge active">${esc((plan.control_strategy || 'setpoint') === 'setpoint' ? tr('plan.strategySetpoint') : (plan.control_strategy || 'setpoint'))}</span></div><p>${esc(tr('plan.houseSummary', { zones: allZones.filter(planZoneEffectiveEnabled).length, demand: allZones.filter(zone => planZoneEffectiveEnabled(zone) && zone.demand).length }))}</p><ul class="plan-events">${houseEvents.map(planEventMarkup).join('')}</ul></article>` : '';
const groupCards = (app.groups || []).map(group => {
const memberIds = new Set(group.zone_ids || []);
@@ -98,7 +100,8 @@ function renderControlPlan() {
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 `<article class="panel plan-card plan-group ${powerEnabled ? '' : 'disabled'}"><div class="plan-card-head"><div><span class="eyebrow">${esc(tr('groups.group'))}</span><h3>${esc(group.name)}</h3></div><span class="badge ${powerEnabled ? 'active' : ''}">${esc(powerEnabled ? tr('common.on') : tr('common.off'))}</span></div><div class="plan-group-state"><strong>${esc(mode)}</strong><span>·</span><strong>${esc(preset)}</strong></div><p>${esc(tr('plan.groupSummary', { zones: members.length, demand }))}</p><ul class="plan-events">${events.length ? events.map(planEventMarkup).join('') : `<li class="muted">${esc(tr(powerEnabled ? 'plan.noEvents' : 'plan.groupOff'))}</li>`}</ul></article>`;
if (!events.length) return '';
return `<article class="panel plan-card plan-group ${powerEnabled ? '' : 'disabled'}"><div class="plan-card-head"><div><span class="eyebrow">${esc(tr('groups.group'))}</span><h3>${esc(group.name)}</h3></div><span class="badge ${powerEnabled ? 'active' : ''}">${esc(powerEnabled ? tr('common.on') : tr('common.off'))}</span></div><div class="plan-group-state"><strong>${esc(mode)}</strong><span>·</span><strong>${esc(preset)}</strong></div><p>${esc(tr('plan.groupSummary', { zones: members.length, demand }))}</p><ul class="plan-events">${events.map(planEventMarkup).join('')}</ul></article>`;
}).join('');
const zoneGroups = new Map();
@@ -109,16 +112,26 @@ function renderControlPlan() {
}));
const zones = allZones.map(zone => {
const events = (zone.next_events || []).slice(0, 2);
if (!events.length) return '';
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 `<article class="panel plan-card plan-zone ${planZoneEffectiveEnabled(zone) ? '' : 'disabled'}"><div class="plan-card-head"><div><span class="eyebrow">${esc(scope)}</span><h3>${esc(zone.zone_name)}</h3></div><span class="badge ${zone.device_manual_override ? 'manual-override' : (planZoneEffectiveEnabled(zone) && zone.demand ? 'active' : '')}">${esc(planState)}</span></div><div class="plan-temp"><span>${fmtTemp(zone.current_temperature)}</span><b>→</b><strong>${esc(target)}<small>°C</small></strong></div><p>${esc(houseModeLabel(zone.mode || 'off'))} · ${esc(zonePresetLabel(zone.preset))}${zone.current_schedule_name ? ` · ${esc(zone.current_schedule_name)}` : ''}</p><ul class="plan-events">${events.length ? events.map(planEventMarkup).join('') : `<li class="muted">${esc(tr('plan.noEvents'))}</li>`}</ul></article>`;
return `<article class="panel plan-card plan-zone ${planZoneEffectiveEnabled(zone) ? '' : 'disabled'}"><div class="plan-card-head"><div><span class="eyebrow">${esc(scope)}</span><h3>${esc(zone.zone_name)}</h3></div><span class="badge ${zone.device_manual_override ? 'manual-override' : (planZoneEffectiveEnabled(zone) && zone.demand ? 'active' : '')}">${esc(planState)}</span></div><div class="plan-temp"><span>${fmtTemp(zone.current_temperature)}</span><b>→</b><strong>${esc(target)}<small>°C</small></strong></div><p>${esc(houseModeLabel(zone.mode || 'off'))} · ${esc(zonePresetLabel(zone.preset))}${zone.current_schedule_name ? ` · ${esc(zone.current_schedule_name)}` : ''}</p><ul class="plan-events">${events.map(planEventMarkup).join('')}</ul></article>`;
}).join('');
const rules = (plan.rules || []).filter(rule => rule.enabled);
const ruleCard = rules.length ? `<article class="panel plan-card plan-rules"><div class="plan-card-head"><div><span class="eyebrow">${esc(tr('plan.rules'))}</span><h3>${esc(tr('plan.ruleCount', { count: rules.length }))}</h3></div></div><ul class="plan-events">${rules.slice(0, 3).map(rule => `<li><time>${esc(automationTriggerLabel(rule))}</time><span>${esc(rule.name)}${esc(rule.action_group_name ? `${tr('groups.group')}: ${rule.action_group_name}` : (rule.action_device_name || ''))}</span></li>`).join('')}</ul></article>` : '';
host.innerHTML = house + groupCards + zones + ruleCard;
const ruleCard = rules.length ? `<article class="panel plan-card plan-rules"><div class="plan-card-head"><div><span class="eyebrow">${esc(tr('plan.rules'))}</span><h3>${esc(tr('plan.ruleCount', { count: rules.length }))}</h3></div></div><ul class="plan-events">${rules.slice(0, 3).map(rule => {
const automation = (app.automations || []).find(item => item.id === rule.id);
const flow = automation?.flow_id ? (app.flows || []).find(item => item.id === automation.flow_id) : null;
const displayName = flow?.name || rule.name;
const target = rule.action_group_name ? `${tr('groups.group')}: ${rule.action_group_name}` : (rule.action_device_name || '');
return `<li><time>${esc(automationTriggerLabel(rule))}</time><span class="plan-rule-summary"><strong>${esc(displayName)}</strong>${target ? `<small>→ ${esc(target)}</small>` : ''}</span></li>`;
}).join('')}</ul></article>` : '';
const content = house + groupCards + zones + ruleCard;
host.innerHTML = content;
if (section) section.hidden = !content;
}
async function loadControlPlan() {
try { app.controlPlan = await api('/api/control-plan'); renderControlPlan(); renderSimulationPage(); }