This commit is contained in:
Mateusz Gruszczyński
2026-09-03 10:26:18 +02:00
parent 8f6169fbe3
commit 4c9f8ff403
31 changed files with 673 additions and 71 deletions
+59 -13
View File
@@ -107,7 +107,11 @@ function renderPingDialog() {
const samples = pingSamples(device.id);
const stats = pingStats(samples);
const last = samples.length ? samples[samples.length - 1] : null;
const state = last?.error ? `<span class="ping-state error">${esc(tr('devices.pingFailed'))}</span>` : `<span class="ping-state ${device.online ? 'online' : ''}">${esc(tr(device.online ? 'status.online' : 'status.offline'))}</span>`;
const state = last?.error
? `<span class="ping-state error">${esc(tr('devices.pingFailed'))}</span>`
: last?.value != null
? `<span class="ping-state online">${esc(tr('devices.pingResponding'))}</span>`
: `<span class="ping-state ${device.online ? 'online' : ''}">${esc(tr(device.online ? 'status.online' : 'status.offline'))}</span>`;
return `<article class="ping-live-card">
<div class="ping-live-head"><div><strong>${esc(device.name)}</strong><small>${esc(device.ip)}</small></div>${state}</div>
${pingSparkline(samples)}
@@ -122,11 +126,12 @@ function renderPingDialog() {
}).join('') : `<div class="empty"><strong>${esc(tr('devices.emptyTitle'))}</strong>${esc(tr('devices.emptyText'))}</div>`;
}
function schedulePingCycle(delay = 1800) {
function schedulePingCycle(delay = null) {
clearTimeout(app.pingMonitor.timer);
app.pingMonitor.timer = null;
if (!app.pingMonitor.running || !$('#pingDialog')?.open) return;
app.pingMonitor.timer = setTimeout(runPingCycle, delay);
const interval = delay == null ? (app.pingMonitor.all ? 5000 : 3000) : delay;
app.pingMonitor.timer = setTimeout(runPingCycle, interval);
}
async function runPingCycle() {
@@ -134,17 +139,20 @@ async function runPingCycle() {
const targets = app.pingMonitor.all ? [...app.devices] : app.devices.filter(device => device.id === app.pingMonitor.targetId);
if (!targets.length) { renderPingDialog(); schedulePingCycle(); return; }
app.pingMonitor.inFlight = true;
await Promise.allSettled(targets.map(async device => {
await Promise.allSettled(targets.map(async (device, index) => {
// Spread all-unit diagnostics and skip a device while explicit manual control is pending.
// This keeps control traffic higher priority than the live diagnostic chart.
if (app.pingMonitor.all && index > 0) await new Promise(resolve => setTimeout(resolve, index * 250));
if (!app.pingMonitor.running || !$('#pingDialog')?.open) return;
if (app.deviceControlQueue[device.id] || app.deviceTemperatureDrafts[device.id]) return;
try {
const updated = await api(`/api/devices/${encodeURIComponent(device.id)}/poll`, { method: 'POST' });
updateDevice(updated);
addPingSample(device.id, updated.response_time_ms);
const result = await api(`/api/devices/${encodeURIComponent(device.id)}/probe`, { method: 'POST' });
addPingSample(device.id, result.response_time_ms);
} catch (error) {
addPingSample(device.id, null, error.message || String(error));
}
}));
app.pingMonitor.inFlight = false;
renderDevices();
renderPingDialog();
schedulePingCycle();
}
@@ -173,24 +181,56 @@ function openDevicePing(id) {
startPingMonitor();
}
async function sendDeviceCommand(id, commandOrFactory) {
if (!confirmManualCommandForDisabledZone(id)) return;
async function sendDeviceCommand(id, commandOrFactory, { disabledZoneConfirmed = false } = {}) {
const disabledZone = disabledZoneForDevice(id);
if (disabledZone && !disabledZoneConfirmed && !confirmManualCommandForDisabledZone(id)) return false;
const previous = app.deviceControlQueue[id] || Promise.resolve();
const request = previous.catch(() => { }).then(() => {
const current = app.devices.find(device => device.id === id);
const command = typeof commandOrFactory === 'function' ? commandOrFactory(current) : commandOrFactory;
return api(`/api/devices/${encodeURIComponent(id)}/command`, { method: 'POST', body: command });
const body = disabledZone ? { ...command, manual_override: true } : command;
return api(`/api/devices/${encodeURIComponent(id)}/command`, { method: 'POST', body });
});
app.deviceControlQueue[id] = request;
try {
const device = await request;
updateDevice(device); renderAll();
} catch (error) { toast(error.message, true); }
finally {
return true;
} catch (error) {
toast(error.message, true);
try { await loadBootstrap(); } catch (_) { }
return false;
} finally {
if (app.deviceControlQueue[id] === request) delete app.deviceControlQueue[id];
}
}
function queueDeviceTemperature(id, delta) {
const device = app.devices.find(item => item.id === id);
if (!device) return;
let draft = app.deviceTemperatureDrafts[id];
const disabledZone = disabledZoneForDevice(id);
if (disabledZone && !draft?.disabledZoneConfirmed) {
if (!confirmManualCommandForDisabledZone(id)) return;
draft = { ...(draft || {}), disabledZoneConfirmed: true };
}
const next = clamp(Number(draft?.target ?? device.target_temperature) + Number(delta), 8, 30);
device.target_temperature = next;
clearTimeout(draft?.timer);
draft = {
...(draft || {}),
target: next,
timer: setTimeout(async () => {
const pending = app.deviceTemperatureDrafts[id];
if (!pending) return;
delete app.deviceTemperatureDrafts[id];
await sendDeviceCommand(id, { target_temperature: pending.target }, { disabledZoneConfirmed: pending.disabledZoneConfirmed === true });
}, 300),
};
app.deviceTemperatureDrafts[id] = draft;
renderDevices();
}
function enqueueClimateControlTask(task) {
const previous = app.climateControlQueue || Promise.resolve();
const request = previous.catch(() => { }).then(task);
@@ -287,6 +327,10 @@ function beginInlineTemperatureEdit(target) {
if (!target || target.querySelector('input') || target.dataset.editable === 'false') return;
const kind = target.dataset.temperatureKind;
const id = target.dataset.id;
if (kind === 'device' && app.deviceTemperatureDrafts[id]) {
clearTimeout(app.deviceTemperatureDrafts[id].timer);
delete app.deviceTemperatureDrafts[id];
}
const value = parseDecimal(target.dataset.value);
if (!Number.isFinite(value) || !kind || !id) return;
const decimals = kind === 'zone' ? 1 : 0;
@@ -344,6 +388,8 @@ function populateDeviceRename(id) {
const device = app.devices.find(item => item.id === id); if (!device) return;
const form = $('#renameDeviceForm'); form.reset();
form.id.value = device.id; form.name.value = device.name; form.ip.value = device.ip; form.port.value = String(device.port ?? 7000); form.protocol_version.value = String(device.protocol_version ?? 0);
const result = $('#deviceConfigCheckResult');
if (result) { result.hidden = true; result.textContent = ''; result.classList.remove('success', 'error'); }
openDialog('renameDeviceDialog');
}