The laptop model's hardware module and a memory-pressure module for any machine (hq research 027/03, 026/05, to-be 42 phase 3). The predecessor's polling auto-profile and mem-guard user scripts become each module's own Go code launched by the node runtime (ADR 0198): a profile switcher woken by the kernel's power-supply uevents, and a guard that warns on RAM, swap or PSI before systemd-oomd acts, on the desktop over the account's bus and always as an event. supergfxctl and triggerhappy are kept as found (research 027 Q1).
181 lines
5.8 KiB
Go
181 lines
5.8 KiB
Go
package main
|
|
|
|
import (
|
|
"path"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// Supply is one entry of /sys/class/power_supply as the kernel reports it.
|
|
type Supply struct {
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
Scope string `json:"scope,omitempty"`
|
|
Status string `json:"status,omitempty"`
|
|
Online *bool `json:"online,omitempty"`
|
|
}
|
|
|
|
// Supplies is every power supply the kernel knows, sorted by name.
|
|
func (m *Machine) Supplies() []Supply {
|
|
var out []Supply
|
|
for _, dir := range m.glob("/sys/class/power_supply/*") {
|
|
s := Supply{
|
|
Name: path.Base(dir),
|
|
Type: m.read(dir + "/type"),
|
|
Scope: m.read(dir + "/scope"),
|
|
Status: m.read(dir + "/status"),
|
|
}
|
|
if v, ok := m.readInt(dir + "/online"); ok {
|
|
on := v == 1
|
|
s.Online = &on
|
|
}
|
|
out = append(out, s)
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
|
return out
|
|
}
|
|
|
|
// system is a supply that powers this machine. A mouse's or a headset's battery reports scope
|
|
// Device, and it says nothing about whether the laptop is on mains.
|
|
func (s Supply) system() bool { return !strings.EqualFold(s.Scope, "Device") }
|
|
|
|
// Source is where the machine draws its power from, and why that was concluded.
|
|
type Source struct {
|
|
OnAC bool `json:"on_ac"`
|
|
Source string `json:"source"`
|
|
Reason string `json:"reason"`
|
|
}
|
|
|
|
// PowerSource decides mains or battery.
|
|
//
|
|
// **A battery that says it is discharging wins over any adapter that says it is online.** The
|
|
// predecessor's script took any `online` file reading 1 as mains, and a USB-C port reports `online`
|
|
// for things that do not power the machine. The battery's own status is the one fact that cannot be
|
|
// misread: it discharges exactly when nothing outside is carrying the load. Only when no battery says
|
|
// so are the adapters asked, and a machine with no system battery at all is on mains.
|
|
func PowerSource(supplies []Supply) Source {
|
|
batteries := 0
|
|
for _, s := range supplies {
|
|
if s.Type == "Battery" && s.system() {
|
|
batteries++
|
|
if strings.EqualFold(s.Status, "Discharging") {
|
|
return Source{OnAC: false, Source: "battery", Reason: s.Name + " is discharging"}
|
|
}
|
|
}
|
|
}
|
|
for _, s := range supplies {
|
|
if (s.Type == "Mains" || strings.HasPrefix(s.Type, "USB")) && s.system() && s.Online != nil && *s.Online {
|
|
return Source{OnAC: true, Source: "ac", Reason: s.Name + " (" + s.Type + ") is online"}
|
|
}
|
|
}
|
|
if batteries == 0 {
|
|
return Source{OnAC: true, Source: "ac", Reason: "this machine has no system battery"}
|
|
}
|
|
return Source{OnAC: false, Source: "battery", Reason: "no mains or USB supply is online"}
|
|
}
|
|
|
|
// Battery is what the battery tool answers.
|
|
type Battery struct {
|
|
Name string `json:"name"`
|
|
Status string `json:"status"`
|
|
ChargePercent *int64 `json:"charge_percent,omitempty"`
|
|
EnergyWh *float64 `json:"energy_wh,omitempty"`
|
|
FullWh *float64 `json:"full_wh,omitempty"`
|
|
DesignWh *float64 `json:"design_wh,omitempty"`
|
|
HealthPercent *float64 `json:"health_percent,omitempty"`
|
|
Cycles *int64 `json:"cycles"`
|
|
CyclesNote string `json:"cycles_note,omitempty"`
|
|
LimitPercent *int64 `json:"charge_limit_percent,omitempty"`
|
|
PowerW *float64 `json:"power_w,omitempty"`
|
|
HoursRemaining *float64 `json:"hours_remaining,omitempty"`
|
|
Technology string `json:"technology,omitempty"`
|
|
Model string `json:"model,omitempty"`
|
|
Manufacturer string `json:"manufacturer,omitempty"`
|
|
}
|
|
|
|
// Batteries reads every system battery.
|
|
func (m *Machine) Batteries() []Battery {
|
|
var out []Battery
|
|
for _, s := range m.Supplies() {
|
|
if s.Type != "Battery" || !s.system() {
|
|
continue
|
|
}
|
|
out = append(out, m.battery(s))
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (m *Machine) battery(s Supply) Battery {
|
|
dir := "/sys/class/power_supply/" + s.Name
|
|
b := Battery{
|
|
Name: s.Name, Status: s.Status,
|
|
Technology: m.read(dir + "/technology"),
|
|
Model: m.read(dir + "/model_name"),
|
|
Manufacturer: strings.TrimSpace(m.read(dir + "/manufacturer")),
|
|
}
|
|
if v, ok := m.readInt(dir + "/capacity"); ok {
|
|
b.ChargePercent = &v
|
|
}
|
|
// Energy in Wh: energy_* (µWh) where the firmware reports it, else charge_* (µAh) times the
|
|
// design minimum voltage, which is how upower converts it too.
|
|
wh := func(energy, charge string) *float64 {
|
|
if v, ok := m.readInt(dir + "/" + energy); ok {
|
|
f := round1(float64(v) / 1e6)
|
|
return &f
|
|
}
|
|
c, okc := m.readInt(dir + "/" + charge)
|
|
volts, okv := m.readInt(dir + "/voltage_min_design")
|
|
if okc && okv {
|
|
f := round1(float64(c) * float64(volts) / 1e12)
|
|
return &f
|
|
}
|
|
return nil
|
|
}
|
|
b.EnergyWh = wh("energy_now", "charge_now")
|
|
b.FullWh = wh("energy_full", "charge_full")
|
|
b.DesignWh = wh("energy_full_design", "charge_full_design")
|
|
if b.FullWh != nil && b.DesignWh != nil && *b.DesignWh > 0 {
|
|
h := round1(*b.FullWh / *b.DesignWh * 100)
|
|
b.HealthPercent = &h
|
|
}
|
|
if v, ok := m.readInt(dir + "/cycle_count"); ok && v > 0 {
|
|
b.Cycles = &v
|
|
} else {
|
|
b.CyclesNote = "the firmware does not report a cycle count (it reads 0)"
|
|
}
|
|
if v, ok := m.readInt(dir + "/charge_control_end_threshold"); ok {
|
|
b.LimitPercent = &v
|
|
}
|
|
if w := m.batteryWatts(dir); w != nil {
|
|
b.PowerW = w
|
|
if strings.EqualFold(s.Status, "Discharging") && b.EnergyWh != nil && *w > 0.5 {
|
|
h := round1(*b.EnergyWh / *w)
|
|
b.HoursRemaining = &h
|
|
}
|
|
}
|
|
return b
|
|
}
|
|
|
|
// batteryWatts is how much the battery is giving or taking, in watts, unsigned: power_now where the
|
|
// firmware reports it, else current times voltage.
|
|
func (m *Machine) batteryWatts(dir string) *float64 {
|
|
if v, ok := m.readInt(dir + "/power_now"); ok {
|
|
f := round1(abs(float64(v)) / 1e6)
|
|
return &f
|
|
}
|
|
i, oki := m.readInt(dir + "/current_now")
|
|
u, oku := m.readInt(dir + "/voltage_now")
|
|
if oki && oku {
|
|
f := round1(abs(float64(i)) * float64(u) / 1e12)
|
|
return &f
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func abs(f float64) float64 {
|
|
if f < 0 {
|
|
return -f
|
|
}
|
|
return f
|
|
}
|