Skip to content

Instantly share code, notes, and snippets.

@killerbees19
Last active May 3, 2024 04:08
Show Gist options
  • Save killerbees19/dacc4598da932ac7b16a62b1e08dbf93 to your computer and use it in GitHub Desktop.
Save killerbees19/dacc4598da932ac7b16a62b1e08dbf93 to your computer and use it in GitHub Desktop.
Supermicro Fan Control
alias fan-auto='systemctl start supermicro-fan-auto.timer'
alias fan-full='systemctl stop supermicro-fan-auto.timer && sleep 5 && sm fan full'
alias fan-pue2='systemctl stop supermicro-fan-auto.timer && sleep 5 && sm fan pue2'
alias fan-optimal='systemctl stop supermicro-fan-auto.timer && sleep 5 && sm fan optimal'
alias fan-heavyio='systemctl stop supermicro-fan-auto.timer && sleep 5 && sm fan heavyio'
alias fan-standard='systemctl stop supermicro-fan-auto.timer && sleep 5 && sm fan standard'

Rules-based fan control for Supermicro boards. Tested at A2SDi+8C+-HLN4F.

Note: PUE2 is not supported at some Supermicro boards! Change DEFAULT_FANMODE if required.

apt install ipmitool

mkdir -p /var/cache/ipmi/sdr
ipmitool sdr dump /var/cache/ipmi/sdr/local
ln -s supermicro /usr/local/sbin/sm

editor \
  /usr/local/sbin/supermicro \
  /usr/local/etc/supermicro.cfg \
  /etc/systemd/system/supermicro-fan.service \
  /etc/systemd/system/supermicro-fan-auto.service \
  /etc/systemd/system/supermicro-fan-auto.timer

systemctl daemon-reload
systemctl enable supermicro-fan.service
systemctl start supermicro-fan

