Files
mesh-catalog/modules/power/cmd/power/state.go
T

153 lines
4.6 KiB
Go

package main
import (
"path/filepath"
"sort"
"strconv"
"strings"
"time"
)
// Supply is one power supply the kernel reports.
type Supply struct {
Name string `json:"name"`
Type string `json:"type"`
Online *bool `json:"online,omitempty"`
Percent *int `json:"percent,omitempty"`
Status string `json:"status,omitempty"`
Device bool `json:"device,omitempty"` // a peripheral's battery (a mouse), not the machine's
Capacity string `json:"capacity_level,omitempty"`
}
// Supplies is every power supply under /sys/class/power_supply, by name.
func (m *Machine) Supplies() []Supply {
var out []Supply
for _, dir := range m.glob("/sys/class/power_supply/*") {
s := Supply{Name: filepath.Base(dir), Type: m.read(dir + "/type")}
if v, ok := m.readInt(dir + "/online"); ok {
on := v == 1
s.Online = &on
}
if v, ok := m.readInt(dir + "/capacity"); ok {
p := int(v)
s.Percent = &p
}
s.Status = m.read(dir + "/status")
s.Device = m.read(dir+"/scope") == "Device"
s.Capacity = m.read(dir + "/capacity_level")
out = append(out, s)
}
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
return out
}
// Source is where the machine draws its power from: on-mains, on-battery, or none when it has no
// Mains supply to say (a desktop, a server, a USB-C source only) — the same reading as the runner's.
func Source(supplies []Supply) string {
found := false
for _, s := range supplies {
if s.Type != "Mains" {
continue
}
found = true
if s.Online != nil && *s.Online {
return "on-mains"
}
}
if found {
return "on-battery"
}
return "none"
}
// Battery is the machine's own battery charge, or nil when it has none.
func Battery(supplies []Supply) (percent *int, status string) {
for _, s := range supplies {
if s.Type == "Battery" && !s.Device && s.Percent != nil {
return s.Percent, s.Status
}
}
return nil, ""
}
// BootTime is when the kernel started, from /proc/stat's btime.
func (m *Machine) BootTime() (time.Time, bool) {
for _, line := range strings.Split(m.read("/proc/stat"), "\n") {
if f := strings.Fields(line); len(f) == 2 && f[0] == "btime" {
if v, err := strconv.ParseInt(f[1], 10, 64); err == nil {
return time.Unix(v, 0).UTC(), true
}
}
}
return time.Time{}, false
}
// Lid is the lid's state ("open", "closed"), or "" on a machine without one.
func (m *Machine) Lid() string {
for _, p := range m.glob("/proc/acpi/button/lid/*/state") {
if f := strings.Fields(m.read(p)); len(f) > 0 {
return f[len(f)-1]
}
}
return ""
}
// LogindSettings is what logind's power keys are set to, the last file to say each one winning, in
// logind's own order: its main file, then the drop-ins by name.
func (m *Machine) LogindSettings() map[string]map[string]string {
keys := []string{"HandlePowerKey", "HandleSuspendKey", "HandleHibernateKey", "HandleLidSwitch",
"HandleLidSwitchExternalPower", "HandleLidSwitchDocked", "IdleAction", "InhibitDelayMaxSec"}
want := map[string]bool{}
for _, k := range keys {
want[k] = true
}
out := map[string]map[string]string{}
files := append([]string{"/etc/systemd/logind.conf"}, sortedGlob(m, "/usr/lib/systemd/logind.conf.d/*.conf")...)
files = append(files, sortedGlob(m, "/etc/systemd/logind.conf.d/*.conf")...)
for _, f := range files {
for _, line := range strings.Split(m.read(f), "\n") {
line = strings.TrimSpace(line)
k, v, ok := strings.Cut(line, "=")
if !ok || strings.HasPrefix(line, "#") || !want[strings.TrimSpace(k)] {
continue
}
out[strings.TrimSpace(k)] = map[string]string{"value": strings.TrimSpace(v), "from": f}
}
}
for _, k := range keys {
if _, set := out[k]; !set {
out[k] = map[string]string{"value": "(logind's default)", "from": ""}
}
}
return out
}
func sortedGlob(m *Machine, pattern string) []string {
got := m.glob(pattern)
sort.Strings(got)
return got
}
// State is the machine's power now: since when it runs, where its power comes from, the battery and
// the lid, logind's settings, and what the module's watcher last saw and holds.
func (m *Machine) State(w *Watcher) map[string]any {
supplies := m.Supplies()
out := map[string]any{"source": Source(supplies), "supplies": orEmpty(supplies)}
if bt, ok := m.BootTime(); ok {
out["booted_at"] = bt.Format(time.RFC3339)
out["up_for"] = time.Since(bt).Round(time.Second).String()
}
if p, status := Battery(supplies); p != nil {
out["battery_percent"] = *p
out["battery_status"] = status
}
if lid := m.Lid(); lid != "" {
out["lid"] = lid
}
out["logind"] = m.LogindSettings()
if w != nil {
out["watcher"] = w.Snapshot()
}
return out
}