├── 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 |
3 |
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 |
--------------------------------------------------------------------------------