Skip to content

Commit bdd4495

Browse files
committed
Add staircase fan curve
1 parent af8e10f commit bdd4495

8 files changed

Lines changed: 310 additions & 34 deletions

File tree

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,25 @@ curves:
584584
- 80: 255
585585
```
586586

587+
#### Staircase
588+
589+
To create a simple, staircase speed curve, use a curve of type `staircase`.
590+
591+
It maintains a static fan speed and avoids constantly changing fan speeds.
592+
593+
```yaml
594+
curves:
595+
- id: staircase_curve
596+
staircase:
597+
sensor: cpu_package
598+
threshold: 6
599+
steps:
600+
# Sensor value (in degress Celsius) -> Speed (0-255)
601+
- 40: 1
602+
- 50: 50
603+
- 80: 255
604+
```
605+
587606
#### PID
588607

589608
If you want to get your hands dirty and use a PID based curve, you can use `pid`:

internal/configuration/config.go

Lines changed: 41 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -189,40 +189,51 @@ func applyTransformations() {
189189
// convert steps in linear curves from strings (with plain numbers or percent values) to floats between 0 and 255
190190
for _, curve := range CurrentConfig.Curves {
191191
if curve.Linear != nil && len(curve.Linear.InSteps) > 0 {
192-
curve.Linear.Steps = make(map[int]float64)
193-
194-
for temp, origstr := range curve.Linear.InSteps {
195-
str := strings.TrimSpace(origstr)
196-
l := len(str)
197-
isPercent := false
198-
if l > 1 && str[l-1] == '%' {
199-
isPercent = true
200-
str = str[:l-1] // cut off '%' because ParseFloat() wouldn't like it
192+
transformCurveSteps(&curve.ID, &curve.Linear.Steps, &curve.Linear.InSteps)
193+
}
194+
if curve.Staircase != nil && len(curve.Staircase.InSteps) > 0 {
195+
if len(curve.Staircase.InSteps) > 0 {
196+
transformCurveSteps(&curve.ID, &curve.Staircase.Steps, &curve.Staircase.InSteps)
197+
} else {
198+
ui.Fatal("Missing steps in curve %s", curve.ID)
199+
}
200+
}
201+
}
202+
}
203+
204+
func transformCurveSteps(ID *string, Steps *map[int]float64, InSteps *map[int]string) {
205+
*Steps = make(map[int]float64)
206+
207+
for temp, origstr := range *InSteps {
208+
str := strings.TrimSpace(origstr)
209+
l := len(str)
210+
isPercent := false
211+
if l > 1 && str[l-1] == '%' {
212+
isPercent = true
213+
str = str[:l-1] // cut off '%' because ParseFloat() wouldn't like it
214+
}
215+
speed, err := strconv.ParseFloat(str, 64)
216+
if err != nil {
217+
ui.Fatal("Invalid curve step value '%s' in %s - must be either just a number or a number followed by '%%'", origstr, *ID)
218+
} else {
219+
if isPercent {
220+
if speed < 0 || speed > 100 {
221+
ui.Fatal("invalid curve step value '%s' (=> %f) in %s - must be between 0%% and 100%%", origstr, speed, *ID)
201222
}
202-
speed, err := strconv.ParseFloat(str, 64)
203-
if err != nil {
204-
ui.Fatal("Invalid curve step value '%s' in %s - must be either just a number or a number followed by '%%'", origstr, curve.ID)
223+
// convert 0-100% into [0..255]
224+
if speed < 1 {
225+
// less than 1% always turns into 0
226+
speed = 0
205227
} else {
206-
if isPercent {
207-
if speed < 0 || speed > 100 {
208-
ui.Fatal("invalid curve step value '%s' (=> %f) in %s - must be between 0%% and 100%%", origstr, speed, curve.ID)
209-
}
210-
// convert 0-100% into [0..255]
211-
if speed < 1 {
212-
// less than 1% always turns into 0
213-
speed = 0
214-
} else {
215-
// 1% turns into 1, 100% turns into 255
216-
// => convert 1..100% to 1..255
217-
// => 0..99 to 0..254 and then add 1
218-
speed = (speed-1)*(254.0/99.0) + 1
219-
}
220-
} else if speed < 0 || speed > 255 {
221-
ui.Fatal("invalid curve step value '%s' in %s - must be between 0 and 255", origstr, curve.ID)
222-
}
223-
curve.Linear.Steps[temp] = speed
228+
// 1% turns into 1, 100% turns into 255
229+
// => convert 1..100% to 1..255
230+
// => 0..99 to 0..254 and then add 1
231+
speed = (speed-1)*(254.0/99.0) + 1
224232
}
233+
} else if speed < 0 || speed > 255 {
234+
ui.Fatal("invalid curve step value '%s' in %s - must be between 0 and 255", origstr, *ID)
225235
}
236+
(*Steps)[temp] = speed
226237
}
227238
}
228239
}

internal/configuration/curves.go

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ type CurveConfig struct {
55
ID string `json:"id"`
66

77
// can be any of the following:
8-
Linear *LinearCurveConfig `json:"linear,omitempty"`
9-
PID *PidCurveConfig `json:"pid,omitempty"`
10-
Function *FunctionCurveConfig `json:"function,omitempty"`
8+
Linear *LinearCurveConfig `json:"linear,omitempty"`
9+
Staircase *StaircaseCurveConfig `json:"staircase,omitempty"`
10+
PID *PidCurveConfig `json:"pid,omitempty"`
11+
Function *FunctionCurveConfig `json:"function,omitempty"`
1112
}
1213

1314
type LinearCurveConfig struct {
@@ -26,6 +27,20 @@ type LinearCurveConfig struct {
2627
Steps map[int]float64 `json:"steps" mapstructure:"-"`
2728
}
2829

30+
type StaircaseCurveConfig struct {
31+
// Sensor is the id of the sensor to use for this curve
32+
Sensor string `json:"sensor"`
33+
// Threshold is the temperature threshold in degrees
34+
Threshold int `json:"threshold"`
35+
// Steps is a map of temperature to relative speed value (in range of 0..255 or alternatively 0%..100%)
36+
// InSteps contains the speed values as strings (like "42" or "11%"), as read from fan2go.yaml
37+
InSteps map[int]string `mapstructure:"steps" json:"-"`
38+
// Steps is created from InSteps on load (LoadConfig()), the strings are converted to floats
39+
// between 0 and 255 (0% is 0, 1% is 1; from there on it's interpolated linearly so 100% is 255).
40+
// If a string only contains a number (without "%"), it's just converted to float
41+
Steps map[int]float64 `json:"steps" mapstructure:"-"`
42+
}
43+
2944
type PidCurveConfig struct {
3045
// Sensor is the id of the sensor to use for this curve
3146
Sensor string `json:"sensor"`

internal/configuration/validation.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,9 @@ func isSensorConfigInUse(config SensorConfig, curves []CurveConfig) bool {
124124
if curveConfig.Linear != nil && curveConfig.Linear.Sensor == config.ID {
125125
return true
126126
}
127+
if curveConfig.Staircase != nil && curveConfig.Staircase.Sensor == config.ID {
128+
return true
129+
}
127130
if curveConfig.PID != nil && curveConfig.PID.Sensor == config.ID {
128131
return true
129132
}
@@ -146,6 +149,9 @@ func validateCurves(config *Configuration) error {
146149
if curveConfig.Linear != nil {
147150
subConfigs++
148151
}
152+
if curveConfig.Staircase != nil {
153+
subConfigs++
154+
}
149155
if curveConfig.PID != nil {
150156
subConfigs++
151157
}
@@ -196,6 +202,20 @@ func validateCurves(config *Configuration) error {
196202
}
197203
}
198204

205+
if curveConfig.Staircase != nil {
206+
if len(curveConfig.Staircase.Sensor) <= 0 {
207+
return fmt.Errorf("curve %s: missing sensorId", curveConfig.ID)
208+
}
209+
210+
if !sensorIdExists(curveConfig.Staircase.Sensor, config) {
211+
return fmt.Errorf("curve %s: no sensor definition with id '%s' found", curveConfig.ID, curveConfig.Staircase.Sensor)
212+
}
213+
214+
if len(curveConfig.Staircase.InSteps) <= 0 {
215+
return fmt.Errorf("curve %s: missing steps", curveConfig.ID)
216+
}
217+
}
218+
199219
if curveConfig.PID != nil {
200220
if len(curveConfig.PID.Sensor) <= 0 {
201221
return fmt.Errorf("curve %s: missing sensorId", curveConfig.ID)

internal/curves/curve.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package curves
22

33
import (
44
"fmt"
5+
"math"
56

67
"github.com/markusressel/fan2go/internal/configuration"
78
"github.com/markusressel/fan2go/internal/util"
@@ -30,6 +31,14 @@ func NewSpeedCurve(config configuration.CurveConfig) (SpeedCurve, error) {
3031
return ret, nil
3132
}
3233

34+
if config.Staircase != nil {
35+
ret := &StaircaseSpeedCurve{
36+
Config: config,
37+
LastTemp: math.MinInt,
38+
}
39+
return ret, nil
40+
}
41+
3342
if config.PID != nil {
3443
pidLoop := util.NewPidLoop(
3544
config.PID.P,

internal/curves/linear_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
package curves
22

33
import (
4+
"testing"
5+
46
"github.com/markusressel/fan2go/internal/configuration"
57
"github.com/markusressel/fan2go/internal/sensors"
68
"github.com/stretchr/testify/assert"
7-
"testing"
89
)
910

1011
// helper function to create a linear curve configuration

internal/curves/staircase.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package curves
2+
3+
import (
4+
"github.com/markusressel/fan2go/internal/ui"
5+
6+
"github.com/markusressel/fan2go/internal/configuration"
7+
"github.com/markusressel/fan2go/internal/sensors"
8+
)
9+
10+
type StaircaseSpeedCurve struct {
11+
Config configuration.CurveConfig `json:"config"`
12+
Value float64 `json:"value"`
13+
14+
LastTemp int
15+
}
16+
17+
func (c *StaircaseSpeedCurve) GetId() string {
18+
return c.Config.ID
19+
}
20+
21+
func (c *StaircaseSpeedCurve) Evaluate() (value float64, err error) {
22+
sensor, _ := sensors.GetSensor(c.Config.Staircase.Sensor)
23+
var measured float64
24+
measured, err = sensor.GetValue()
25+
if err != nil {
26+
ui.Warning("Curve %s: Error getting sensor value: %v", c.Config.ID, err)
27+
return c.Value, err
28+
}
29+
30+
steps := c.Config.Staircase.Steps
31+
32+
var targetTemp int
33+
for temp := range steps {
34+
if measured >= float64(temp)*1000 {
35+
targetTemp = max(targetTemp, temp)
36+
}
37+
}
38+
if targetTemp < c.LastTemp && (c.LastTemp-int(measured/1000)) < c.Config.Staircase.Threshold {
39+
targetTemp = c.LastTemp
40+
}
41+
42+
c.LastTemp = targetTemp
43+
value = steps[targetTemp]
44+
45+
ui.Debug("Evaluating curve '%s'. Sensor '%s' temp '%.0f°'. Desired speed: %.2f", c.Config.ID, sensor.GetId(), measured/1000, value)
46+
c.SetValue(value)
47+
return value, nil
48+
}
49+
50+
func (c *StaircaseSpeedCurve) SetValue(value float64) {
51+
valueMu.Lock()
52+
defer valueMu.Unlock()
53+
c.Value = value
54+
}
55+
56+
func (c *StaircaseSpeedCurve) CurrentValue() float64 {
57+
valueMu.Lock()
58+
defer valueMu.Unlock()
59+
return c.Value
60+
}

0 commit comments

Comments
 (0)