3
# Simple script implementing a temperature dependent fan speed control
4
# Supported Linux kernel versions: 2.6.5 and later
8
# Usage: fancontrol [CONFIGFILE]
11
# bash, grep, sed, cut, sleep, readlink, lm_sensors :)
13
# Please send any questions, comments or success stories to
14
# marius.reiner@hdev.de
17
# For configuration instructions and warnings please see fancontrol.txt, which
18
# can be found in the doc/ directory or at the website mentioned above.
21
# Copyright 2003 Marius Reiner <marius.reiner@hdev.de>
22
# Copyright (C) 2007-2014 Jean Delvare <jdelvare@suse.de>
24
# This program is free software; you can redistribute it and/or modify
25
# it under the terms of the GNU General Public License as published by
26
# the Free Software Foundation; either version 2 of the License, or
27
# (at your option) any later version.
29
# This program is distributed in the hope that it will be useful,
30
# but WITHOUT ANY WARRANTY; without even the implied warranty of
31
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
32
# GNU General Public License for more details.
34
# You should have received a copy of the GNU General Public License
35
# along with this program; if not, write to the Free Software
36
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
41
PIDFILE="/var/run/fancontrol.pid"
50
echo "Loading configuration from $1 ..."
53
echo "Error: Can't read configuration file" >&2
57
# grep configuration from file
58
INTERVAL=$(grep -E '^INTERVAL=.*$' $1 | sed -e 's/INTERVAL=//g')
59
DEVPATH=$(grep -E '^DEVPATH=.*$' $1 | sed -e 's/DEVPATH= *//g')
60
DEVNAME=$(grep -E '^DEVNAME=.*$' $1 | sed -e 's/DEVNAME= *//g')
61
FCTEMPS=$(grep -E '^FCTEMPS=.*$' $1 | sed -e 's/FCTEMPS=//g')
62
MINTEMP=$(grep -E '^MINTEMP=.*$' $1 | sed -e 's/MINTEMP=//g')
63
MAXTEMP=$(grep -E '^MAXTEMP=.*$' $1 | sed -e 's/MAXTEMP=//g')
64
MINSTART=$(grep -E '^MINSTART=.*$' $1 | sed -e 's/MINSTART=//g')
65
MINSTOP=$(grep -E '^MINSTOP=.*$' $1 | sed -e 's/MINSTOP=//g')
67
FCFANS=$(grep -E '^FCFANS=.*$' $1 | sed -e 's/FCFANS=//g')
68
MINPWM=$(grep -E '^MINPWM=.*$' $1 | sed -e 's/MINPWM=//g')
69
MAXPWM=$(grep -E '^MAXPWM=.*$' $1 | sed -e 's/MAXPWM=//g')
70
AVERAGE=$(grep -E '^AVERAGE=.*$' $1 | sed -e 's/AVERAGE=//g')
72
# Check whether all mandatory settings are set
73
if [[ -z ${INTERVAL} || -z ${FCTEMPS} || -z ${MINTEMP} || -z ${MAXTEMP} || -z ${MINSTART} || -z ${MINSTOP} ]]
75
echo "Some mandatory settings missing, please check your config file!" >&2
78
if [ "$INTERVAL" -le 0 ]
80
echo "Error in configuration file:" >&2
81
echo "INTERVAL must be at least 1" >&2
85
# write settings to arrays for easier use and print them
87
echo "Common settings:"
88
echo " INTERVAL=$INTERVAL"
93
if ! echo $fcv | grep -E -q '='
95
echo "Error in configuration file:" >&2
96
echo "FCTEMPS value is improperly formatted" >&2
100
AFCPWM[$fcvcount]=$(echo $fcv |cut -d'=' -f1)
101
AFCTEMP[$fcvcount]=$(echo $fcv |cut -d'=' -f2)
102
AFCFAN[$fcvcount]=$(echo $FCFANS |sed -e 's/ /\n/g' |grep -E "${AFCPWM[$fcvcount]}" |cut -d'=' -f2)
103
AFCMINTEMP[$fcvcount]=$(echo $MINTEMP |sed -e 's/ /\n/g' |grep -E "${AFCPWM[$fcvcount]}" |cut -d'=' -f2)
104
AFCMAXTEMP[$fcvcount]=$(echo $MAXTEMP |sed -e 's/ /\n/g' |grep -E "${AFCPWM[$fcvcount]}" |cut -d'=' -f2)
105
AFCMINSTART[$fcvcount]=$(echo $MINSTART |sed -e 's/ /\n/g' |grep -E "${AFCPWM[$fcvcount]}" |cut -d'=' -f2)
106
AFCMINSTOP[$fcvcount]=$(echo $MINSTOP |sed -e 's/ /\n/g' |grep -E "${AFCPWM[$fcvcount]}" |cut -d'=' -f2)
107
AFCMINPWM[$fcvcount]=$(echo $MINPWM |sed -e 's/ /\n/g' |grep -E "${AFCPWM[$fcvcount]}" |cut -d'=' -f2)
108
[ -z "${AFCMINPWM[$fcvcount]}" ] && AFCMINPWM[$fcvcount]=0
109
AFCMAXPWM[$fcvcount]=$(echo $MAXPWM |sed -e 's/ /\n/g' |grep -E "${AFCPWM[$fcvcount]}" |cut -d'=' -f2)
110
[ -z "${AFCMAXPWM[$fcvcount]}" ] && AFCMAXPWM[$fcvcount]=255
111
AFCAVERAGE[$fcvcount]=$(echo $AVERAGE |sed -e 's/ /\n/g' |grep -E "${AFCPWM[$fcvcount]}" |cut -d'=' -f2)
112
[ -z "${AFCAVERAGE[$fcvcount]}" ] && AFCAVERAGE[$fcvcount]=1
114
# verify the validity of the settings
115
if [ "${AFCMINTEMP[$fcvcount]}" -ge "${AFCMAXTEMP[$fcvcount]}" ]
117
echo "Error in configuration file (${AFCPWM[$fcvcount]}):" >&2
118
echo "MINTEMP must be less than MAXTEMP" >&2
121
if [ "${AFCMAXPWM[$fcvcount]}" -gt 255 ]
123
echo "Error in configuration file (${AFCPWM[$fcvcount]}):" >&2
124
echo "MAXPWM must be at most 255" >&2
127
if [ "${AFCMINSTOP[$fcvcount]}" -ge "${AFCMAXPWM[$fcvcount]}" ]
129
echo "Error in configuration file (${AFCPWM[$fcvcount]}):" >&2
130
echo "MINSTOP must be less than MAXPWM" >&2
133
if [ "${AFCMINSTOP[$fcvcount]}" -lt "${AFCMINPWM[$fcvcount]}" ]
135
echo "Error in configuration file (${AFCPWM[$fcvcount]}):" >&2
136
echo "MINSTOP must be greater than or equal to MINPWM" >&2
139
if [ "${AFCMINPWM[$fcvcount]}" -lt 0 ]
141
echo "Error in configuration file (${AFCPWM[$fcvcount]}):" >&2
142
echo "MINPWM must be at least 0" >&2
145
if [ "${AFCAVERAGE[$fcvcount]}" -lt 1 ]
147
echo "Error in configuration file (${AFCPWM[$fcvcount]}):" >&2
148
echo "AVERAGE must be at least 1" >&2
151
declare -a PREVIOUSTEMP_$fcvcount
154
echo "Settings for ${AFCPWM[$fcvcount]}:"
155
echo " Depends on ${AFCTEMP[$fcvcount]}"
156
echo " Controls ${AFCFAN[$fcvcount]}"
157
echo " MINTEMP=${AFCMINTEMP[$fcvcount]}"
158
echo " MAXTEMP=${AFCMAXTEMP[$fcvcount]}"
159
echo " MINSTART=${AFCMINSTART[$fcvcount]}"
160
echo " MINSTOP=${AFCMINSTOP[$fcvcount]}"
161
echo " MINPWM=${AFCMINPWM[$fcvcount]}"
162
echo " MAXPWM=${AFCMAXPWM[$fcvcount]}"
163
echo " AVERAGE=${AFCAVERAGE[$fcvcount]}"
164
let fcvcount=fcvcount+1
169
function DevicePath()
171
if [ -h "$1/device" ]
173
readlink -f "$1/device" | sed -e 's/^\/sys\///'
177
function DeviceName()
181
cat "$1/name" | sed -e 's/[[:space:]=]/_/g'
182
elif [ -r "$1/device/name" ]
184
cat "$1/device/name" | sed -e 's/[[:space:]=]/_/g'
188
function ValidateDevices()
190
local OLD_DEVPATH="$1" OLD_DEVNAME="$2" outdated=0
191
local entry device name path
193
for entry in $OLD_DEVPATH
195
device=$(echo "$entry" | sed -e 's/=[^=]*$//')
196
path=$(echo "$entry" | sed -e 's/^[^=]*=//')
198
if [ "$(DevicePath "$device")" != "$path" ]
200
echo "Device path of $device has changed" >&2
205
for entry in $OLD_DEVNAME
207
device=$(echo "$entry" | sed -e 's/=[^=]*$//')
208
name=$(echo "$entry" | sed -e 's/^[^=]*=//')
210
if [ "$(DeviceName "$device")" != "$name" ]
212
echo "Device name of $device has changed" >&2
220
function FixupDeviceFiles
223
local fcvcount pwmo tsen fan
226
while (( $fcvcount < ${#AFCPWM[@]} )) # go through all pwm outputs
228
pwmo=${AFCPWM[$fcvcount]}
229
AFCPWM[$fcvcount]=${pwmo//$DEVICE\/device/$DEVICE}
230
if [ "${AFCPWM[$fcvcount]}" != "$pwmo" ]
232
echo "Adjusing $pwmo -> ${AFCPWM[$fcvcount]}"
234
let fcvcount=$fcvcount+1
238
while (( $fcvcount < ${#AFCTEMP[@]} )) # go through all temp inputs
240
tsen=${AFCTEMP[$fcvcount]}
241
AFCTEMP[$fcvcount]=${tsen//$DEVICE\/device/$DEVICE}
242
if [ "${AFCTEMP[$fcvcount]}" != "$tsen" ]
244
echo "Adjusing $tsen -> ${AFCTEMP[$fcvcount]}"
246
let fcvcount=$fcvcount+1
250
while (( $fcvcount < ${#AFCFAN[@]} )) # go through all fan inputs
252
fan=${AFCFAN[$fcvcount]}
253
AFCFAN[$fcvcount]=${fan//$DEVICE\/device/$DEVICE}
254
if [ "${AFCFAN[$fcvcount]}" != "$fan" ]
256
echo "Adjusing $fan -> ${AFCFAN[$fcvcount]}"
258
let fcvcount=$fcvcount+1
262
# Some drivers moved their attributes from hard device to class device
268
for entry in $DEVPATH
270
device=$(echo "$entry" | sed -e 's/=[^=]*$//')
272
if [ -e "$device/name" ]
274
FixupDeviceFiles "$device"
279
# Check that all referenced sysfs files exist
282
local outdated=0 fcvcount pwmo tsen fan
285
while (( $fcvcount < ${#AFCPWM[@]} )) # go through all pwm outputs
287
pwmo=${AFCPWM[$fcvcount]}
290
echo "Error: file $pwmo doesn't exist" >&2
293
let fcvcount=$fcvcount+1
297
while (( $fcvcount < ${#AFCTEMP[@]} )) # go through all temp inputs
299
tsen=${AFCTEMP[$fcvcount]}
302
echo "Error: file $tsen doesn't exist" >&2
305
let fcvcount=$fcvcount+1
309
while (( $fcvcount < ${#AFCFAN[@]} )) # go through all fan inputs
311
# A given PWM output can control several fans
312
for fan in $(echo ${AFCFAN[$fcvcount]} | sed -e 's/+/ /')
316
echo "Error: file $fan doesn't exist" >&2
320
let fcvcount=$fcvcount+1
323
if [ $outdated -eq 1 ]
326
echo "At least one referenced file is missing. Either some required kernel" >&2
327
echo "modules haven't been loaded, or your configuration file is outdated." >&2
328
echo "In the latter case, you should run pwmconfig again." >&2
334
if [ "$1" == "--check" ]
340
LoadConfig /etc/fancontrol
349
LoadConfig /etc/fancontrol
352
# Detect path to sensors
353
if echo "${AFCPWM[0]}" | grep -E -q '^/'
356
elif echo "${AFCPWM[0]}" | grep -E -q '^hwmon[0-9]'
359
elif echo "${AFCPWM[0]}" | grep -E -q '^[1-9]*[0-9]-[0-9abcdef]{4}'
361
DIR=/sys/bus/i2c/devices
363
echo "$0: Invalid path to sensors" >&2
369
echo $0: 'No sensors found! (did you load the necessary modules?)' >&2
374
# Check for configuration change
375
if [ "$DIR" != "/" ] && [ -z "$DEVPATH" -o -z "$DEVNAME" ]
377
echo "Configuration is too old, please run pwmconfig again" >&2
380
if [ "$DIR" = "/" -a -n "$DEVPATH" ]
382
echo "Unneeded DEVPATH with absolute device paths" >&2
385
if ! ValidateDevices "$DEVPATH" "$DEVNAME"
387
echo "Configuration appears to be outdated, please run pwmconfig again" >&2
390
if [ "$DIR" = "/sys/class/hwmon" ]
392
FixupFiles "$DEVPATH"
398
echo "File $PIDFILE exists, is fancontrol already running?" >&2
403
# associative arrays to hold pwmN device name as key, and as value the
404
# pwmN_enable and pwmN values as they were before fancontrol was started
405
declare -A PWM_ENABLE_ORIG_STATE
406
declare -A PWM_ORIG_STATE
409
function pwmdisable()
411
local ENABLE=${1}_enable
413
# No enable file? Just set to max
420
# Try to restore pwmN and pwmN_enable value to the same state as before
421
# fancontrol start. Restoring the pwmN value is tried first, before the
422
# pwmN_enable mode switch.
423
# Some chips seem to need this to properly restore fan operation,
424
# when activating automatic (2) mode.
425
if [ ${PWM_ENABLE_ORIG_STATE[${1}]} ]
427
#restore the pwmN value
428
if [ "$DEBUG" != "" ]
430
echo "Restoring ${1} original value of ${PWM_ORIG_STATE[${1}]}"
432
echo ${PWM_ORIG_STATE[${1}]} > ${1} 2> /dev/null
433
# restore the pwmN_enable value, if it is not 1.
434
# 1 is already set through fancontrol and setting it again might just
435
# reset the pwmN value.
436
if [ ${PWM_ENABLE_ORIG_STATE[${1}]} != 1 ]
438
if [ "$DEBUG" != "" ]
440
echo "Restoring $ENABLE original value of ${PWM_ENABLE_ORIG_STATE[${1}]}"
442
echo ${PWM_ENABLE_ORIG_STATE[${1}]} > $ENABLE 2> /dev/null
443
# check if setting pwmN_enable value was successful. Checking the
444
# pwmN value makes no sense, as it might already have been altered
446
if [ "$(cat $ENABLE)" = ${PWM_ENABLE_ORIG_STATE[${1}]} ]
450
# if pwmN_enable is manual (1), check if restoring the pwmN value worked
451
elif [ "$(cat ${1})" = ${PWM_ORIG_STATE[${1}]} ]
458
echo 0 > $ENABLE 2> /dev/null
459
if [ "$(cat $ENABLE)" -eq 0 ]
465
# It didn't work, try pwmN_enable=1 pwmN=255
466
echo 1 > $ENABLE 2> /dev/null
468
if [ "$(cat $ENABLE)" -eq 1 -a "$(cat $1)" -ge 190 ]
475
echo "$ENABLE stuck to" "$(cat $ENABLE)" >&2
482
local ENABLE=${1}_enable
486
# save the original pwmN_control state, e.g. 1 for manual or 2 for auto,
487
# and the value from pwmN
488
local PWM_CONTROL_ORIG=$(cat $ENABLE)
489
local PWM_ORIG=$(cat ${1})
490
if [ "$DEBUG" != "" ]
492
echo "Saving $ENABLE original value as $PWM_CONTROL_ORIG"
493
echo "Saving ${1} original value as $PWM_ORIG"
495
#check for degenerate case where these values might be empty
496
if [ $PWM_CONTROL_ORIG ] && [ $PWM_ORIG ]
498
PWM_ENABLE_ORIG_STATE[${1}]=$PWM_CONTROL_ORIG
499
PWM_ORIG_STATE[${1}]=$PWM_ORIG
501
# enable manual control by fancontrol
502
echo 1 > $ENABLE 2> /dev/null
511
function restorefans()
513
local status=$1 fcvcount pwmo
515
echo 'Aborting, restoring fans...'
517
while (( $fcvcount < ${#AFCPWM[@]} )) # go through all pwm outputs
519
pwmo=${AFCPWM[$fcvcount]}
521
let fcvcount=$fcvcount+1
523
echo 'Verify fans have returned to full speed'
528
trap 'restorefans 0' SIGQUIT SIGTERM
529
trap 'restorefans 1' SIGHUP SIGINT
532
function UpdateFanSpeeds
535
local pwmo tsens fan mint maxt minsa minso minpwm maxpwm
536
local tval tlastval pwmpval fanval min_fanval one_fan one_fanval
540
while (( $fcvcount < ${#AFCPWM[@]} )) # go through all pwm outputs
542
#hopefully shorter vars will improve readability:
543
pwmo=${AFCPWM[$fcvcount]}
544
tsens=${AFCTEMP[$fcvcount]}
545
fan=${AFCFAN[$fcvcount]}
546
let mint="${AFCMINTEMP[$fcvcount]}*1000"
547
let maxt="${AFCMAXTEMP[$fcvcount]}*1000"
548
minsa=${AFCMINSTART[$fcvcount]}
549
minso=${AFCMINSTOP[$fcvcount]}
550
minpwm=${AFCMINPWM[$fcvcount]}
551
maxpwm=${AFCMAXPWM[$fcvcount]}
552
avg=${AFCAVERAGE[$fcvcount]}
554
read tlastval < ${tsens}
557
echo "Error reading temperature from $DIR/$tsens"
561
read pwmpval < ${pwmo}
564
echo "Error reading PWM value from $DIR/$pwmo"
568
# copy PREVIOUSTEMP_$fcvcount array to prevtemp
569
declare -a 'prevtemp=(${'"PREVIOUSTEMP_$fcvcount"'[@]})'
570
# add new element to the end of the array
571
prevtemp+=($tlastval)
572
# if needed, remove the first element of the array
573
if [ "${#prevtemp[@]}" -gt $avg ]
575
prevtemp=("${prevtemp[@]:1}")
577
# calculate the average value of all elements
578
tval=$(( ( ${prevtemp[@]/%/+}0 ) / ${#prevtemp[@]} ))
579
# copy prevtemp back to PREVIOUSTEMP_$fcvcount
580
eval "PREVIOUSTEMP_$fcvcount=(\"${prevtemp[@]}\")"
582
# If fanspeed-sensor output shall be used, do it
587
# A given PWM output can control several fans
588
for one_fan in $(echo $fan | sed -e 's/+/ /')
590
read one_fanval < ${one_fan}
593
echo "Error reading Fan value from $DIR/$one_fan" >&2
597
# Remember the minimum, it only matters if it is 0
598
if [ $one_fanval -lt $min_fanval ]
600
min_fanval=$one_fanval
607
fanval="$fanval/$one_fanval"
611
min_fanval=1 # set it to a non zero value, so the rest of the script still works
615
if [ "$DEBUG" != "" ]
624
echo "minpwm=$minpwm"
625
echo "maxpwm=$maxpwm"
626
echo "tlastval=$tlastval"
627
echo "prevtemp=${prevtemp[@]}"
629
echo "pwmpval=$pwmpval"
630
echo "fanval=$fanval"
631
echo "min_fanval=$min_fanval"
634
if (( $tval <= $mint ))
635
then pwmval=$minpwm # below min temp, use defined min pwm
636
elif (( $tval >= $maxt ))
637
then pwmval=$maxpwm # over max temp, use defined max pwm
639
# calculate the new value from temperature and settings
640
pwmval="(${tval}-${mint})*(${maxpwm}-${minso})/(${maxt}-${mint})+${minso}"
641
if [ $pwmpval -eq 0 -o $min_fanval -eq 0 ]
642
then # if fan was stopped start it using a safe value
644
# Sleep while still handling signals
649
echo $pwmval > $pwmo # write new value to pwm output
652
echo "Error writing PWM value to $DIR/$pwmo" >&2
655
if [ "$DEBUG" != "" ]
657
echo "new pwmval=$pwmval"
659
let fcvcount=$fcvcount+1
663
echo 'Enabling PWM on fans...'
665
while (( $fcvcount < ${#AFCPWM[@]} )) # go through all pwm outputs
667
pwmo=${AFCPWM[$fcvcount]}
671
echo "Error enabling PWM on $DIR/$pwmo" >&2
674
let fcvcount=$fcvcount+1
677
echo 'Starting automatic fan control...'
679
# main loop calling the main function at specified intervals
683
# Sleep while still handling signals