Skip to content

Add staircase fan curve#486

Merged
markusressel merged 10 commits into
markusressel:masterfrom
0xIO32:curve_staircase
Jun 21, 2026
Merged

Add staircase fan curve#486
markusressel merged 10 commits into
markusressel:masterfrom
0xIO32:curve_staircase

Conversation

@0xIO32

@0xIO32 0xIO32 commented May 31, 2026

Copy link
Copy Markdown
Contributor

Add a fan curve similar to linear, but without linear interpolation (i.e. it looks for the highest step that is still below or equal the sensors value and takes the fan speed as-is). The threshold prevents a "start-stop-start-stop" as described in #444 .

This fan curve prevents the fan from constantly ramping up. It usually is quieter and "less annoying".

@0xIO32 0xIO32 force-pushed the curve_staircase branch from d17e798 to bdd4495 Compare May 31, 2026 22:12
@markusressel

Copy link
Copy Markdown
Owner

@0xIO32 Can you please provide a description for this PR?
Whats the use-case / issue that this is intended for?
I saw your mention in #444 , but that is not what you created the PR for, right?

    • Replaced raw  sensor.GetValue()  with the smoothed, background-polled  sensor.GetMovingAvg()  to utilize standard architecture patterns and avoid expensive, redundant active polling.
    • Added checking for  !exists || sensor == nil  to prevent runtime panics.
    • Replaced the  0 -initialized  targetTemp  logic with a  math.MinInt  sentinel. This allows negative step temperatures (e.g.,  -10°C ) to evaluate correctly and allows step values at exactly  0°C
      without conflicts.
    • Cleaned up tab indentation styling.
2. Simplified Configuration Verification (config.go):
    • Removed the nested redundant length checks in  applyTransformations() , making sure empty step configs trigger a fatal config error correctly.
3. Added Tests & Verified (staircase_test.go):
    • Added  TestStaircaseCurveWithNegativeTemperatures  to assert proper hysteresis and matching of sub-zero steps.
    • Added  TestStaircaseCurveWithStepAtZero  to verify steps configured at exactly  0°C  function properly.
    • Ran  make test  and  go test  on the packages; all test suites compiled successfully and passed.
@markusressel

Copy link
Copy Markdown
Owner

AI PR description:

Overview

This PR introduces a new fan curve type: Staircase Fan Curve.

A staircase speed curve 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, increasing hardware lifespan and offering a more consistent acoustic profile.

It implements:

  • Discrete temperature-to-speed step mappings.
  • Configurable hysteresis threshold (threshold) to delay down-ramping when temperatures fall.
  • Use of sensor moving averages to filter out instantaneous spike readings.

Configuration Example

To configure a staircase curve, define it under curves in your fan2go.yaml configuration:

curves:
  - id: staircase_curve
    staircase:
      sensor: cpu_package
      threshold: 6 # Temperature drop threshold (in °C) before reducing fan speed
      steps:
        # Temperature (in °C) -> Speed (0-255 or 0%-100%)
        - 40: 10%
        - 50: 50
        - 80: 255

Changes

1. Configuration & Validation

  • Curve Definition: Added StaircaseCurveConfig struct to curves.go to model configuration variables.
  • Config Loading: Updated applyTransformations() in config.go to transform user-defined steps (which can be percentages like 10% or raw integers) to float values [0..255].
  • Configuration Validation: Enhanced validateCurves() in validation.go to ensure that configured staircase curves have valid sensor IDs and non-empty steps.

2. Curve Implementation

  • Evaluation Logic: Implemented StaircaseSpeedCurve in staircase.go which:
    • Retrieves the sensor moving average to match thresholds safely.
    • Matches temperature steps correctly using a sentinel (math.MinInt) for negative temperature step support.
    • Evaluates down-ramping hysteresis against the configured threshold.
  • Curve Construction: Integrated staircase curves into the curve builder in curve.go.

3. Testing

  • Added unit tests in staircase_test.go verifying:
    • Curve evaluation below minimum steps, intermediate steps, and at maximum steps.
    • Hysteresis thresholds for down-ramping.
    • Correct matching of negative temperature steps (e.g. sub-zero environments).
    • Proper matching of steps configured at exactly 0°C.

4. Miscellaneous & Documentation

  • Documented configuration format and usage in README.md.
  • Aligned AttachFanRpmCurveData() in hwmon.go to return errors on empty datasets consistently.

@markusressel

Copy link
Copy Markdown
Owner

@0xIO32 I have pushed some changes, let me know if you see any issues with them.

@markusressel

Copy link
Copy Markdown
Owner

Configurable hysteresis threshold (threshold) to delay down-ramping when temperatures fall.

I wonder if it would make more sense to configure a time duration threshold, instead of a difference in sensor input 🤔 But since I have no use case for this anyway we can use whatever you need.

@0xIO32

0xIO32 commented Jun 20, 2026

