The host-agent's isCPUChip function was missing rp1_adc (Raspberry Pi RP1 ADC), which is already detected in the monitoring package. This sync ensures consistent CPU temperature chip detection across both code paths.
248 lines
6.8 KiB
Go
248 lines
6.8 KiB
Go
package sensors
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"math"
|
|
"strings"
|
|
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
// TemperatureData contains parsed temperature readings from sensors
|
|
type TemperatureData struct {
|
|
CPUPackage float64 // Overall CPU package temperature
|
|
CPUMax float64 // Maximum CPU temperature
|
|
Cores map[string]float64 // Per-core temperatures (e.g., "Core 0": 45.0)
|
|
NVMe map[string]float64 // NVMe drive temperatures (e.g., "nvme0": 42.0)
|
|
GPU map[string]float64 // GPU temperatures (e.g., "amdgpu-pci-0400": 55.0)
|
|
Available bool // Whether any temperature data was found
|
|
}
|
|
|
|
// Parse extracts temperature data from sensors -j JSON output
|
|
func Parse(jsonStr string) (*TemperatureData, error) {
|
|
if strings.TrimSpace(jsonStr) == "" {
|
|
return nil, fmt.Errorf("empty sensors output")
|
|
}
|
|
|
|
var sensorsData map[string]interface{}
|
|
if err := json.Unmarshal([]byte(jsonStr), &sensorsData); err != nil {
|
|
return nil, fmt.Errorf("failed to parse sensors JSON: %w", err)
|
|
}
|
|
|
|
data := &TemperatureData{
|
|
Cores: make(map[string]float64),
|
|
NVMe: make(map[string]float64),
|
|
GPU: make(map[string]float64),
|
|
}
|
|
|
|
foundCPUChip := false
|
|
|
|
// Parse each sensor chip
|
|
for chipName, chipData := range sensorsData {
|
|
chipMap, ok := chipData.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
chipLower := strings.ToLower(chipName)
|
|
|
|
// Handle CPU temperature sensors
|
|
if isCPUChip(chipLower) {
|
|
foundCPUChip = true
|
|
parseCPUTemps(chipMap, data)
|
|
}
|
|
|
|
// Handle NVMe temperature sensors
|
|
if strings.Contains(chipName, "nvme") {
|
|
parseNVMeTemps(chipName, chipMap, data)
|
|
}
|
|
|
|
// Handle GPU temperature sensors
|
|
if strings.Contains(chipLower, "amdgpu") || strings.Contains(chipLower, "nouveau") {
|
|
parseGPUTemps(chipName, chipMap, data)
|
|
}
|
|
}
|
|
|
|
// If we got CPU temps, calculate max from cores if package not available
|
|
if data.CPUPackage == 0 && len(data.Cores) > 0 {
|
|
for _, temp := range data.Cores {
|
|
if temp > data.CPUMax {
|
|
data.CPUMax = temp
|
|
}
|
|
}
|
|
// Use max core temp as package temp if not available
|
|
data.CPUPackage = data.CPUMax
|
|
}
|
|
|
|
data.Available = foundCPUChip || len(data.NVMe) > 0 || len(data.GPU) > 0
|
|
|
|
log.Debug().
|
|
Bool("available", data.Available).
|
|
Float64("cpuPackage", data.CPUPackage).
|
|
Float64("cpuMax", data.CPUMax).
|
|
Int("coreCount", len(data.Cores)).
|
|
Int("nvmeCount", len(data.NVMe)).
|
|
Int("gpuCount", len(data.GPU)).
|
|
Msg("Parsed temperature data")
|
|
|
|
return data, nil
|
|
}
|
|
|
|
func isCPUChip(chipLower string) bool {
|
|
cpuChips := []string{
|
|
"coretemp", "k10temp", "zenpower", "k8temp", "acpitz",
|
|
"it87", "nct6687", "nct6775", "nct6776", "nct6779",
|
|
"nct6791", "nct6792", "nct6793", "nct6795", "nct6796",
|
|
"nct6797", "nct6798", "w83627", "f71882",
|
|
"cpu_thermal", "rp1_adc", "rpitemp",
|
|
}
|
|
|
|
for _, chip := range cpuChips {
|
|
if strings.Contains(chipLower, chip) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func parseCPUTemps(chipMap map[string]interface{}, data *TemperatureData) {
|
|
foundPackageTemp := false
|
|
var chipletTemps []float64
|
|
|
|
for sensorName, sensorData := range chipMap {
|
|
sensorMap, ok := sensorData.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
sensorNameLower := strings.ToLower(sensorName)
|
|
|
|
// Look for Package id (Intel) or Tdie/Tctl (AMD)
|
|
if strings.Contains(sensorName, "Package id") ||
|
|
strings.Contains(sensorName, "Tdie") ||
|
|
strings.Contains(sensorNameLower, "tctl") {
|
|
if tempVal := extractTempInput(sensorMap); !math.IsNaN(tempVal) {
|
|
data.CPUPackage = tempVal
|
|
foundPackageTemp = true
|
|
if tempVal > data.CPUMax {
|
|
data.CPUMax = tempVal
|
|
}
|
|
}
|
|
}
|
|
|
|
// Look for AMD chiplet temperatures
|
|
if strings.HasPrefix(sensorName, "Tccd") {
|
|
if tempVal := extractTempInput(sensorMap); !math.IsNaN(tempVal) && tempVal > 0 {
|
|
chipletTemps = append(chipletTemps, tempVal)
|
|
if tempVal > data.CPUMax {
|
|
data.CPUMax = tempVal
|
|
}
|
|
}
|
|
}
|
|
|
|
// Look for SuperIO chip CPU temperature fields
|
|
if strings.Contains(sensorNameLower, "cputin") ||
|
|
strings.Contains(sensorNameLower, "cpu temperature") ||
|
|
(strings.Contains(sensorNameLower, "temp") && strings.Contains(sensorNameLower, "cpu")) {
|
|
if tempVal := extractTempInput(sensorMap); !math.IsNaN(tempVal) && tempVal > 0 {
|
|
if !foundPackageTemp {
|
|
data.CPUPackage = tempVal
|
|
foundPackageTemp = true
|
|
}
|
|
if tempVal > data.CPUMax {
|
|
data.CPUMax = tempVal
|
|
}
|
|
}
|
|
}
|
|
|
|
// Look for individual core temperatures
|
|
if strings.Contains(sensorName, "Core ") {
|
|
if tempVal := extractTempInput(sensorMap); !math.IsNaN(tempVal) {
|
|
data.Cores[sensorName] = tempVal
|
|
if tempVal > data.CPUMax {
|
|
data.CPUMax = tempVal
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If no package temp but we have chiplet temps, use highest chiplet
|
|
if !foundPackageTemp && len(chipletTemps) > 0 {
|
|
for _, temp := range chipletTemps {
|
|
if temp > data.CPUPackage {
|
|
data.CPUPackage = temp
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func parseNVMeTemps(chipName string, chipMap map[string]interface{}, data *TemperatureData) {
|
|
for sensorName, sensorData := range chipMap {
|
|
sensorMap, ok := sensorData.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
// Look for Composite temperature (main NVMe temp)
|
|
if strings.Contains(sensorName, "Composite") {
|
|
if tempVal := extractTempInput(sensorMap); !math.IsNaN(tempVal) {
|
|
data.NVMe[chipName] = tempVal
|
|
log.Debug().
|
|
Str("chip", chipName).
|
|
Float64("temp", tempVal).
|
|
Msg("Found NVMe temperature")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func parseGPUTemps(chipName string, chipMap map[string]interface{}, data *TemperatureData) {
|
|
for sensorName, sensorData := range chipMap {
|
|
sensorMap, ok := sensorData.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
sensorNameLower := strings.ToLower(sensorName)
|
|
|
|
// Look for GPU temperature fields
|
|
if strings.Contains(sensorNameLower, "edge") ||
|
|
strings.Contains(sensorNameLower, "junction") ||
|
|
strings.Contains(sensorNameLower, "mem") ||
|
|
strings.Contains(sensorNameLower, "temp1") {
|
|
if tempVal := extractTempInput(sensorMap); !math.IsNaN(tempVal) {
|
|
// Use sensor name as key (e.g., "edge", "junction")
|
|
key := fmt.Sprintf("%s_%s", chipName, sensorName)
|
|
data.GPU[key] = tempVal
|
|
log.Debug().
|
|
Str("chip", chipName).
|
|
Str("sensor", sensorName).
|
|
Float64("temp", tempVal).
|
|
Msg("Found GPU temperature")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func extractTempInput(sensorMap map[string]interface{}) float64 {
|
|
// Look for temp*_input field (the actual temperature reading)
|
|
for key, value := range sensorMap {
|
|
if strings.HasSuffix(key, "_input") {
|
|
switch v := value.(type) {
|
|
case float64:
|
|
return v
|
|
case int:
|
|
return float64(v)
|
|
case string:
|
|
// Raspberry Pi reports in millidegrees as string
|
|
var milliTemp float64
|
|
if _, err := fmt.Sscanf(v, "%f", &milliTemp); err == nil {
|
|
// Convert from millidegrees to degrees
|
|
return milliTemp / 1000.0
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return math.NaN()
|
|
}
|