fan-control: continuous linear curve (replaces discrete step-bands)
Replace the step-band fan curve with a continuous linear ramp — the bands flapped at edges (e.g. 45<->65%). Web-researched: linear + 2-3C hysteresis is the homelab standard; PID is overkill for this slow thermal loop. fan% now interpolates between env-tunable anchors: COOL 50C/30% -> 83C/100% (~2.1%/C; ~51% at the ~60C equilibrium) QUIET 68C/20% -> 83C/100% (near-silent until ~70C) Both reach 100% at the 83C ceiling. Anti-oscillation: asymmetric hysteresis (fc_decide) + a MIN_STEP (3%) min-change threshold. 41 bash tests green; deployed + verified live (59C -> 49%, smooth). [ci skip] Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
945c1936e3
commit
324f2dc3bf
4 changed files with 67 additions and 62 deletions
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env bash
|
||||
# Unit tests for the pure functions in fan-control.sh.
|
||||
# Sources the script (main is guarded), exercises curve/decide/presence/parse.
|
||||
# Sources the script (main is guarded), exercises curve/decide/resolve/presence/parse.
|
||||
# Run: bash infra/scripts/test-fan-control.sh
|
||||
|
||||
set -uo pipefail
|
||||
|
|
@ -15,35 +15,31 @@ eq() { # <description> <expected> <actual>
|
|||
fi
|
||||
}
|
||||
|
||||
# --- COOL curve (power-tuned 2026-06-05: knee at 60%) ---
|
||||
eq "cool 40 -> 30" 30 "$(fc_curve cool 40)"
|
||||
eq "cool 54 -> 30" 30 "$(fc_curve cool 54)"
|
||||
eq "cool 55 -> 50" 50 "$(fc_curve cool 55)"
|
||||
eq "cool 63 -> 50" 50 "$(fc_curve cool 63)"
|
||||
eq "cool 64 -> 60" 60 "$(fc_curve cool 64)"
|
||||
eq "cool 72 -> 60" 60 "$(fc_curve cool 72)"
|
||||
eq "cool 73 -> 80" 80 "$(fc_curve cool 73)"
|
||||
eq "cool 78 -> 80" 80 "$(fc_curve cool 78)"
|
||||
eq "cool 79 -> 100" 100 "$(fc_curve cool 79)"
|
||||
eq "cool 91 -> 100" 100 "$(fc_curve cool 91)"
|
||||
# --- COOL curve (continuous linear: 30% @50C .. 100% @83C) ---
|
||||
eq "cool <=T_LO clamps" 30 "$(fc_curve cool 40)"
|
||||
eq "cool 50 -> 30" 30 "$(fc_curve cool 50)"
|
||||
eq "cool 55 -> 41" 41 "$(fc_curve cool 55)"
|
||||
eq "cool 60 -> 51" 51 "$(fc_curve cool 60)"
|
||||
eq "cool 64 -> 60" 60 "$(fc_curve cool 64)"
|
||||
eq "cool 70 -> 72" 72 "$(fc_curve cool 70)"
|
||||
eq "cool 75 -> 83" 83 "$(fc_curve cool 75)"
|
||||
eq "cool 83 -> 100" 100 "$(fc_curve cool 83)"
|
||||
eq "cool >=T_HI clamps" 100 "$(fc_curve cool 90)"
|
||||
|
||||
# --- QUIET curve ---
|
||||
eq "quiet 50 -> 20" 20 "$(fc_curve quiet 50)"
|
||||
eq "quiet 72 -> 20" 20 "$(fc_curve quiet 72)"
|
||||
eq "quiet 73 -> 40" 40 "$(fc_curve quiet 73)"
|
||||
eq "quiet 77 -> 40" 40 "$(fc_curve quiet 77)"
|
||||
eq "quiet 78 -> 65" 65 "$(fc_curve quiet 78)"
|
||||
eq "quiet 81 -> 65" 65 "$(fc_curve quiet 81)"
|
||||
eq "quiet 82 -> 100" 100 "$(fc_curve quiet 82)"
|
||||
# --- QUIET curve (continuous linear: 20% @68C .. 100% @83C) ---
|
||||
eq "quiet <=T_LO clamps" 20 "$(fc_curve quiet 60)"
|
||||
eq "quiet 68 -> 20" 20 "$(fc_curve quiet 68)"
|
||||
eq "quiet 70 -> 31" 31 "$(fc_curve quiet 70)"
|
||||
eq "quiet 75 -> 57" 57 "$(fc_curve quiet 75)"
|
||||
eq "quiet 80 -> 84" 84 "$(fc_curve quiet 80)"
|
||||
eq "quiet 83 -> 100" 100 "$(fc_curve quiet 83)"
|
||||
|
||||
# --- decide: hysteresis ---
|
||||
eq "decide uninit -> target" 60 "$(fc_decide cool 68 -1 3)"
|
||||
eq "decide ramp up now" 60 "$(fc_decide cool 68 25 3)"
|
||||
eq "decide equal holds" 60 "$(fc_decide cool 64 60 3)"
|
||||
eq "decide down held in band" 80 "$(fc_decide cool 70 80 3)" # 70+3=73 still 80% -> hold
|
||||
eq "decide down past band" 60 "$(fc_decide cool 69 80 3)" # 69+3=72 -> 60% < 80 -> drop
|
||||
eq "decide 100 holds" 100 "$(fc_decide cool 77 100 3)" # 77+3=80 -> 100 -> hold
|
||||
eq "decide 100 drops" 80 "$(fc_decide cool 75 100 3)" # 75+3=78 -> 80 < 100 -> drop
|
||||
# --- decide: asymmetric hysteresis (ramp up now, ease down only past the deadband) ---
|
||||
eq "decide uninit -> target" 68 "$(fc_decide cool 68 -1 3)"
|
||||
eq "decide ramp up now" 68 "$(fc_decide cool 68 25 3)"
|
||||
eq "decide equal holds" 62 "$(fc_decide cool 65 62 3)"
|
||||
eq "decide down held" 72 "$(fc_decide cool 68 72 3)" # curve(68)=68<72 but curve(71)=75 !<72 -> hold
|
||||
eq "decide down past" 60 "$(fc_decide cool 64 72 3)" # curve(64)=60, curve(67)=66<72 -> drop
|
||||
|
||||
# --- fc_clamp / fc_resolve: HA mode resolution ---
|
||||
eq "clamp over 100" 100 "$(fc_clamp 150)"
|
||||
|
|
@ -51,11 +47,10 @@ eq "clamp under 0" 0 "$(fc_clamp -5)"
|
|||
eq "clamp passthrough" 45 "$(fc_clamp 45)"
|
||||
eq "resolve manual=slider" 42 "$(fc_resolve manual 64 42 cool -1 3)"
|
||||
eq "resolve manual clamped" 100 "$(fc_resolve manual 64 150 cool -1 3)"
|
||||
eq "resolve cool=cool curve" 60 "$(fc_resolve cool 64 0 cool -1 3)"
|
||||
eq "resolve quiet=quiet curve" 65 "$(fc_resolve quiet 80 0 cool -1 3)"
|
||||
eq "resolve auto+empty=cool" 60 "$(fc_resolve auto 64 0 cool -1 3)"
|
||||
eq "resolve auto+present=quiet" 20 "$(fc_resolve auto 64 0 quiet -1 3)"
|
||||
eq "resolve cool hysteresis" 60 "$(fc_resolve cool 69 0 cool 80 3)"
|
||||
eq "resolve cool=cool curve" 51 "$(fc_resolve cool 60 0 cool -1 3)"
|
||||
eq "resolve quiet=quiet curve" 73 "$(fc_resolve quiet 78 0 cool -1 3)"
|
||||
eq "resolve auto+empty=cool" 51 "$(fc_resolve auto 60 0 cool -1 3)"
|
||||
eq "resolve auto+present=quiet" 31 "$(fc_resolve auto 70 0 quiet -1 3)"
|
||||
|
||||
# --- presence ---
|
||||
now=1000000
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue