Let the fan settle downward on a re-assert

A symmetric deadband catches the fan on the way up and holds it there:
on iz-pve0 under load, 17 of 20 samples ran at 38% while the curve
wanted 34-36%, and nothing would bring it down until the request fell a
full 8 points. Damping was turning into a permanently louder machine.

A re-assert may now settle the speed downward to the current request.
Upward moves still require a full deadband crossing, so the drift this
was built to prevent cannot come back - covered by a test.

Verified under a live migration: settled 38 -> 36 on the first
re-assert, then one change in ten minutes while the request wandered
36-42, CPU stable at 66-69c and disks a degree cooler.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Zeb Hering
2026-08-28 22:33:38 -07:00
parent be440e95a3
commit 4dffbb070c
3 changed files with 39 additions and 5 deletions

View File

@@ -32,6 +32,13 @@ settle — measurably worse than stock. Measured on iz-pve0: stock iDRAC held on
minutes off the same signal. A request for full speed is never held back, and the held
value is re-pushed every `FAN_REASSERT_INTERVAL` in case the BMC forgets it.
A deadband alone makes a machine permanently louder: it catches the fan on the way up and
then nothing brings it down until the request falls a full deadband. Measured on `iz-pve0`
under load, 17 of 20 samples ran at 38% while the curve wanted 34-36%. So a re-assert -
and only a re-assert - is allowed to settle the speed **downward** to what is actually
wanted. Upward moves still need a full deadband crossing, which is what stops this
reintroducing the drift it was built to prevent.
Note that iDRAC's own profile regulates to a target *RPM* (closed loop, rock steady),
while manual control sets a *PWM* percentage (open loop, so measured RPM still wanders a
percent or two at a fixed setpoint). That residual is inherent to manual control, not

View File

@@ -117,6 +117,14 @@ selftest() {
# leak is what let the setpoint drift 32 -> 36 -> 31 on iz-pve0.
check "re-assert re-pushes held value" 32 "$(next_fan_speed 34 32)"
echo "settling downward (damping must not make the machine permanently louder):"
check "holds above the want between re-asserts" 38 "$(settle_fan_speed 34 38 0)"
check "settles down to the want on re-assert" 34 "$(settle_fan_speed 34 38 1)"
check "small rise still held on re-assert" 38 "$(settle_fan_speed 42 38 1)"
check "real rise still adopted" 46 "$(settle_fan_speed 46 38 1)"
# The old upward leak: a re-assert must never drift the setpoint up.
check "no upward drift on re-assert" 32 "$(settle_fan_speed 36 32 1)"
echo "trend verdict (load is not a fault - only cooling that cannot keep up is):"
TREND_SAMPLES=5
HIGH_FAN_SPEED=50
@@ -293,12 +301,12 @@ one_pass() {
# Hold the current speed unless the change is worth making - see the deadband
# note in monitor.sh. Chasing every degree is what made this louder than stock.
local target now held=""
target=$(next_fan_speed "$DECIMAL_CURRENT_FAN_SPEED" "$APPLIED_FAN_SPEED")
[ "$target" = "$DECIMAL_CURRENT_FAN_SPEED" ] || held=" held, want $DECIMAL_CURRENT_FAN_SPEED"
local target now due=0 held=""
now=$(date +%s)
if [ "$target" != "$APPLIED_FAN_SPEED" ] ||
[ $((now - APPLIED_FAN_SPEED_AT)) -ge "$FAN_REASSERT_INTERVAL" ]; then
[ $((now - APPLIED_FAN_SPEED_AT)) -ge "$FAN_REASSERT_INTERVAL" ] && due=1
target=$(settle_fan_speed "$DECIMAL_CURRENT_FAN_SPEED" "$APPLIED_FAN_SPEED" "$due")
[ "$target" = "$DECIMAL_CURRENT_FAN_SPEED" ] || held=" held, want $DECIMAL_CURRENT_FAN_SPEED"
if [ "$target" != "$APPLIED_FAN_SPEED" ] || [ "$due" = 1 ]; then
apply_user_fan_control "$target"
APPLIED_FAN_SPEED=$target
APPLIED_FAN_SPEED_AT=$now

View File

@@ -97,6 +97,25 @@ next_fan_speed() {
fi
}
# settle_fan_speed <wanted> <applied> <re-assert due, 0|1>
#
# A symmetric deadband catches the fan on the way up and then holds it there:
# measured on iz-pve0 under load, 17 of 20 samples ran at 38% while the curve
# wanted 34-36%, and nothing would bring it down until the request fell a full
# deadband. That is how damping turns into a permanently louder machine.
#
# So on a re-assert - and only then - let the speed settle DOWNWARD to what is
# actually wanted. Upward moves still require a full deadband crossing, so this
# cannot bring back the leak that let the setpoint drift 32 -> 36 -> 31.
settle_fan_speed() {
local wanted=$1 applied=$2 due=$3 target
target=$(next_fan_speed "$wanted" "$applied")
if [ "$due" = 1 ] && [ -n "$applied" ] && [ "$wanted" -lt "$target" ]; then
target=$wanted
fi
echo "$target"
}
ALERT_EMAIL=${ALERT_EMAIL:-Servers@ntfy1.izebra.xyz}
ALERT_COOLDOWN=${ALERT_COOLDOWN:-3600}
STATE_DIR=${STATE_DIR:-/root/fan_speed/state}