This commit is contained in:
Mateusz Gruszczyński
2026-08-28 12:56:36 +02:00
parent 556a031358
commit 615b2836b7
12 changed files with 1279 additions and 331 deletions
+51 -22
View File
@@ -87,13 +87,24 @@ function temporarySessionStatus(zone) {
const safetyRemaining = secondsUntil(session.safety_expires_at);
const hardRemaining = secondsUntil(session.expires_at);
const startRemaining = secondsUntil(session.started_at);
const pending = !session.activated_at && zone.local_thermostat_power !== true;
const legacyActive = !session.activated_at
&& Number(session.generation ?? 0) === 0
&& (session.start_kind || 'now') === 'now'
&& zone.local_thermostat_power === true
&& (!session.started_at || new Date(session.started_at).getTime() <= Date.now());
const pending = !session.activated_at && !legacyActive;
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.state === 'waiting_master') {
return {countdown:startRemaining != null ? formatExtendedCountdown(startRemaining) : '00:00', detail:tr('zones.temporaryWaitingMaster'), kind:session.finish_kind, pending:true};
}
if (session.state === 'paused_manual') {
return {countdown:'—', detail:tr('zones.temporaryPausedManual'), kind:session.finish_kind, pending};
}
if (pending) {
return {
countdown:startRemaining != null && startRemaining > 0 ? formatExtendedCountdown(startRemaining) : '00:00',
@@ -1437,6 +1448,12 @@ function populateTemporaryThermostat(id) {
form.start_at.value = dateTimeLocalValue(new Date(Date.now() + 60 * 60 * 1000));
form.until.value = dateTimeLocalValue(new Date(Date.now() + 2 * 60 * 60 * 1000));
const session = zone.temporary_quick_thermostat;
const activeSession = !!session?.activated_at || (!!session
&& Number(session.generation ?? 0) === 0
&& (session.start_kind || 'now') === 'now'
&& zone.local_thermostat_power === true
&& (!session.started_at || new Date(session.started_at).getTime() <= Date.now()));
form.dataset.activeSession = activeSession ? 'true' : 'false';
if (session) {
form.start_kind.value = session.start_kind || (new Date(session.started_at).getTime() > Date.now() ? 'at' : 'now');
if (form.start_kind.value === 'delay' && session.started_at) {
@@ -1448,24 +1465,32 @@ function populateTemporaryThermostat(id) {
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') {
const reference = (!session.activated_at && zone.local_thermostat_power !== true && session.started_at)
? new Date(session.started_at).getTime()
: Date.now();
form.duration_minutes.value = Math.max(1, Math.ceil((new Date(session.expires_at).getTime() - reference) / 60000));
if (session.finish_kind === 'duration') {
if (session.duration_seconds) {
form.duration_minutes.value = Math.max(1, Math.round(Number(session.duration_seconds) / 60));
} else if (session.expires_at) {
const base = new Date(session.activated_at || session.started_at || Date.now()).getTime();
form.duration_minutes.value = Math.max(1, Math.round((new Date(session.expires_at).getTime() - base) / 60000));
}
}
if (session.safety_expires_at) {
const reference = (!session.activated_at && zone.local_thermostat_power !== true && session.started_at)
? new Date(session.started_at).getTime()
: Date.now();
form.max_duration_minutes.value = Math.max(1, Math.ceil((new Date(session.safety_expires_at).getTime() - reference) / 60000));
if (session.expires_at && session.finish_kind !== 'duration') form.until.value = dateTimeLocalValue(session.expires_at);
if (session.safety_duration_seconds) {
form.max_duration_minutes.value = Math.max(1, Math.round(Number(session.safety_duration_seconds) / 60));
} else if (session.safety_expires_at) {
const base = new Date(session.activated_at || session.started_at || Date.now()).getTime();
form.max_duration_minutes.value = Math.max(1, Math.round((new Date(session.safety_expires_at).getTime() - base) / 60000));
} else if (['temperature_reached','temperature_stable'].includes(session.finish_kind)) {
form.max_duration_minutes.value = '';
}
}
// Once ownership has started, the historical start is informational. Editing changes only
// target/finish rules; rescheduling requires stopping the session and creating a new one.
form.start_kind.disabled = activeSession;
form.start_delay_minutes.disabled = activeSession;
form.start_at.disabled = activeSession;
$$('[data-temporary-start-delay]').forEach(button => { button.disabled = activeSession; });
updateTemporaryThermostatFields();
const active = $('#temporaryThermostatActive');
active.hidden = !session;
@@ -2172,9 +2197,13 @@ $('#temporaryThermostatStop')?.addEventListener('click', async event => {
$('#temporaryThermostatForm')?.addEventListener('submit', async event => {
event.preventDefault();
const form = event.currentTarget, raw = Object.fromEntries(new FormData(form));
const request = {start_kind:raw.start_kind, finish_kind:raw.finish_kind, target_temperature:parseDecimal(raw.target_temperature)};
if (raw.start_kind === 'delay') request.start_delay_minutes = Number(raw.start_delay_minutes);
if (raw.start_kind === 'at') {
const activeSession = form.dataset.activeSession === 'true';
const zone = app.zones.find(item => item.id === raw.zone_id);
const existingSession = zone?.temporary_quick_thermostat;
const startKind = activeSession ? (existingSession?.start_kind || 'now') : raw.start_kind;
const request = {start_kind:startKind, finish_kind:raw.finish_kind, target_temperature:parseDecimal(raw.target_temperature)};
if (!activeSession && startKind === 'delay') request.start_delay_minutes = Number(raw.start_delay_minutes);
if (!activeSession && startKind === 'at') {
const startAt = new Date(raw.start_at);
if (!Number.isFinite(startAt.getTime())) return toast(tr('zones.temporaryInvalidStartAt'), true);
request.start_at = startAt.toISOString();
@@ -2183,9 +2212,9 @@ $('#temporaryThermostatForm')?.addEventListener('submit', async event => {
if (raw.finish_kind === 'until') {
const until = new Date(raw.until);
if (!Number.isFinite(until.getTime())) return toast(tr('zones.temporaryInvalidUntil'), true);
const effectiveStart = raw.start_kind === 'delay'
? new Date(Date.now() + Number(raw.start_delay_minutes) * 60000)
: raw.start_kind === 'at' ? new Date(raw.start_at) : new Date();
const effectiveStart = activeSession ? new Date()
: startKind === 'delay' ? new Date(Date.now() + Number(raw.start_delay_minutes) * 60000)
: startKind === 'at' ? new Date(raw.start_at) : new Date();
if (Number.isFinite(effectiveStart.getTime()) && until <= effectiveStart) return toast(tr('zones.temporaryEndAfterStart'), true);
request.until = until.toISOString();
}
@@ -2198,9 +2227,9 @@ $('#temporaryThermostatForm')?.addEventListener('submit', async event => {
}
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(raw.start_kind === 'now' ? 'zones.temporaryStarted' : 'zones.temporaryScheduledToast'));
const updatedZone = await api(`/api/zones/${encodeURIComponent(raw.zone_id)}/control`, {method:'POST', body:{temporary_quick_thermostat:request}});
const index = app.zones.findIndex(item => item.id === updatedZone.id); if (index >= 0) app.zones[index] = updatedZone;
form.closest('dialog').close(); renderAll(); scheduleControlPlanLoad(); toast(tr(activeSession || startKind === 'now' ? 'zones.temporaryStarted' : 'zones.temporaryScheduledToast'));
} catch (error) { toast(error.message, true); }
finally { $('#temporaryThermostatSubmit').disabled = false; }
});