Report whether a battery powers the machine and whether anything measures its draw (hq ADR 0255)

The power module is on every machine and its bar blocks apply only where the
hardware is. Two capabilities read from the kernel let the controller compose them
there and nowhere else: battery (a Battery supply not scoped to a device) and
power-meter (a system battery, a powercap package counter, or a graphics device
reporting its power).
This commit is contained in:
jochen
2026-10-08 14:44:55 +02:00
parent 800aabed83
commit e41ab3a505
3 changed files with 226 additions and 0 deletions
+2
View File
@@ -187,6 +187,8 @@ func Default(runner Runner) []Detector {
privileged{},
graphicalSession{},
seat{},
battery{},
powerMeter{},
commandCapability{
name: CapContainerRuntime, command: "docker", args: []string{"info", "--format", "{{.ServerVersion}}"},
why: "asks the daemon for its version — a running daemon, not an installed client",
+125
View File
@@ -0,0 +1,125 @@
package profile
import (
"context"
"os"
"path/filepath"
"sort"
"strings"
)
// What this machine has to say about its own power (novox/hq ADR 0255): whether it has a battery,
// and whether anything on it can measure the power it draws.
//
// **Hardware, read from the kernel, as `seat` is.** A module that shows a battery, or the power a
// machine draws, is on every machine — the power module holds node-power everywhere — and what it
// shows depends on what is there. The controller composes such a piece only where the machine
// reported the capability, so a desktop never gets a battery block that would sit empty, and the
// bar never asks at run time what the mesh could have said once.
// Named power capabilities.
const (
// CapBattery is a battery that powers this machine: a power supply of type Battery that is not
// a device's (a mouse's or a headset's battery has scope Device).
CapBattery = "battery"
// CapPowerMeter is something here that measures the power the machine, or part of it, draws:
// the machine's battery, the processor package's energy counter (powercap, Intel's and AMD's
// RAPL), or a graphics device that reports its own power.
CapPowerMeter = "power-meter"
)
// Where the kernel reports supplies and energy counters. Variables so a test can point them at a
// directory it made, rather than at whatever this machine happens to have.
var (
PowerSupplySource = "/sys/class/power_supply"
PowercapSource = "/sys/class/powercap"
NvidiaSource = "/proc/driver/nvidia/gpus"
)
// systemBatteries is every battery that powers this machine, by name.
func systemBatteries() []string {
entries, err := os.ReadDir(PowerSupplySource)
if err != nil {
return nil
}
var out []string
for _, e := range entries {
dir := filepath.Join(PowerSupplySource, e.Name())
if readTrimmed(filepath.Join(dir, "type")) != "Battery" {
continue
}
if readTrimmed(filepath.Join(dir, "scope")) == "Device" {
continue
}
// A bay with no battery in it still has a directory.
if readTrimmed(filepath.Join(dir, "present")) == "0" {
continue
}
out = append(out, e.Name())
}
sort.Strings(out)
return out
}
func readTrimmed(path string) string {
b, err := os.ReadFile(path)
if err != nil {
return ""
}
return strings.TrimSpace(string(b))
}
type battery struct{}
func (battery) Name() string { return CapBattery }
func (battery) Detect(context.Context) Verdict {
const how = "/sys/class/power_supply/*/type — a Battery whose scope is not Device"
if found := systemBatteries(); len(found) > 0 {
return Verdict{Name: CapBattery, Present: true, Detail: strings.Join(found, ", "), How: how}
}
return Verdict{Name: CapBattery, Present: false, Detail: "no battery powers this machine", How: how}
}
type powerMeter struct{}
func (powerMeter) Name() string { return CapPowerMeter }
// Detect names every meter it found. Only that one exists: whether its counter can be read is the
// reader's question — powercap's energy counter is root's to read, and the power module samples it
// as root and publishes only a rate.
func (powerMeter) Detect(context.Context) Verdict {
const how = "a system battery; /sys/class/powercap/*/name starting package; a graphics device's " +
"hwmon power1_average or power1_input; /proc/driver/nvidia/gpus"
var found []string
for _, b := range systemBatteries() {
found = append(found, "battery "+b)
}
if entries, err := os.ReadDir(PowercapSource); err == nil {
for _, e := range entries {
dir := filepath.Join(PowercapSource, e.Name())
if strings.HasPrefix(readTrimmed(filepath.Join(dir, "name")), "package") {
if _, err := os.Stat(filepath.Join(dir, "energy_uj")); err == nil {
found = append(found, "processor package "+e.Name())
}
}
}
}
if cards, err := filepath.Glob(filepath.Join(SeatSource, "card*", "device", "hwmon", "hwmon*")); err == nil {
for _, hw := range cards {
for _, f := range []string{"power1_average", "power1_input"} {
if _, err := os.Stat(filepath.Join(hw, f)); err == nil {
found = append(found, "graphics "+strings.Split(strings.TrimPrefix(hw, SeatSource+"/"), "/")[0])
break
}
}
}
}
if gpus, err := os.ReadDir(NvidiaSource); err == nil && len(gpus) > 0 {
found = append(found, "graphics nvidia")
}
if len(found) == 0 {
return Verdict{Name: CapPowerMeter, Present: false, Detail: "nothing here measures the power drawn", How: how}
}
return Verdict{Name: CapPowerMeter, Present: true, Detail: strings.Join(found, ", "), How: how}
}
+99
View File
@@ -0,0 +1,99 @@
package profile
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
)
// Defends novox/hq ADR 0255: a machine says whether a battery powers it and whether anything on it
// measures the power it draws, read from the kernel, so the controller composes a battery's or a power
// draw's block only where there is one.
func write(t *testing.T, path, content string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(content+"\n"), 0o644); err != nil {
t.Fatal(err)
}
}
// sysfs points every power source at a directory of its own, empty unless a case fills it.
func sysfs(t *testing.T) string {
t.Helper()
dir := t.TempDir()
old := [4]string{PowerSupplySource, PowercapSource, SeatSource, NvidiaSource}
PowerSupplySource = filepath.Join(dir, "power_supply")
PowercapSource = filepath.Join(dir, "powercap")
SeatSource = filepath.Join(dir, "drm")
NvidiaSource = filepath.Join(dir, "nvidia")
t.Cleanup(func() { PowerSupplySource, PowercapSource, SeatSource, NvidiaSource = old[0], old[1], old[2], old[3] })
return dir
}
func TestALaptopHasABatteryAndAMeter(t *testing.T) {
dir := sysfs(t)
write(t, dir+"/power_supply/ACAD/type", "Mains")
write(t, dir+"/power_supply/BAT1/type", "Battery")
write(t, dir+"/power_supply/BAT1/scope", "System")
write(t, dir+"/power_supply/BAT1/present", "1")
write(t, dir+"/powercap/intel-rapl:0/name", "package-0")
write(t, dir+"/powercap/intel-rapl:0/energy_uj", "1")
b := battery{}.Detect(context.Background())
if !b.Present || b.Detail != "BAT1" {
t.Fatalf("battery: %+v", b)
}
m := powerMeter{}.Detect(context.Background())
if !m.Present || m.Detail != "battery BAT1, processor package intel-rapl:0" {
t.Fatalf("power meter: %+v", m)
}
}
func TestADevicesBatteryIsNotTheMachines(t *testing.T) {
// The case worth a test: a desktop with a wireless mouse reports a Battery, and a battery block
// composed for it would show the mouse's charge as the machine's.
dir := sysfs(t)
write(t, dir+"/power_supply/hidpp_battery_0/type", "Battery")
write(t, dir+"/power_supply/hidpp_battery_0/scope", "Device")
write(t, dir+"/power_supply/BAT0/type", "Battery")
write(t, dir+"/power_supply/BAT0/present", "0")
if b := (battery{}).Detect(context.Background()); b.Present {
t.Fatalf("a mouse's battery, or an empty bay, was taken for the machine's: %+v", b)
}
if m := (powerMeter{}).Detect(context.Background()); m.Present {
t.Fatalf("a device's battery was taken for a meter: %+v", m)
}
}
func TestADesktopMeasuresItsProcessorAndItsGraphics(t *testing.T) {
dir := sysfs(t)
write(t, dir+"/powercap/intel-rapl:0/name", "package-0")
write(t, dir+"/powercap/intel-rapl:0/energy_uj", "1")
// A sub-domain is part of the package, not a second meter.
write(t, dir+"/powercap/intel-rapl:0:0/name", "core")
write(t, dir+"/powercap/intel-rapl:0:0/energy_uj", "1")
write(t, dir+"/drm/card1/device/hwmon/hwmon4/power1_average", "8000000")
write(t, dir+"/nvidia/0000:01:00.0/information", "x")
if b := (battery{}).Detect(context.Background()); b.Present {
t.Fatalf("a desktop has a battery: %+v", b)
}
m := powerMeter{}.Detect(context.Background())
if !m.Present || m.Detail != "processor package intel-rapl:0, graphics card1, graphics nvidia" {
t.Fatalf("power meter: %+v", m)
}
}
func TestAMachineWithNothingToReadHasNeither(t *testing.T) {
sysfs(t)
b, m := battery{}.Detect(context.Background()), powerMeter{}.Detect(context.Background())
if b.Present || m.Present || b.How == "" || m.How == "" || !strings.Contains(m.Detail, "nothing") {
t.Fatalf("%+v %+v", b, m)
}
}