feat(doctor): battery health readout (#80)
Some checks failed
Check / eval (push) Has been cancelled

Report-only cycle_count + retained capacity (charge_* or energy_*) as
% of design on system batteries; soft warn below 70% of design; skip
when attrs missing. NOMARCHY_POWER_SUPPLY_ROOT for the checks.doctor
fixture (test_power has no cycle/design attrs).

Verified: V2 — nix flake check --no-build; checks.doctor green (fake
BAT0 cycles/% + Device-scope ignore); local BAT0 smoke (energy_*).
This commit is contained in:
Bernardo Magri
2026-07-11 10:06:13 +01:00
parent 3132ba5ac8
commit 71786bda6b
7 changed files with 114 additions and 16 deletions

View File

@@ -263,6 +263,61 @@ if [ "$bat_seen" -eq 1 ] && [ "$bat_limited" -eq 0 ]; then
skip "battery charge limit (none set — charges to 100%)"
fi
# ── battery health (report-only, BACKLOG #80) ────────────────────────
# cycle_count + retained capacity (charge_* µAh or energy_* µWh). Same
# system-battery filter as charge-limit/notify. Self-gates when no
# battery or the firmware omits the attrs (desktops, bare VMs). Override
# the sysfs root with NOMARCHY_POWER_SUPPLY_ROOT for the checks.doctor
# fixture (test_power has no cycle/design attrs).
ps_root="${NOMARCHY_POWER_SUPPLY_ROOT:-/sys/class/power_supply}"
bat_health_seen=0
for bat in "$ps_root"/*/; do
[ -d "$bat" ] || continue
[ "$(cat "$bat/type" 2>/dev/null)" = Battery ] || continue
[ "$(cat "$bat/scope" 2>/dev/null || echo System)" = Device ] && continue
name=$(basename "$bat")
cycles=$(cat "$bat/cycle_count" 2>/dev/null || true)
case "$cycles" in ''|*[!0-9]*) cycles= ;; esac
# Some firmwares export 0 forever — treat as unknown, not "brand new".
if [ -n "$cycles" ] && [ "$cycles" -eq 0 ]; then cycles=; fi
full=""; design=""
if [ -r "$bat/charge_full" ] && [ -r "$bat/charge_full_design" ]; then
full=$(cat "$bat/charge_full" 2>/dev/null || true)
design=$(cat "$bat/charge_full_design" 2>/dev/null || true)
elif [ -r "$bat/energy_full" ] && [ -r "$bat/energy_full_design" ]; then
full=$(cat "$bat/energy_full" 2>/dev/null || true)
design=$(cat "$bat/energy_full_design" 2>/dev/null || true)
fi
case "$full" in ''|*[!0-9]*) full= ;; esac
case "$design" in ''|*[!0-9]*) design= ;; esac
pct=""
if [ -n "$full" ] && [ -n "$design" ] && [ "$design" -gt 0 ]; then
pct=$(( full * 100 / design ))
fi
[ -n "$cycles" ] || [ -n "$pct" ] || continue
bat_health_seen=1
detail=""
[ -n "$cycles" ] && detail="${cycles} cycles"
if [ -n "$pct" ]; then
[ -n "$detail" ] && detail="$detail, "
detail="${detail}${pct}% of design capacity"
fi
# Soft warn only — wear isn't a doctor "fix", just a heads-up.
if [ -n "$pct" ] && [ "$pct" -lt 70 ]; then
warn "$name health: $detail" \
"battery wear is normal over years — replace when runtime suffers"
else
ok "$name health: $detail"
fi
done
if [ "$bat_health_seen" -eq 0 ]; then
skip "battery health (no system battery / no cycle or design capacity attrs)"
fi
# ── hibernate / sleep (BACKLOG #76) ──────────────────────────────────
# Read-only: is there a working hibernate path? zram is RAM-only and can't
# hold a resume image, so hibernation needs a disk swap (partition or file)