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 }