├── image.png ├── extras ├── logo.png ├── user_scripts.png └── fan_speed_graph.png ├── README.md └── fan_speed_control.sh /image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IDmedia/fan-control-script/HEAD/image.png -------------------------------------------------------------------------------- /extras/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IDmedia/fan-control-script/HEAD/extras/logo.png -------------------------------------------------------------------------------- /extras/user_scripts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IDmedia/fan-control-script/HEAD/extras/user_scripts.png -------------------------------------------------------------------------------- /extras/fan_speed_graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IDmedia/fan-control-script/HEAD/extras/fan_speed_graph.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | logo 3 |
4 | 5 | 6 | # Unraid Fan Control Script 7 | This Bash script enables automatic adjustment of fan speed in Unraid based on the temperature of your hard drives in the array. You can customize the disks to include or exclude, as well as adjust temperature settings for different fan control scenarios. 8 | 9 | 10 | ![Screenshot from User Scripts](extras/user_scripts.png) 11 | 12 | 13 | ## Prerequisites 14 | 15 | Before using this script, make sure you have completed the following steps: 16 | 17 | 1. Enable Manual Fan Speed Control in Unraid: 18 | Edit the "/boot/syslinux/syslinux.cfg" file and change the line: 19 | ``` 20 | append initrd=/bzroot 21 | ``` 22 | to: 23 | ``` 24 | append initrd=/bzroot acpi_enforce_resources=lax 25 | ``` 26 | 2. Set BIOS Settings: 27 | Set the PWM headers you want to control to 100%/255 and mode to PWM in your BIOS. 28 | 29 | (Optional) I recommend installing the `Dynamix System Temperature` plugin to easily monitor your fans' speed on the dashboard. 30 | 31 | 32 | ## Identify fan headers 33 | * To identify fan headers, use the command `sensors -uA`. 34 | * Utilize `pwmconfig` to find the correct fan header. 35 | * Test PWM pins from the terminal using attributes like pwm[1-5], pwm[1-5]_enable, and pwm[1-5]_mode. 36 | 37 | 38 | ## Usage: 39 | 40 | Utilize the `User Scripts` plugin to set up a new script. 41 | 42 | * Name: Fan Control Script 43 | * Description: Automatically adjust fan speed based on array temperature. 44 | * Schedule: Custom -> `*/5 * * * *` 45 | * Script: Contents of fan_speed_control.sh 46 | 47 | The script dynamically optimizes fan speed based on disk temperatures, executing this process every 5 minutes. When manually executed, it offers informative messages about the current state and the actions taken. In case of unexpected conditions, it sets the fan speed to the maximum. 48 | 49 | 50 | # Configuration 51 | 52 | Adjust these parameters to suit your preferences. All configurable options are listed at the top of the script. 53 | 54 | 55 | # Graph the Fan Curve 56 | 57 | ![Fan Curve Screenshot](extras/fan_speed_graph.png) 58 | 59 | To generate a graphical representation of the fan curve, use the following command to create an image that plots temperature against PWM value. Note that `gnuplot` must be installed, which is not available on UnRAID, so run this command from another Linux machine: 60 | 61 | ``` 62 | ./fan_speed_control.sh --generate-graph-data --output-file fan_speed_graph.png 63 | ``` 64 | 65 | 66 | ## Inter-tech/Norco Case Owners 67 | I suggest reviewing this informative blog post: [The mysterious 6-pin fan header on my Inter-Tech server cases](https://blog.cavelab.dev/2021/03/inter-tech-case-fan-header/). It provides valuable insights into effectively controlling the three large array fans. 68 | 69 | 70 | ### Feel free to contribute, report issues, or suggest improvements! If you find this repository useful, don't forget to star it :) 71 | 72 | 73 | Donate with PayPal 74 | 75 | -------------------------------------------------------------------------------- /fan_speed_control.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Automatically adjusts fan speed based on hard drive temperatures 4 | 5 | # Prerequisites: 6 | # 1. Enable manual fan speed control in Unraid 7 | # This can be done by editing "/boot/syslinux/syslinux.cfg" 8 | # Right beneath "label Unraid OS" you will have to change: 9 | # "append initrd=/bzroot" to "append initrd=/bzroot acpi_enforce_resources=lax" 10 | # 2. Set the PWM headers you want to control to 100%/255 and mode to PWM in your BIOS 11 | 12 | # Tips: 13 | # In order to see what fan headers Unraid sees use "sensors -uA" 14 | # Another useful tool is "pwmconfig". Makes it easier to find the correct fan header 15 | # You may test your pwm pins from the terminal. Here is a list of attributes: 16 | # pwm[1-5] - this file stores PWM duty cycle or DC value (fan speed) in range: 17 | # 0 (lowest speed) to 255 (full) 18 | # pwm[1-5]_enable - this file controls mode of fan/temperature control: 19 | # * 0 Fan control disabled (fans set to maximum speed) 20 | # * 1 Manual mode, write to pwm[0-5] any value 0-255 21 | # * 2 "Thermal Cruise" mode 22 | # * 3 "Fan Speed Cruise" mode 23 | # * 4 "Smart Fan III" mode (NCT6775F only) 24 | # * 5 "Smart Fan IV" mode 25 | # pwm[1-5]_mode - controls if output is PWM or DC level 26 | # * 0 DC output 27 | # * 1 PWM output 28 | 29 | # Generate a fan curve graph (requires gnuplot) 30 | # Run on another Linux machine with: ./fan_speed_control.sh --generate-graph-data 31 | 32 | # Maximum PWM value for fan speed 33 | # Applied when parity is running or disk temperature is too high 34 | # WARNING: Altering this value is generally not recommended 35 | MAX_PWM=255 36 | 37 | # Minimum PWM value for fan speed 38 | MIN_PWM=170 39 | 40 | # Disk temperature range for dynamic fan speed adjustment 41 | LOW_TEMP=35 42 | HIGH_TEMP=48 43 | 44 | # Disks to monitor 45 | # Include disks by type and exclude by name (specified in disk.ini) 46 | INCLUDE_DISK_TYPE_PARITY=1 47 | INCLUDE_DISK_TYPE_DATA=1 48 | INCLUDE_DISK_TYPE_CACHE=1 49 | INCLUDE_DISK_TYPE_FLASH=0 50 | INCLUDE_DISK_TYPE_UNASSIGNED=1 51 | 52 | EXCLUDE_DISK_BY_NAME=( 53 | "cache_system" 54 | "cache_system2" 55 | ) 56 | 57 | # Array fans to be controlled by this script 58 | ARRAY_FANS=( 59 | "/sys/class/hwmon/hwmon4/pwm1" 60 | "/sys/class/hwmon/hwmon4/pwm4" 61 | ) 62 | 63 | ############################################################ 64 | 65 | # Parse command-line arguments 66 | generate_graph_data=false 67 | while [[ "$#" -gt 0 ]]; do 68 | case $1 in 69 | --generate-graph-data) 70 | generate_graph_data=true 71 | ;; 72 | --output-file) 73 | if [[ -n "$2" ]]; then 74 | graph_image_file="$2" 75 | shift 76 | else 77 | echo "Error: --output-file requires a non-empty argument." 78 | exit 1 79 | fi 80 | ;; 81 | esac 82 | shift 83 | done 84 | 85 | 86 | # Function to check if a file exists 87 | check_file_exists() { 88 | local file_path=$1 89 | if [[ ! -f $file_path ]]; then 90 | echo "Error: $file_path does not exist." 91 | exit 1 92 | fi 93 | } 94 | 95 | # Function to calculate fan PWM based on temperature 96 | calculate_fan_pwm() { 97 | local temp=$1 98 | local fan_pwm 99 | 100 | if (( temp <= LOW_TEMP )); then 101 | fan_pwm=$MIN_PWM 102 | elif (( temp > LOW_TEMP && temp <= HIGH_TEMP )); then 103 | pwm_steps=$((HIGH_TEMP - LOW_TEMP)) 104 | pwm_increment=$(( (MAX_PWM - MIN_PWM) / pwm_steps )) 105 | fan_pwm=$(( ((temp - LOW_TEMP) * pwm_increment) + MIN_PWM )) 106 | else 107 | fan_pwm=$MAX_PWM 108 | fi 109 | 110 | echo $fan_pwm 111 | } 112 | 113 | if $generate_graph_data; then 114 | padded_low_temp=$((LOW_TEMP - 5)) 115 | padded_high_temp=$((HIGH_TEMP + 5)) 116 | data_file=$(mktemp) 117 | 118 | min_pwm=$MAX_PWM 119 | max_pwm=0 120 | 121 | # Generate data points for the graph 122 | for temp in $(seq $padded_low_temp $padded_high_temp); do 123 | fan_pwm=$(calculate_fan_pwm $temp) 124 | echo "$temp $fan_pwm" >> $data_file 125 | (( fan_pwm < min_pwm )) && min_pwm=$fan_pwm 126 | (( fan_pwm > max_pwm )) && max_pwm=$fan_pwm 127 | done 128 | 129 | # Check if gnuplot is available 130 | if ! command -v gnuplot &> /dev/null; then 131 | echo "gnuplot is not installed. Please install gnuplot and try again." 132 | exit 1 133 | fi 134 | 135 | # Create gnuplot script 136 | graph_image_file="fan_speed_graph.png" 137 | gnuplot_script=$(mktemp) 138 | cat << EOF > $gnuplot_script 139 | set terminal jpeg size 1200,800 enhanced 140 | set output "$graph_image_file" 141 | set xlabel "Temperature (°C)" 142 | set ylabel "Fan PWM" 143 | set title "Fan PWM vs Temperature" 144 | set grid 145 | set key left top 146 | set xrange [$padded_low_temp:$padded_high_temp] 147 | set yrange [$min_pwm:260] 148 | set xtics 1 149 | set ytics 10 150 | plot '$data_file' using 1:2 with linespoints title "Fan Speed" 151 | EOF 152 | 153 | # Run gnuplot with the created script 154 | gnuplot $gnuplot_script 155 | 156 | # Clean up 157 | rm $data_file $gnuplot_script 158 | echo "Graph image generated in $graph_image_file" 159 | exit 0 160 | fi 161 | 162 | # Check for the existence of required files 163 | check_file_exists "/var/local/emhttp/disks.ini" 164 | check_file_exists "/var/local/emhttp/var.ini" 165 | 166 | # Make a list of disk types the user wants to monitor 167 | declare -A include_disk_types 168 | include_disk_types[Parity]=$INCLUDE_DISK_TYPE_PARITY 169 | include_disk_types[Data]=$INCLUDE_DISK_TYPE_DATA 170 | include_disk_types[Cache]=$INCLUDE_DISK_TYPE_CACHE 171 | include_disk_types[Flash]=$INCLUDE_DISK_TYPE_FLASH 172 | include_disk_types[Unassigned]=$INCLUDE_DISK_TYPE_UNASSIGNED 173 | 174 | # Make a list of all the existing disks 175 | declare -a disk_list_all 176 | while IFS='= ' read var val; do 177 | if [[ $var == \[*] ]]; then 178 | disk_name=${var:2:-2} 179 | disk_list_all+=($disk_name) 180 | eval declare -A ${disk_name}_data 181 | elif [[ $val ]]; then 182 | eval ${disk_name}_data[$var]=$val 183 | fi 184 | done < /var/local/emhttp/disks.ini 185 | 186 | # Check if /usr/local/emhttp/state/devs.ini exists and parse it 187 | if [[ -f /usr/local/emhttp/state/devs.ini ]]; then 188 | while IFS='= ' read var val; do 189 | if [[ $var == \[*] ]]; then 190 | disk_name=${var:2:-2} 191 | disk_list_all+=($disk_name) 192 | eval declare -A ${disk_name}_data 193 | eval ${disk_name}_data[type]="Unassigned" 194 | elif [[ $val ]]; then 195 | eval ${disk_name}_data[$var]=$val 196 | fi 197 | done < /usr/local/emhttp/state/devs.ini 198 | fi 199 | 200 | # Filter disk list based on criteria 201 | declare -a disk_list 202 | for disk in "${disk_list_all[@]}"; do 203 | disk_name=${disk}_data[name] 204 | disk_type=${disk}_data[type] 205 | disk_id=${disk}_data[id] 206 | disk_type_filter=${include_disk_types[${!disk_type}]} 207 | 208 | if [[ ! -z "${!disk_id}" ]] && \ 209 | [[ "${disk_type_filter}" -ne 0 ]] && \ 210 | [[ ! " ${EXCLUDE_DISK_BY_NAME[*]} " =~ " ${disk} " ]]; then 211 | disk_list+=($disk) 212 | fi 213 | done 214 | 215 | # Check temperature 216 | declare -A disk_state 217 | declare -A disk_temp 218 | disk_max_temp_value=0 219 | disk_max_temp_name=null 220 | disk_active_num=0 221 | 222 | for disk in "${disk_list[@]}" 223 | do 224 | # Check disk state 225 | eval state_value=${disk}_data[spundown] 226 | if (( ${state_value} == 1 )) 227 | then 228 | state=spundown 229 | disk_state[${disk}]=spundown 230 | else 231 | state=spunup 232 | disk_state[${disk}]=spunup 233 | disk_active_num=$((disk_active_num+1)) 234 | fi 235 | 236 | # Check disk temperature 237 | temp=${disk}_data[temp] 238 | if [[ "$state" == "spunup" ]] 239 | then 240 | if [[ "${!temp}" =~ ^[0-9]+$ ]] 241 | then 242 | disk_temp[${disk}]=${!temp} 243 | if (( "${!temp}" > "$disk_max_temp_value" )) 244 | then 245 | disk_max_temp_value=${!temp} 246 | disk_max_temp_name=$disk 247 | fi 248 | else 249 | disk_temp[$disk]=unknown 250 | fi 251 | else 252 | disk_temp[$disk]=na 253 | fi 254 | done 255 | 256 | # Check if parity is running 257 | disk_parity=$(awk -F'=' '$1=="mdResync" {gsub(/"/, "", $2); print $2}' /var/local/emhttp/var.ini) 258 | 259 | # Linear PWM Logic 260 | pwm_steps=$((HIGH_TEMP - LOW_TEMP - 1)) 261 | pwm_increment=$(( (MAX_PWM - MIN_PWM) / pwm_steps)) 262 | 263 | # Print heighest disk temp if at least one is active 264 | if [[ $disk_active_num -gt 0 ]]; then 265 | echo "Hottest disk is $disk_max_temp_name at $disk_max_temp_value°C" 266 | fi 267 | 268 | # Calculate new fan speed 269 | # Handle cases where no disks are found 270 | if [[ ${#disk_list[@]} -gt 0 && ${#disk_list[@]} -ne ${#disk_temp[@]} ]] 271 | then 272 | fan_msg="No disks included or unable to read all disks" 273 | fan_pwm=$MAX_PWM 274 | 275 | # Parity is running 276 | elif [[ "$disk_parity" -gt 0 ]] 277 | then 278 | fan_msg="Parity-Check is running" 279 | fan_pwm=$MAX_PWM 280 | 281 | # All disk are spun down 282 | elif [[ $disk_active_num -eq 0 ]] 283 | then 284 | fan_msg="All disks are in standby mode" 285 | fan_pwm=$MIN_PWM 286 | 287 | # Hottest disk is below the LOW_TEMP threshold 288 | elif (( $disk_max_temp_value <= $LOW_TEMP )) 289 | then 290 | fan_msg="Temperature of $disk_max_temp_value°C is below LOW_TEMP ($LOW_TEMP°C)" 291 | fan_pwm=$MIN_PWM 292 | 293 | # Hottest disk is between LOW_TEMP and HIGH_TEMP 294 | elif (( $disk_max_temp_value > $LOW_TEMP && $disk_max_temp_value <= $HIGH_TEMP )) 295 | then 296 | fan_msg="Temperature of $disk_max_temp_value°C is between LOW_TEMP ($LOW_TEMP°C) and HIGH_TEMP ($HIGH_TEMP°C)" 297 | fan_pwm=$(calculate_fan_pwm $disk_max_temp_value) 298 | 299 | # Hottest disk is between HIGH_TEMP and HIGH_TEMP 300 | elif (( $disk_max_temp_value > $HIGH_TEMP && $disk_max_temp_value <= $HIGH_TEMP )) 301 | then 302 | fan_msg="Temperature of $disk_max_temp_value°C is between HIGH_TEMP ($HIGH_TEMP°C) and HIGH_TEMP ($HIGH_TEMP°C)" 303 | fan_pwm=$MAX_PWM 304 | 305 | # Hottest disk is below the LOW_TEMP threshold 306 | elif (( $disk_max_temp_value > $HIGH_TEMP )) 307 | then 308 | fan_msg="Temperature of $disk_max_temp_value°C is above HIGH_TEMP ($HIGH_TEMP°C)" 309 | fan_pwm=$MAX_PWM 310 | 311 | # Handle any unexpected condition 312 | else 313 | fan_msg="An unexpected condition occurred" 314 | fan_pwm=$MAX_PWM 315 | fi 316 | 317 | # Apply fan speed 318 | for fan in "${ARRAY_FANS[@]}" 319 | do 320 | # Set fan mode to 1 if necessary 321 | pwm_mode=$(cat "${fan}_enable") 322 | if [[ $pwm_mode -ne 1 ]]; then 323 | echo 1 > "${fan}_enable" 324 | fi 325 | 326 | # Set fan speed 327 | echo $fan_pwm > $fan 328 | done 329 | 330 | pwm_percent=$(( (fan_pwm * 100) / $MAX_PWM )) 331 | echo "$fan_msg, setting fans to $fan_pwm PWM ($pwm_percent%)" 332 | --------------------------------------------------------------------------------