package proc import ( "fmt" "os" "path/filepath" "strconv" "strings" "time" ) type AMDReader struct { hwmonDir string driver string energyState map[string]EnergyState // tracks (path -> energy state) for energy*_input calculations } type EnergyState struct { lastEnergy uint64 lastTime time.Time } func NewAMDReader() (*AMDReader, error) { // Locate AMD k10temp or zenpower hwmon path matches, err := filepath.Glob("/sys/class/hwmon/hwmon*") if err != nil { return nil, err } for _, dir := range matches { b, err := os.ReadFile(filepath.Join(dir, "name")) if err != nil { continue } drv := strings.TrimSpace(string(b)) if drv == "k10temp" || drv == "zenpower" { return &AMDReader{ hwmonDir: dir, driver: drv, }, nil } } return nil, fmt.Errorf("k10temp/zenpower driver not found under /sys/class/hwmon/") } func (r *AMDReader) ReadTemperatureSensors() ([]TempSensor, error) { inputs, err := filepath.Glob(filepath.Join(r.hwmonDir, "temp*_input")) if err != nil || len(inputs) == 0 { return nil, fmt.Errorf("no temperature inputs found in %s", r.hwmonDir) } temps := []TempSensor{} for _, inputPath := range inputs { valBytes, err := os.ReadFile(inputPath) if err != nil { continue } milliC, err := strconv.ParseFloat(strings.TrimSpace(string(valBytes)), 64) if err != nil { continue } labelPath := strings.TrimSuffix(inputPath, "_input") + "_label" label := "" if lBytes, err := os.ReadFile(labelPath); err == nil { label = strings.TrimSpace(string(lBytes)) } else { label = filepath.Base(strings.TrimSuffix(inputPath, "_input")) } temps = append(temps, TempSensor{ Name: label, TempC: milliC / 1000.0, }) } return temps, nil } func (r *AMDReader) ReadPowerSensors() ([]PowerSensor, error) { hwmonSensors := r.readHwmonPower() if len(hwmonSensors) > 0 { return hwmonSensors, nil } return r.readRaplPower(), nil } func (r *AMDReader) readHwmonPower() []PowerSensor { var powerSensors []PowerSensor getLabel := func(inputPath string) string { labelPath := strings.TrimSuffix(inputPath, "_input") + "_label" if lBytes, err := os.ReadFile(labelPath); err == nil { return strings.TrimSpace(string(lBytes)) } return filepath.Base(strings.TrimSuffix(inputPath, "_input")) } powerInputs, _ := filepath.Glob(filepath.Join(r.hwmonDir, "power*_input")) for _, inputPath := range powerInputs { valBytes, err := os.ReadFile(inputPath) if err != nil { continue } microWatts, err := strconv.ParseFloat(strings.TrimSpace(string(valBytes)), 64) if err != nil { continue } powerSensors = append(powerSensors, PowerSensor{ Name: getLabel(inputPath), Watts: microWatts / 1_000_000.0, }) } energyInputs, _ := filepath.Glob(filepath.Join(r.hwmonDir, "energy*_input")) if len(energyInputs) > 0 { r.calculateEnergyDelta(energyInputs, getLabel, &powerSensors) } return powerSensors } func (r *AMDReader) readRaplPower() []PowerSensor { var powerSensors []PowerSensor // Match both top-level and nested RAPL zone directories patterns := []string{ "/sys/class/powercap/intel-rapl*/energy_uj", "/sys/class/powercap/intel-rapl/intel-rapl*/energy_uj", "/sys/class/powercap/intel-rapl/intel-rapl*/*/energy_uj", } var energyInputs []string seen := make(map[string]bool) for _, pattern := range patterns { matches, _ := filepath.Glob(pattern) for _, match := range matches { // Resolve symlinks to get real sysfs path realPath, err := filepath.EvalSymlinks(match) if err != nil { realPath = match } if !seen[realPath] { seen[realPath] = true energyInputs = append(energyInputs, realPath) } } } if len(energyInputs) == 0 { return powerSensors } getLabel := func(inputPath string) string { dir := filepath.Dir(inputPath) if nameBytes, err := os.ReadFile(filepath.Join(dir, "name")); err == nil { return strings.TrimSpace(string(nameBytes)) } return filepath.Base(dir) } r.calculateEnergyDelta(energyInputs, getLabel, &powerSensors) return powerSensors } func (r *AMDReader) calculateEnergyDelta(inputs []string, getLabel func(string) string, out *[]PowerSensor) { now := time.Now() if r.energyState == nil { r.energyState = make(map[string]EnergyState) } for _, inputPath := range inputs { // Canonicalize path to resolve symlinks and prevent map key duplication realPath, err := filepath.EvalSymlinks(inputPath) if err != nil { realPath = inputPath } valBytes, err := os.ReadFile(realPath) if err != nil { continue } microJoules, err := strconv.ParseUint(strings.TrimSpace(string(valBytes)), 10, 64) if err != nil { continue } prevState, exists := r.energyState[realPath] r.energyState[realPath] = EnergyState{ lastEnergy: microJoules, lastTime: now, } // Initial seed step: power delta requires two distinct reads if !exists { continue } timeDiff := now.Sub(prevState.lastTime).Seconds() if timeDiff <= 0 { continue } maxRange := uint64(1<<64 - 1) rangeBytes, err := os.ReadFile(filepath.Join(filepath.Dir(realPath), "max_energy_range_uj")) if err == nil { if parsedMax, err := strconv.ParseUint(strings.TrimSpace(string(rangeBytes)), 10, 64); err == nil { maxRange = parsedMax } } var energyDiff uint64 if microJoules >= prevState.lastEnergy { energyDiff = microJoules - prevState.lastEnergy } else { energyDiff = maxRange - prevState.lastEnergy + microJoules } watts := (float64(energyDiff) / 1_000_000.0) / timeDiff *out = append(*out, PowerSensor{ Name: getLabel(realPath), Watts: watts, }) } }