diff --git a/pkg/fancontroller/fancontroller.go b/pkg/fancontroller/fancontroller.go index 299ef08..c384f0a 100644 --- a/pkg/fancontroller/fancontroller.go +++ b/pkg/fancontroller/fancontroller.go @@ -86,20 +86,31 @@ func (f *fanControllerLinear) GetFanSpeedPercent(temperature float64) uint8 { return f.overrideOpts.Percent } - if temperature <= f.config.Steps[0].Temperature { - return f.config.Steps[0].Percent - } - if temperature >= f.config.Steps[1].Temperature { - return f.config.Steps[1].Percent + steps := f.config.Steps + + // Below minimum temperature: use minimum fan speed + if temperature <= steps[0].Temperature { + return steps[0].Percent } - // Calculate slope - slope := float64(f.config.Steps[1].Percent-f.config.Steps[0].Percent) / (f.config.Steps[1].Temperature - f.config.Steps[0].Temperature) + // Above maximum temperature: use maximum fan speed + lastIdx := len(steps) - 1 + if temperature >= steps[lastIdx].Temperature { + return steps[lastIdx].Percent + } - // Calculate speed - speed := float64(f.config.Steps[0].Percent) + slope*(temperature-f.config.Steps[0].Temperature) + // Find the bracket where steps[i].Temperature <= temperature < steps[i+1].Temperature + for i := 0; i < lastIdx; i++ { + if temperature >= steps[i].Temperature && temperature < steps[i+1].Temperature { + // Linear interpolation between steps[i] and steps[i+1] + slope := float64(steps[i+1].Percent-steps[i].Percent) / (steps[i+1].Temperature - steps[i].Temperature) + speed := float64(steps[i].Percent) + slope*(temperature-steps[i].Temperature) + return uint8(speed) + } + } - return uint8(speed) + // Fallback (should not reach here due to above checks) + return steps[lastIdx].Percent } func (f *fanControllerLinear) IsAutomaticSpeed() bool { diff --git a/pkg/fancontroller/fancontroller_test.go b/pkg/fancontroller/fancontroller_test.go index d534767..49f1f62 100644 --- a/pkg/fancontroller/fancontroller_test.go +++ b/pkg/fancontroller/fancontroller_test.go @@ -45,6 +45,55 @@ func TestFanControllerLinear_GetFanSpeed(t *testing.T) { } } +func TestFanControllerLinear_GetFanSpeedMultipleSteps(t *testing.T) { + t.Parallel() + + // Typical 5-step fan curve configuration + config := fancontroller.Config{ + Steps: []fancontroller.Step{ + {Temperature: 40, Percent: 30}, + {Temperature: 50, Percent: 50}, + {Temperature: 60, Percent: 70}, + {Temperature: 70, Percent: 90}, + {Temperature: 75, Percent: 100}, + }, + } + + controller, err := fancontroller.NewLinearFanController(config) + if err != nil { + t.Fatalf("Failed to create fan controller: %v", err) + } + + testCases := []struct { + name string + temperature float64 + expected uint8 + }{ + {"below minimum", 30, 30}, // Below 40°C: use minimum 30% + {"at step 0", 40, 30}, // At 40°C: 30% + {"between step 0-1", 45, 40}, // Midpoint 40-50°C: 40% + {"at step 1", 50, 50}, // At 50°C: 50% + {"between step 1-2", 55, 60}, // Midpoint 50-60°C: 60% + {"at step 2", 60, 70}, // At 60°C: 70% + {"between step 2-3", 65, 80}, // Midpoint 60-70°C: 80% + {"at step 3", 70, 90}, // At 70°C: 90% + {"between step 3-4", 72, 94}, // 70 + (100-90)*(72-70)/(75-70) = 90 + 4 = 94% + {"at step 4", 75, 100}, // At 75°C: 100% + {"above maximum", 80, 100}, // Above 75°C: use maximum 100% + {"well above maximum", 90, 100}, // Well above: still 100% + } + + for _, tc := range testCases { + expected := tc.expected + temperature := tc.temperature + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + speed := controller.GetFanSpeedPercent(temperature) + assert.Equal(t, expected, speed, "Temperature %.1f°C should yield %d%% fan speed", temperature, expected) + }) + } +} + func TestFanControllerLinear_GetFanSpeedWithOverride(t *testing.T) { t.Parallel()