fix(temperature): parse string sensor values without zeroing readings (#1224)

This commit is contained in:
rcourtman 2026-02-09 14:00:09 +00:00
parent 0d6fffbb1c
commit cedf0c8f0f
4 changed files with 151 additions and 7 deletions

View file

@ -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)

View file

@ -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,

View file

@ -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) {

View file

@ -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"])
}
}