This commit is contained in:
Mateusz Gruszczyński
2026-09-21 22:58:30 +02:00
parent 3b969c8754
commit d80005de33
33 changed files with 722 additions and 94 deletions
+81 -3
View File
@@ -23,7 +23,8 @@ function renderSummary() {
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 emergencyActive = !!(app.house?.emergency_stop_enabled || app.controlPlan?.emergency_stop_active);
const demand = emergencyActive ? 0 : 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)}<small>°C</small>`;
@@ -46,9 +47,23 @@ function renderHouseClimate() {
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 powerOn = $('#housePowerOn'), powerOff = $('#housePowerOff');
const emergencyButton = $('#houseEmergencyStop');
const emergencyActive = !!(app.house?.emergency_stop_enabled || app.controlPlan?.emergency_stop_active);
const hasAutomaticControl = (app.zones || []).some(zone => zone.enabled)
|| (app.schedules || []).some(item => item.enabled)
|| (app.automations || []).some(item => item.enabled);
if (powerOn) { powerOn.classList.remove('active'); powerOn.removeAttribute('aria-pressed'); }
if (powerOff) { powerOff.classList.remove('active'); powerOff.removeAttribute('aria-pressed'); }
if (emergencyButton) {
emergencyButton.hidden = !emergencyActive && !hasAutomaticControl;
emergencyButton.dataset.value = emergencyActive ? 'false' : 'true';
emergencyButton.classList.toggle('resume', emergencyActive);
emergencyButton.classList.toggle('active', emergencyActive);
emergencyButton.setAttribute('aria-pressed', String(emergencyActive));
emergencyButton.textContent = tr(emergencyActive ? 'house.emergencyResume' : 'house.emergencyStop');
emergencyButton.title = tr(emergencyActive ? 'house.emergencyResumeHint' : 'house.emergencyStopHint');
}
node.innerHTML = `<div class="house-climate-head"><div><span class="eyebrow">${esc(tr('house.seasonMode'))}</span><h3>${esc(tr('house.smartThermostat'))}</h3><p>${esc(tr('house.setpointStrategy'))}</p></div><button type="button" class="outside-pill" data-action="open-outdoor-history" title="${esc(tr('house.outdoorHistoryOpen'))}" aria-label="${esc(tr('house.outdoorHistoryOpen'))}"><small>${esc(tr('house.outdoor'))}</small><strong>${esc(outdoor)}</strong></button></div>
<div class="house-mode-row">
<button class="${mode === 'cool' ? 'active' : ''}" data-action="house-mode" data-value="cool" aria-pressed="${mode === 'cool'}">${esc(tr('mode.cool'))}</button>
@@ -58,6 +73,68 @@ function renderHouseClimate() {
<div class="preset-row house-preset-row">${['auto', 'comfort', 'sleep', 'away'].map(p => `<button class="${preset === p ? 'active' : ''}" data-action="house-preset" data-value="${p}" aria-pressed="${preset === p}">${esc(p === 'sleep' ? tr('house.sleepAll') : p === 'comfort' ? tr('house.comfortAll') : p === 'away' ? tr('house.awayAll') : tr('house.autoAll'))}</button>`).join('')}</div>`;
}
function planBlockedReasonLabel(reason) {
const key = {
emergency_stop: 'plan.reasonEmergency',
manual_override: 'plan.reasonManual',
zone_disabled: 'plan.reasonDisabled',
offline: 'plan.reasonOffline',
no_active_thermostat_intent: 'plan.reasonNoIntent',
}[reason];
return key ? tr(key) : reason;
}
function planZoneReason(zone) {
if (zone.blocked_reason) return planBlockedReasonLabel(zone.blocked_reason);
if (zone.local_thermostat_power === false) return tr('plan.reasonLocalOff');
if (zone.local_thermostat_power === true) return tr('plan.reasonLocalOn');
const current = zone.current_temperature == null ? NaN : Number(zone.current_temperature);
const target = zone.target_temperature == null ? NaN : Number(zone.target_temperature);
if (zone.demand && Number.isFinite(current) && Number.isFinite(target)) {
if (zone.mode === 'heat') return tr('plan.reasonHeatDemand', { current: fmtTemp(current), target: fmtTemp(target) });
if (zone.mode === 'cool') return tr('plan.reasonCoolDemand', { current: fmtTemp(current), target: fmtTemp(target) });
}
if (Number.isFinite(current) && Number.isFinite(target)) {
return tr('plan.reasonTargetSatisfied', { current: fmtTemp(current), target: fmtTemp(target) });
}
if (zone.current_schedule_name) return tr('plan.reasonSchedule', { schedule: zone.current_schedule_name });
return tr('plan.reasonWaiting');
}
function planReasonCard(plan) {
const zones = (plan.zones || []).map(zone => {
const reason = planZoneReason(zone);
const source = zone.control_owner === 'direct_manual' ? tr('plan.reasonSourceManual')
: zone.control_owner === 'local_thermostat' ? tr('plan.reasonSourceThermostat')
: zone.current_schedule_name ? tr('plan.reasonSourceSchedule', { schedule: zone.current_schedule_name })
: tr('plan.reasonSourceAutomation');
return `<li><div><strong>${esc(zone.zone_name)}</strong><small>${esc(source)}</small></div><span>${esc(reason)}</span></li>`;
}).join('');
const recentRules = (plan.rules || [])
.filter(rule => rule.enabled && rule.last_fired_at)
.sort((a, b) => new Date(b.last_fired_at) - new Date(a.last_fired_at))
.slice(0, 4)
.map(rule => {
const when = new Date(rule.last_fired_at).toLocaleString(locale(), { weekday: 'short', hour: '2-digit', minute: '2-digit' });
const triggerDevice = rule.trigger_device_id ? (app.devices || []).find(device => device.id === rule.trigger_device_id) : null;
const current = triggerDevice?.current_temperature == null ? null : Number(triggerDevice.current_temperature);
const trigger = Number.isFinite(current)
? tr('plan.reasonTriggerReading', { trigger: automationTriggerLabel(rule), current: fmtTemp(current) })
: automationTriggerLabel(rule);
return `<li><div><strong>${esc(rule.name)}</strong><small>${esc(trigger)}</small></div><span>${esc(tr('plan.reasonLastTriggered', { when }))}</span></li>`;
}).join('');
const emergency = plan.emergency_stop_active
? `<div class="plan-reason-alert"><strong>${esc(tr('house.emergencyActive'))}</strong><span>${esc(tr('house.emergencyActiveHint'))}</span></div>`
: '';
const rules = recentRules
? `<div class="plan-reason-subsection"><h4>${esc(tr('plan.recentTriggers'))}</h4><ul class="plan-reason-list">${recentRules}</ul></div>`
: '';
return `<article class="panel plan-card plan-reasons"><div class="plan-card-head"><div><span class="eyebrow">${esc(tr('plan.reasonsEyebrow'))}</span><h3>${esc(tr('plan.reasonsTitle'))}</h3></div></div>${emergency}<div class="plan-reason-subsection"><h4>${esc(tr('plan.currentReasons'))}</h4><ul class="plan-reason-list">${zones || `<li class="plan-reason-empty">${esc(tr('plan.noReasons'))}</li>`}</ul></div>${rules}</article>`;
}
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)}`;
@@ -121,7 +198,7 @@ function renderControlPlan() {
}).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 => {
const ruleCard = rules.length ? `<article class="panel plan-card plan-rules ${plan.emergency_stop_active ? 'disabled' : ''}"><div class="plan-card-head"><div><span class="eyebrow">${esc(tr('plan.rules'))}</span><h3>${esc(tr(plan.emergency_stop_active ? 'plan.ruleCountPaused' : '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;
@@ -129,7 +206,8 @@ function renderControlPlan() {
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;
const reasonCard = planReasonCard(plan);
const content = house + groupCards + zones + ruleCard + reasonCard;
host.innerHTML = content;
if (section) section.hidden = !content;
}