# Optional: Update fan speed by temperature
systemctl enable --now supermicro-fan-auto.timer
# OPTIONAL: Cronjob
0 10 * * * root if [ ! -e "/tmp/.disable-supermicro-cronjob" ]; then /usr/bin/systemctl stop supermicro-fan-auto.timer && /usr/bin/sleep 30 && /usr/local/sbin/supermicro fan heavyio >/dev/null; fi
0 22 * * * root if [ ! -e "/tmp/.disable-supermicro-cronjob" ]; then /usr/bin/systemctl start supermicro-fan-auto.timer; fi
#!/bin/bash
set -euf -o pipefail
IPMITOOL_ARGS=()
DEFAULT_FANMODE="optimal"
DEFAULT_FANSPEED=( 100 100 )
DEFAULT_FANSLEEP=5
AUTOFAN_SOURCES=()
AUTOFAN_PENALTY=()
AUTOFAN_BASE=()
AUTOFAN_MIN=()
AUTOFAN_MAX=()
# Optional: Override configuration vars
if [[ -f /usr/local/etc/supermicro.cfg ]]
then
#shellcheck disable=SC1091
source /usr/local/etc/supermicro.cfg
fi
function argc
{
if [[ "$1" -lt "$2" ]]
then usage
fi
}
function usage
{
cat << EOF >&2
Usage: $0 <COMMAND> [<ARGS> ...]
Display current fan settings:
$0 fan
Update fan settings:
$0 fan standard|optimal|pue[2]|heavy[io]
$0 fan full [<PERCENT> ...]
$0 fan default
Adjust fan settings based on sensor temperatures:
$0 fan auto
EOF
exit 1
}
function stderr
{
printf '%s\n' "$@" 1>&2
}
function intval
{
local i
i=${1//[[:space:]]}
i=${i#"${i%%[!0]*}"}
printf '%d' "$i" 2>/dev/null || :
}
function hex2int
{
printf '%d' "0x${1//[[:space:]]}" 2>/dev/null || :
}
function int2hex
{
printf '0x%02x' "$1"
}
function execIPMI
{
ipmitool -c "${IPMITOOL_ARGS[@]}" "$@"
}
function sendRawCommand
{
execIPMI raw "$@" | sed '/./,$!d'
}
function getFanModeName
{
local name="Standard"
case "$1" in
1) name="Full" ;;
2) name="Optimal" ;;
3) name="PUE2" ;;
4) name="HeavyIO" ;;
esac
printf '%s' "$name"
}
function getFanModeKey
{
getFanModeName "${1,,}"
}
function getFanModeID
{
local -i fanmode=0
case "${1,,}" in
full) fanmode=1 ;;
optimal) fanmode=2 ;;
pue*) fanmode=3 ;;
heavy*|io) fanmode=4 ;;
esac
printf '%d' "$fanmode"
}
function getFanMode
{
hex2int "$(sendRawCommand 0x30 0x45 0x00)"
}
function setFanMode
{
local -i fanmode
local -i current
local hex
fanmode=$(getFanModeID "$1")
current=$(getFanMode)
if (( current != fanmode ))
then
hex=$(int2hex "$fanmode")
stderr "Setting new fan mode: $hex ($(getFanModeName "$fanmode"))"
sendRawCommand 0x30 0x45 0x01 "$hex"
if [[ "$fanmode" -eq "$(getFanModeID "full")" ]]
then
sleep "$DEFAULT_FANSLEEP"
fi
else
stderr "Current fan mode not changed: $(int2hex "$current") ($(getFanModeName "$current"))"
fi
}
function getFanSpeed
{
hex2int "$(sendRawCommand 0x30 0x70 0x66 0x00 "$(int2hex "$1")")"
}
function setFanSpeed
{
local zone
local value
zone=$(int2hex "$1")
value=$(int2hex "$2")
stderr "Setting fan speed of zone $zone: $value ($2%)"
sendRawCommand 0x30 0x70 0x66 0x01 "$zone" "$value"
}
function loadTemperatureValues
{
local result k v
result=$(execIPMI sdr type Temperature)
declare -g -A temperatureValues
while IFS=',' read -r k v _
do
k=${k,,}
k=${k/% */}
k=${k/%[a-z][0-9]/}
v=$(intval "$v")
if [[ -z "${temperatureValues[$k]-}" || "${temperatureValues[$k]}" -lt "$v" ]]
then
temperatureValues[$k]="$v"
fi
done <<< "$result"
for k in "${AUTOFAN_SOURCES[@]}"
do
k=${k,,}
k=${k/% */}
k=${k/%[a-z][0-9]/}
v=$("smCustomSource${k^^}" ||:)
v=$(intval "$v")
if [[ -z "${temperatureValues[$k]-}" || "${temperatureValues[$k]}" -lt "$v" ]]
then
temperatureValues[$k]="$v"
fi
done
}
function processFanSpeedRules
{
local mode
mode=$1
shift
local key
key=$1
shift
local -i value
value=$(intval "$1")
shift
for line in "$@"
do
IFS=" " read -r sensor temperature zones <<< "$line"
temperature=$(intval "$temperature")
sensor=${sensor,,}
if [[ "$sensor" == "$key" ]]
then
if [[ "$mode" == "min" && "$value" -le "$temperature" ]] || [[ "$mode" == "max" && "$value" -ge "$temperature" ]]
then
if [[ "$mode" == "min" && -n "${AUTOFAN_BASE[*]-}" ]]
then saveFanSpeedZones "${AUTOFAN_BASE[*]}"
else saveFanSpeedZones "$zones"
fi
fanSpeedFixed=1
return
fi
printf -v "fanSpeed${mode^}Temp" '%s' "$temperature"
read -r -a "fanSpeedZones${mode^}" <<< "$zones"
return
fi
done
}
function saveFanSpeedZones
{
local -i i; i=0
for speed in $1
do
if [[ "$speed" -gt 0 ]] && [[ -z "${fanSpeedZones[$i]-}" || "${fanSpeedZones[$i]}" -lt "$speed" ]]
then
saveFanSpeedZone "$i" "$speed"
fi
i=$(( i + 1 ))
done
}
function saveFanSpeedZone
{
fanSpeedZones[$1]=$2
fanSpeedChanged=1
}
function adjustFanSpeeds
{
declare -g -i fanSpeedFixed
declare -g -A fanSpeedZones
declare -g -a fanSpeedZonesMin
declare -g -a fanSpeedZonesMax
declare -g -i fanSpeedChanged
declare -g -i fanSpeedPenalty
declare -i fanSpeedMinTemp
declare -i fanSpeedMaxTemp
local -i time
local -i percent
local -i value
local -i start
local -i end
local final
local dow
local s
local e
fanSpeedPenalty=100
time=$(intval "$(date +%H%M)")
for rule in "${AUTOFAN_PENALTY[@]}"
do
IFS=" " read -r dow s e percent final <<< "$rule"
start=$(intval "$s")
end=$(intval "$e")
if [[ "$dow" != "*" ]]
then
dow=$(intval "$dow")
if [[ "$dow" -ne "$(date +%u)" && "$dow" -ne "$(date +%w)" ]]
then
continue
fi
fi
if [[ "$end" -lt "$start" && "$time" -le "$end" ]] \
|| [[ "$end" -lt "$start" && "$time" -ge "$start" ]] \
|| [[ "$time" -ge "$start" && "$time" -le "$end" ]]
then
fanSpeedPenalty=$(intval "$percent")
if [[ -n "$final" ]]
then
break
fi
fi
done
fanSpeedChanged=0
loadTemperatureValues
for key in "${!temperatureValues[@]}"
do
value=${temperatureValues[$key]}
fanSpeedZonesMin=()
fanSpeedZonesMax=()
fanSpeedMinTemp=0
fanSpeedMaxTemp=0
fanSpeedFixed=0
processFanSpeedRules min "$key" "$value" "${AUTOFAN_MIN[@]}"
processFanSpeedRules max "$key" "$value" "${AUTOFAN_MAX[@]}"
if [[ "$fanSpeedFixed" -ne 1 ]]
then
c1=$(intval "${#fanSpeedZonesMin[@]}")
c2=$(intval "${#fanSpeedZonesMax[@]}")
c=$(( c1 > c2 ? c1 : c2 ))
for (( i=0; i < c; i++ ))
do
minSpeed=$(intval "${fanSpeedZonesMin[$i]-0}")
maxSpeed=$(intval "${fanSpeedZonesMax[$i]-0}")
if [[ "$minSpeed" -gt 0 && "$maxSpeed" -gt 0 ]]
then
speed=$(bc -l <<< "result = ($maxSpeed - $minSpeed) / 100 * (100 / (($fanSpeedMaxTemp - $fanSpeedMinTemp) / ($value - $fanSpeedMinTemp))) + $minSpeed; scale=0; result/1")
if [[ "$speed" -gt 0 ]] && [[ -z "${fanSpeedZones[$i]-}" || "${fanSpeedZones[$i]}" -lt "$speed" ]]
then
saveFanSpeedZone "$i" "$speed"
fi
fi
done
fi
done
if (( fanSpeedChanged ))
then
setFanMode "full"
for key in "${!fanSpeedZones[@]}"
do
value=$(bc -l <<< "result = ${fanSpeedZones[$key]-0} / 100 * $fanSpeedPenalty; if ( result < 1 ) result=1; scale=0; result/1")
value=$(( value > 100 ? 100 : value ))
if [[ "$value" -gt 0 ]]
then
setFanSpeed "$key" "$value"
fi
done
fi
}
argc $# 1
MODE=${1,,}
shift
case "$MODE" in
fan)
if [[ "$#" -eq 0 ]]
then
mode=$(getFanMode)
echo "Current fan mode: $(int2hex "$mode") ($(getFanModeName "$mode"))"
c=${#DEFAULT_FANSPEED[@]}
for (( i=0; i < c; i++ ))
do
speed=$(getFanSpeed "$i")
echo "Fan speed of zone $(int2hex "$i"): $(int2hex "$speed") ($speed%)"
done
exit 0
fi
#argc $# 1
mode=${1,,}
if [[ "$mode" == "default" ]]
then
setFanMode "$DEFAULT_FANMODE"
exit $?
elif [[ "$mode" == "auto" ]]
then
adjustFanSpeeds
exit $?
fi
fanmode=$(getFanModeID "$mode")
setFanMode "$(getFanModeKey "$fanmode")"
shift
if [[ "$fanmode" -eq $(getFanModeID "full") ]]
then
FANZONES=( "$@" )
c=$(( ${#FANZONES[@]} - 1 ))
if (( c >= 0 ))
then
for (( i=0; i <= c; i++ ))
do
v=$(intval "${FANZONES[i]}")
if (( v > 0 ))
then
setFanSpeed "$i" "${FANZONES[i]}"
fi
done
else
c=$(( ${#DEFAULT_FANSPEED[@]} - 1 ))
for (( i=0; i <= c; i++ ))
do
setFanSpeed "$i" "${DEFAULT_FANSPEED[i]}"
done
fi
fi
;;
*)
usage
;;
esac
[Unit]
Description=Supermicro Auto Fan Mode (Service)
Requires=openipmi.service
After=openipmi.service
[Service]
ExecStart=/usr/local/sbin/supermicro fan auto
Nice=-10
[Unit]
Description=Supermicro Auto Fan Mode (Timer)
[Timer]
OnBootSec=60
OnUnitInactiveSec=45
[Install]
WantedBy=basic.target
[Unit]
Description=Supermicro Fan Control
Requires=openipmi.service
After=openipmi.service
[Service]
Type=oneshot
RemainAfterExit=true
StandardOutput=journal
ExecStart=/usr/local/sbin/supermicro fan full 50 50
ExecStop=/usr/local/sbin/supermicro fan default
Nice=-10
[Install]
WantedBy=multi-user.target
# It's possible to include remote host arguments for ipmitool here.
# Create SDR cache file: ipmitool sdr dump /var/cache/ipmi/sdr/local
IPMITOOL_ARGS=( -S "/var/cache/ipmi/sdr/local" )
# Default fan mode.
# FANSPEED IGNORED!
DEFAULT_FANMODE="pue2"
# FULL: Default fan speed(s).
# One value per cooling zone.
# Values in percent as integer.
DEFAULT_FANSPEED=( 100 100 )
# FULL: Workaround for race condition.
# Otherwise it ignores new speed value(s).
# Use a value between 1 to 5 seconds.
DEFAULT_FANSLEEP=1
# Rules for auto fan adjustment.
# Use "0" for no change at a zone.
# <SENSOR> <TEMP> <ZONESPEED> ...
AUTOFAN_MIN=( "cpu 40 1 1" "system 40 0 1" "peripheral 40 0 1" "dimm 50 0 1" )
AUTOFAN_MAX=( "cpu 80 100 50" "system 60 0 100" "peripheral 60 0 100" "dimm 70 0 100" )
## Time-based multiplicator in percent.
## 100 is the original calculated speed.
## Day of week: 0-7; Start/end time: HHMM
## <DOW> <START> <END> <PERCENT> <BREAK>
#AUTOFAN_PENALTY=( "* 2200 0600 50 !" )
## Optional: Base speed for all cooling zones at auto fan mode.
## Only used if temperatures are lower than all triggers.
## This could lead to freaky up/down spinning of fans!
#AUTOFAN_BASE=( 1 1 )
## Custom temperature sources.
## Calls: smCustomSource<NAME>
#AUTOFAN_SOURCES=( "ssd" "hdd" )
#
#function smCustomSourceSSD
#{
# hddtemp -n SAT:/dev/sd{a..d} | sort -n | tail -n 1 || echo 0
#}
#
#function smCustomSourceHDD
#{
# curl --fail --silent --max-time 3 \
# --header "Content-Type: application/json" \
# --header "Authorization: Bearer 1337-EXAMPLE" \
# --data '{"names": ["ada0", "ada1", "ada2", "ada3", "ada4", "ada5", "ada6", "ada7"], "powermode": "STANDBY"}' \
# "https://truenas.example.net/api/v2.0/disk/temperatures" \
# | jq -r '[ .[] ] | max | tonumber' 2>/dev/null \
# || \
# (
# stderr "Failed to fetch HDD temperature via TrueNAS API!"
# echo 0
# )
#}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment