v0.4.4
This commit is contained in:
+364
-109
@@ -16,8 +16,12 @@ const app = {
|
||||
devices: [], zones: [], schedules: [], automations: [], accessTokens: [], settings: null, system: {}, outdoorTemperature: null,
|
||||
token: localStorage.getItem('gree_controller_token') || '', ws: null, wsTimer: null,
|
||||
currentView: 'dashboard', loading: false, language: preferredLanguage, theme: preferredTheme, connectionStatus: 'connecting',
|
||||
languages: [], translations: {}, locales: {}, historyReadings: [], historyZone: 'all',
|
||||
languages: [], translations: {}, locales: {},
|
||||
historyTab: 'overview', historyData: {zones:[], devices:[], sensors:[]}, historyCounts: {},
|
||||
historyZone: 'all', historyDevice: 'all', historySensor: 'all', historyLoading: false,
|
||||
customChartSeries: [], savedCharts: [], zoneControlSeq: {}, zoneTemperatureTimers: {},
|
||||
};
|
||||
try { app.savedCharts = JSON.parse(localStorage.getItem('gree_controller_saved_charts') || '[]'); } catch (_) { app.savedCharts = []; }
|
||||
|
||||
const $ = (selector, root = document) => root.querySelector(selector);
|
||||
const $$ = (selector, root = document) => [...root.querySelectorAll(selector)];
|
||||
@@ -143,11 +147,15 @@ async function api(path, options = {}) {
|
||||
}
|
||||
|
||||
function toast(message, error = false) {
|
||||
const node = $('#toast');
|
||||
node.textContent = message;
|
||||
node.className = error ? 'show error' : 'show';
|
||||
clearTimeout(node._timer);
|
||||
node._timer = setTimeout(() => node.className = '', 3200);
|
||||
const host = $('#toastStack'); if (!host) return;
|
||||
const item = document.createElement('div');
|
||||
item.className = `toast-item ${error ? 'error' : 'success'}`;
|
||||
item.innerHTML = `<span class="toast-icon">${error ? '!' : '✓'}</span><div class="toast-copy"><strong>${esc(error ? tr('toast.errorTitle') : tr('toast.successTitle'))}</strong><span>${esc(message)}</span></div><button class="toast-close" type="button" aria-label="Close">×</button><i class="toast-progress"></i>`;
|
||||
host.appendChild(item);
|
||||
requestAnimationFrame(() => item.classList.add('show'));
|
||||
const remove = () => { item.classList.remove('show'); item.classList.add('leaving'); setTimeout(() => item.remove(), 220); };
|
||||
item.querySelector('.toast-close').addEventListener('click', remove);
|
||||
item._timer = setTimeout(remove, error ? 5200 : 3600);
|
||||
}
|
||||
|
||||
function showTokenDialog() {
|
||||
@@ -341,12 +349,6 @@ function fillSelects() {
|
||||
if (zoneSelect) { const currentZone = zoneSelect.value; zoneSelect.innerHTML = zoneOptions; if ([...zoneSelect.options].some(o => o.value === currentZone)) zoneSelect.value = currentZone; }
|
||||
const presetZone = $('#schedulePresetZone');
|
||||
if (presetZone) { const currentZone = presetZone.value; presetZone.innerHTML = zoneOptions; if ([...presetZone.options].some(o => o.value === currentZone)) presetZone.value = currentZone; }
|
||||
const history = $('#historyZone');
|
||||
if (history) {
|
||||
const historyCurrent = history.value || 'all';
|
||||
history.innerHTML = `<option value="all">${esc(tr('history.allZones'))}</option>${zoneOptions}`;
|
||||
if ([...history.options].some(o => o.value === historyCurrent)) history.value = historyCurrent;
|
||||
}
|
||||
}
|
||||
|
||||
function renderAccessTokens() {
|
||||
@@ -383,15 +385,64 @@ function formatDuration(seconds) {
|
||||
return `${days ? `${days}d ` : ''}${hours}h ${minutes}m`;
|
||||
}
|
||||
|
||||
function showView(name) {
|
||||
const VIEW_ROUTES = {dashboard:'/dashboard', devices:'/devices', zones:'/zones', schedules:'/schedules', automations:'/automations', settings:'/settings', logs:'/events'};
|
||||
const HISTORY_TABS = ['overview','zones','devices','sensors','custom'];
|
||||
|
||||
function currentHistoryPath() {
|
||||
const tab = HISTORY_TABS.includes(app.historyTab) ? app.historyTab : 'overview';
|
||||
const params = new URLSearchParams();
|
||||
const hours = $('#historyHours')?.value || new URLSearchParams(location.search).get('hours') || '24';
|
||||
if (hours !== '24') params.set('hours', hours);
|
||||
if (tab === 'zones' && app.historyZone !== 'all') params.set('zone', app.historyZone);
|
||||
if (tab === 'devices' && app.historyDevice !== 'all') params.set('device', app.historyDevice);
|
||||
if (tab === 'sensors' && app.historySensor !== 'all') params.set('sensor', app.historySensor);
|
||||
if (tab === 'custom' && app.customChartSeries.length) params.set('chart', encodeChartSpec(app.customChartSeries));
|
||||
const query = params.toString();
|
||||
return `/history/${tab}${query ? `?${query}` : ''}`;
|
||||
}
|
||||
|
||||
function updateBrowserUrl(path, replace=false) {
|
||||
const current = `${location.pathname}${location.search}`;
|
||||
if (current === path) return;
|
||||
history[replace ? 'replaceState' : 'pushState']({}, '', path);
|
||||
}
|
||||
|
||||
function showView(name, {push=true, scroll=true}={}) {
|
||||
app.currentView = name;
|
||||
$$('.view').forEach(view => view.classList.toggle('active', view.dataset.view === name));
|
||||
$$('.bottom-nav button').forEach(button => button.classList.toggle('active', button.dataset.nav === name || (button.dataset.nav === 'more' && ['schedules','automations','settings','logs'].includes(name))));
|
||||
window.scrollTo({top: 0, behavior: 'smooth'});
|
||||
if (name === 'history' && app.zones.length) loadHistory();
|
||||
if (push) updateBrowserUrl(name === 'history' ? currentHistoryPath() : (VIEW_ROUTES[name] || '/dashboard'));
|
||||
if (scroll) window.scrollTo({top: 0, behavior: 'smooth'});
|
||||
if (name === 'history') { renderHistoryNavigation(); loadHistory(); }
|
||||
if (name === 'logs') loadLogs();
|
||||
}
|
||||
|
||||
function showHistoryTab(tab, {push=true, load=true}={}) {
|
||||
app.historyTab = HISTORY_TABS.includes(tab) ? tab : 'overview';
|
||||
renderHistoryNavigation();
|
||||
if (push) updateBrowserUrl(currentHistoryPath());
|
||||
if (load) loadHistory();
|
||||
}
|
||||
|
||||
function applyRouteFromLocation() {
|
||||
const parts = location.pathname.split('/').filter(Boolean);
|
||||
const first = parts[0] || 'dashboard';
|
||||
const params = new URLSearchParams(location.search);
|
||||
if (first === 'history') {
|
||||
app.historyTab = HISTORY_TABS.includes(parts[1]) ? parts[1] : 'overview';
|
||||
app.historyZone = params.get('zone') || 'all';
|
||||
app.historyDevice = params.get('device') || 'all';
|
||||
app.historySensor = params.get('sensor') || 'all';
|
||||
const hours = params.get('hours');
|
||||
if (hours && ['6','24','168','720'].includes(hours) && $('#historyHours')) $('#historyHours').value = hours;
|
||||
if (app.historyTab === 'custom' && params.get('chart')) app.customChartSeries = decodeChartSpec(params.get('chart'));
|
||||
showView('history', {push:false, scroll:false});
|
||||
return;
|
||||
}
|
||||
const reverse = Object.entries(VIEW_ROUTES).find(([,path]) => path === `/${first}`);
|
||||
showView(reverse?.[0] || 'dashboard', {push:false, scroll:false});
|
||||
}
|
||||
|
||||
async function sendDeviceCommand(id, command) {
|
||||
try {
|
||||
const device = await api(`/api/devices/${encodeURIComponent(id)}/command`, {method:'POST', body:command});
|
||||
@@ -405,12 +456,27 @@ function updateDevice(device) {
|
||||
}
|
||||
|
||||
async function sendZoneControl(id, patch) {
|
||||
const sequence = (app.zoneControlSeq[id] || 0) + 1;
|
||||
app.zoneControlSeq[id] = sequence;
|
||||
try {
|
||||
const zone = await api(`/api/zones/${encodeURIComponent(id)}/control`, {method:'POST', body:patch});
|
||||
if (app.zoneControlSeq[id] !== sequence) return;
|
||||
const index = app.zones.findIndex(item => item.id === zone.id);
|
||||
if (index >= 0) app.zones[index] = zone; else app.zones.push(zone);
|
||||
renderSummary(); renderZones();
|
||||
} catch (error) { toast(error.message, true); }
|
||||
} catch (error) {
|
||||
if (app.zoneControlSeq[id] === sequence) { await loadBootstrap(); toast(error.message, true); }
|
||||
}
|
||||
}
|
||||
|
||||
function queueZoneTemperature(zone, value) {
|
||||
const next = Math.round(clamp(value, 8, 30) * 2) / 2;
|
||||
zone.manual_setpoint = next;
|
||||
zone.setpoint = next;
|
||||
zone.effective_setpoint = next;
|
||||
renderZones();
|
||||
clearTimeout(app.zoneTemperatureTimers[zone.id]);
|
||||
app.zoneTemperatureTimers[zone.id] = setTimeout(() => sendZoneControl(zone.id, {setpoint: next}), 160);
|
||||
}
|
||||
|
||||
function showDiscoveryNames(ids) {
|
||||
@@ -496,16 +562,68 @@ function populateAutomation(id) {
|
||||
openDialog('automationDialog');
|
||||
}
|
||||
|
||||
function encodeChartSpec(series) {
|
||||
try {
|
||||
const raw = JSON.stringify(series || []);
|
||||
return btoa(raw).replace(/\+/g,'-').replace(/\//g,'_').replace(/=+$/,'');
|
||||
} catch (_) { return ''; }
|
||||
}
|
||||
|
||||
function decodeChartSpec(encoded) {
|
||||
try {
|
||||
const padded = String(encoded || '').replace(/-/g,'+').replace(/_/g,'/').padEnd(Math.ceil(String(encoded || '').length / 4) * 4, '=');
|
||||
const value = JSON.parse(atob(padded));
|
||||
return Array.isArray(value) ? value.filter(item => typeof item === 'string').slice(0, 16) : [];
|
||||
} catch (_) { return []; }
|
||||
}
|
||||
|
||||
function historyEntityOptions() {
|
||||
const zones = app.zones.map(zone => `<option value="${esc(zone.id)}">${esc(zone.name)}</option>`).join('');
|
||||
const devices = app.devices.map(device => `<option value="${esc(device.id)}">${esc(device.name)}</option>`).join('');
|
||||
const entities = [...new Set([
|
||||
...app.historyData.sensors.map(row => row.entity_id),
|
||||
...app.zones.map(zone => zone.ha_entity_id).filter(Boolean),
|
||||
app.settings?.home_assistant?.outdoor_entity_id,
|
||||
].filter(Boolean))].sort();
|
||||
const sensors = entities.map(entity => `<option value="${esc(entity)}">${esc(entity)}</option>`).join('');
|
||||
return {zones, devices, sensors, entities};
|
||||
}
|
||||
|
||||
function renderHistoryNavigation() {
|
||||
$$('#historyTabs [data-history-tab]').forEach(button => button.classList.toggle('active', button.dataset.historyTab === app.historyTab));
|
||||
const host = $('#historyContextControls'); if (!host) return;
|
||||
const options = historyEntityOptions();
|
||||
if (app.historyTab === 'zones') {
|
||||
host.innerHTML = `<label><span>${esc(tr('common.zone'))}</span><select id="historyZoneSelect"><option value="all">${esc(tr('history.allZones'))}</option>${options.zones}</select></label>`;
|
||||
const select=$('#historyZoneSelect'); if ([...select.options].some(option=>option.value===app.historyZone)) select.value=app.historyZone;
|
||||
} else if (app.historyTab === 'devices') {
|
||||
host.innerHTML = `<label><span>${esc(tr('common.device'))}</span><select id="historyDeviceSelect"><option value="all">${esc(tr('history.allDevices'))}</option>${options.devices}</select></label>`;
|
||||
const select=$('#historyDeviceSelect'); if ([...select.options].some(option=>option.value===app.historyDevice)) select.value=app.historyDevice;
|
||||
} else if (app.historyTab === 'sensors') {
|
||||
host.innerHTML = `<label><span>${esc(tr('history.haSensor'))}</span><select id="historySensorSelect"><option value="all">${esc(tr('history.allSensors'))}</option>${options.sensors}</select></label>`;
|
||||
const select=$('#historySensorSelect'); if ([...select.options].some(option=>option.value===app.historySensor)) select.value=app.historySensor;
|
||||
} else if (app.historyTab === 'custom') {
|
||||
host.innerHTML = `<span class="history-context-hint">${esc(tr('history.customHint'))}</span>`;
|
||||
} else {
|
||||
host.innerHTML = `<span class="history-context-hint">${esc(tr('history.overviewHint'))}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadHistory() {
|
||||
const zoneId = $('#historyZone')?.value || 'all';
|
||||
if (app.historyLoading) return;
|
||||
app.historyLoading = true;
|
||||
const hours = $('#historyHours')?.value || '24';
|
||||
try {
|
||||
const data = await api(`/api/history?zone_id=${encodeURIComponent(zoneId)}&hours=${encodeURIComponent(hours)}&limit=12000`);
|
||||
const readings = data.readings || [];
|
||||
app.historyReadings = readings; app.historyZone = zoneId;
|
||||
renderHistorySummary(readings, zoneId);
|
||||
drawHistoryCharts(readings, zoneId);
|
||||
} catch (error) { toast(error.message, true); }
|
||||
const data = await api(`/api/history?scope=overview&hours=${encodeURIComponent(hours)}&limit=20000`);
|
||||
app.historyData = {zones:data.zones || [], devices:data.devices || [], sensors:data.sensors || []};
|
||||
app.historyCounts = data.counts || {};
|
||||
renderHistoryNavigation();
|
||||
renderHistoryPage();
|
||||
if (app.currentView === 'history') updateBrowserUrl(currentHistoryPath(), true);
|
||||
} catch (error) {
|
||||
toast(error.message, true);
|
||||
renderHistoryPage();
|
||||
} finally { app.historyLoading = false; }
|
||||
}
|
||||
|
||||
const HISTORY_COLORS = ['--accent','--info','--warning','--purple','--danger','--teal','--orange','--blue'];
|
||||
@@ -515,38 +633,6 @@ function cssColor(name, fallback) {
|
||||
return value || fallback;
|
||||
}
|
||||
|
||||
function latestByZone(readings) {
|
||||
const map = new Map();
|
||||
readings.forEach(row => map.set(row.zone_id, row));
|
||||
return map;
|
||||
}
|
||||
|
||||
function renderHistorySummary(readings, zoneId) {
|
||||
const host = $('#historySummary'); if (!host) return;
|
||||
if (!readings.length) {
|
||||
host.innerHTML = `<div class="empty compact"><strong>${esc(tr('history.noData'))}</strong></div>`;
|
||||
return;
|
||||
}
|
||||
if (zoneId === 'all') {
|
||||
const latest = latestByZone(readings);
|
||||
host.innerHTML = app.zones.filter(z => latest.has(z.id)).map(zone => {
|
||||
const row = latest.get(zone.id);
|
||||
return `<div class="history-stat"><small>${esc(zone.name)}</small><strong>${fmtTemp(row.control_temperature)}</strong><span>${esc(tr('history.targetShort'))} ${fmtTemp(row.target_temperature)}</span></div>`;
|
||||
}).join('');
|
||||
return;
|
||||
}
|
||||
const row = readings[readings.length - 1];
|
||||
const cards = [
|
||||
[tr('history.greeSensor'), fmtTemp(row.gree_temperature), tr('history.sensor')],
|
||||
[tr('history.roomSensor'), fmtTemp(row.external_temperature), tr('history.homeAssistant')],
|
||||
[tr('history.controlTemperature'), fmtTemp(row.control_temperature), row.control_source || '—'],
|
||||
[tr('history.comfortTarget'), fmtTemp(row.target_temperature), row.active_preset || '—'],
|
||||
[tr('history.deviceSetpoint'), fmtTemp(row.device_setpoint), (row.mode || '—').toUpperCase()],
|
||||
[tr('history.outdoorTemperature'), fmtTemp(row.outdoor_temperature), tr('history.assist')],
|
||||
];
|
||||
host.innerHTML = cards.map(([label,value,detail]) => `<div class="history-stat"><small>${esc(label)}</small><strong>${esc(value)}</strong><span>${esc(detail)}</span></div>`).join('');
|
||||
}
|
||||
|
||||
function timeLabel(ts, hours) {
|
||||
const options = Number(hours) > 48 ? {day:'2-digit', month:'2-digit', hour:'2-digit'} : {hour:'2-digit', minute:'2-digit'};
|
||||
return new Date(ts).toLocaleString(locale(), options);
|
||||
@@ -564,6 +650,7 @@ function prepareCanvas(canvas, height) {
|
||||
}
|
||||
|
||||
function drawEmptyChart(canvas, height=340) {
|
||||
if (!canvas) return;
|
||||
const {ctx,width} = prepareCanvas(canvas,height);
|
||||
ctx.fillStyle = cssColor('--muted','#888'); ctx.font = '13px system-ui'; ctx.textAlign='center';
|
||||
ctx.fillText(tr('history.noData'), width/2, height/2);
|
||||
@@ -571,17 +658,18 @@ function drawEmptyChart(canvas, height=340) {
|
||||
|
||||
function drawLineChart(canvas, series, rows, {height=340, minValue=null, maxValue=null, binaryLabels=false}={}) {
|
||||
if (!canvas || !rows.length || !series.length) return drawEmptyChart(canvas, height);
|
||||
const sortedRows = [...rows].sort((a,b)=>new Date(a.timestamp)-new Date(b.timestamp));
|
||||
const {ctx,width} = prepareCanvas(canvas,height);
|
||||
const text=cssColor('--muted','#888'), grid=cssColor('--grid','#333'), panel=cssColor('--surface','#111');
|
||||
const text=cssColor('--muted','#888'), grid=cssColor('--grid','#333');
|
||||
const pad={left:54,right:20,top:20,bottom:42};
|
||||
const allValues=[];
|
||||
series.forEach(s => rows.forEach(r => { const v=s.value(r); if(Number.isFinite(v)) allValues.push(v); }));
|
||||
series.forEach(item => sortedRows.forEach(row => { const value=item.value(row); if(Number.isFinite(value)) allValues.push(value); }));
|
||||
if (!allValues.length) return drawEmptyChart(canvas,height);
|
||||
let min = minValue == null ? Math.floor(Math.min(...allValues)-1) : minValue;
|
||||
let max = maxValue == null ? Math.ceil(Math.max(...allValues)+1) : maxValue;
|
||||
if (max-min < 2) { min-=1; max+=1; }
|
||||
const firstTs = new Date(rows[0].timestamp).getTime();
|
||||
const lastTs = new Date(rows[rows.length-1].timestamp).getTime();
|
||||
const firstTs = new Date(sortedRows[0].timestamp).getTime();
|
||||
const lastTs = new Date(sortedRows[sortedRows.length-1].timestamp).getTime();
|
||||
const span = Math.max(1,lastTs-firstTs);
|
||||
const x = row => pad.left + (new Date(row.timestamp).getTime()-firstTs)/span*(width-pad.left-pad.right);
|
||||
const y = value => pad.top + (max-value)/(max-min)*(height-pad.top-pad.bottom);
|
||||
@@ -593,76 +681,232 @@ function drawLineChart(canvas, series, rows, {height=340, minValue=null, maxValu
|
||||
}
|
||||
const ticks=5;
|
||||
for(let i=0;i<=ticks;i++){
|
||||
const idx=Math.min(rows.length-1,Math.round(i*(rows.length-1)/ticks)); const px=x(rows[idx]);
|
||||
ctx.textAlign='center'; ctx.fillText(timeLabel(rows[idx].timestamp,$('#historyHours')?.value),px,height-14);
|
||||
const idx=Math.min(sortedRows.length-1,Math.round(i*(sortedRows.length-1)/ticks)); const px=x(sortedRows[idx]);
|
||||
ctx.textAlign='center'; ctx.fillText(timeLabel(sortedRows[idx].timestamp,$('#historyHours')?.value),px,height-14);
|
||||
}
|
||||
series.forEach(s => {
|
||||
ctx.beginPath(); ctx.strokeStyle=s.color; ctx.lineWidth=s.width||2; ctx.setLineDash(s.dash||[]);
|
||||
series.forEach(item => {
|
||||
ctx.beginPath(); ctx.strokeStyle=item.color; ctx.lineWidth=item.width||2; ctx.setLineDash(item.dash||[]);
|
||||
let started=false, prev=null;
|
||||
rows.forEach(row => {
|
||||
const value=s.value(row); if(!Number.isFinite(value)) return;
|
||||
sortedRows.forEach(row => {
|
||||
const value=item.value(row); if(!Number.isFinite(value)) return;
|
||||
const px=x(row),py=y(value);
|
||||
if(!started||prev===null){ctx.moveTo(px,py);started=true;}
|
||||
else if(s.step){ctx.lineTo(px,y(prev));ctx.lineTo(px,py);} else ctx.lineTo(px,py);
|
||||
else if(item.step){ctx.lineTo(px,y(prev));ctx.lineTo(px,py);} else ctx.lineTo(px,py);
|
||||
prev=value;
|
||||
});
|
||||
ctx.stroke(); ctx.setLineDash([]);
|
||||
});
|
||||
canvas._history={rows,series};
|
||||
}
|
||||
|
||||
function renderLegend(host, series) {
|
||||
if (!host) return;
|
||||
host.innerHTML=series.map(s=>`<span><i class="legend-line" style="--legend-color:${esc(s.color)}"></i>${esc(s.label)}</span>`).join('');
|
||||
host.innerHTML=series.map(item=>`<span><i class="legend-line" style="--legend-color:${esc(item.color)}"></i>${esc(item.label)}</span>`).join('');
|
||||
}
|
||||
|
||||
function drawHistoryCharts(readings, zoneId) {
|
||||
const tempCanvas=$('#historyTemperatureChart'), opCanvas=$('#historyOperationChart');
|
||||
if (!readings.length) {
|
||||
drawEmptyChart(tempCanvas,360); drawEmptyChart(opCanvas,260);
|
||||
$('#historyTemperatureLegend').innerHTML=''; $('#historyOperationLegend').innerHTML=''; return;
|
||||
}
|
||||
if (zoneId === 'all') {
|
||||
const zones=app.zones.filter(z=>readings.some(r=>r.zone_id===z.id));
|
||||
const rows=readings;
|
||||
const tempSeries=zones.map((zone,index)=>({
|
||||
label:zone.name,
|
||||
color:cssColor(HISTORY_COLORS[index%HISTORY_COLORS.length],`hsl(${index*67%360} 70% 55%)`),
|
||||
value:r=>r.zone_id===zone.id?historyNumber(r.control_temperature):NaN,
|
||||
}));
|
||||
const outdoorColor=cssColor('--muted-strong','#999');
|
||||
tempSeries.push({label:tr('history.outdoorTemperature'),color:outdoorColor,dash:[6,5],value:r=>historyNumber(r.outdoor_temperature)});
|
||||
drawLineChart(tempCanvas,tempSeries,rows,{height:360}); renderLegend($('#historyTemperatureLegend'),tempSeries);
|
||||
function historyChartMarkup(id, title, hint, compact=false) {
|
||||
return `<div class="panel chart-panel history-chart-card"><div class="chart-title-row"><div><h3>${esc(title)}</h3><p>${esc(hint || '')}</p></div></div><div class="chart-wrap ${compact?'compact-chart':''}"><canvas id="${esc(id)}" width="1000" height="${compact?300:420}"></canvas></div><div class="legend" id="${esc(id)}Legend"></div></div>`;
|
||||
}
|
||||
|
||||
const targetSeries=zones.map((zone,index)=>({
|
||||
label:`${zone.name} · ${tr('history.targetShort')}`,
|
||||
color:cssColor(HISTORY_COLORS[index%HISTORY_COLORS.length],`hsl(${index*67%360} 70% 55%)`),
|
||||
dash:[5,4], value:r=>r.zone_id===zone.id?historyNumber(r.target_temperature):NaN,
|
||||
}));
|
||||
drawLineChart(opCanvas,targetSeries,rows,{height:260}); renderLegend($('#historyOperationLegend'),targetSeries);
|
||||
function historySeriesColor(index) {
|
||||
return cssColor(HISTORY_COLORS[index % HISTORY_COLORS.length], `hsl(${(index*67)%360} 68% 55%)`);
|
||||
}
|
||||
|
||||
function renderHistorySummary() {
|
||||
const host=$('#historySummary'); if(!host) return;
|
||||
const deviceRows=app.historyData.devices, zoneRows=app.historyData.zones, sensorRows=app.historyData.sensors;
|
||||
const cards=[
|
||||
[tr('history.deviceSamples'), app.historyCounts.devices ?? deviceRows.length, tr('history.greeHistory')],
|
||||
[tr('history.zoneSamples'), app.historyCounts.zones ?? zoneRows.length, tr('history.zoneHistory')],
|
||||
[tr('history.haSamples'), app.historyCounts.ha ?? sensorRows.length, tr('history.haHistory')],
|
||||
];
|
||||
host.innerHTML=cards.map(([label,value,detail])=>`<div class="history-stat"><small>${esc(label)}</small><strong>${esc(value)}</strong><span>${esc(detail)}</span></div>`).join('');
|
||||
}
|
||||
|
||||
function renderOverviewHistory() {
|
||||
const host=$('#historyCharts');
|
||||
host.innerHTML = historyChartMarkup('overviewIndoorChart',tr('history.allGreeIndoor'),tr('history.allGreeIndoorHint'))
|
||||
+ historyChartMarkup('overviewOutdoorChart',tr('history.allOutdoor'),tr('history.allOutdoorHint'))
|
||||
+ historyChartMarkup('overviewZonesChart',tr('history.allZoneControl'),tr('history.allZoneControlHint'));
|
||||
|
||||
const deviceRows=app.historyData.devices;
|
||||
const indoorSeries=app.devices.map((device,index)=>({label:device.name,color:historySeriesColor(index),value:row=>!row.zone_id&&!row.entity_id&&row.device_id===device.id?historyNumber(row.indoor_temperature):NaN}));
|
||||
drawLineChart($('#overviewIndoorChart'),indoorSeries,deviceRows,{height:360}); renderLegend($('#overviewIndoorChartLegend'),indoorSeries);
|
||||
|
||||
const outdoorDeviceSeries=app.devices.filter(device=>deviceRows.some(row=>row.device_id===device.id&&Number.isFinite(historyNumber(row.outdoor_temperature)))).map((device,index)=>({label:`${device.name} · ${tr('history.greeOutdoor')}`,color:historySeriesColor(index),value:row=>!row.zone_id&&!row.entity_id&&row.device_id===device.id?historyNumber(row.outdoor_temperature):NaN}));
|
||||
const outdoorEntities=[...new Set(app.historyData.sensors.filter(row=>row.kind==='outdoor').map(row=>row.entity_id))];
|
||||
const outdoorHaSeries=outdoorEntities.map((entity,index)=>({label:`HA · ${entity}`,color:historySeriesColor(index+outdoorDeviceSeries.length),dash:[6,4],value:row=>row.entity_id===entity?historyNumber(row.temperature):NaN}));
|
||||
const outdoorRows=[...deviceRows,...app.historyData.sensors.filter(row=>row.kind==='outdoor')];
|
||||
const outdoorSeries=[...outdoorDeviceSeries,...outdoorHaSeries];
|
||||
drawLineChart($('#overviewOutdoorChart'),outdoorSeries,outdoorRows,{height:320}); renderLegend($('#overviewOutdoorChartLegend'),outdoorSeries);
|
||||
|
||||
const zoneRows=app.historyData.zones;
|
||||
const zoneSeries=app.zones.map((zone,index)=>({label:zone.name,color:historySeriesColor(index),value:row=>row.zone_id===zone.id?historyNumber(row.control_temperature):NaN}));
|
||||
drawLineChart($('#overviewZonesChart'),zoneSeries,zoneRows,{height:340}); renderLegend($('#overviewZonesChartLegend'),zoneSeries);
|
||||
}
|
||||
|
||||
function renderZoneHistory() {
|
||||
const selected=app.historyZone;
|
||||
const rows=selected==='all'?app.historyData.zones:app.historyData.zones.filter(row=>row.zone_id===selected);
|
||||
const host=$('#historyCharts');
|
||||
host.innerHTML=historyChartMarkup('zoneTemperatureChart',tr('history.temperatureOverview'),tr('history.temperatureOverviewHint'))+historyChartMarkup('zoneOperationChart',tr('history.operationOverview'),tr('history.operationOverviewHint'),true);
|
||||
if(selected==='all'){
|
||||
const series=app.zones.map((zone,index)=>({label:zone.name,color:historySeriesColor(index),value:row=>row.zone_id===zone.id?historyNumber(row.control_temperature):NaN}));
|
||||
drawLineChart($('#zoneTemperatureChart'),series,rows,{height:360}); renderLegend($('#zoneTemperatureChartLegend'),series);
|
||||
const targets=app.zones.map((zone,index)=>({label:`${zone.name} · ${tr('history.targetShort')}`,color:historySeriesColor(index),dash:[5,4],value:row=>row.zone_id===zone.id?historyNumber(row.target_temperature):NaN}));
|
||||
drawLineChart($('#zoneOperationChart'),targets,rows,{height:260}); renderLegend($('#zoneOperationChartLegend'),targets);
|
||||
return;
|
||||
}
|
||||
|
||||
const temperatureSeries=[
|
||||
{label:tr('history.greeSensor'),color:cssColor('--accent','#3ecf8e'),value:r=>historyNumber(r.gree_temperature)},
|
||||
{label:tr('history.roomSensor'),color:cssColor('--info','#60a5fa'),value:r=>historyNumber(r.external_temperature)},
|
||||
{label:tr('history.controlTemperature'),color:cssColor('--teal','#2dd4bf'),width:2.8,value:r=>historyNumber(r.control_temperature)},
|
||||
{label:tr('history.comfortTarget'),color:cssColor('--warning','#f59e0b'),dash:[7,5],value:r=>historyNumber(r.target_temperature)},
|
||||
{label:tr('history.deviceSetpoint'),color:cssColor('--purple','#a78bfa'),dash:[3,4],value:r=>historyNumber(r.device_setpoint)},
|
||||
{label:tr('history.outdoorTemperature'),color:cssColor('--muted-strong','#9ca3af'),dash:[2,5],value:r=>historyNumber(r.outdoor_temperature)},
|
||||
{label:tr('history.greeSensor'),color:cssColor('--accent','#3ecf8e'),value:row=>historyNumber(row.gree_temperature)},
|
||||
{label:tr('history.roomSensor'),color:cssColor('--info','#60a5fa'),value:row=>historyNumber(row.external_temperature)},
|
||||
{label:tr('history.controlTemperature'),color:cssColor('--teal','#2dd4bf'),width:2.8,value:row=>historyNumber(row.control_temperature)},
|
||||
{label:tr('history.comfortTarget'),color:cssColor('--warning','#f59e0b'),dash:[7,5],value:row=>historyNumber(row.target_temperature)},
|
||||
{label:tr('history.deviceSetpoint'),color:cssColor('--purple','#a78bfa'),dash:[3,4],value:row=>historyNumber(row.device_setpoint)},
|
||||
{label:tr('history.outdoorTemperature'),color:cssColor('--muted-strong','#9ca3af'),dash:[2,5],value:row=>historyNumber(row.outdoor_temperature)},
|
||||
];
|
||||
drawLineChart(tempCanvas,temperatureSeries,readings,{height:360}); renderLegend($('#historyTemperatureLegend'),temperatureSeries);
|
||||
|
||||
drawLineChart($('#zoneTemperatureChart'),temperatureSeries,rows,{height:360}); renderLegend($('#zoneTemperatureChartLegend'),temperatureSeries);
|
||||
const operationSeries=[
|
||||
{label:tr('history.fanSpeed'),color:cssColor('--info','#60a5fa'),step:true,value:r=>historyNumber(r.fan_speed)},
|
||||
{label:tr('history.demand'),color:cssColor('--accent','#3ecf8e'),step:true,width:2.4,value:r=>r.demand?4.5:0.5},
|
||||
{label:tr('history.power'),color:cssColor('--warning','#f59e0b'),step:true,dash:[5,4],value:r=>r.power?3.5:0.5},
|
||||
{label:tr('history.fanSpeed'),color:cssColor('--info','#60a5fa'),step:true,value:row=>historyNumber(row.fan_speed)},
|
||||
{label:tr('history.demand'),color:cssColor('--accent','#3ecf8e'),step:true,width:2.4,value:row=>row.demand?4.5:0.5},
|
||||
{label:tr('history.power'),color:cssColor('--warning','#f59e0b'),step:true,dash:[5,4],value:row=>row.power?3.5:0.5},
|
||||
];
|
||||
drawLineChart(opCanvas,operationSeries,readings,{height:260,minValue:0,maxValue:5,binaryLabels:true}); renderLegend($('#historyOperationLegend'),operationSeries);
|
||||
drawLineChart($('#zoneOperationChart'),operationSeries,rows,{height:260,minValue:0,maxValue:5,binaryLabels:true}); renderLegend($('#zoneOperationChartLegend'),operationSeries);
|
||||
}
|
||||
|
||||
function renderDeviceHistory() {
|
||||
const selected=app.historyDevice;
|
||||
const rows=selected==='all'?app.historyData.devices:app.historyData.devices.filter(row=>row.device_id===selected);
|
||||
const host=$('#historyCharts');
|
||||
host.innerHTML=historyChartMarkup('deviceIndoorChart',tr('history.deviceIndoor'),tr('history.deviceIndoorHint'))+historyChartMarkup('deviceOutdoorChart',tr('history.deviceOutdoor'),tr('history.deviceOutdoorHint'))+historyChartMarkup('deviceTargetChart',tr('history.deviceTargets'),tr('history.deviceTargetsHint'),true);
|
||||
const devices=selected==='all'?app.devices:app.devices.filter(device=>device.id===selected);
|
||||
const indoor=devices.map((device,index)=>({label:device.name,color:historySeriesColor(index),value:row=>row.device_id===device.id?historyNumber(row.indoor_temperature):NaN}));
|
||||
const outdoor=devices.filter(device=>rows.some(row=>row.device_id===device.id&&Number.isFinite(historyNumber(row.outdoor_temperature)))).map((device,index)=>({label:device.name,color:historySeriesColor(index),value:row=>row.device_id===device.id?historyNumber(row.outdoor_temperature):NaN}));
|
||||
const targets=devices.map((device,index)=>({label:device.name,color:historySeriesColor(index),dash:[6,4],value:row=>row.device_id===device.id?historyNumber(row.target_temperature):NaN}));
|
||||
drawLineChart($('#deviceIndoorChart'),indoor,rows,{height:350}); renderLegend($('#deviceIndoorChartLegend'),indoor);
|
||||
drawLineChart($('#deviceOutdoorChart'),outdoor,rows,{height:310}); renderLegend($('#deviceOutdoorChartLegend'),outdoor);
|
||||
drawLineChart($('#deviceTargetChart'),targets,rows,{height:260}); renderLegend($('#deviceTargetChartLegend'),targets);
|
||||
}
|
||||
|
||||
function renderSensorHistory() {
|
||||
const selected=app.historySensor;
|
||||
const rows=selected==='all'?app.historyData.sensors:app.historyData.sensors.filter(row=>row.entity_id===selected);
|
||||
const entities=[...new Set(rows.map(row=>row.entity_id))];
|
||||
const host=$('#historyCharts');
|
||||
host.innerHTML=historyChartMarkup('haSensorsChart',tr('history.haSensors'),tr('history.haSensorsHint'));
|
||||
const series=entities.map((entity,index)=>({label:entity,color:historySeriesColor(index),dash:rows.some(row=>row.entity_id===entity&&row.kind==='outdoor')?[6,4]:[],value:row=>row.entity_id===entity?historyNumber(row.temperature):NaN}));
|
||||
drawLineChart($('#haSensorsChart'),series,rows,{height:370}); renderLegend($('#haSensorsChartLegend'),series);
|
||||
}
|
||||
|
||||
function customSeriesOptions() {
|
||||
const items=[];
|
||||
app.devices.forEach(device=>{
|
||||
items.push([`device|${device.id}|indoor`,`${device.name} · ${tr('history.indoorTemperature')}`]);
|
||||
items.push([`device|${device.id}|outdoor`,`${device.name} · ${tr('history.greeOutdoor')}`]);
|
||||
items.push([`device|${device.id}|target`,`${device.name} · ${tr('history.deviceTarget')}`]);
|
||||
});
|
||||
app.zones.forEach(zone=>{
|
||||
items.push([`zone|${zone.id}|control`,`${zone.name} · ${tr('history.controlTemperature')}`]);
|
||||
items.push([`zone|${zone.id}|gree`,`${zone.name} · ${tr('history.greeSensor')}`]);
|
||||
items.push([`zone|${zone.id}|external`,`${zone.name} · ${tr('history.roomSensor')}`]);
|
||||
items.push([`zone|${zone.id}|target`,`${zone.name} · ${tr('history.comfortTarget')}`]);
|
||||
items.push([`zone|${zone.id}|device_target`,`${zone.name} · ${tr('history.deviceSetpoint')}`]);
|
||||
items.push([`zone|${zone.id}|outdoor`,`${zone.name} · ${tr('history.outdoorTemperature')}`]);
|
||||
});
|
||||
[...new Set(app.historyData.sensors.map(row=>row.entity_id))].forEach(entity=>items.push([`ha|${entity}|temperature`,`HA · ${entity}`]));
|
||||
return items;
|
||||
}
|
||||
|
||||
function customSeriesDefinition(key,index=0) {
|
||||
const [kind,id,field]=String(key).split('|');
|
||||
const color=historySeriesColor(index);
|
||||
if(kind==='device'){
|
||||
const device=app.devices.find(item=>item.id===id); if(!device) return null;
|
||||
const labels={indoor:tr('history.indoorTemperature'),outdoor:tr('history.greeOutdoor'),target:tr('history.deviceTarget')};
|
||||
const fields={indoor:'indoor_temperature',outdoor:'outdoor_temperature',target:'target_temperature'};
|
||||
return {key,label:`${device.name} · ${labels[field]||field}`,color,dash:field==='target'?[6,4]:[],value:row=>!row.zone_id&&!row.entity_id&&row.device_id===id?historyNumber(row[fields[field]]):NaN};
|
||||
}
|
||||
if(kind==='zone'){
|
||||
const zone=app.zones.find(item=>item.id===id); if(!zone) return null;
|
||||
const fields={control:['control_temperature',tr('history.controlTemperature')],gree:['gree_temperature',tr('history.greeSensor')],external:['external_temperature',tr('history.roomSensor')],target:['target_temperature',tr('history.comfortTarget')],device_target:['device_setpoint',tr('history.deviceSetpoint')],outdoor:['outdoor_temperature',tr('history.outdoorTemperature')]};
|
||||
const info=fields[field]; if(!info) return null;
|
||||
return {key,label:`${zone.name} · ${info[1]}`,color,dash:['target','device_target','outdoor'].includes(field)?[6,4]:[],value:row=>row.zone_id===id?historyNumber(row[info[0]]):NaN};
|
||||
}
|
||||
if(kind==='ha') return {key,label:`HA · ${id}`,color,dash:[3,4],value:row=>row.entity_id===id?historyNumber(row.temperature):NaN};
|
||||
return null;
|
||||
}
|
||||
|
||||
function persistSavedCharts() {
|
||||
localStorage.setItem('gree_controller_saved_charts',JSON.stringify(app.savedCharts.slice(0,30)));
|
||||
}
|
||||
|
||||
function renderCustomBuilder() {
|
||||
const host=$('#historyCustomBuilder'); if(!host) return;
|
||||
if(app.historyTab!=='custom'){host.innerHTML='';host.classList.remove('active');return;}
|
||||
host.classList.add('active');
|
||||
const options=customSeriesOptions();
|
||||
const selected=app.customChartSeries.map((key,index)=>customSeriesDefinition(key,index)).filter(Boolean);
|
||||
host.innerHTML=`<div class="panel custom-chart-panel"><div class="chart-title-row"><div><h3>${esc(tr('history.customTitle'))}</h3><p>${esc(tr('history.customDescription'))}</p></div></div>
|
||||
<div class="custom-chart-add"><select id="customSeriesSelect">${options.map(([value,label])=>`<option value="${esc(value)}">${esc(label)}</option>`).join('')}</select><button class="secondary" data-history-action="add-series">${esc(tr('history.addSeries'))}</button></div>
|
||||
<div class="custom-series-list">${selected.length?selected.map((item,index)=>`<span class="custom-series-chip"><i style="--chip-color:${esc(item.color)}"></i>${esc(item.label)}<button data-history-action="remove-series" data-index="${index}" aria-label="Remove">×</button></span>`).join(''):`<span class="field-note">${esc(tr('history.noCustomSeries'))}</span>`}</div>
|
||||
<div class="custom-chart-save"><input id="customChartName" placeholder="${esc(tr('history.chartName'))}"><button class="secondary" data-history-action="save-chart">${esc(tr('actions.save'))}</button><button class="primary" data-history-action="copy-chart-link">${esc(tr('history.copyLink'))}</button></div>
|
||||
<div class="saved-chart-list">${app.savedCharts.length?app.savedCharts.map(item=>`<div class="saved-chart-row"><button data-history-action="load-chart" data-id="${esc(item.id)}"><strong>${esc(item.name)}</strong><small>${esc(item.series.length)} ${esc(tr('history.series'))}</small></button><button class="danger" data-history-action="delete-chart" data-id="${esc(item.id)}">×</button></div>`).join(''):`<span class="field-note">${esc(tr('history.noSavedCharts'))}</span>`}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderCustomHistory() {
|
||||
renderCustomBuilder();
|
||||
const host=$('#historyCharts');
|
||||
host.innerHTML=historyChartMarkup('customHistoryChart',tr('history.customChart'),tr('history.customChartHint'));
|
||||
const series=app.customChartSeries.map((key,index)=>customSeriesDefinition(key,index)).filter(Boolean);
|
||||
const rows=[...app.historyData.devices,...app.historyData.zones,...app.historyData.sensors];
|
||||
drawLineChart($('#customHistoryChart'),series,rows,{height:390}); renderLegend($('#customHistoryChartLegend'),series);
|
||||
}
|
||||
|
||||
function renderHistoryPage() {
|
||||
renderHistorySummary();
|
||||
renderCustomBuilder();
|
||||
if(app.historyTab==='overview') renderOverviewHistory();
|
||||
else if(app.historyTab==='zones') renderZoneHistory();
|
||||
else if(app.historyTab==='devices') renderDeviceHistory();
|
||||
else if(app.historyTab==='sensors') renderSensorHistory();
|
||||
else renderCustomHistory();
|
||||
}
|
||||
|
||||
function drawCurrentChartIfVisible() {
|
||||
if (app.currentView === 'history') drawHistoryCharts(app.historyReadings || [], app.historyZone || 'all');
|
||||
if (app.currentView === 'history') renderHistoryPage();
|
||||
}
|
||||
|
||||
async function handleHistoryAction(button) {
|
||||
const action=button.dataset.historyAction;
|
||||
if(action==='add-series'){
|
||||
const value=$('#customSeriesSelect')?.value;
|
||||
if(value && !app.customChartSeries.includes(value)) app.customChartSeries.push(value);
|
||||
renderCustomHistory(); updateBrowserUrl(currentHistoryPath(), true); return;
|
||||
}
|
||||
if(action==='remove-series'){
|
||||
app.customChartSeries.splice(Number(button.dataset.index),1);
|
||||
renderCustomHistory(); updateBrowserUrl(currentHistoryPath(), true); return;
|
||||
}
|
||||
if(action==='save-chart'){
|
||||
if(!app.customChartSeries.length) return toast(tr('history.noCustomSeries'), true);
|
||||
const name=$('#customChartName')?.value.trim() || tr('history.customChart');
|
||||
const item={id:`chart-${Date.now()}`,name,series:[...app.customChartSeries],hours:$('#historyHours')?.value||'24'};
|
||||
app.savedCharts.unshift(item); persistSavedCharts(); renderCustomHistory(); toast(tr('history.chartSaved')); return;
|
||||
}
|
||||
if(action==='load-chart'){
|
||||
const item=app.savedCharts.find(entry=>entry.id===button.dataset.id); if(!item) return;
|
||||
app.customChartSeries=[...item.series]; if($('#historyHours')&&item.hours) $('#historyHours').value=String(item.hours);
|
||||
renderCustomHistory(); updateBrowserUrl(currentHistoryPath()); return;
|
||||
}
|
||||
if(action==='delete-chart'){
|
||||
app.savedCharts=app.savedCharts.filter(entry=>entry.id!==button.dataset.id); persistSavedCharts(); renderCustomHistory(); return;
|
||||
}
|
||||
if(action==='copy-chart-link'){
|
||||
if(!app.customChartSeries.length) return toast(tr('history.noCustomSeries'), true);
|
||||
const path=currentHistoryPath(); updateBrowserUrl(path, true); const link=`${location.origin}${path}`;
|
||||
try { await navigator.clipboard.writeText(link); } catch (_) { const area=document.createElement('textarea'); area.value=link; document.body.appendChild(area); area.select(); document.execCommand('copy'); area.remove(); }
|
||||
toast(tr('history.linkCopied')); return;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadLogs() {
|
||||
@@ -711,6 +955,8 @@ document.addEventListener('click', async event => {
|
||||
if (button.dataset.go) { $('#moreDialog').close(); showView(button.dataset.go); return; }
|
||||
if (button.dataset.open) { const form = document.getElementById(button.dataset.open.replace('Dialog','Form')); if (form) form.reset(); if (button.dataset.open === 'zoneDialog') updateZoneSensorFields(); if (button.dataset.open === 'scheduleDialog') updateSchedulePresetField(); openDialog(button.dataset.open); return; }
|
||||
if (button.hasAttribute('data-close')) { button.closest('dialog')?.close(); return; }
|
||||
if (button.dataset.historyTab) { showHistoryTab(button.dataset.historyTab); return; }
|
||||
if (button.dataset.historyAction) { await handleHistoryAction(button); return; }
|
||||
if (button.dataset.scheduleTemplate) {
|
||||
const zoneId = $('#schedulePresetZone')?.value; if (!zoneId) return toast(tr('schedules.chooseZone'), true);
|
||||
if (!confirm(tr('schedules.replaceConfirm'))) return;
|
||||
@@ -740,7 +986,7 @@ document.addEventListener('click', async event => {
|
||||
try { const result=await api('/api/house/preset',{method:'POST',body:{preset:button.dataset.value}}); app.zones=result.zones||app.zones; renderZones(); toast(tr('house.presetUpdated')); }
|
||||
catch(error){ toast(error.message,true); } return;
|
||||
}
|
||||
if (action === 'zone-temperature') { const zone=app.zones.find(v=>v.id===button.dataset.id); if(zone) { const base=Number(zone.effective_setpoint ?? zone.setpoint); return sendZoneControl(zone.id,{setpoint:clamp(base+Number(button.dataset.delta),8,30)}); } }
|
||||
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});
|
||||
if (action === 'edit-zone') return populateZone(button.dataset.id);
|
||||
@@ -768,9 +1014,7 @@ $('#discoverButton').addEventListener('click', () => {
|
||||
openDialog('discoverDialog');
|
||||
});
|
||||
$('#historyRefresh').addEventListener('click', loadHistory);
|
||||
$('#historyZone').addEventListener('change', loadHistory);
|
||||
$('#historyHours').addEventListener('change', loadHistory);
|
||||
$('#historyHours').addEventListener('change', loadHistory);
|
||||
$('#historyHours').addEventListener('change', () => { updateBrowserUrl(currentHistoryPath(), true); loadHistory(); });
|
||||
$('#logsRefresh').addEventListener('click', loadLogs);
|
||||
$('#languageSelect').addEventListener('change', event => setLanguage(event.target.value));
|
||||
$('#themeSelect').addEventListener('change', event => setTheme(event.target.value));
|
||||
@@ -899,6 +1143,15 @@ $('#haTest').addEventListener('click', async () => {
|
||||
} catch(error){toast(error.message,true);}
|
||||
});
|
||||
|
||||
document.addEventListener('change', event => {
|
||||
const target=event.target;
|
||||
if(target.id==='historyZoneSelect'){app.historyZone=target.value;updateBrowserUrl(currentHistoryPath());renderHistoryPage();}
|
||||
else if(target.id==='historyDeviceSelect'){app.historyDevice=target.value;updateBrowserUrl(currentHistoryPath());renderHistoryPage();}
|
||||
else if(target.id==='historySensorSelect'){app.historySensor=target.value;updateBrowserUrl(currentHistoryPath());renderHistoryPage();}
|
||||
});
|
||||
|
||||
window.addEventListener('popstate', applyRouteFromLocation);
|
||||
|
||||
let historyResizeTimer;
|
||||
window.addEventListener('resize', () => { if (app.currentView === 'history') { clearTimeout(historyResizeTimer); historyResizeTimer=setTimeout(drawCurrentChartIfVisible,120); } });
|
||||
window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', () => { if (app.theme === 'system') applyTheme(); });
|
||||
@@ -911,6 +1164,8 @@ async function startApplication() {
|
||||
updateZoneSensorFields();
|
||||
updateSchedulePresetField();
|
||||
await loadBootstrap();
|
||||
applyRouteFromLocation();
|
||||
if (location.pathname === '/') updateBrowserUrl('/dashboard', true);
|
||||
}
|
||||
|
||||
startApplication().catch(error => console.error('Application startup failed:', error));
|
||||
|
||||
Reference in New Issue
Block a user