This commit is contained in:
Mateusz Gruszczyński
2026-09-14 16:32:28 +02:00
parent c3fbc5ccc6
commit 021cbddba5
68 changed files with 7780 additions and 202 deletions
+69 -2
View File
@@ -35,6 +35,15 @@ function renderHistoryNavigation() {
} 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 === 'energy') {
const energyDevices = app.devices.filter(device => device.capabilities?.energy_meter === true || !!device.ha_energy_entity_id);
if (!app.historyEnergyDevice && energyDevices.length) app.historyEnergyDevice = energyDevices[0].id;
const deviceOptions = energyDevices.map(device => `<option value="${esc(device.id)}">${esc(device.name)} · ${esc(deviceTransportLabel(device))}</option>`).join('');
host.innerHTML = energyDevices.length
? `<label><span>${esc(tr('common.device'))}</span><select id="historyEnergyDeviceSelect">${deviceOptions}</select></label><label><span>${esc(tr('history.bucket'))}</span><select id="historyEnergyInterval"><option value="hourly">${esc(tr('energy.hourly'))}</option><option value="daily">${esc(tr('energy.daily'))}</option><option value="monthly">${esc(tr('energy.monthly'))}</option></select></label>`
: `<span class="history-context-hint">${esc(tr('energy.noData'))}</span>`;
const deviceSelect = $('#historyEnergyDeviceSelect'); if (deviceSelect && [...deviceSelect.options].some(option => option.value === app.historyEnergyDevice)) deviceSelect.value = app.historyEnergyDevice;
const intervalSelect = $('#historyEnergyInterval'); if (intervalSelect) intervalSelect.value = app.historyEnergyInterval;
} 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;
@@ -50,6 +59,13 @@ async function loadHistory() {
app.historyLoading = true;
const hours = $('#historyHours')?.value || '24';
try {
if (app.historyTab === 'energy') {
await loadEnergyHistory();
renderHistoryNavigation();
renderHistoryPage();
if (app.currentView === 'history') updateBrowserUrl(currentHistoryPath(), true);
return;
}
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 || {};
@@ -99,8 +115,12 @@ function redrawHistoryChart(id) {
const canvas = document.getElementById(id);
if (!runtime || !canvas) return;
canvas.dataset.chartZoom = String(clamp(Number(app.chartZooms[id] || 1), 1, MAX_CHART_ZOOM));
drawLineChart(canvas, runtime.series, runtime.rows, runtime.options);
renderLegend(document.getElementById(`${id}Legend`), runtime.series);
if (runtime.kind === 'energy') {
drawEnergyBarChart(canvas, runtime.buckets, runtime.options);
} else {
drawLineChart(canvas, runtime.series, runtime.rows, runtime.options);
renderLegend(document.getElementById(`${id}Legend`), runtime.series);
}
updateChartZoomControls(id);
}
@@ -418,6 +438,53 @@ function drawLineChart(canvas, series, rows, { height = 340, minValue = null, ma
bindChartTooltip(canvas, visibleSeries, sortedRows, { pad, width, height, firstTs, lastTs, binaryLabels });
}
function drawEnergyBarChart(canvas, buckets, { height = 340 } = {}) {
if (!canvas) return;
const options = { height };
if (canvas.id) chartRuntime.set(canvas.id, { kind: 'energy', buckets, options });
if (!buckets?.length) return drawEmptyChart(canvas, height);
const prepared = prepareCanvas(canvas, height);
const { ctx, width } = prepared;
height = prepared.height;
const text = cssColor('--muted', '#888');
const grid = cssColor('--grid', '#333');
const fill = cssColor('--accent', '#3ecf8e');
const pad = { left: 58, right: 18, top: 20, bottom: 48 };
const values = buckets.map(row => Math.max(0, Number(row.consumption_kwh) || 0));
const max = Math.max(0.1, ...values) * 1.1;
const plotW = width - pad.left - pad.right;
const plotH = height - pad.top - pad.bottom;
const slot = plotW / Math.max(1, buckets.length);
const barW = Math.max(2, Math.min(slot * 0.72, 42));
ctx.font = '10px system-ui'; ctx.fillStyle = text; ctx.strokeStyle = grid; ctx.lineWidth = 1;
for (let i = 0; i <= 5; i++) {
const value = max * i / 5;
const y = pad.top + plotH - (value / max) * plotH;
ctx.beginPath(); ctx.moveTo(pad.left, y); ctx.lineTo(width - pad.right, y); ctx.stroke();
ctx.textAlign = 'right'; ctx.fillText(`${value.toFixed(value < 1 ? 2 : 1)}`, pad.left - 8, y + 3);
}
ctx.fillStyle = fill;
buckets.forEach((row, index) => {
const value = values[index];
const x = pad.left + slot * index + (slot - barW) / 2;
const barH = (value / max) * plotH;
ctx.fillRect(x, pad.top + plotH - barH, barW, barH);
});
ctx.fillStyle = text;
const ticks = Math.min(6, buckets.length);
for (let i = 0; i < ticks; i++) {
const index = ticks === 1 ? 0 : Math.round(i * (buckets.length - 1) / (ticks - 1));
const x = pad.left + slot * index + slot / 2;
ctx.textAlign = 'center';
ctx.fillText(timeLabel(buckets[index].start, $('#historyHours')?.value), x, height - 18);
}
ctx.textAlign = 'left';
ctx.fillText('kWh', 8, pad.top + 4);
const legend = document.getElementById(`${canvas.id}Legend`);
if (legend) legend.innerHTML = `<span class="legend-item"><i class="legend-line" style="--legend-color:${esc(fill)}"></i><span>kWh</span></span>`;
updateChartZoomControls(canvas.id);
}
function renderLegend(host, series) {
if (!host) return;
const chartId = host.id.replace(/Legend$/, '');