From e39f78fcc5e5245432360a6948a85bfaedb889e2 Mon Sep 17 00:00:00 2001 From: Michiel Berger Date: Fri, 1 May 2026 09:54:08 +0200 Subject: [PATCH] Fix two timeline bugs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. /api/timeline silently defaulted missing export_rate to None → 0, which broke the LP for dad's terugleveringskosten scenario. With export=0, charging during surplus has no benefit, so the battery stayed half-empty while solar exported. Fix: when the param is absent, fall back to DEFAULTS['export_rate'] (matches the form's initial value). Explicit blank still means 'use saldering toggle'. 2. uPlot charts had legend disabled. Re-enabled with per-series value formatters so hovering shows '14:00 NL · −1.70 kWh/h' style values. --- src/pluginbattery/static/app.js | 40 +++++++++++++++++--------- src/pluginbattery/static/style.css | 5 +++- src/pluginbattery/templates/index.html | 4 +-- src/pluginbattery/web.py | 7 ++++- 4 files changed, 39 insertions(+), 17 deletions(-) diff --git a/src/pluginbattery/static/app.js b/src/pluginbattery/static/app.js index bee63a0..6a1f4fe 100644 --- a/src/pluginbattery/static/app.js +++ b/src/pluginbattery/static/app.js @@ -376,32 +376,46 @@ function paintTimeline(d) { const grid = batteryOn ? d.hours.map(h => h.grid_with_bat) : d.hours.map(h => h.grid_no_bat); - drawChart("irr", ["Time", "W/m²"], xs, irr, "tl-chart-irradiance", - { stroke: "#fbbf24", fill: "rgba(251, 191, 36, 0.2)" }); - drawChart("soc", ["Time", "kWh"], xs, soc, "tl-chart-soc", - { stroke: "#4ade80", fill: "rgba(74, 222, 128, 0.2)" }); - drawChart("grid", ["Time", "kWh/h"], xs, grid, "tl-chart-grid", - { stroke: batteryOn ? "#93c5fd" : "#fb7185", - fill: batteryOn ? "rgba(147, 197, 253, 0.18)" : "rgba(251, 113, 133, 0.18)", - zeroline: true }); + drawChart("irr", ["Time", "W/m²"], xs, irr, "tl-chart-irradiance", { + stroke: "#fbbf24", fill: "rgba(251, 191, 36, 0.2)", + fmtY: (v) => v == null ? "" : `${v.toFixed(0)} W/m²`, + }); + drawChart("soc", ["Time", "kWh"], xs, soc, "tl-chart-soc", { + stroke: "#4ade80", fill: "rgba(74, 222, 128, 0.2)", + fmtY: (v) => v == null ? "" : `${v.toFixed(2)} kWh`, + }); + drawChart("grid", ["Time", "kWh/h"], xs, grid, "tl-chart-grid", { + stroke: batteryOn ? "#93c5fd" : "#fb7185", + fill: batteryOn ? "rgba(147, 197, 253, 0.18)" : "rgba(251, 113, 133, 0.18)", + zeroline: true, + fmtY: (v) => v == null ? "" : `${v >= 0 ? "+" : ""}${v.toFixed(2)} kWh/h`, + }); } function drawChart(key, axes, xs, ys, containerId, opts = {}) { const el = document.getElementById(containerId); if (charts[key]) charts[key].destroy(); el.innerHTML = ""; - const data = [xs, ys]; + const fmtY = opts.fmtY || ((v) => v == null ? "" : v.toFixed(2)); const u = new uPlot({ width: el.clientWidth, height: el.clientHeight, - cursor: { y: false, lock: false }, - legend: { show: false }, + cursor: { y: false, lock: false, focus: { prox: 16 } }, + legend: { show: true, live: true }, series: [ - {}, { + label: "time", + // Show ts as "Mon 17 Jun, 14:00" in the legend + value: (u, t) => t == null ? "" : new Date(t * 1000).toLocaleString("nl-NL", { + weekday: "short", day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit", + }), + }, + { + label: axes[1], stroke: opts.stroke || "#93c5fd", fill: opts.fill, width: 1.5, points: { show: false }, + value: (u, v) => fmtY(v), }, ], axes: [ @@ -426,7 +440,7 @@ function drawChart(key, axes, xs, ys, containerId, opts = {}) { ctx.restore(); }], } : {}, - }, data, el); + }, [xs, ys], el); charts[key] = u; } diff --git a/src/pluginbattery/static/style.css b/src/pluginbattery/static/style.css index 0999ad2..f3f4781 100644 --- a/src/pluginbattery/static/style.css +++ b/src/pluginbattery/static/style.css @@ -247,8 +247,11 @@ table#leaderboard tbody tr a { color: inherit; text-decoration: underline dotted /* uPlot dark-theme tweaks */ .uplot { color: var(--text); } -.uplot .u-legend { color: var(--text); font-size: 0.75rem; } +.uplot .u-legend { color: var(--text); font-size: 0.75rem; padding: 0.15rem 0; } +.uplot .u-legend th, .uplot .u-legend td { color: var(--text); padding: 0 0.4rem; } +.uplot .u-legend .u-marker { display: inline-block; } .uplot .u-axis { color: var(--muted); } +.uplot .u-cursor-x, .uplot .u-cursor-y { stroke: var(--accent); } table#leaderboard tbody tr:hover { background: var(--panel2); } table#leaderboard tbody tr:first-child { background: rgba(74, 222, 128, 0.08); } diff --git a/src/pluginbattery/templates/index.html b/src/pluginbattery/templates/index.html index 0362490..01dd5e6 100644 --- a/src/pluginbattery/templates/index.html +++ b/src/pluginbattery/templates/index.html @@ -5,7 +5,7 @@ Honest battery payback — vs thuisbatterijgids.nl - + @@ -194,6 +194,6 @@ - + diff --git a/src/pluginbattery/web.py b/src/pluginbattery/web.py index 4cf2746..e610beb 100644 --- a/src/pluginbattery/web.py +++ b/src/pluginbattery/web.py @@ -392,7 +392,12 @@ def timeline(): saldering = q.get("saldering", "false").lower() == "true" eta = float(q.get("eta", DEFAULTS["eta"])) raw_export = q.get("export_rate") - if raw_export in (None, "", "null"): + if raw_export is None: + # Param absent → use the configured default (matches the form's + # initial value). 'export_rate=' (explicit empty) still means + # 'fall back to saldering toggle'. + export_rate = DEFAULTS.get("export_rate") + elif raw_export in ("", "null"): export_rate = None else: export_rate = float(raw_export)