Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 0 additions & 18 deletions .github/workflows/release-drafter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,6 @@ on:
- main
- master

# pull_request event is required only for autolabeler
pull_request:
# Only following types are handled by the action, but one can default to all as well
types: [ opened, reopened, synchronize ]
# pull_request_target event is required for autolabeler to support PRs from forks
# pull_request_target:
# types: [ opened, reopened, synchronize ]

permissions:
contents: read

Expand All @@ -28,16 +20,6 @@ jobs:
pull-requests: write
runs-on: ubuntu-latest
steps:
# (Optional) GitHub Enterprise requires GHE_HOST variable set
#- name: Set GHE_HOST
# run: |
# echo "GHE_HOST=${GITHUB_SERVER_URL##https:\/\/}" >> $GITHUB_ENV

# Drafts your next Release notes as Pull Requests are merged into "master"
- uses: release-drafter/release-drafter@v7
# (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml
# with:
# config-name: my-config.yml
# disable-autolabeler: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,7 @@ temperature sensors.

#### Linear

To create a simple, linear speed curve, use a curve of type `linear`.
To create a linear speed curve, use a curve of type `linear`.

This curve type can be used with a min/max sensor value, where the min temp will result in a curve value of `0` and the
max temp will result in a curve value of `255`:
Expand Down Expand Up @@ -584,6 +584,27 @@ curves:
- 80: 255
```

#### Staircase

To create a staircase speed curve, use a curve of type `staircase`.

It maintains static fan speeds at defined temperature ranges.
This prevents fans from constantly ramping up and down (oscillating) in response to minor,
rapid fluctuations in sensor temperature, offering a more consistent acoustic profile.

```yaml
curves:
- id: staircase_curve
staircase:
sensor: cpu_package
threshold: 6
steps:
# Sensor value (in degrees Celsius) -> Speed (0-255)
- 40: 1
- 50: 50
- 80: 255
```

#### PID

If you want to get your hands dirty and use a PID based curve, you can use `pid`:
Expand Down
71 changes: 41 additions & 30 deletions internal/configuration/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,40 +189,51 @@ func applyTransformations() {
// convert steps in linear curves from strings (with plain numbers or percent values) to floats between 0 and 255
for _, curve := range CurrentConfig.Curves {
if curve.Linear != nil && len(curve.Linear.InSteps) > 0 {
curve.Linear.Steps = make(map[int]float64)

for temp, origstr := range curve.Linear.InSteps {
str := strings.TrimSpace(origstr)
l := len(str)
isPercent := false
if l > 1 && str[l-1] == '%' {
isPercent = true
str = str[:l-1] // cut off '%' because ParseFloat() wouldn't like it
transformCurveSteps(&curve.ID, &curve.Linear.Steps, &curve.Linear.InSteps)
}
if curve.Staircase != nil {
if len(curve.Staircase.InSteps) > 0 {
transformCurveSteps(&curve.ID, &curve.Staircase.Steps, &curve.Staircase.InSteps)
} else {
ui.Fatal("Missing steps in curve %s", curve.ID)
}
}
}
}

func transformCurveSteps(ID *string, Steps *map[int]float64, InSteps *map[int]string) {
*Steps = make(map[int]float64)

for temp, origstr := range *InSteps {
str := strings.TrimSpace(origstr)
l := len(str)
isPercent := false
if l > 1 && str[l-1] == '%' {
isPercent = true
str = str[:l-1] // cut off '%' because ParseFloat() wouldn't like it
}
speed, err := strconv.ParseFloat(str, 64)
if err != nil {
ui.Fatal("Invalid curve step value '%s' in %s - must be either just a number or a number followed by '%%'", origstr, *ID)
} else {
if isPercent {
if speed < 0 || speed > 100 {
ui.Fatal("invalid curve step value '%s' (=> %f) in %s - must be between 0%% and 100%%", origstr, speed, *ID)
}
speed, err := strconv.ParseFloat(str, 64)
if err != nil {
ui.Fatal("Invalid curve step value '%s' in %s - must be either just a number or a number followed by '%%'", origstr, curve.ID)
// convert 0-100% into [0..255]
if speed < 1 {
// less than 1% always turns into 0
speed = 0
} else {
if isPercent {
if speed < 0 || speed > 100 {
ui.Fatal("invalid curve step value '%s' (=> %f) in %s - must be between 0%% and 100%%", origstr, speed, curve.ID)
}
// convert 0-100% into [0..255]
if speed < 1 {
// less than 1% always turns into 0
speed = 0
} else {
// 1% turns into 1, 100% turns into 255
// => convert 1..100% to 1..255
// => 0..99 to 0..254 and then add 1
speed = (speed-1)*(254.0/99.0) + 1
}
} else if speed < 0 || speed > 255 {
ui.Fatal("invalid curve step value '%s' in %s - must be between 0 and 255", origstr, curve.ID)
}
curve.Linear.Steps[temp] = speed
// 1% turns into 1, 100% turns into 255
// => convert 1..100% to 1..255
// => 0..99 to 0..254 and then add 1
speed = (speed-1)*(254.0/99.0) + 1
}
} else if speed < 0 || speed > 255 {
ui.Fatal("invalid curve step value '%s' in %s - must be between 0 and 255", origstr, *ID)
}
(*Steps)[temp] = speed
}
}
}
Expand Down
26 changes: 23 additions & 3 deletions internal/configuration/curves.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ type CurveConfig struct {
ID string `json:"id"`

// can be any of the following:
Linear *LinearCurveConfig `json:"linear,omitempty"`
PID *PidCurveConfig `json:"pid,omitempty"`
Function *FunctionCurveConfig `json:"function,omitempty"`
Linear *LinearCurveConfig `json:"linear,omitempty"`
Staircase *StaircaseCurveConfig `json:"staircase,omitempty"`
PID *PidCurveConfig `json:"pid,omitempty"`
Function *FunctionCurveConfig `json:"function,omitempty"`
}

type LinearCurveConfig struct {
Expand All @@ -26,6 +27,25 @@ type LinearCurveConfig struct {
Steps map[int]float64 `json:"steps" mapstructure:"-"`
}

type StaircaseCurveConfig struct {
// Sensor is the id of the sensor to use for this curve
Sensor string `json:"sensor"`
// Hysteresis configuration
Hysteresis HysteresisConfig `json:"hysteresis"`
// Steps is a map of temperature to relative speed value (in range of 0..255 or alternatively 0%..100%)
// InSteps contains the speed values as strings (like "42" or "11%"), as read from fan2go.yaml
InSteps map[int]string `mapstructure:"steps" json:"-"`
// Steps is created from InSteps on load (LoadConfig()), the strings are converted to floats
// between 0 and 255 (0% is 0, 1% is 1; from there on it's interpolated linearly so 100% is 255).
// If a string only contains a number (without "%"), it's just converted to float
Steps map[int]float64 `json:"steps" mapstructure:"-"`
}

type HysteresisConfig struct {
// Temperature drop threshold in degrees before reducing fan speed
Down int `json:"down,omitempty"`
}

type PidCurveConfig struct {
// Sensor is the id of the sensor to use for this curve
Sensor string `json:"sensor"`
Expand Down
20 changes: 20 additions & 0 deletions internal/configuration/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ func isSensorConfigInUse(config SensorConfig, curves []CurveConfig) bool {
if curveConfig.Linear != nil && curveConfig.Linear.Sensor == config.ID {
return true
}
if curveConfig.Staircase != nil && curveConfig.Staircase.Sensor == config.ID {
return true
}
if curveConfig.PID != nil && curveConfig.PID.Sensor == config.ID {
return true
}
Expand All @@ -146,6 +149,9 @@ func validateCurves(config *Configuration) error {
if curveConfig.Linear != nil {
subConfigs++
}
if curveConfig.Staircase != nil {
subConfigs++
}
if curveConfig.PID != nil {
subConfigs++
}
Expand Down Expand Up @@ -196,6 +202,20 @@ func validateCurves(config *Configuration) error {
}
}

if curveConfig.Staircase != nil {
if len(curveConfig.Staircase.Sensor) <= 0 {
return fmt.Errorf("curve %s: missing sensorId", curveConfig.ID)
}

if !sensorIdExists(curveConfig.Staircase.Sensor, config) {
return fmt.Errorf("curve %s: no sensor definition with id '%s' found", curveConfig.ID, curveConfig.Staircase.Sensor)
}

if len(curveConfig.Staircase.InSteps) <= 0 {
return fmt.Errorf("curve %s: missing steps", curveConfig.ID)
}
}

if curveConfig.PID != nil {
if len(curveConfig.PID.Sensor) <= 0 {
return fmt.Errorf("curve %s: missing sensorId", curveConfig.ID)
Expand Down
9 changes: 9 additions & 0 deletions internal/curves/curve.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package curves

import (
"fmt"
"math"

"github.com/markusressel/fan2go/internal/configuration"
"github.com/markusressel/fan2go/internal/util"
Expand Down Expand Up @@ -30,6 +31,14 @@ func NewSpeedCurve(config configuration.CurveConfig) (SpeedCurve, error) {
return ret, nil
}

if config.Staircase != nil {
ret := &StaircaseSpeedCurve{
Config: config,
LastTemp: math.MinInt,
}
return ret, nil
}

if config.PID != nil {
pidLoop := util.NewPidLoop(
config.PID.P,
Expand Down
3 changes: 2 additions & 1 deletion internal/curves/linear_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package curves

import (
"testing"

"github.com/markusressel/fan2go/internal/configuration"
"github.com/markusressel/fan2go/internal/sensors"
"github.com/stretchr/testify/assert"
"testing"
)

// helper function to create a linear curve configuration
Expand Down
62 changes: 62 additions & 0 deletions internal/curves/staircase.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package curves

import (
"fmt"
"math"

"github.com/markusressel/fan2go/internal/ui"

"github.com/markusressel/fan2go/internal/configuration"
"github.com/markusressel/fan2go/internal/sensors"
)

type StaircaseSpeedCurve struct {
Config configuration.CurveConfig `json:"config"`
Value float64 `json:"value"`

LastTemp int
}

func (c *StaircaseSpeedCurve) GetId() string {
return c.Config.ID
}

func (c *StaircaseSpeedCurve) Evaluate() (value float64, err error) {
sensor, exists := sensors.GetSensor(c.Config.Staircase.Sensor)
if !exists || sensor == nil {
ui.Warning("Curve %s: Sensor not found with id '%s'", c.Config.ID, c.Config.Staircase.Sensor)
return c.Value, fmt.Errorf("sensor not found with id '%s'", c.Config.Staircase.Sensor)
}

measured := sensor.GetMovingAvg()
steps := c.Config.Staircase.Steps

targetTemp := math.MinInt
for temp := range steps {
if measured >= float64(temp)*1000 {
targetTemp = max(targetTemp, temp)
}
}
if targetTemp < c.LastTemp && (c.LastTemp-int(measured/1000)) < c.Config.Staircase.Hysteresis.Down {
targetTemp = c.LastTemp
}

c.LastTemp = targetTemp
value = steps[targetTemp]

ui.Debug("Evaluating curve '%s'. Sensor '%s' temp '%.0f°'. Desired speed: %.2f", c.Config.ID, sensor.GetId(), measured/1000, value)
c.SetValue(value)
return value, nil
}

func (c *StaircaseSpeedCurve) SetValue(value float64) {
valueMu.Lock()
defer valueMu.Unlock()
c.Value = value
}

func (c *StaircaseSpeedCurve) CurrentValue() float64 {
valueMu.Lock()
defer valueMu.Unlock()
return c.Value
}
Loading
Loading