Skip to content

Repository files navigation

UniFi Intelligent Fan Control

Advanced temperature management for Ubiquiti UniFi OS devices with fan control.

Confirmed working on: UCG-Max, UCG-Fibre, UXG-Fibre, UDM-SE, UDM-Pro-Max, UDR7, UNVR

Not supported: UniFi switches (USW line). They run BusyBox sh with no bash, no ubnt-systool for temperature, and no systemd — and their fans are firmware controlled rather than exposed as writable /sys/class/hwmon/*/pwm*. Confirmed on a USW Enterprise 48 PoE running 7.5.9: no /sys/class/hwmon/*/pwm* entries exist at all. This needs consoles and gateways running full UniFi OS.

This project is built and maintained independently. If it keeps your UniFi gear cool and quiet, consider supporting it.

Features

  • 🎛️ Four Operational States:
    • OFF: Fan disabled (temp < activation threshold)
    • TAPER: Post-cooling minimum speed period
    • ACTIVE: Quadratic response curve (temp ≥ activation threshold)
    • EMERGENCY: Immediate full speed (255 PWM) (critical temps)
  • 🚨 Emergency Override: Instant full speed at critical temps with hysteresis for stable transitions
  • 📈 Quadratic Response: Progressive cooling curve for optimal noise/performance
  • 🧠 Enhanced Adaptive Learning: Intelligent PWM optimization with temperature trend analysis
  • 📉 Exponential Smoothing: Noise-resistant temperature tracking
  • 🛡️ Robust Safety Systems:
    • Speed limits and thermal protection
    • Hardware validation
    • Sensor failure detection and recovery
    • Configuration validation
  • 🔄 State Transition Hysteresis: Prevents rapid state oscillation
  • 🔍 Multi-Fan Auto-Detection: Automatically discovers and controls all active fan channels
    • Searches hwmon class directories first (UCG-Max, UNVR)
    • Falls back to raw sysfs device paths when needed (UDM-SE)
    • Identifies active fans by RPM reading and write-tests each channel
    • All detected fans receive the same PWM value
  • Drive Temperature Floor: Raises PWM for a hot NVMe or SATA drive without changing the CPU curve

Installation

curl -fsSL https://raw.githubusercontent.com/iceteaSA/unifi-fan-control/main/install.sh | sudo bash

By default, the installer resolves the latest tagged release, downloads its runtime tarball, and verifies the tarball against that release's SHA256SUMS. The installed version is recorded in /data/fan-control/VERSION.

Pin a Release

Use a version when you need a known build:

curl -fsSL https://raw.githubusercontent.com/iceteaSA/unifi-fan-control/main/install.sh | sudo FAN_CONTROL_VERSION=v1.2.0 bash

FAN_CONTROL_VERSION accepts v1.2.0 or 1.2.0. Pinned installs verify the matching release tarball before replacing installed files.

Release Download DNS Failures

GitHub redirects verified release downloads from github.com to release-assets.githubusercontent.com. Both names must resolve. If the installer names release-assets.githubusercontent.com, fix the device's DNS resolver or allow that host first. That is a resolver failure, not a broken installer or release.

If the installer reports that raw.githubusercontent.com is reachable and the resolver cannot be fixed immediately, a one-time fallback is available for one specific tag:

curl -fsSL https://raw.githubusercontent.com/iceteaSA/unifi-fan-control/v1.2.0/install.sh | sudo FAN_CONTROL_ALLOW_UNVERIFIED=v1.2.0 bash

This bypasses SHA256 verification for that install. It still validates the downloaded files before writing them, but it is not the normal or preferred path. FAN_CONTROL_ALLOW_UNVERIFIED must exactly match the tag being installed.

Using a Different Branch

For development builds:

curl -fsSL https://raw.githubusercontent.com/iceteaSA/unifi-fan-control/main/install.sh | sudo FAN_CONTROL_BRANCH=feature/example bash

Branch installs download individual files from GitHub and are unverified. Do not use them for production routers. FAN_CONTROL_VERSION and FAN_CONTROL_BRANCH cannot be used together.

Manual Installation

If you prefer to inspect the code before installation:

# Clone the repository
git clone https://github.com/iceteaSA/unifi-fan-control.git
cd unifi-fan-control

# Run the installer from a checkout or extracted release tarball
sudo ./install.sh

When all four runtime files are beside install.sh, the installer uses those local files without a network request.

Configuration

Edit /data/fan-control/config:

# Core Thresholds
MIN_TEMP=60            # Base threshold (°C)
MAX_TEMP=85            # Critical temperature (°C)
HYSTERESIS=5           # Temperature buffer (°C)

