diff --git a/internal/monitoring/temperature.go b/internal/monitoring/temperature.go index dc08ff227..638e4e31c 100644 --- a/internal/monitoring/temperature.go +++ b/internal/monitoring/temperature.go @@ -712,8 +712,8 @@ func extractTempInput(sensorMap map[string]interface{}) float64 { case int: return float64(v) case string: - if f, err := strconv.ParseFloat(v, 64); err == nil { - return f + if parsed, ok := parseStringTemperature(v); ok { + return parsed } } } @@ -721,6 +721,26 @@ func extractTempInput(sensorMap map[string]interface{}) float64 { return math.NaN() } +func parseStringTemperature(value string) (float64, bool) { + value = strings.TrimSpace(value) + if value == "" { + return 0, false + } + + parsed, err := strconv.ParseFloat(value, 64) + if err != nil { + if _, scanErr := fmt.Sscanf(value, "%f", &parsed); scanErr != nil { + return 0, false + } + } + + if math.Abs(parsed) >= 1000 { + parsed = parsed / 1000.0 + } + + return parsed, true +} + // extractCoreNumber extracts the core number from a sensor name like "Core 0" func extractCoreNumber(name string) int { parts := strings.Fields(name) diff --git a/internal/monitoring/temperature_test.go b/internal/monitoring/temperature_test.go index 10739911b..530945a40 100644 --- a/internal/monitoring/temperature_test.go +++ b/internal/monitoring/temperature_test.go @@ -119,6 +119,43 @@ func TestTemperatureCollector_ParseSensorsJSON_Complex(t *testing.T) { assert.Equal(t, 60.5, temp.CPUPackage) // Tctl mapped to package } +func TestExtractTempInput_StringValues(t *testing.T) { + t.Run("parses celsius strings with suffix", func(t *testing.T) { + got := extractTempInput(map[string]interface{}{ + "temp1_input": "+44.5°C", + }) + assert.InDelta(t, 44.5, got, 0.0001) + }) + + t.Run("parses millidegree strings", func(t *testing.T) { + got := extractTempInput(map[string]interface{}{ + "temp1_input": "39000", + }) + assert.InDelta(t, 39.0, got, 0.0001) + }) +} + +func TestTemperatureCollector_ParseSensorsJSON_StringTemps(t *testing.T) { + tc := &TemperatureCollector{} + jsonStr := `{ + "coretemp-isa-0000": { + "Package id 0": { "temp1_input": "+47.0°C" }, + "Core 0": { "temp2_input": "45.0" } + }, + "nvme-pci-0100": { + "Composite": { "temp1_input": "42000" } + } + }` + + temp, err := tc.parseSensorsJSON(jsonStr) + require.NoError(t, err) + require.NotNil(t, temp) + assert.True(t, temp.Available) + assert.InDelta(t, 47.0, temp.CPUPackage, 0.0001) + require.Len(t, temp.NVMe, 1) + assert.InDelta(t, 42.0, temp.NVMe[0].Temp, 0.0001) +} + func TestTemperatureCollector_HelperMethods(t *testing.T) { // extractCoreNumber // Private methods are hard to test directly from separate package if using _test, diff --git a/internal/sensors/parser.go b/internal/sensors/parser.go index fefdaed29..942b5b2dd 100644 --- a/internal/sensors/parser.go +++ b/internal/sensors/parser.go @@ -5,6 +5,7 @@ import ( "fmt" "math" "sort" + "strconv" "strings" "github.com/rs/zerolog/log" @@ -279,11 +280,8 @@ func extractTempInput(sensorMap map[string]interface{}) float64 { 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 + if parsed, ok := parseStringTemperature(v); ok { + return parsed } } } @@ -291,6 +289,32 @@ func extractTempInput(sensorMap map[string]interface{}) float64 { return math.NaN() } +// parseStringTemperature parses numeric string temperature values. +// It preserves normal degree values (e.g., "45.0", "+45.0C") and only converts +// probable millidegree values (e.g., "42000") down to degrees. +func parseStringTemperature(value string) (float64, bool) { + value = strings.TrimSpace(value) + if value == "" { + return 0, false + } + + parsed, err := strconv.ParseFloat(value, 64) + if err != nil { + // Fall back to parsing numeric prefixes such as "+45.0°C". + if _, scanErr := fmt.Sscanf(value, "%f", &parsed); scanErr != nil { + return 0, false + } + } + + // lm-sensors fallback on some platforms can report millidegrees as raw strings. + // Convert only when the magnitude strongly indicates millidegrees. + if math.Abs(parsed) >= 1000 { + parsed = parsed / 1000.0 + } + + return parsed, true +} + // parseFansAndOther extracts fan speeds and other temperature readings from a sensor chip. // This captures DDR5/RAM temps, motherboard temps, additional NVMe sensors, fan speeds, etc. func parseFansAndOther(chipName string, chipMap map[string]interface{}, data *TemperatureData) { diff --git a/internal/sensors/parser_additional_test.go b/internal/sensors/parser_additional_test.go index a351ac633..09da75b66 100644 --- a/internal/sensors/parser_additional_test.go +++ b/internal/sensors/parser_additional_test.go @@ -100,3 +100,66 @@ func TestExtractNVMeCompositeTemp(t *testing.T) { } }) } + +func TestExtractTempInput_StringTemperatures(t *testing.T) { + t.Run("preserves normal celsius values", func(t *testing.T) { + sensorMap := map[string]interface{}{ + "temp1_input": "+45.5°C", + } + + got := extractTempInput(sensorMap) + if math.Abs(got-45.5) > 0.0001 { + t.Fatalf("extractTempInput() = %v, want 45.5", got) + } + }) + + t.Run("converts millidegree strings", func(t *testing.T) { + sensorMap := map[string]interface{}{ + "temp1_input": "42000", + } + + got := extractTempInput(sensorMap) + if math.Abs(got-42.0) > 0.0001 { + t.Fatalf("extractTempInput() = %v, want 42.0", got) + } + }) +} + +func TestParse_WithStringTemperatureValues(t *testing.T) { + input := `{ + "coretemp-isa-0000": { + "Package id 0": { + "temp1_input": "+50.0°C" + }, + "Core 0": { + "temp2_input": "48.0" + } + }, + "nvme-pci-0100": { + "Composite": { + "temp1_input": "39000" + } + } + }` + + data, err := Parse(input) + if err != nil { + t.Fatalf("Parse() error: %v", err) + } + + if !data.Available { + t.Fatal("Expected Available to be true") + } + + if math.Abs(data.CPUPackage-50.0) > 0.0001 { + t.Fatalf("CPUPackage = %v, want 50.0", data.CPUPackage) + } + + if math.Abs(data.Cores["Core 0"]-48.0) > 0.0001 { + t.Fatalf("Core 0 = %v, want 48.0", data.Cores["Core 0"]) + } + + if math.Abs(data.NVMe["nvme0"]-39.0) > 0.0001 { + t.Fatalf("nvme0 = %v, want 39.0", data.NVMe["nvme0"]) + } +}