This commit is contained in:
Mateusz Gruszczyński
2026-08-28 10:04:35 +02:00
parent d159795267
commit eb325cc0c8
15 changed files with 392 additions and 653 deletions
+58 -5
View File
@@ -86,12 +86,25 @@ 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 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 (pending) {
return {
countdown:startRemaining != null && startRemaining > 0 ? formatExtendedCountdown(startRemaining) : '00:00',
detail:startRemaining != null && startRemaining > 0
? tr('zones.temporaryScheduledStatus', {time:dateTime(session.started_at)})
: tr('zones.temporaryStartingStatus'),
kind:session.finish_kind,
pending:true,
};
}
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);
@@ -123,6 +136,8 @@ function updateTemporaryThermostatCountdowns() {
if (status) {
$('#temporaryThermostatActiveCountdown').textContent = status.countdown;
$('#temporaryThermostatActiveDetail').textContent = status.detail;
const label = $('#temporaryThermostatActiveLabel');
if (label) label.textContent = tr(status.pending ? 'zones.temporaryScheduled' : 'zones.temporaryActive');
}
}
}
@@ -841,7 +856,7 @@ function zoneCard(zone, detailed = true) {
const localRequestedOn = localThermostatPower === true ? true : (localThermostatPower === false ? false : !!device?.power);
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 active ${temporaryStatus?.pending ? 'scheduled' : ''}" 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) {
@@ -1389,16 +1404,23 @@ const dateTimeLocalValue = value => {
function updateTemporaryThermostatFields() {
const form = $('#temporaryThermostatForm'); if (!form) return;
const startKind = form.start_kind.value;
const kind = form.finish_kind.value;
const startDelay = $('#temporaryStartDelayFields');
const startAt = $('#temporaryStartAtFields');
const duration = $('#temporaryDurationFields');
const until = $('#temporaryUntilFields');
const temperature = $('#temporaryTemperatureFields');
const schedule = $('#temporaryScheduleFields');
startDelay.hidden = startKind !== 'delay';
startAt.hidden = startKind !== 'at';
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.start_delay_minutes.required = startKind === 'delay';
form.start_at.required = startKind === 'at';
form.duration_minutes.required = kind === 'duration';
form.until.required = kind === 'until';
form.hold_minutes.required = kind === 'temperature_stable';
@@ -1410,9 +1432,17 @@ function populateTemporaryThermostat(id) {
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.start_kind.value = 'now';
form.start_delay_minutes.value = '30';
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;
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) {
form.start_delay_minutes.value = Math.max(1, Math.ceil((new Date(session.started_at).getTime() - Date.now()) / 60000));
}
if (form.start_kind.value === 'at' && session.started_at) form.start_at.value = dateTimeLocalValue(session.started_at);
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;
@@ -1420,10 +1450,18 @@ function populateTemporaryThermostat(id) {
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.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.safety_expires_at) {
form.max_duration_minutes.value = Math.max(1, Math.ceil((new Date(session.safety_expires_at).getTime() - Date.now()) / 60000));
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));
} else if (['temperature_reached','temperature_stable'].includes(session.finish_kind)) {
form.max_duration_minutes.value = '';
}
@@ -2110,6 +2148,11 @@ $('#zoneForm').addEventListener('submit', async event => {
});
$('#temporaryFinishKind')?.addEventListener('change', updateTemporaryThermostatFields);
$('#temporaryStartKind')?.addEventListener('change', updateTemporaryThermostatFields);
$$('[data-temporary-start-delay]').forEach(button => button.addEventListener('click', () => {
const form = $('#temporaryThermostatForm');
form.start_delay_minutes.value = button.dataset.temporaryStartDelay;
}));
$$('[data-temporary-duration]').forEach(button => button.addEventListener('click', () => {
const form = $('#temporaryThermostatForm');
form.duration_minutes.value = button.dataset.temporaryDuration;
@@ -2129,11 +2172,21 @@ $('#temporaryThermostatStop')?.addEventListener('click', async event => {
$('#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)};
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 startAt = new Date(raw.start_at);
if (!Number.isFinite(startAt.getTime())) return toast(tr('zones.temporaryInvalidStartAt'), true);
request.start_at = startAt.toISOString();
}
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);
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();
if (Number.isFinite(effectiveStart.getTime()) && until <= effectiveStart) return toast(tr('zones.temporaryEndAfterStart'), true);
request.until = until.toISOString();
}
if (['temperature_reached','temperature_stable'].includes(raw.finish_kind)) {
@@ -2147,7 +2200,7 @@ $('#temporaryThermostatForm')?.addEventListener('submit', async event => {
$('#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'));
form.closest('dialog').close(); renderAll(); scheduleControlPlanLoad(); toast(tr(raw.start_kind === 'now' ? 'zones.temporaryStarted' : 'zones.temporaryScheduledToast'));
} catch (error) { toast(error.message, true); }
finally { $('#temporaryThermostatSubmit').disabled = false; }
});