# Fan Behavior
MIN_PWM=91        # Minimum active speed (0-255)
MAX_PWM=255       # Maximum speed (0-255)
MAX_PWM_STEP=25   # Maximum speed change per adjustment
                  # Note: Due to hardware limitations, actual PWM values may vary slightly from requested values

# Drive Temperature Floor
# auto detects a readable NVMe or SATA drive; false skips detection entirely
DRIVE_TEMP_ENABLED=auto
DRIVE_MIN_TEMP=50        # Start raising the PWM floor (°C)
DRIVE_MAX_TEMP=70        # Reach maximum PWM (°C)
DRIVE_CHECK_INTERVAL=60  # Drive temperature polling interval (seconds)

# Advanced Tuning
ALPHA=20          # Smoothing factor, lower values make the smoothed temp follow raw temp more closely (0-100 raw→smooth)
DEADBAND=1        # Temperature stability threshold (°C)
LEARNING_RATE=5   # Hourly PWM optimization step size
TAPER_MINS=90     # Cool-down duration (minutes)
CHECK_INTERVAL=15 # Temperature check frequency (seconds)

# Auto-detects all active fan channels by default (recommended)
# Set to false to use FAN_PWM_DEVICE as a single manual override instead
FAN_PWM_AUTODETECT=true
# Only used when FAN_PWM_AUTODETECT=false
FAN_PWM_DEVICE="/sys/class/hwmon/hwmon0/pwm1"
OPTIMAL_PWM_FILE="/data/fan-control/optimal_pwm"

Note: The script automatically checks for missing configuration parameters and adds them with default values if they're not present in the config file. This ensures that all required parameters are always available, even if you've edited the config file manually.

Apply changes:

systemctl restart fan-control.service

Operational Overview

State Trigger Condition Exit Condition Behavior
OFF <65°C (60+5) Temp ≥ 65°C Fan disabled
TAPER Temp ≤ 60°C from ACTIVE Temp ≥ 67°C or timer elapsed Minimum speed for configured mins
ACTIVE 65°C - 85°C Temp ≤ 60°C or Temp ≥ 85°C Quadratic speed response
EMERGENCY ≥85°C Temp ≤ 80°C (with hysteresis) Immediate full speed (255 PWM)

State Transitions

  • OFF → ACTIVE: Temperature rises above activation threshold (65°C)
  • ACTIVE → TAPER: Temperature drops below minimum threshold (60°C)
  • ACTIVE → EMERGENCY: Temperature reaches critical level (85°C)
  • TAPER → OFF: Cool-down period (default: 90 minutes) completes
  • TAPER → ACTIVE: Temperature rises significantly above activation threshold (67°C, with 2°C buffer)
  • EMERGENCY → ACTIVE: Temperature drops significantly below critical level (80°C, with 5°C hysteresis)

Monitoring & Logging

Key operational signals:

# Temperature Monitoring
TEMP: RAW=68℃ | SMOOTH=65℃ | DELTA=-3℃

# Speed Calculations
CALC: temp_diff=5℃ | range=20℃ | speed=100pwm

# State Transitions
STATE: OFF→ACTIVE (67℃ ≥ 65℃)
STATE: ACTIVE→TAPER (59℃ ≤ 60℃)
STATE: →EMERGENCY (86℃ ≥ 85℃)
STATE: EMERGENCY→ACTIVE (79℃ ≤ 80℃)
STATE: TAPER→ACTIVE (67℃ ≥ 67℃)

# Speed Changes
SET: 55→80pwm | Reason: Ramp-up limited: 55→80pwm
SET: 120→255pwm | Reason: EMERGENCY: Temp 86℃ ≥ 85℃

# Drive Temperature Floor
DRIVE: Detected /dev/nvme0n1 via nvme | Temp=47℃ | wctemp=83℃

# Enhanced Learning System
LEARNING: 80→85pwm (+5 (rising temp 2℃)) [Rate=7]
LEARNING: 95→90pwm (-5 (stable below threshold)) [Rate=5]
LEARNING: 100→99pwm (-1 (efficiency optimization)) [Rate=5]

# Error Handling
ERROR: Failed to read temperature (attempt 1)
ALERT: Multiple temperature read failures - using last known temperature
SAFETY: Activating emergency mode due to sensor failure

# Configuration Validation
CONFIG: Invalid MIN_TEMP value: 25 (should be between 30 and 80), using default: 60
CONFIG: Updating configuration file with corrected values

# Configuration Management
CONFIG: Missing parameter detected: CHECK_INTERVAL
CONFIG: Updating configuration file with 1 missing parameters
CONFIG: Configuration file updated successfully

