cockpit: battery/power card; van-battery publishes thresholds in state
New "Battery / Power" card shows mains vs battery, charge % with a severity- colored bar (only bites while on battery), the last alert level, and the configured warn/shutdown thresholds. van-battery now includes warn_levels + shutdown_level in /run/van-battery/state.json so the card stays self-describing instead of hardcoding the numbers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
52e948654e
commit
473a75c729
@@ -28,6 +28,11 @@
|
||||
<div id="thermal"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Battery / Power</h3>
|
||||
<div id="battery"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>WAN Failover</h3>
|
||||
<div id="failover"></div>
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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 = `<p class="muted">van-battery daemon not running (no state file).</p>`;
|
||||
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
|
||||
? `<span class="pill warn">on battery</span>`
|
||||
: `<span class="pill ok">on mains</span>`;
|
||||
|
||||
let html = `<p>Power: ${src} Charge: <span class="pill ${pillClass}">${esc(cap)}%</span></p>`;
|
||||
html += `<div class="bar"><div class="bar-fill ${lvl}" style="width:${Math.max(0, Math.min(100, cap))}%"></div></div>`;
|
||||
if (b.on_battery && b.alerted_below != null)
|
||||
html += `<p class="muted">Low-battery alert sent at ${esc(b.alerted_below)}%.</p>`;
|
||||
html += `<p class="muted">On battery: alerts at ${esc(warns.join("/"))}%, auto-shutdown at ${esc(shut)}%.` +
|
||||
` Alerts: <code>journalctl -u van-battery</code></p>`;
|
||||
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();
|
||||
|
||||
+4
-2
@@ -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"])
|
||||
|
||||
Reference in New Issue
Block a user