package main import ( "context" "path" "sort" "strconv" "strings" ) // Sensor is one temperature, fan or power reading from hwmon. type Sensor struct { Chip string `json:"chip"` Label string `json:"label"` Value float64 `json:"value"` } // DGPU is the discrete GPU as the PCI bus and its driver see it. type DGPU struct { Address string `json:"pci_address"` Runtime string `json:"runtime_status"` Name string `json:"name,omitempty"` TempC *float64 `json:"temp_c,omitempty"` PowerW *float64 `json:"power_w,omitempty"` PState string `json:"pstate,omitempty"` Note string `json:"note,omitempty"` } // hwmon reads every hwmon reading of one kind: "temp" (°C), "fan" (RPM) or "power" (W). func (m *Machine) hwmon(kind string) []Sensor { var out []Sensor for _, dir := range m.glob("/sys/class/hwmon/hwmon*") { chip := m.read(dir + "/name") inputs := m.glob(dir + "/" + kind + "*_input") if kind == "power" { inputs = append(inputs, m.glob(dir+"/power*_average")...) } for _, in := range inputs { v, ok := m.readInt(in) if !ok { continue } base := path.Base(in) stem := base[:strings.LastIndex(base, "_")] label := m.read(dir + "/" + stem + "_label") if label == "" { label = base } else if strings.HasSuffix(base, "_average") { label += " (average)" } value := float64(v) switch kind { case "temp": value = round1(value / 1000) case "power": value = round1(value / 1e6) } out = append(out, Sensor{Chip: chip, Label: label, Value: value}) } } sort.Slice(out, func(i, j int) bool { if out[i].Chip != out[j].Chip { return out[i].Chip < out[j].Chip } return out[i].Label < out[j].Label }) return out } // dgpu finds the NVIDIA display controller and, only when it is already awake, asks its driver for // its temperature and draw. **Asking wakes it**: nvidia-smi brings a suspended GPU out of D3, which // is the power a reading of power draw should not cost. func (m *Machine) dgpu(ctx context.Context) *DGPU { for _, dir := range m.glob("/sys/bus/pci/devices/*") { if m.read(dir+"/vendor") != "0x10de" || !strings.HasPrefix(m.read(dir+"/class"), "0x03") { continue } g := &DGPU{Address: path.Base(dir), Runtime: m.read(dir + "/power/runtime_status")} if g.Runtime != "active" { g.Note = "the discrete GPU is " + g.Runtime + "; not woken to be read" return g } out, err := m.Run(ctx, "nvidia-smi", "--query-gpu=name,temperature.gpu,power.draw,pstate", "--format=csv,noheader,nounits") if err != nil { g.Note = "nvidia-smi: " + err.Error() return g } f := strings.Split(strings.TrimSpace(strings.SplitN(out, "\n", 2)[0]), ",") if len(f) >= 4 { g.Name = strings.TrimSpace(f[0]) if t, err := strconv.ParseFloat(strings.TrimSpace(f[1]), 64); err == nil { g.TempC = &t } if w, err := strconv.ParseFloat(strings.TrimSpace(f[2]), 64); err == nil { w = round1(w) g.PowerW = &w } g.PState = strings.TrimSpace(f[3]) } return g } return nil } // Thermals is what the thermals tool answers. type Thermals struct { Temperatures []Sensor `json:"temperatures_c"` Fans []Sensor `json:"fans_rpm"` DGPU *DGPU `json:"dgpu,omitempty"` Profile string `json:"platform_profile,omitempty"` Hottest *Sensor `json:"hottest,omitempty"` } func (m *Machine) Thermals(ctx context.Context) Thermals { t := Thermals{Temperatures: m.hwmon("temp"), Fans: m.hwmon("fan"), DGPU: m.dgpu(ctx), Profile: m.read("/sys/firmware/acpi/platform_profile")} if t.Temperatures == nil { t.Temperatures = []Sensor{} } if t.Fans == nil { t.Fans = []Sensor{} } for i := range t.Temperatures { if t.Hottest == nil || t.Temperatures[i].Value > t.Hottest.Value { h := t.Temperatures[i] t.Hottest = &h } } return t } // PowerDraw is what the power-draw tool answers. type PowerDraw struct { Source Source `json:"source"` BatteryW *float64 `json:"battery_w,omitempty"` BatteryFlow string `json:"battery_flow,omitempty"` CPUPackageW *float64 `json:"apu_package_w,omitempty"` DGPU *DGPU `json:"dgpu,omitempty"` Note string `json:"note"` } func (m *Machine) PowerDraw(ctx context.Context) PowerDraw { p := PowerDraw{Source: PowerSource(m.Supplies()), DGPU: m.dgpu(ctx), Note: "on battery, battery_w is what the whole machine draws; on mains it is only what the battery takes or gives"} for _, b := range m.Batteries() { if b.PowerW != nil { w := *b.PowerW p.BatteryW = &w switch strings.ToLower(b.Status) { case "discharging": p.BatteryFlow = "discharging" case "charging": p.BatteryFlow = "charging" default: p.BatteryFlow = strings.ToLower(b.Status) } break } } // The integrated GPU's hwmon reports the whole APU's package power (PPT) on this model. for _, s := range m.hwmon("power") { if s.Chip == "amdgpu" && s.Label == "PPT" { w := s.Value p.CPUPackageW = &w } } return p }