#!/bin/sh # quiet-fan.sh - quiet the fan on a UniFi Dream Router (UDR) # # Gives Ubiquiti's own fan controller (uhwd) a calmer table via # /etc/ustd/fan.conf. Nothing else on the router is changed. # # sh quiet-fan.sh install apply the quiet table (backs up any existing one) # sh quiet-fan.sh status show temperature and fan speed, change nothing # sh quiet-fan.sh undo remove the table, back to the factory curve # # Tested on one UDR, UniFi OS 5.1.33, October 2026. Use at your own risk. CONF=/etc/ustd/fan.conf H=/sys/class/hwmon/hwmon0 CPU=/sys/class/thermal/thermal_zone0/temp measure() { for i in 1 2 3; do echo " cpu=$(( $(cat $CPU) / 1000 ))C pwm=$(cat $H/pwm1) rpm=$(cat $H/fan1_input)" [ "$i" -lt 3 ] && sleep 5 done } preflight() { if [ "$(id -u)" != 0 ]; then echo "Please run this as root (log in with: ssh root@)."; exit 1 fi model=$(ubnt-device-info model_short 2>/dev/null) if [ "$model" != "UDR" ]; then echo "This router reports model '${model:-unknown}', not 'UDR'." echo "The fan table is only tested on the UniFi Dream Router. Nothing was changed." exit 1 fi if [ ! -d /etc/ustd ] || [ ! -r $H/pwm1 ] || [ ! -r $H/fan1_input ] || [ ! -r $CPU ]; then echo "Could not find Ubiquiti's fan controller or the fan sensors. Nothing was changed." exit 1 fi } load() { # usdbd reads fan.conf and hands it to uhwd; uhwd restarts itself for a second. systemctl restart usdbd echo " Waiting 10 seconds for the fan to settle..." sleep 10 } case "${1:-}" in install) preflight others=$(systemctl list-units --all --plain --no-legend 2>/dev/null | awk '{print $1}' | grep -i fan) if [ -n "$others" ]; then echo "Found other fan services that would fight the new table:" echo "$others" | sed 's/^/ /' echo "Stop them first, e.g.: systemctl disable --now . Nothing was changed." exit 1 fi echo "Before:"; measure if [ -f $CONF ]; then backup=/root/fan.conf.backup-$(date +%Y%m%d-%H%M%S) cp $CONF "$backup" && echo " Saved your existing table to $backup" fi cat > $CONF.new <<'CONF' { "2": ["cpu", -40, 70, 16, 16], "3": ["cpu", 70, 80, 39, 39], "4": ["cpu", 80, 90, 70, 70], "5": ["cpu", 90, 200, 100, 100], "12": ["flash", -40, 60, 16, 16], "13": ["flash", 60, 65, 39, 39], "14": ["flash", 65, 70, 70, 70], "15": ["flash", 70, 200, 100, 100], "PID": {"cpu": [82, -0.1, -0.1, 0.0, false, 2, 10], "flash": [62, -0.1, -0.1, 0.0, false, 2, 10]}, "targets": {"1": ["all"]}, "standby": 10, "calc_type": "table" } CONF if ! python3 -m json.tool $CONF.new > /dev/null 2>&1; then rm -f $CONF.new; echo "The new table did not validate. Nothing was changed."; exit 1 fi mv $CONF.new $CONF load echo "After:"; measure echo echo "Done. Expect pwm=36 and roughly 600-700 rpm at normal temperatures." echo "rpm above 0 means the fan is spinning. To undo: sh $0 undo" ;; status) if [ -f $CONF ]; then echo "Quiet table: installed ($CONF)"; else echo "Quiet table: not installed (factory curve)"; fi measure ;; undo) preflight if [ ! -f $CONF ]; then echo "No quiet table installed. Nothing to undo."; exit 0; fi mv $CONF /root/fan.conf.removed-$(date +%Y%m%d-%H%M%S) load echo "Back to the factory curve:"; measure ;; *) echo "Usage: sh $0 install | status | undo"; exit 1 ;; esac