diff --git a/cockpit/vanrouter/index.html b/cockpit/vanrouter/index.html index 1994314..5f14918 100644 --- a/cockpit/vanrouter/index.html +++ b/cockpit/vanrouter/index.html @@ -28,6 +28,11 @@
+
+

Battery / Power

+
+
+

WAN Failover

diff --git a/cockpit/vanrouter/vanrouter.css b/cockpit/vanrouter/vanrouter.css index 04a733c..0143e9c 100644 --- a/cockpit/vanrouter/vanrouter.css +++ b/cockpit/vanrouter/vanrouter.css @@ -13,6 +13,11 @@ th { color: #6a6e73; font-weight: 600; } .ok { background: #bde5b8; color: #1e4f18; } .bad { background: #f0b8b8; color: #5f1414; } .warn { background: #f5d9a8; color: #5f4414; } +.bar { height: 10px; border-radius: 5px; background: #e5e5e5; overflow: hidden; max-width: 320px; margin: 4px 0; } +.bar-fill { height: 100%; transition: width 0.3s; } +.bar-fill.ok { background: #3e8635; } +.bar-fill.warn { background: #e08a00; } +.bar-fill.crit { background: #c9190b; } .muted { color: #6a6e73; font-size: 12px; } .active-wan { font-weight: 700; color: #0066cc; } .btn { margin-left: 6px; padding: 3px 10px; cursor: pointer; } diff --git a/cockpit/vanrouter/vanrouter.js b/cockpit/vanrouter/vanrouter.js index 4bc07fe..35bebb5 100644 --- a/cockpit/vanrouter/vanrouter.js +++ b/cockpit/vanrouter/vanrouter.js @@ -97,6 +97,45 @@ function renderThermal(th) { el.innerHTML = html; } +/* ---------- Battery / power source (van-battery daemon state) ---------- */ + +async function readBattery() { + try { + return JSON.parse(await run(["cat", "/run/van-battery/state.json"])); + } catch (e) { + return null; + } +} + +function renderBattery(b) { + const el = document.getElementById("battery"); + if (!b || b.capacity == null) { + el.innerHTML = `

van-battery daemon not running (no state file).

`; + return; + } + const cap = b.capacity; + const shut = b.shutdown_level != null ? b.shutdown_level : 10; + const warns = b.warn_levels || []; + const lowest = warns.length ? Math.min.apply(null, warns) : 25; + + // Charge severity only bites while on battery; on mains it's just charging/full. + let lvl = "ok"; + if (b.on_battery) lvl = cap <= shut ? "crit" : cap <= lowest ? "warn" : "ok"; + const pillClass = { ok: "ok", warn: "warn", crit: "bad" }[lvl]; + + const src = b.on_battery + ? `on battery` + : `on mains`; + + let html = `

Power: ${src}   Charge: ${esc(cap)}%

`; + html += `
`; + if (b.on_battery && b.alerted_below != null) + html += `

Low-battery alert sent at ${esc(b.alerted_below)}%.

`; + html += `

On battery: alerts at ${esc(warns.join("/"))}%, auto-shutdown at ${esc(shut)}%.` + + ` Alerts: journalctl -u van-battery

`; + el.innerHTML = html; +} + /* ---------- WAN failover (van-failover daemon state) ---------- */ async function readFailover() { @@ -225,9 +264,10 @@ async function restartAP() { async function refresh() { try { - const [ap, th, fo, wan] = await Promise.all([readAP(), readThermal(), readFailover(), readWAN()]); + const [ap, th, bat, fo, wan] = await Promise.all([readAP(), readThermal(), readBattery(), readFailover(), readWAN()]); renderAP(ap); renderThermal(th); + renderBattery(bat); renderFailover(fo); renderWAN(wan); document.getElementById("updated").textContent = "updated " + new Date().toLocaleTimeString(); diff --git a/power/van-battery b/power/van-battery index b6d0944..06fc39b 100644 --- a/power/van-battery +++ b/power/van-battery @@ -150,13 +150,15 @@ def poweroff(cfg): log(f"poweroff failed: {e}", "crit") -def write_state(on_batt, cap, armed): +def write_state(on_batt, cap, armed, warn_levels, shutdown_level): STATE_DIR.mkdir(parents=True, exist_ok=True) payload = { "updated": datetime.now(timezone.utc).isoformat(timespec="seconds"), "on_battery": on_batt, "capacity": cap, "alerted_below": armed, # lowest level alerted this discharge episode, or null + "warn_levels": warn_levels, + "shutdown_level": shutdown_level, } tmp = STATE_PATH.with_suffix(".tmp") tmp.write_text(json.dumps(payload)) @@ -216,7 +218,7 @@ def main(): prev_on_batt = on_batt try: - write_state(on_batt, cap, last_alerted) + write_state(on_batt, cap, last_alerted, cfg["warn_levels"], shutdown_level) except OSError as e: log(f"state write failed: {e}", "warn") time.sleep(cfg["poll_interval"])