# Deployed version
CONFIG: fan-control vX.Y.Z starting

# System Status
STATUS: State=ACTIVE | PWM=120 | Temp=72℃
STATUS: State=EMERGENCY | PWM=255 | Temp=86℃

View logs with:

journalctl -u fan-control.service -f          # Live monitoring
journalctl -u fan-control.service --since "10 minutes ago"  # Recent history

Technical Implementation

  • Quadratic Response Curve:

$$ PWM = MIN_{PWM} + \frac{(temp_{diff}^2 \times (MAX_{PWM} - MIN_{PWM}))}{temp_{range}^2} $$

Where:
temp_diff = current_temp - activation_temp
temp_range = MAX_TEMP - activation_temp

  • Exponential Smoothing:

$$ smoothed_{temp} = \frac{\alpha \times previous_{smooth} + (100 - \alpha) \times raw_{temp}}{100} $$

(α configured via ALPHA parameter)


  • Enhanced Adaptive Learning:

    • Adjusts optimal PWM based on thermal performance every 30 minutes (configurable)
    • Uses adaptive learning rate based on temperature stability
    • Implements three learning strategies:
      1. Proactive PWM increase when temperature is rising
      2. PWM reduction when temperature is stable below threshold
      3. Efficiency optimization when running faster than necessary with stable temperatures
  • Robust Error Handling:

    • Tracks consecutive temperature reading failures
    • Implements safety measures after multiple failures
    • Uses last known temperature when readings fail
    • Activates fans proactively during sensor uncertainty
  • Configuration Validation:

    • Validates all parameters against reasonable ranges
    • Automatically corrects invalid settings
    • Prevents misconfiguration issues
  • Hardware PWM Limitations:
    Due to device hardware limitations, the actual PWM values applied may differ from the requested values (e.g., setting 50 might result in ~48, or 100 might result in ~92)

Maintenance

# Service Management
systemctl status fan-control.service   # Current state
systemctl restart fan-control.service  # Apply config changes

# Full Removal
/data/fan-control/uninstall.sh

Which version am I running?

cat /data/fan-control/VERSION
journalctl -u fan-control.service | grep starting | tail -1
# CONFIG: fan-control v1.1.1 starting

Neither prints anything on builds older than v1.0.0 — those predate version identity.

Updating

Re-run the installer. There is no auto-update: this runs as root, and a self-updating root daemon is a large attack surface for a fan controller.

curl -fsSL https://raw.githubusercontent.com/iceteaSA/unifi-fan-control/main/install.sh | sudo bash

Your config is preserved. /data/fan-control/config is never overwritten by an install or upgrade — only the scripts and the service unit are replaced. No backup step is needed.

Does it survive a UniFi OS update?

A normal firmware update, yes. A factory reset, no.

UniFi OS runs root as an overlay: the firmware is a read-only lower layer, and anything you install lands in the upper layer. Both halves of this install live there — the systemd unit and /data/fan-control — so they share one fate. A firmware update swaps the lower layer and leaves the upper alone.

What does remove it: factory reset, reset2defaults, re-adoption, or any recovery flow that rebuilds the overlay. If the service disappears and other things you installed went with it, that was the overlay rather than this script. Reinstall with the one-liner above.

Project Structure

  • fan-control.sh: The main script that monitors temperature and controls fan speed
  • VERSION: Bare SemVer identity for the deployed daemon
  • install.sh: Installation script that copies files and sets up the systemd service
    • Uses local runtime files first, then a pinned release, branch, or latest release
    • Verifies release tarballs and rejects unsafe archive contents before installation
  • uninstall.sh: Script to remove the fan control system
  • fan-control.service: Systemd service configuration
  • tests/: Sandboxed test suite (no device, no root required); run with tests/run-tests.sh
  • release-please-config.json / .release-please-manifest.json: Tagged-release automation configuration
  • .github/workflows/release.yml: Builds and verifies tagged release assets

Star History

Star History Chart

Credits & Acknowledgments

ko-fi


Disclaimer: Community project - Not affiliated with Ubiquiti Inc.
Compatibility: Verified on UniFi OS 4.0.0+ | UCG-Max, UCG-Fibre, UXG-Fibre, UDM-SE, UDM-Pro-Max, UDR7, UNVR
License: MIT

About

Intelligent adaptive fan controller for UniFi OS devices. Multi-fan auto-detection, quadratic response curve, learning PWM optimization. Works on UDM-SE, UDM-Pro-Max, UCG-Max, UCG-Fibre, UXG-Fibre, UDR7, UNVR.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

237 stars

Watchers

14 watching

Forks

Releases

Sponsor this project

Contributors

Languages