v0.14.0
This commit is contained in:
+69
-2
@@ -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$/, '');
|
||||
|
||||
Reference in New Issue
Block a user