Copy link
Copy Markdown
Contributor Author

@0xIO32 Can you please provide a description for this PR? Whats the use-case / issue that this is intended for?

I just added a description.

Somehow in my mind I thought I already explained it in the commit msg or README.md, but apparently not.

Intended use: alternative for linear fan curve.

I saw your mention in #444 , but that is not what you created the PR for, right?

I noticed #444 after already making this pr.

@0xIO32

0xIO32 commented Jun 20, 2026

Copy link
Copy Markdown
Contributor Author

@0xIO32 I have pushed some changes, let me know if you see any issues with them.

Only a merge commit or are there changes in functionality? Just to know if I only should do testing again or also look at the changes.

@markusressel

markusressel commented Jun 20, 2026

Copy link
Copy Markdown
Owner

@0xIO32 I have pushed some changes, let me know if you see any issues with them.

Only a merge commit or are there changes in functionality? Just to know if I only should do testing again or also look at the changes.

Both, see: 0a2539f

@0xIO32

0xIO32 commented Jun 20, 2026

Copy link
Copy Markdown
Contributor Author

Disclaimer: I haven't coded in go that long, primarily other languages like Rust.

Still 2,5 things I noticed:

-	value = steps[targetTemp]
+	if targetTemp == math.MinInt {
+		value = 0.0
+	} else {
+		value = steps[targetTemp]
+	}

Isn't 0 the default if the value isn't present in steps map?

-	if targetTemp < c.LastTemp && (c.LastTemp-int(measured/1000)) < c.Config.Staircase.Threshold {
+
+	if targetTemp < c.LastTemp && c.LastTemp != math.MinInt && (c.LastTemp-int(measured/1000)) < c.Config.Staircase.Threshold {

How is it possible for targetTemp < c.LastTemp to be true, if c.LastTemp equals to math.MinInt ?

-			targetTemp = max(targetTemp, temp)
+			if targetTemp == math.MinInt || temp > targetTemp {
+				targetTemp = temp
+			}

Same here.

@markusressel

Copy link
Copy Markdown
Owner

Sorry for the noise, this release-drafter error is killing me.

@0xIO32

0xIO32 commented Jun 20, 2026

Copy link
Copy Markdown
Contributor Author

Just tested this build, only for a few minutes. Nothing unusual.

@markusressel

Copy link
Copy Markdown
Owner

Isn't 0 the default if the value isn't present in steps map?

Apparently it is, I didn't know that.

How is it possible for targetTemp < c.LastTemp to be true, if c.LastTemp equals to math.MinInt ?

It isn't, thx. I have simplified the implementation.

@markusressel

Copy link
Copy Markdown
Owner

Configurable hysteresis threshold (threshold) to delay down-ramping when temperatures fall.

@0xIO32 Is this intentional? Only the down-ramping is supposed to be debounced?

@0xIO32

0xIO32 commented Jun 20, 2026

Copy link
Copy Markdown
Contributor Author

Configurable hysteresis threshold (threshold) to delay down-ramping when temperatures fall.

@0xIO32 Is this intentional? Only the down-ramping is supposed to be debounced?

From my understanding it is. Reacting to a temperature rise should be instant to protect the hardware, reducing the fan speed can be delayed.

It is supposed to be a standard staircase / stepped fan curve, although I couldn't actually find a good detailed description about that.

@markusressel

Copy link
Copy Markdown
Owner

From my understanding it is. Reacting to a temperature rise should be instant to protect the hardware, reducing the fan speed can be delayed.

It is supposed to be a standard staircase / stepped fan curve, although I couldn't actually find a good detailed description about that.

Alright, I just wanted to make sure because the thing that it probably is supposed to resolve is "too quick speed step changes", and technically that can still occur in the upwards case. But we can keep it like this for now and see if the need for thresholds in both directions is needed.

But I have one thing I am still thinking about:
Is threshold really a good name for that config option? It wasn't very intuitive to me, and even after reading the description it isn't very clear to my brain. Since it actually is a hysteresis, maybe we should make the config represent that right away like this?

curves:
  - id: staircase_curve
    staircase:
      sensor: cpu_package
      hysteresis:
        down: 6   # Temperature drop threshold (in °C) before reducing fan speed
      steps:
        # Temperature (in °C) -> Speed (0-255 or 0%-100%)
        - 40: 10%
        - 50: 50
        - 80: 255

That would also easily allow us to add an "up" option in the future.

@0xIO32

0xIO32 commented Jun 21, 2026

Copy link
Copy Markdown
Contributor Author

The name shouldn't really matter, so I am also fine with hysteresis if you prefer that name.

@markusressel markusressel added the enhancement New feature or request label Jun 21, 2026
@markusressel

Copy link
Copy Markdown
Owner

I think that's it for now, looks great!
Merging now.

@markusressel markusressel merged commit 0eef24d into markusressel:master Jun 21, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants