v0.13.0
This commit is contained in:
Vendored
+3
-1
@@ -16,8 +16,10 @@ async function loadBootstrap(showMessage = false) {
|
||||
app.system = data.system || {};
|
||||
app.systemSnapshotAt = Date.now();
|
||||
app.outdoorTemperature = Number.isFinite(Number(data.outdoor_temperature)) ? Number(data.outdoor_temperature) : null;
|
||||
app.controlPlan = data.control_plan || app.controlPlan;
|
||||
app.controlPlanRevision = Number.isFinite(Number(data.control_plan_revision)) ? Number(data.control_plan_revision) : app.controlPlanRevision;
|
||||
renderAll();
|
||||
loadControlPlan();
|
||||
if (!data.control_plan) scheduleControlPlanLoad(0);
|
||||
if (app.settings?.debug?.overlay_enabled) loadDebugBacklog();
|
||||
if (showMessage) toast(tr('common.updated'));
|
||||
if ($('#tokenDialog').open) $('#tokenDialog').close();
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ const app = {
|
||||
historyTab: 'overview', historyData: { zones: [], devices: [], sensors: [] }, historyCounts: {},
|
||||
historyZone: 'all', historyDevice: 'all', historySensor: 'all', historyLoading: false,
|
||||
customChartSeries: [], savedCharts: [], chartZooms: {}, chartHiddenSeries: {}, zoneControlSeq: {}, groupControlSeq: {}, zoneControlQueue: {}, groupControlQueue: {}, deviceControlQueue: {}, houseControlQueue: null, climateControlQueue: null, groupCustomDrafts: {}, zoneTemperatureTimers: {}, deviceTemperatureDrafts: {},
|
||||
controlPlan: null, controlPlanTimer: null, debugLines: [], debugBacklogLoaded: false, debugFilter: 'all', sensorAliases: {}, flowSharedInputs: [], flowSharedInputValueCache: {}, flowSharedInputValueRequests: {}, flowSharedInputValueTimer: null,
|
||||
controlPlan: null, controlPlanRevision: null, controlPlanPushReady: false, controlPlanTimer: null, debugLines: [], debugBacklogLoaded: false, debugFilter: 'all', sensorAliases: {}, flowSharedInputs: [], flowSharedInputValueCache: {}, flowSharedInputValueRequests: {}, flowSharedInputValueTimer: null,
|
||||
simulationScope: 'units', simulationTarget: 'all', standaloneSimulation: false, dashboardTab: 'main', settingsTab: 'app', systemSnapshotAt: Date.now(),
|
||||
pingMonitor: { targetId: '', all: false, running: false, timer: null, inFlight: false, samples: {} },
|
||||
flowDraft: null, flowSelectedNodeId: null, flowSelectedNodeIds: [], flowConnectFrom: null, flowDirty: false, flowZoom: 1,
|
||||
|
||||
+38
-3
@@ -133,9 +133,38 @@ function renderControlPlan() {
|
||||
host.innerHTML = content;
|
||||
if (section) section.hidden = !content;
|
||||
}
|
||||
function applyControlPlan(plan, revision = null) {
|
||||
if (!plan || typeof plan !== 'object') return false;
|
||||
const hasRevision = revision !== null && revision !== undefined && revision !== '';
|
||||
const hasCurrentRevision = app.controlPlanRevision !== null && app.controlPlanRevision !== undefined && app.controlPlanRevision !== '';
|
||||
const nextRevision = hasRevision ? Number(revision) : NaN;
|
||||
const currentRevision = hasCurrentRevision ? Number(app.controlPlanRevision) : NaN;
|
||||
if (Number.isFinite(nextRevision) && Number.isFinite(currentRevision) && nextRevision < currentRevision) return false;
|
||||
app.controlPlan = plan;
|
||||
if (Number.isFinite(nextRevision)) app.controlPlanRevision = nextRevision;
|
||||
renderControlPlan();
|
||||
renderSimulationPage();
|
||||
return true;
|
||||
}
|
||||
|
||||
function controlPlanWebSocketReady() {
|
||||
return !!app.ws && app.ws.readyState === WebSocket.OPEN && app.controlPlanPushReady;
|
||||
}
|
||||
|
||||
async function loadControlPlan() {
|
||||
try { app.controlPlan = await api('/api/control-plan'); renderControlPlan(); renderSimulationPage(); }
|
||||
try {
|
||||
const plan = await api('/api/control-plan');
|
||||
// A fallback request may have started while disconnected and finish after WS resync.
|
||||
// Never let that older unversioned HTTP response overwrite a revisioned pushed plan.
|
||||
if (!controlPlanWebSocketReady()) applyControlPlan(plan);
|
||||
}
|
||||
catch (error) { console.warn('Unable to load control plan:', error); }
|
||||
finally {
|
||||
if (!controlPlanWebSocketReady()) {
|
||||
clearTimeout(app.controlPlanTimer);
|
||||
app.controlPlanTimer = setTimeout(loadControlPlan, 10000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function simulationTime(value, options = {}) {
|
||||
@@ -409,8 +438,14 @@ function renderSimulationPage() {
|
||||
rulesHost.innerHTML = activeRules.length ? activeRules.map(rule => `<article class="simulation-rule-card"><div class="simulation-rule-head"><div><span class="eyebrow">${esc(tr('common.automation'))}</span><h3>${esc(rule.name)}</h3></div><span class="badge active">${esc(automationTriggerLabel(rule))}</span></div><p>${esc(tr('simulation.ruleOnDevice', { device: rule.action_device_name || tr('common.noDevice') }))}</p><div class="simulation-rule-action">${esc(simulationRuleAction(rule))}</div><div class="simulation-rule-meta"><small>${esc(tr('automations.last'))}: ${esc(dateTime(rule.last_fired_at))}</small><small>${esc(tr('simulation.nextReady'))}: ${esc(dateTime(rule.next_ready_at))}</small></div></article>`).join('') : `<div class="empty">${esc(tr('simulation.noRules'))}</div>`;
|
||||
}
|
||||
|
||||
function scheduleControlPlanLoad() {
|
||||
function scheduleControlPlanLoad(delay = 180) {
|
||||
if (controlPlanWebSocketReady()) return;
|
||||
clearTimeout(app.controlPlanTimer);
|
||||
app.controlPlanTimer = setTimeout(loadControlPlan, 180);
|
||||
app.controlPlanTimer = setTimeout(loadControlPlan, delay);
|
||||
}
|
||||
|
||||
function stopControlPlanFallback() {
|
||||
clearTimeout(app.controlPlanTimer);
|
||||
app.controlPlanTimer = null;
|
||||
}
|
||||
|
||||
|
||||
+13
-3
@@ -15,10 +15,20 @@ async function handleWebSocketMessage(event) {
|
||||
const message = JSON.parse(event.data);
|
||||
if (message.event === 'bootstrap') {
|
||||
const data = message.data; app.devices = data.devices || []; app.zones = data.zones || []; app.groups = data.groups || []; app.schedules = data.schedules || []; app.automations = data.automations || [];
|
||||
app.flows = data.flows || []; await loadSettingsSections(data.house?.mode || app.settings?.house_mode || 'cool'); app.system = data.system || app.system; app.systemSnapshotAt = Date.now(); app.outdoorTemperature = Number.isFinite(Number(data.outdoor_temperature)) ? Number(data.outdoor_temperature) : app.outdoorTemperature; renderAll(); scheduleControlPlanLoad(); if (app.settings?.debug?.overlay_enabled) loadDebugBacklog(); return;
|
||||
app.flows = data.flows || []; await loadSettingsSections(data.house?.mode || app.settings?.house_mode || 'cool'); app.system = data.system || app.system; app.systemSnapshotAt = Date.now(); app.outdoorTemperature = Number.isFinite(Number(data.outdoor_temperature)) ? Number(data.outdoor_temperature) : app.outdoorTemperature;
|
||||
if (data.control_plan) {
|
||||
app.controlPlan = data.control_plan;
|
||||
app.controlPlanRevision = Number.isFinite(Number(data.control_plan_revision)) ? Number(data.control_plan_revision) : app.controlPlanRevision;
|
||||
app.controlPlanPushReady = true;
|
||||
stopControlPlanFallback();
|
||||
} else {
|
||||
app.controlPlanPushReady = false;
|
||||
}
|
||||
renderAll(); if (!data.control_plan) scheduleControlPlanLoad(0); if (app.settings?.debug?.overlay_enabled) loadDebugBacklog(); return;
|
||||
}
|
||||
const data = message.data || {};
|
||||
if (['device.updated', 'device.created'].includes(message.event)) { updateDevice(data); renderSummary(); renderDevices(); renderGroups(); scheduleControlPlanLoad(); }
|
||||
if (message.event === 'control_plan.updated') { app.controlPlanPushReady = true; stopControlPlanFallback(); applyControlPlan(data.plan, data.revision); }
|
||||
else if (['device.updated', 'device.created'].includes(message.event)) { updateDevice(data); renderSummary(); renderDevices(); renderGroups(); scheduleControlPlanLoad(); }
|
||||
else if (message.event === 'device.deleted') { app.devices = app.devices.filter(v => v.id !== data.id); renderAll(); scheduleControlPlanLoad(); }
|
||||
else if (message.event === 'devices.discovered') { (data.devices || []).forEach(updateDevice); renderAll(); scheduleControlPlanLoad(); }
|
||||
else if (message.event === 'zone.updated') { const i = app.zones.findIndex(v => v.id === data.id); if (i >= 0) app.zones[i] = data; else app.zones.push(data); renderSummary(); renderHouseClimate(); renderZones(); renderDevices(); renderGroups(); scheduleControlPlanLoad(); }
|
||||
@@ -63,7 +73,7 @@ function connectWebSocket() {
|
||||
const query = app.token ? `?token=${encodeURIComponent(app.token)}` : '';
|
||||
const ws = new WebSocket(`${protocol}//${location.host}${withBase('/ws')}${query}`); app.ws = ws;
|
||||
ws.onopen = () => updateConnectionIndicator('connected');
|
||||
ws.onclose = () => { updateConnectionIndicator('disconnected'); app.wsTimer = setTimeout(connectWebSocket, 3000); };
|
||||
ws.onclose = () => { app.controlPlanPushReady = false; updateConnectionIndicator('disconnected'); scheduleControlPlanLoad(0); app.wsTimer = setTimeout(connectWebSocket, 3000); };
|
||||
ws.onerror = () => updateConnectionIndicator('connectionError');
|
||||
let messageQueue = Promise.resolve();
|
||||
ws.onmessage = event => {
|
||||
|
||||
Reference in New Issue
Block a user