v0.8.3
This commit is contained in:
+174
-5
@@ -58,6 +58,16 @@ const formatCountdown = seconds => {
|
||||
const remainder = safe % 60;
|
||||
return `${String(minutes).padStart(2,'0')}:${String(remainder).padStart(2,'0')}`;
|
||||
};
|
||||
const formatExtendedCountdown = seconds => {
|
||||
if (!Number.isFinite(seconds)) return '∞';
|
||||
const safe = Math.max(0, Math.floor(seconds));
|
||||
const hours = Math.floor(safe / 3600);
|
||||
const minutes = Math.floor((safe % 3600) / 60);
|
||||
const remainder = safe % 60;
|
||||
return hours > 0
|
||||
? `${String(hours).padStart(2,'0')}:${String(minutes).padStart(2,'0')}:${String(remainder).padStart(2,'0')}`
|
||||
: `${String(minutes).padStart(2,'0')}:${String(remainder).padStart(2,'0')}`;
|
||||
};
|
||||
function updateLocalThermostatCountdowns() {
|
||||
$$('[data-local-resume-countdown]').forEach(node => {
|
||||
const remaining = localResumeSeconds(node.dataset.localResumeCountdown);
|
||||
@@ -66,6 +76,56 @@ function updateLocalThermostatCountdowns() {
|
||||
: tr('zones.localThermostatResuming');
|
||||
});
|
||||
}
|
||||
function temporarySessionStatus(zone) {
|
||||
const session = zone?.temporary_quick_thermostat;
|
||||
if (!session) return null;
|
||||
const now = Date.now();
|
||||
const secondsUntil = value => {
|
||||
const timestamp = value ? new Date(value).getTime() : NaN;
|
||||
return Number.isFinite(timestamp) ? Math.max(0, Math.ceil((timestamp - now) / 1000)) : null;
|
||||
};
|
||||
const safetyRemaining = secondsUntil(session.safety_expires_at);
|
||||
const hardRemaining = secondsUntil(session.expires_at);
|
||||
const target = Number(session.temperature_target ?? zone.effective_setpoint ?? zone.setpoint);
|
||||
const targetText = Number.isFinite(target) ? target.toFixed(1) : '--';
|
||||
const tolerance = Number(session.tolerance_c ?? 0.3).toFixed(1);
|
||||
const operatorKey = ({within:'zones.temporaryWithinShort',at_or_below:'zones.temporaryAtOrBelowShort',at_or_above:'zones.temporaryAtOrAboveShort'})[session.temperature_operator || 'within'] || 'zones.temporaryWithinShort';
|
||||
const condition = tr(operatorKey, {target:targetText, tolerance});
|
||||
|
||||
if (session.finish_kind === 'temperature_stable') {
|
||||
const started = session.condition_started_at ? new Date(session.condition_started_at).getTime() : NaN;
|
||||
const holdSeconds = Number(session.hold_seconds || 0);
|
||||
if (Number.isFinite(started)) {
|
||||
const holdRemaining = Math.max(0, Math.ceil((started + holdSeconds * 1000 - now) / 1000));
|
||||
const effectiveRemaining = safetyRemaining == null ? holdRemaining : Math.min(holdRemaining, safetyRemaining);
|
||||
return {countdown:formatExtendedCountdown(effectiveRemaining), detail:tr('zones.temporaryHoldingStatus', {condition}), kind:session.finish_kind};
|
||||
}
|
||||
return {countdown:safetyRemaining == null ? '∞' : formatExtendedCountdown(safetyRemaining), detail:tr('zones.temporaryWaitingStable', {condition}), kind:session.finish_kind};
|
||||
}
|
||||
if (session.finish_kind === 'temperature_reached') {
|
||||
return {countdown:safetyRemaining == null ? '∞' : formatExtendedCountdown(safetyRemaining), detail:tr('zones.temporaryWaitingReached', {condition}), kind:session.finish_kind};
|
||||
}
|
||||
const detailKey = session.finish_kind === 'schedule_boundary' ? 'zones.temporaryUntilScheduleStatus' : 'zones.temporaryTimeStatus';
|
||||
return {countdown:formatExtendedCountdown(hardRemaining), detail:tr(detailKey), kind:session.finish_kind};
|
||||
}
|
||||
function updateTemporaryThermostatCountdowns() {
|
||||
$$('[data-temporary-countdown-zone]').forEach(node => {
|
||||
const zone = app.zones.find(item => item.id === node.dataset.temporaryCountdownZone);
|
||||
const status = temporarySessionStatus(zone);
|
||||
if (!status) return;
|
||||
node.textContent = status.countdown;
|
||||
node.title = status.detail;
|
||||
});
|
||||
const dialog = $('#temporaryThermostatDialog');
|
||||
if (dialog?.open) {
|
||||
const zone = app.zones.find(item => item.id === $('#temporaryThermostatForm')?.elements.zone_id.value);
|
||||
const status = temporarySessionStatus(zone);
|
||||
if (status) {
|
||||
$('#temporaryThermostatActiveCountdown').textContent = status.countdown;
|
||||
$('#temporaryThermostatActiveDetail').textContent = status.detail;
|
||||
}
|
||||
}
|
||||
}
|
||||
const haSensorLabel = entity => app.sensorAliases?.[entity] || app.settings?.home_assistant?.sensor_aliases?.[entity] || entity;
|
||||
|
||||
function updateConnectionIndicator(status) {
|
||||
@@ -756,11 +816,15 @@ function zoneCard(zone, detailed = true) {
|
||||
const deviceManualOverride = zone.device_manual_override === true;
|
||||
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
|
||||
@@ -768,21 +832,24 @@ function zoneCard(zone, detailed = true) {
|
||||
: (hasManualOverride ? tr('zones.manualNoBoundary') : tr('zones.scheduleControl')));
|
||||
const manualTakeover = deviceManualOverride
|
||||
? `<div class="manual-override-panel"><div><strong>${esc(tr('zones.manualDeviceControl'))}</strong><p>${esc(tr('zones.manualDeviceDescription'))}</p></div><button type="button" data-action="zone-resume-automation" data-resume="device" data-id="${esc(zone.id)}">${esc(tr('zones.resumeAutomation'))}</button></div>`
|
||||
: localThermostatOverride
|
||||
: localThermostatOverride && !temporarySession
|
||||
? `<div class="manual-override-panel local-thermostat-panel"><div><strong>${esc(tr('zones.localThermostatControl'))}</strong>${localThermostatPower === false && localThermostatResumeAt
|
||||
? `<p data-local-resume-countdown="${esc(localThermostatResumeAt)}">${esc(tr('zones.localThermostatOffDescriptionTimed', {time:formatCountdown(localResumeSeconds(localThermostatResumeAt))}))}</p>`
|
||||
: `<p>${esc(tr(localThermostatPower ? 'zones.localThermostatOnDescription' : 'zones.localThermostatOffDescription'))}</p>`}</div><button type="button" data-action="zone-resume-automation" data-resume="local" data-id="${esc(zone.id)}">${esc(tr(localThermostatPower === false ? 'zones.resumeNow' : 'zones.resumeAutomation'))}</button></div>`
|
||||
: '';
|
||||
const localPowerDisabled = !device || device.enabled === false ? ' disabled' : '';
|
||||
const localRequestedOn = localThermostatPower === true ? true : (localThermostatPower === false ? false : !!device?.power);
|
||||
const localThermostatPowerControl = `<div class="zone-local-thermostat-power"><button type="button" class="zone-unit-power-toggle ${localRequestedOn ? 'active' : ''}" title="${esc(tr('zones.unitPowerLocalHint'))}" data-action="zone-device-power" data-id="${esc(zone.id)}" data-value="${localRequestedOn ? 'false' : 'true'}" aria-pressed="${localRequestedOn ? 'true' : 'false'}"${localPowerDisabled}>${esc(tr(localRequestedOn ? 'zones.turnThermostatOff' : 'zones.turnThermostatOn'))}</button></div>`;
|
||||
const localThermostatPowerControl = `<button type="button" class="zone-unit-power-toggle ${localRequestedOn ? 'active' : ''}" title="${esc(tr('zones.unitPowerLocalHint'))}" data-action="zone-device-power" data-id="${esc(zone.id)}" data-value="${localRequestedOn ? 'false' : 'true'}" aria-pressed="${localRequestedOn ? 'true' : 'false'}"${localPowerDisabled}><span aria-hidden="true">⏻</span><b>${esc(tr(localRequestedOn ? 'common.on' : 'common.off'))}</b></button>`;
|
||||
const temporaryThermostatControl = temporarySession
|
||||
? `<button type="button" class="temporary-thermostat-toggle active" data-action="zone-open-temporary" data-id="${esc(zone.id)}" title="${esc(temporaryStatus?.detail || tr('zones.temporaryActive'))}"><span aria-hidden="true">⏱</span><b data-temporary-countdown-zone="${esc(zone.id)}">${esc(temporaryStatus?.countdown || '—')}</b></button>`
|
||||
: `<button type="button" class="temporary-thermostat-toggle" data-action="zone-open-temporary" data-id="${esc(zone.id)}" title="${esc(tr('zones.temporaryStartHint'))}"${localPowerDisabled}><span aria-hidden="true">⏱</span><b>${esc(tr('zones.temporaryShort'))}</b></button>`;
|
||||
|
||||
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'} ${deviceManualOverride ? 'manual-takeover' : ''}" 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="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><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'))}" title="${esc(tr('zones.automationToggleHint'))}"><span>${zone.enabled ? '✓' : '○'}</span>${esc(state)}</button></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(zoneRuntimeStatusLabel(zone, effectiveMode))}</span><span>${esc(houseModeLabel(effectiveMode))} · ${esc(zonePresetLabel(displayPreset))}</span><small>${esc(override)}</small><small><strong>${esc(tr('zones.controlOwner'))}:</strong> ${esc(zoneControlOwnerLabel(zone))}${zoneControlOwnerMeta(zone) ? ` · ${esc(zoneControlOwnerMeta(zone))}` : ''}</small></div>
|
||||
@@ -800,8 +867,7 @@ function zoneCard(zone, detailed = true) {
|
||||
}
|
||||
|
||||
return `<article class="list-card zone-thermostat quick-control-card quick-thermostat-control ${zone.demand ? 'demanding' : ''} ${zone.enabled ? '' : 'zone-disabled'} ${deviceManualOverride ? 'manual-takeover' : ''}" 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(displayPreset))}</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>
|
||||
${localThermostatPowerControl}
|
||||
<div class="list-card-head"><div><h3>${esc(zone.name)}</h3><p>${esc(device?.name || tr('common.noDevice'))} · ${esc(zonePresetLabel(displayPreset))}</p></div><div class="zone-quick-actions">${localThermostatPowerControl}${temporaryThermostatControl}</div></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 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('')}
|
||||
@@ -1314,6 +1380,64 @@ function populateZone(id) {
|
||||
form.enabled.checked = item.enabled; updateZoneSensorFields(); openDialog('zoneDialog');
|
||||
}
|
||||
|
||||
const dateTimeLocalValue = value => {
|
||||
const date = value instanceof Date ? value : new Date(value);
|
||||
if (!Number.isFinite(date.getTime())) return '';
|
||||
const pad = number => String(number).padStart(2, '0');
|
||||
return `${date.getFullYear()}-${pad(date.getMonth()+1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
||||
};
|
||||
|
||||
function updateTemporaryThermostatFields() {
|
||||
const form = $('#temporaryThermostatForm'); if (!form) return;
|
||||
const kind = form.finish_kind.value;
|
||||
const duration = $('#temporaryDurationFields');
|
||||
const until = $('#temporaryUntilFields');
|
||||
const temperature = $('#temporaryTemperatureFields');
|
||||
const schedule = $('#temporaryScheduleFields');
|
||||
duration.hidden = kind !== 'duration';
|
||||
until.hidden = kind !== 'until';
|
||||
temperature.hidden = !['temperature_reached','temperature_stable'].includes(kind);
|
||||
schedule.hidden = kind !== 'schedule_boundary';
|
||||
$('#temporaryHoldField').hidden = kind !== 'temperature_stable';
|
||||
form.duration_minutes.required = kind === 'duration';
|
||||
form.until.required = kind === 'until';
|
||||
form.hold_minutes.required = kind === 'temperature_stable';
|
||||
}
|
||||
|
||||
function populateTemporaryThermostat(id) {
|
||||
const zone = app.zones.find(item => item.id === id); if (!zone) return;
|
||||
const form = $('#temporaryThermostatForm');
|
||||
form.reset();
|
||||
form.elements.zone_id.value = zone.id;
|
||||
form.target_temperature.value = Number(zone.manual_setpoint ?? zone.effective_setpoint ?? zone.setpoint ?? 23).toFixed(1);
|
||||
form.until.value = dateTimeLocalValue(new Date(Date.now() + 2 * 60 * 60 * 1000));
|
||||
const session = zone.temporary_quick_thermostat;
|
||||
if (session) {
|
||||
form.finish_kind.value = session.finish_kind || 'duration';
|
||||
if (session.temperature_target != null) form.target_temperature.value = Number(session.temperature_target).toFixed(1);
|
||||
if (session.temperature_operator) form.temperature_operator.value = session.temperature_operator;
|
||||
form.tolerance_c.value = Number(session.tolerance_c ?? 0.3).toFixed(1);
|
||||
if (session.hold_seconds) form.hold_minutes.value = Math.max(1, Math.round(Number(session.hold_seconds) / 60));
|
||||
if (session.expires_at) {
|
||||
form.until.value = dateTimeLocalValue(session.expires_at);
|
||||
if (session.finish_kind === 'duration') form.duration_minutes.value = Math.max(1, Math.ceil((new Date(session.expires_at).getTime() - Date.now()) / 60000));
|
||||
}
|
||||
if (session.safety_expires_at) {
|
||||
form.max_duration_minutes.value = Math.max(1, Math.ceil((new Date(session.safety_expires_at).getTime() - Date.now()) / 60000));
|
||||
} else if (['temperature_reached','temperature_stable'].includes(session.finish_kind)) {
|
||||
form.max_duration_minutes.value = '';
|
||||
}
|
||||
}
|
||||
updateTemporaryThermostatFields();
|
||||
const active = $('#temporaryThermostatActive');
|
||||
active.hidden = !session;
|
||||
$('#temporaryThermostatStop').dataset.id = zone.id;
|
||||
$('#temporaryThermostatSubmit').textContent = tr(session ? 'zones.temporaryUpdate' : 'zones.temporaryStart');
|
||||
updateTemporaryThermostatCountdowns();
|
||||
openDialog('temporaryThermostatDialog');
|
||||
updateTemporaryThermostatCountdowns();
|
||||
}
|
||||
|
||||
function populateSchedule(id) {
|
||||
const item = app.schedules.find(v => v.id === id); if (!item) return;
|
||||
const form = $('#scheduleForm'); form.reset(); fillSelects();
|
||||
@@ -1824,6 +1948,7 @@ document.addEventListener('click', async event => {
|
||||
if (action === 'group-mode') return sendGroupControl(button.dataset.id,{mode:button.dataset.value});
|
||||
if (action === 'group-preset') return sendGroupControl(button.dataset.id,{preset:button.dataset.value});
|
||||
if (action === 'zone-device-power') return sendZoneLocalPower(button.dataset.id, button.dataset.value === 'true');
|
||||
if (action === 'zone-open-temporary') return populateTemporaryThermostat(button.dataset.id);
|
||||
if (action === 'zone-temperature') { const zone=app.zones.find(v=>v.id===button.dataset.id); if(zone) { const base=Number(zone.manual_setpoint ?? zone.effective_setpoint ?? zone.setpoint); queueZoneTemperature(zone, base+Number(button.dataset.delta)); } return; }
|
||||
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});
|
||||
@@ -1984,6 +2109,49 @@ $('#zoneForm').addEventListener('submit', async event => {
|
||||
catch(error){toast(error.message,true);}
|
||||
});
|
||||
|
||||
$('#temporaryFinishKind')?.addEventListener('change', updateTemporaryThermostatFields);
|
||||
$$('[data-temporary-duration]').forEach(button => button.addEventListener('click', () => {
|
||||
const form = $('#temporaryThermostatForm');
|
||||
form.duration_minutes.value = button.dataset.temporaryDuration;
|
||||
}));
|
||||
|
||||
$('#temporaryThermostatStop')?.addEventListener('click', async event => {
|
||||
const id = event.currentTarget.dataset.id; if (!id) return;
|
||||
try {
|
||||
event.currentTarget.disabled = true;
|
||||
const zone = await api(`/api/zones/${encodeURIComponent(id)}/control`, {method:'POST', body:{clear_temporary_quick_thermostat:true}});
|
||||
const index = app.zones.findIndex(item => item.id === zone.id); if (index >= 0) app.zones[index] = zone;
|
||||
$('#temporaryThermostatDialog').close(); renderAll(); scheduleControlPlanLoad(); toast(tr('zones.temporaryStopped'));
|
||||
} catch (error) { toast(error.message, true); }
|
||||
finally { event.currentTarget.disabled = false; }
|
||||
});
|
||||
|
||||
$('#temporaryThermostatForm')?.addEventListener('submit', async event => {
|
||||
event.preventDefault();
|
||||
const form = event.currentTarget, raw = Object.fromEntries(new FormData(form));
|
||||
const request = {finish_kind:raw.finish_kind, target_temperature:parseDecimal(raw.target_temperature)};
|
||||
if (raw.finish_kind === 'duration') request.duration_minutes = Number(raw.duration_minutes);
|
||||
if (raw.finish_kind === 'until') {
|
||||
const until = new Date(raw.until);
|
||||
if (!Number.isFinite(until.getTime())) return toast(tr('zones.temporaryInvalidUntil'), true);
|
||||
request.until = until.toISOString();
|
||||
}
|
||||
if (['temperature_reached','temperature_stable'].includes(raw.finish_kind)) {
|
||||
request.temperature_operator = raw.temperature_operator;
|
||||
request.tolerance_c = parseDecimal(raw.tolerance_c);
|
||||
if (raw.finish_kind === 'temperature_stable') request.hold_minutes = Number(raw.hold_minutes);
|
||||
const safety = Number(raw.max_duration_minutes);
|
||||
if (Number.isFinite(safety) && safety > 0) request.max_duration_minutes = safety;
|
||||
}
|
||||
try {
|
||||
$('#temporaryThermostatSubmit').disabled = true;
|
||||
const zone = await api(`/api/zones/${encodeURIComponent(raw.zone_id)}/control`, {method:'POST', body:{temporary_quick_thermostat:request}});
|
||||
const index = app.zones.findIndex(item => item.id === zone.id); if (index >= 0) app.zones[index] = zone;
|
||||
form.closest('dialog').close(); renderAll(); scheduleControlPlanLoad(); toast(tr('zones.temporaryStarted'));
|
||||
} catch (error) { toast(error.message, true); }
|
||||
finally { $('#temporaryThermostatSubmit').disabled = false; }
|
||||
});
|
||||
|
||||
$('#groupForm')?.addEventListener('submit', async event => {
|
||||
event.preventDefault();
|
||||
const form=event.currentTarget, raw=Object.fromEntries(new FormData(form));
|
||||
@@ -2293,4 +2461,5 @@ async function startApplication() {
|
||||
}
|
||||
|
||||
setInterval(updateLocalThermostatCountdowns, 1000);
|
||||
setInterval(updateTemporaryThermostatCountdowns, 1000);
|
||||
startApplication().catch(error => console.error('Application startup failed:', error));
|
||||
|
||||
@@ -402,6 +402,48 @@
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<dialog id="temporaryThermostatDialog">
|
||||
<form method="dialog" id="temporaryThermostatForm" class="dialog-form temporary-thermostat-form">
|
||||
<input type="hidden" name="zone_id">
|
||||
<div class="dialog-head"><div><span class="eyebrow" data-i18n="zones.quickThermostat">Szybki termostat</span><h2 data-i18n="zones.temporaryTitle">Włącz czasowo</h2></div><button type="button" data-close>×</button></div>
|
||||
<div id="temporaryThermostatActive" class="temporary-active-summary" hidden>
|
||||
<div><small data-i18n="zones.temporaryActive">Aktywny tryb czasowy</small><strong id="temporaryThermostatActiveCountdown">—</strong><span id="temporaryThermostatActiveDetail"></span></div>
|
||||
<button type="button" class="danger" id="temporaryThermostatStop" data-i18n="zones.temporaryStop">Zakończ teraz</button>
|
||||
</div>
|
||||
<label><span data-i18n="zones.temporaryTarget">Temperatura szybkiego termostatu</span><input type="number" name="target_temperature" min="8" max="30" step="0.5" required></label>
|
||||
<label><span data-i18n="zones.temporaryFinishRule">Kiedy wrócić do automatyki</span><select name="finish_kind" id="temporaryFinishKind">
|
||||
<option value="duration" data-i18n="zones.temporaryDuration">Po określonym czasie</option>
|
||||
<option value="until" data-i18n="zones.temporaryUntil">O konkretnej dacie i godzinie</option>
|
||||
<option value="temperature_reached" data-i18n="zones.temporaryReached">Po osiągnięciu warunku temperatury</option>
|
||||
<option value="temperature_stable" data-i18n="zones.temporaryStable">Po utrzymaniu warunku temperatury</option>
|
||||
<option value="schedule_boundary" data-i18n="zones.temporaryScheduleBoundary">Przy następnym przejściu harmonogramu</option>
|
||||
</select></label>
|
||||
|
||||
<div id="temporaryDurationFields" class="temporary-rule-fields">
|
||||
<label><span data-i18n="zones.temporaryDurationMinutes">Czas działania (min)</span><input type="number" name="duration_minutes" min="1" max="14400" value="60"></label>
|
||||
<div class="temporary-presets"><button type="button" data-temporary-duration="30">30 min</button><button type="button" data-temporary-duration="60">1 h</button><button type="button" data-temporary-duration="120">2 h</button><button type="button" data-temporary-duration="240">4 h</button></div>
|
||||
</div>
|
||||
|
||||
<div id="temporaryUntilFields" class="temporary-rule-fields" hidden>
|
||||
<label><span data-i18n="zones.temporaryUntilDate">Wyłącz o</span><input type="datetime-local" name="until"></label>
|
||||
</div>
|
||||
|
||||
<div id="temporaryTemperatureFields" class="temporary-rule-fields" hidden>
|
||||
<div class="two"><label><span data-i18n="zones.temporaryCondition">Warunek</span><select name="temperature_operator">
|
||||
<option value="within" data-i18n="zones.temporaryWithin">w zakresie celu ± tolerancja</option>
|
||||
<option value="at_or_below" data-i18n="zones.temporaryAtOrBelow">temperatura ≤ cel</option>
|
||||
<option value="at_or_above" data-i18n="zones.temporaryAtOrAbove">temperatura ≥ cel</option>
|
||||
</select></label><label><span data-i18n="zones.temporaryTolerance">Tolerancja °C</span><input type="number" name="tolerance_c" min="0.1" max="3" step="0.1" value="0.3"></label></div>
|
||||
<label id="temporaryHoldField"><span data-i18n="zones.temporaryHoldMinutes">Warunek musi trwać (min)</span><input type="number" name="hold_minutes" min="1" max="1440" value="60"></label>
|
||||
<label><span data-i18n="zones.temporarySafetyLimit">Limit bezpieczeństwa (min, opcjonalnie)</span><input type="number" name="max_duration_minutes" min="1" max="14400" value="240"></label>
|
||||
<p class="field-note" data-i18n="zones.temporaryStableHint">Jeśli temperatura wyjdzie poza warunek, licznik utrzymania zeruje się i zaczyna od nowa po ponownym wejściu w zakres.</p>
|
||||
</div>
|
||||
|
||||
<div id="temporaryScheduleFields" class="temporary-rule-fields" hidden><p class="field-note" data-i18n="zones.temporaryScheduleHint">Szybki termostat zakończy się przy najbliższej zmianie aktywnego wpisu harmonogramu tej strefy.</p></div>
|
||||
<div class="form-actions"><button type="button" class="secondary" data-close data-i18n="actions.cancel">Anuluj</button><button class="primary" type="submit" id="temporaryThermostatSubmit" data-i18n="zones.temporaryStart">Włącz czasowo</button></div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<dialog id="groupDialog">
|
||||
<form method="dialog" id="groupForm" class="dialog-form">
|
||||
<input type="hidden" name="id">
|
||||
|
||||
+22
-4
@@ -786,11 +786,26 @@ body.simulation-standalone [data-view="simulation"] { min-height:calc(100vh - 36
|
||||
.quick-device-control .temperature-control { gap:12px; padding:12px 0 10px; }
|
||||
.quick-device-control .current-line { margin:-3px 0 10px; font-size:11px; }
|
||||
.quick-device-control .device-toggles.quick-control-row { margin-top:1px; }
|
||||
.quick-thermostat-control .zone-local-thermostat-power { display:flex; margin-top:6px; }
|
||||
.quick-thermostat-control .zone-local-thermostat-power .zone-unit-power-toggle { width:100%; min-height:30px; font-size:10px; }
|
||||
.quick-thermostat-control .zone-local-thermostat-power .zone-unit-power-toggle.active { color:var(--accent-text); background:var(--accent); font-weight:800; }
|
||||
.quick-thermostat-control .zone-quick-actions { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:5px; min-width:128px; }
|
||||
.quick-thermostat-control .zone-quick-actions button { display:flex; align-items:center; justify-content:center; gap:5px; min-width:0; min-height:30px; padding:5px 7px; font-size:10px; white-space:nowrap; }
|
||||
.quick-thermostat-control .zone-quick-actions button span { font-size:13px; line-height:1; }
|
||||
.quick-thermostat-control .zone-quick-actions button b { overflow:hidden; font-size:10px; font-variant-numeric:tabular-nums; text-overflow:ellipsis; }
|
||||
.quick-thermostat-control .zone-unit-power-toggle.active { color:var(--accent-text); background:var(--accent); font-weight:800; }
|
||||
.quick-thermostat-control .temporary-thermostat-toggle.active { border-color:color-mix(in srgb,var(--warning) 55%,var(--line)); color:var(--warning); background:var(--warning-soft); }
|
||||
.quick-thermostat-control .thermostat-main { margin:8px 0 7px; }
|
||||
.quick-thermostat-control .zone-enable-toggle { min-height:28px; padding:4px 8px; font-size:10px; }
|
||||
.zone-config-card .zone-enable-toggle { min-height:28px; padding:4px 8px; font-size:10px; }
|
||||
|
||||
.temporary-thermostat-form { gap:16px; }
|
||||
.temporary-rule-fields { display:grid; gap:12px; padding:13px; border:1px solid var(--line); border-radius:14px; background:var(--surface-muted); }
|
||||
.temporary-rule-fields[hidden] { display:none; }
|
||||
.temporary-presets { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:5px; }
|
||||
.temporary-presets button { min-height:32px; padding:6px; font-size:10px; }
|
||||
.temporary-active-summary { display:flex; align-items:center; justify-content:space-between; gap:12px; padding:12px 13px; border:1px solid color-mix(in srgb,var(--warning) 45%,var(--line)); border-radius:14px; background:var(--warning-soft); }
|
||||
.temporary-active-summary[hidden] { display:none; }
|
||||
.temporary-active-summary > div { display:grid; gap:3px; min-width:0; }
|
||||
.temporary-active-summary small, .temporary-active-summary span { color:var(--muted); font-size:10px; line-height:1.35; }
|
||||
.temporary-active-summary strong { color:var(--warning); font-size:22px; font-variant-numeric:tabular-nums; letter-spacing:.02em; }
|
||||
.temporary-active-summary .danger { flex:0 0 auto; }
|
||||
|
||||
@media (max-width: 700px) {
|
||||
[data-view="dashboard"] .hero { min-height:145px; padding:20px; }
|
||||
@@ -807,6 +822,9 @@ body.simulation-standalone [data-view="simulation"] { min-height:calc(100vh - 36
|
||||
.dashboard-group-strip .group-preset-row { grid-template-columns:repeat(2,minmax(0,1fr)); }
|
||||
.dashboard-disclosure > summary { padding:12px 13px; }
|
||||
.dashboard-disclosure-body { padding:10px; }
|
||||
.quick-thermostat-control .list-card-head { align-items:center; }
|
||||
.quick-thermostat-control .zone-quick-actions { min-width:116px; }
|
||||
.temporary-presets { grid-template-columns:repeat(2,minmax(0,1fr)); }
|
||||
}
|
||||
|
||||
/* 0.6.9 dashboard density and fit fixes */
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'gree-controller-v082-ui-i18n-ha-freshness';
|
||||
const CACHE = 'gree-controller-v083-temporary-quick-thermostat';
|
||||
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')];
|
||||
|
||||
Reference in New Issue
Block a user