This commit is contained in:
Mateusz Gruszczyński
2026-08-25 19:21:27 +02:00
parent dce9ef7270
commit dcdb821c60
12 changed files with 120 additions and 47 deletions
+35 -10
View File
@@ -686,24 +686,41 @@ function zoneCard(zone, detailed = true) {
const target = Number(zone.effective_setpoint ?? zone.setpoint);
const manual = zone.manual_preset || 'auto';
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 hasManualOverride = zone.manual_preset != null || zone.manual_setpoint != null;
const override = 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 enabledControl = detailed
? `<span class="badge ${zone.enabled ? 'active' : ''}">${esc(state)}</span>`
: `<button type="button" class="zone-enable-toggle ${zone.enabled ? 'active' : ''}" data-action="zone-enabled" data-id="${esc(zone.id)}" data-value="${zone.enabled ? 'false' : 'true'}" aria-label="${esc(tr(zone.enabled ? 'zones.disable' : 'zones.enable'))}"><span>${zone.enabled ? '✓' : '○'}</span>${esc(state)}</button>`;
const quickClass = detailed ? '' : ' quick-control-card quick-thermostat-control';
const quickRow = detailed ? '' : ' quick-control-row';
return `<article class="list-card zone-thermostat${quickClass} ${zone.demand ? 'demanding' : ''} ${zone.enabled ? '' : 'zone-disabled'}">
<div class="list-card-head"><div><h3>${esc(zone.name)}</h3><p>${esc(device?.name || tr('common.noDevice'))} · ${esc(zonePresetLabel(zone.active_preset))}</p></div>${enabledControl}</div>
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 `<article class="list-card zone-config-card ${zone.demand ? 'demanding' : ''} ${zone.enabled ? '' : 'zone-disabled'}" data-zone-config="${esc(zone.id)}">
<div class="list-card-head"><div><span class="eyebrow">${esc(tr('zones.configuration'))}</span><h3>${esc(zone.name)}</h3><p>${esc(device?.name || tr('common.noDevice'))} · ${esc(tr('groups.group'))}: ${esc(groupText)}</p></div><span class="badge ${zone.enabled ? 'active' : ''}">${esc(state)}</span></div>
<div class="zone-config-status">
<div class="zone-config-temperature"><small>${esc(tr('zones.currentStatus'))}</small><div><span>${fmtTemp(roomTemperature)}</span><b>→</b><strong>${Number.isFinite(target) ? `${target.toFixed(1)}°C` : '—'}</strong></div></div>
<div class="zone-config-runtime"><span class="badge ${zone.enabled && zone.demand ? 'active' : ''}">${esc(zone.enabled ? (zone.demand ? tr('zones.requesting') : tr('zones.satisfied')) : tr('common.disabled'))}</span><span>${esc(houseModeLabel(effectiveMode))} · ${esc(zonePresetLabel(zone.active_preset))}</span><small>${esc(override)}</small></div>
</div>
<div class="zone-config-grid">
<div><small>${esc(tr('zones.modePolicy'))}</small><strong>${esc(policy)}</strong></div>
<div><small>${esc(tr('zones.source'))}</small><strong>${esc(zoneStrategyLabel(zone))}</strong></div>
<div><small>${esc(tr('zones.hysteresis'))}</small><strong>${Number(zone.hysteresis ?? 0.6).toFixed(1)}°C</strong></div>
<div><small>${esc(tr('zones.smartFan'))}</small><strong>${esc(tr(zone.smart_fan === false ? 'common.off' : 'common.on'))}</strong></div>
</div>
<div class="sensor-detail">${esc(sensorDetails)}</div>
<div class="card-footer"><small>${esc(tr('zones.controlOnDashboard'))}</small><div class="card-menu"><button class="primary" data-action="zone-go-control" data-id="${esc(zone.id)}">${esc(tr('zones.controlNow'))}</button><button data-action="edit-zone" data-id="${esc(zone.id)}">${esc(tr('actions.edit'))}</button><button class="danger" data-action="delete-zone" data-id="${esc(zone.id)}">${esc(tr('actions.delete'))}</button></div></div>
</article>`;
}
return `<article class="list-card zone-thermostat quick-control-card quick-thermostat-control ${zone.demand ? 'demanding' : ''} ${zone.enabled ? '' : 'zone-disabled'}" data-zone-card="${esc(zone.id)}">
<div class="list-card-head"><div><h3>${esc(zone.name)}</h3><p>${esc(device?.name || tr('common.noDevice'))} · ${esc(zonePresetLabel(zone.active_preset))}</p></div><button type="button" class="zone-enable-toggle ${zone.enabled ? 'active' : ''}" data-action="zone-enabled" data-id="${esc(zone.id)}" data-value="${zone.enabled ? 'false' : 'true'}" aria-label="${esc(tr(zone.enabled ? 'zones.disable' : 'zones.enable'))}"><span>${zone.enabled ? '✓' : '○'}</span>${esc(state)}</button></div>
<div class="thermostat-main"><div><small>${esc(tr('zones.measurement'))}</small><strong>${fmtTemp(roomTemperature)}</strong></div><div class="temperature-control compact"><button data-action="zone-temperature" data-id="${esc(zone.id)}" data-delta="-0.5"></button><div class="target-temp compact">${Number.isFinite(target)?target.toFixed(1):'--'}<small>°C</small></div><button data-action="zone-temperature" data-id="${esc(zone.id)}" data-delta="0.5">+</button></div><div><small>${esc(tr('zones.deviceTarget'))}</small><strong>${fmtTemp(zone.device_setpoint)}</strong></div></div>
<div class="preset-row${quickRow}${detailed ? '' : ' quick-control-row-4'}">
<div class="preset-row quick-control-row quick-control-row-4">
${['auto','comfort','sleep','away'].map(preset=>`<button class="${manual===preset?'active':''}" data-action="zone-preset" data-id="${esc(zone.id)}" data-value="${preset}">${esc(preset==='sleep'?tr('zones.sleepNow'):zonePresetLabel(preset))}</button>`).join('')}
</div>
<div class="mode-row zone-mode-row${quickRow}${detailed ? '' : ' quick-control-row-3'}"><button class="${mode==='house'?'active':''}" data-action="zone-mode" data-id="${esc(zone.id)}" data-value="house">${esc(tr('zones.followHouseShort'))}</button><button class="${mode==='heat'?'active':''}" data-action="zone-mode" data-id="${esc(zone.id)}" data-value="heat">${esc(modeLabel('heat'))}</button><button class="${mode==='cool'?'active':''}" data-action="zone-mode" data-id="${esc(zone.id)}" data-value="cool">${esc(modeLabel('cool'))}</button></div>
<div class="mode-row zone-mode-row quick-control-row quick-control-row-3"><button class="${mode==='house'?'active':''}" data-action="zone-mode" data-id="${esc(zone.id)}" data-value="house">${esc(tr('zones.followHouseShort'))}</button><button class="${mode==='heat'?'active':''}" data-action="zone-mode" data-id="${esc(zone.id)}" data-value="heat">${esc(modeLabel('heat'))}</button><button class="${mode==='cool'?'active':''}" data-action="zone-mode" data-id="${esc(zone.id)}" data-value="cool">${esc(modeLabel('cool'))}</button></div>
<div class="zone-state-line"><span>${zone.demand ? esc(tr('zones.requesting')) : esc(tr('zones.satisfied'))}</span><span>${esc(override)}</span></div>
${detailed ? `<div class="sensor-detail">${esc(sensorDetails)}</div><div class="card-footer"><small>${esc(tr('zones.quickHintSmart'))}</small><div class="card-menu"><button data-action="edit-zone" data-id="${esc(zone.id)}">${esc(tr('actions.edit'))}</button><button class="danger" data-action="delete-zone" data-id="${esc(zone.id)}">${esc(tr('actions.delete'))}</button></div></div>` : ''}
</article>`;
}
@@ -1685,6 +1702,14 @@ document.addEventListener('click', async event => {
if (action === 'zone-mode') return sendZoneControl(button.dataset.id,{mode:button.dataset.value});
if (action === 'zone-preset') return sendZoneControl(button.dataset.id,{preset:button.dataset.value});
if (action === 'zone-enabled') return sendZoneControl(button.dataset.id,{enabled:button.dataset.value==='true'});
if (action === 'zone-go-control') {
showView('dashboard', {scroll:false});
const target = [...document.querySelectorAll('[data-zone-card]')].find(card => card.dataset.zoneCard === button.dataset.id);
const disclosure = target?.closest('details.dashboard-disclosure');
if (disclosure) disclosure.open = true;
requestAnimationFrame(() => target?.scrollIntoView({behavior:'smooth', block:'center'}));
return;
}
if (action === 'debug-clear') { app.debugLines = []; renderDebugOverlay(); return; }
if (action === 'edit-group') return populateGroup(button.dataset.id);
if (action === 'delete-group') return deleteEntity('groups', button.dataset.id, 'groups.group');
+2 -2
View File
@@ -79,7 +79,7 @@
<section class="view" data-view="zones">
<div class="section-heading"><div><span class="eyebrow" data-i18n="zones.automation">Automation</span><h1 data-i18n="nav.zones">Zones</h1></div><button class="secondary" data-open="zoneDialog" data-i18n="zones.new">New zone</button></div>
<p class="lead" data-i18n="zones.description">Each zone is a fast thermostat. It normally follows the house heating/cooling mode and changes the AC setpoint instead of cycling power.</p>
<p class="lead" data-i18n="zones.description">Zones define thermostat logic and sensor configuration. Use Quick thermostats on the Dashboard for current temperature changes and presets.</p>
<div class="list-grid" id="zoneList"></div>
</section>
@@ -375,7 +375,7 @@
<label><span data-i18n="common.name">Name</span><input name="name" required data-i18n-placeholder="placeholder.livingRoom" placeholder="Living room"></label>
<label><span data-i18n="common.device">Device</span><select name="device_id" required></select></label>
<div class="copy-settings-row"><select id="copyZoneSource"><option value="" data-i18n="zones.copyFrom">Copy thermostat settings from…</option></select><button type="button" class="secondary" id="copyZoneSettings" data-i18n="zones.copySettings">Copy settings</button></div>
<div class="two"><label><span data-i18n="zones.modePolicy">Mode policy</span><select name="mode_policy"><option value="house" data-i18n="zones.followHouse">Follow global mode</option><option value="cool" data-i18n="mode.cool">Cooling only</option><option value="heat" data-i18n="mode.heat">Heating only</option></select></label><label><span data-i18n="common.targetC">Manual target °C</span><input type="text" inputmode="decimal" step="0.5" min="8" max="30" name="setpoint" value="23"></label></div>
<label><span data-i18n="zones.modePolicy">Mode policy</span><select name="mode_policy"><option value="house" data-i18n="zones.followHouse">Follow global mode</option><option value="cool" data-i18n="zones.coolOnly">Cooling only</option><option value="heat" data-i18n="zones.heatOnly">Heating only</option></select></label><input type="hidden" name="setpoint" value="23">
<p class="field-note" data-i18n="zones.modePolicyHint">If this zone should use the global Heating/Cooling mode selected in GREE Controller, leave “Follow global mode” unchanged. Cooling only / Heating only forces a fixed mode for this zone.</p>
<details class="profile-editor"><summary data-i18n="zones.profileTemperatures">Comfort / sleep / away temperatures</summary>
<div class="profile-grid"><strong data-i18n="mode.cool">Cooling</strong><label><span data-i18n="preset.comfort">Comfort</span><input type="text" inputmode="decimal" step="0.5" name="cool_comfort_setpoint" value="23"></label><label><span data-i18n="preset.sleep">Sleep</span><input type="text" inputmode="decimal" step="0.5" name="cool_sleep_setpoint" value="24.5"></label><label><span data-i18n="preset.away">Away</span><input type="text" inputmode="decimal" step="0.5" name="cool_away_setpoint" value="27"></label></div>
+30 -12
View File
@@ -864,16 +864,34 @@ body.simulation-standalone [data-view="simulation"] { min-height:calc(100vh - 36
}
[data-view="dashboard"] .dashboard-summary-card { margin-bottom:10px; }
/* 0.7.0: desktop fit and cleaner unit controls. */
#zoneList .zone-thermostat .card-footer { align-items:flex-start; }
#zoneList .zone-thermostat .card-footer > small {
flex:1 1 auto;
min-width:0;
overflow:visible;
text-overflow:clip;
white-space:normal;
line-height:1.35;
}
#zoneList .zone-thermostat .card-menu { flex:0 0 auto; }
/* 0.7.2: Zones are configuration cards; live controls stay on the Dashboard. */
.zone-config-card { display:grid; gap:12px; overflow:hidden; }
.zone-config-card.demanding { border-color:color-mix(in srgb,var(--accent) 38%,var(--line)); }
.zone-config-card.zone-disabled .zone-config-status { opacity:.68; }
.zone-config-status { display:grid; grid-template-columns:minmax(150px,.8fr) minmax(180px,1.2fr); align-items:center; gap:12px; padding:11px 12px; border:1px solid var(--line); border-radius:14px; background:var(--surface-muted); }
.zone-config-temperature > small, .zone-config-grid small { display:block; margin-bottom:4px; color:var(--muted); font-size:9px; text-transform:uppercase; letter-spacing:.06em; }
.zone-config-temperature > div { display:flex; align-items:baseline; gap:7px; }
.zone-config-temperature span { color:var(--muted); font-size:18px; }
.zone-config-temperature b { color:var(--muted); font-size:11px; }
.zone-config-temperature strong { font-size:23px; letter-spacing:-.03em; }
.zone-config-runtime { display:grid; justify-items:end; gap:4px; min-width:0; color:var(--text-soft); font-size:11px; text-align:right; }
.zone-config-runtime > span:not(.badge), .zone-config-runtime small { max-width:100%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.zone-config-runtime small { color:var(--muted); }
.zone-config-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:7px; }
.zone-config-grid > div { min-width:0; padding:9px 10px; border:1px solid var(--line); border-radius:12px; background:var(--surface-muted); }
.zone-config-grid strong { display:block; overflow:hidden; font-size:11px; text-overflow:ellipsis; white-space:nowrap; }
#zoneList .zone-config-card .sensor-detail { margin-top:0; }
#zoneList .zone-config-card .card-footer { align-items:flex-start; margin-top:0; }
#zoneList .zone-config-card .card-footer > small { flex:1 1 auto; min-width:0; overflow:visible; text-overflow:clip; white-space:normal; line-height:1.35; }
#zoneList .zone-config-card .card-menu { flex:0 0 auto; flex-wrap:wrap; justify-content:flex-end; }
#zoneList .zone-config-card .card-menu .primary { color:var(--accent-text); background:var(--accent); }
/* 0.7.1: quick thermostats and quick manual control now use the shared quick-control classes above. */
@media (max-width:620px) {
.zone-config-status { grid-template-columns:1fr; }
.zone-config-runtime { justify-items:start; text-align:left; }
.zone-config-grid { grid-template-columns:1fr; }
#zoneList .zone-config-card .card-footer { flex-direction:column; }
#zoneList .zone-config-card .card-menu { width:100%; justify-content:flex-start; }
}
/* 0.7.1+: quick thermostats and quick manual control use the shared quick-control classes above. */
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'gree-controller-v071-quick-controls';
const CACHE = 'gree-controller-v072-zone-config';
const SCOPE = new URL(self.registration.scope).pathname.replace(/\/$/, '');
const path = value => `${SCOPE}${value.startsWith('/') ? value : `/${value}`}` || '/';
const ASSETS = [path('/'), path('/styles.css'), path('/app.js'), path('/theme-init.js'), path('/favicon.svg'), path('/manifest.webmanifest'), path('/lang/index.json'), path('/lang/en.json')];