Fix PSU power chart: use name-based SDR matching instead of entity ID

MSI servers place PSU_POWER_IN/OUT sensors on entity 3.0, not 10.N
(the IPMI "Power Supply" entity). The old parser filtered by entity ID
and found nothing, so the dashboard fell back to DCMI which reports
roughly half the actual draw.

Now delegates to collector.PSUSlotsFromSDR — the same name-based
matching already used in the Power Fit benchmark.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-19 18:39:21 +03:00
parent df1385d3d6
commit b89580c24d

View File

@@ -1,8 +1,10 @@
package platform
import (
"bee/audit/internal/collector"
"bufio"
"encoding/json"
"fmt"
"os"
"os/exec"
"sort"
@@ -339,63 +341,44 @@ func compactAmbientTempName(chip, name string) string {
}
// samplePSUPower reads per-PSU input power via IPMI SDR.
// It parses `ipmitool sdr elist full` output looking for Power Supply entity
// sensors (entity ID "10.N") that report a value in Watts.
// Uses collector.PSUSlotsFromSDR (name-based matching) which works across
// vendors where PSU sensors may not carry entity ID "10.N".
// Returns nil when IPMI is unavailable or no PSU Watt sensors exist.
func samplePSUPower() []PSUReading {
out, err := exec.Command("ipmitool", "sdr", "elist", "full").Output()
out, err := exec.Command("ipmitool", "sdr").Output()
if err != nil || len(out) == 0 {
return nil
}
// map slot → reading (keep highest-watt value per slot in case of duplicates)
type entry struct {
name string
powerW float64
}
bySlot := map[int]entry{}
for _, line := range strings.Split(string(out), "\n") {
parts := strings.Split(line, "|")
if len(parts) < 5 {
continue
}
entityID := strings.TrimSpace(parts[3]) // e.g. "10.1"
if !strings.HasPrefix(entityID, "10.") {
continue // not a Power Supply entity
}
slotStr := strings.TrimPrefix(entityID, "10.")
slot, err := strconv.Atoi(slotStr)
if err != nil {
continue
}
valueField := strings.TrimSpace(parts[4]) // e.g. "740.00 Watts"
if !strings.Contains(strings.ToLower(valueField), "watts") {
continue
}
valueFields := strings.Fields(valueField)
if len(valueFields) < 2 {
continue
}
w, err := strconv.ParseFloat(valueFields[0], 64)
if err != nil || w <= 0 {
continue
}
sensorName := strings.TrimSpace(parts[0])
if existing, ok := bySlot[slot]; !ok || w > existing.powerW {
bySlot[slot] = entry{name: sensorName, powerW: w}
}
}
if len(bySlot) == 0 {
slots := collector.PSUSlotsFromSDR(string(out))
if len(slots) == 0 {
return nil
}
slots := make([]int, 0, len(bySlot))
for s := range bySlot {
slots = append(slots, s)
// Collect slot keys and sort for stable output.
keys := make([]int, 0, len(slots))
for k := range slots {
n, err := strconv.Atoi(k)
if err == nil {
keys = append(keys, n)
}
}
sort.Ints(slots)
psus := make([]PSUReading, 0, len(slots))
for _, s := range slots {
e := bySlot[s]
psus = append(psus, PSUReading{Slot: s, Name: e.name, PowerW: e.powerW})
sort.Ints(keys)
psus := make([]PSUReading, 0, len(keys))
for _, k := range keys {
entry := slots[strconv.Itoa(k)]
// Prefer AC input power; fall back to DC output power.
var w float64
if entry.InputW != nil && *entry.InputW > 0 {
w = *entry.InputW
} else if entry.OutputW != nil && *entry.OutputW > 0 {
w = *entry.OutputW
}
if w <= 0 {
continue
}
psus = append(psus, PSUReading{Slot: k + 1, Name: fmt.Sprintf("PSU%d", k+1), PowerW: w})
}
if len(psus) == 0 {
return nil
}
return psus
}