#!/bin/bash # # Script implementing a temperature dependent fan speed control. # # Usage: fancontrol [CONFIGFILE] # # Version 1.1 # # Dependencies: # * awk (for the math expression evaluation) # * lm_sensors :) # * A correctly configured config file, default /etc/fancontrol, see pwmconfig # for creating an initial version. # # For configuration instructions and warnings please see fancontrol.txt, which # can be found in the lm_sensors package. # # Copyright 2003 Marius Reiner # http://www.hdev.de/fancontrol/ up to version 0.63 # # Copyright 2006, 2009 Volker Kuhlmann # http://volker.top.geek.nz/ versions 1.0, 1.1 # # Copyright (C) 2007-2008 Jean Delvare # # Changelog: # 1.1, 03 Jan 2009, by Volker Kuhlmann: # * Added changes from Jean Delvare's 0.67. # * Kept temp threshold and curve calculations. # * Using bash string functions instead of calling awk/sed/cut/egrep. # * This improved script is upwards compatible with lm-sensor's. # * TODO: re-write calc expressions for bash's integer arithmetic. # 0.67, 2007-2008: # * by Jean Delvare # 1.0, 24 Jan 2006, by Volker Kuhlmann: # * Added hysteresis when turning fan on/off, preventing the fan from going # between off and minimum every $INTERVAL seconds. # This change is upwards compatible; the script's behaviour is unchanged # with older config files. # TODO: add hysteresis handling to pwmconfig # * Fixed the sleep constructs to be usable with system service scripts. # * Rudimentary provision for multiple (different) control curves. # * Tidied up script, replacing sub-optimal shell constructs. # 0.63, 2003: # * by Marius Reiner # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # #DEBUG=1 MAX=255 PIDFILE=/var/run/fancontrol.pid SENSDIR=/proc/sys/dev/sensors SSENSDIR=/sys/bus/i2c/devices HWMONDIR=/sys/class/hwmon # $1: config file to load function LoadConfig { echo "Loading configuration from $1 ..." while read -r; do case "$REPLY" in INTERVAL=*) INTERVAL="${REPLY#*=}";; FCTEMPS=*) FCTEMPS="${REPLY#*=}";; MINTEMP=*) MINTEMP="${REPLY#*=}";; MAXTEMP=*) MAXTEMP="${REPLY#*=}";; MINSTART=*) MINSTART="${REPLY#*=}";; MINSTOP=*) MINSTOP="${REPLY#*=}";; # Optional settings: FCFANS=*) FCFANS="${REPLY#*=}";; THRESHTEMP=*) THRTEMP="${REPLY#*=}";; MINPWM=*) MINPWM="${REPLY#*=}";; MAXPWM=*) MAXPWM="${REPLY#*=}";; esac done < "$1" # Check whether all mandatory settings are set. if [[ -z ${INTERVAL} || -z ${FCTEMPS} || -z ${MINTEMP} || -z ${MAXTEMP} || -z ${MINSTART} || -z ${MINSTOP} ]] then echo "Some mandatory settings missing, please check your config file!" exit 1 fi if [ "$INTERVAL" -le 0 ]; then echo "Error in configuration file:" echo "INTERVAL must be at least 1" exit 1 fi # Write settings to arrays for easier use and print them. getpwmval() { local pwm for pwm in $1; do case "$pwm" in *$2=*) echo "${pwm#*=}"; break;; esac done } echo echo "Common settings:" echo " INTERVAL=$INTERVAL" let fcvcount=0 for fcv in $FCTEMPS; do case "$fcv" in *=*) : ;; *) echo "Error in configuration file:" echo "FCTEMPS value is improperly formatted." exit 1;; esac AFCPWM[$fcvcount]="${fcv%%=*}" # field 1 AFCTEMP[$fcvcount]="${fcv#*=}" # field 2 AFCFAN[$fcvcount]=`getpwmval "$FCFANS" "${AFCPWM[$fcvcount]}"` AFCMINTEMP[$fcvcount]=`getpwmval "$MINTEMP" "${AFCPWM[$fcvcount]}"` AFCMAXTEMP[$fcvcount]=`getpwmval "$MAXTEMP" "${AFCPWM[$fcvcount]}"` AFCTHRTEMP[$fcvcount]=`getpwmval "$THRTEMP" "${AFCPWM[$fcvcount]}"` AFCMINSTART[$fcvcount]=`getpwmval "$MINSTART" "${AFCPWM[$fcvcount]}"` AFCMINSTOP[$fcvcount]=`getpwmval "$MINSTOP" "${AFCPWM[$fcvcount]}"` AFCMINPWM[$fcvcount]=`getpwmval "$MINPWM" "${AFCPWM[$fcvcount]}"` AFCMAXPWM[$fcvcount]=`getpwmval "$MAXPWM" "${AFCPWM[$fcvcount]}"` # Defaults: test -z "${AFCTHRTEMP[$fcvcount]}" && AFCTHRTEMP[$fcvcount]=0 test -z "${AFCMINPWM[$fcvcount]}" && AFCMINPWM[$fcvcount]=0 test -z "${AFCMAXPWM[$fcvcount]}" && AFCMAXPWM[$fcvcount]=255 # Verify the validity of the settings. if [ "${AFCMINTEMP[$fcvcount]}" -ge "${AFCMAXTEMP[$fcvcount]}" ]; then echo "Error in configuration file (${AFCPWM[$fcvcount]}):" echo "MINTEMP must be less than MAXTEMP" exit 1 fi if [ "${AFCMINPWM[$fcvcount]}" -ge "${AFCMAXPWM[$fcvcount]}" ]; then echo "Error in configuration file (${AFCPWM[$fcvcount]}):" echo "MINPWM must be less than MAXPWM" exit 1 fi if [ "${AFCMINPWM[$fcvcount]}" -lt 0 ]; then echo "Error in configuration file (${AFCPWM[$fcvcount]}):" echo "MINPWM must be at least 0" exit 1 fi if [ "${AFCMAXPWM[$fcvcount]}" -gt 255 ]; then echo "Error in configuration file (${AFCPWM[$fcvcount]}):" echo "MAXPWM must be at most 255" exit 1 fi if [ "${AFCMINSTOP[$fcvcount]}" -ge "${AFCMAXPWM[$fcvcount]}" ]; then echo "Error in configuration file (${AFCPWM[$fcvcount]}):" echo "MINSTOP must be less than MAXPWM" exit 1 fi if [ "${AFCMINSTOP[$fcvcount]}" -lt "${AFCMINPWM[$fcvcount]}" ]; then echo "Error in configuration file (${AFCPWM[$fcvcount]}):" echo "MINSTOP must be greater than or equal to MINPWM" exit 1 fi echo echo "Settings for ${AFCPWM[$fcvcount]}:" echo " Depends on ${AFCTEMP[$fcvcount]}" echo " Controls ${AFCFAN[$fcvcount]}" echo " MINTEMP=${AFCMINTEMP[$fcvcount]}" echo " MAXTEMP=${AFCMAXTEMP[$fcvcount]}" echo " THRESHTEMP=${AFCTHRTEMP[$fcvcount]}" echo " MINSTART=${AFCMINSTART[$fcvcount]}" echo " MINSTOP=${AFCMINSTOP[$fcvcount]}" echo " MINPWM=${AFCMINPWM[$fcvcount]}" echo " MAXPWM=${AFCMAXPWM[$fcvcount]}" let fcvcount=fcvcount+1 done echo } # $1 = full pwm file name function pwmdisable() { if [ "$SYSFS" = "1" ]; then ENABLE=${1}_enable # Without enable file, just set to max. if [ ! -f $ENABLE ]; then echo $MAX > $1 return 0 fi # Try pwmN_enable=0 echo 0 > $ENABLE 2>/dev/null if [ `cat $ENABLE` -eq 0 ]; then # Success return 0 fi # It didn't work, try pwmN_enable=1 pwmN=255 echo 1 > $ENABLE 2>/dev/null echo $MAX > $1 if [ `cat $ENABLE` -eq 1 -a `cat $1` -ge 190 ]; then # Success return 0 fi # Nothing worked echo "$ENABLE stuck to" `cat $ENABLE` >&2 return 1 else echo $MAX 0 > $1 fi } # $1 = full pwm file name function pwmenable() { if [ "$SYSFS" = "1" ]; then ENABLE=${1}_enable if [ -f $ENABLE ]; then echo 1 > $ENABLE 2>/dev/null if [ $? -ne 0 ]; then return 1 fi fi echo $MAX > $1 else echo $MAX 1 > $1 fi } # Set fans to maximum and exit. function restorefans() { local status="$1" echo "Aborting, restoring fans..." let fcvcount=0 while (( $fcvcount < ${#AFCPWM[@]} )); do # go through all pwm outputs pwmo=${AFCPWM[$fcvcount]} pwmdisable $SENSDIR/$pwmo let fcvcount=$fcvcount+1 done echo "Verify fans have returned to full speed." rm -f "$PIDFILE" exit "$status" } # Function evaluating the mathematical expression. function calc () { awk "BEGIN { print $@ }" # Alternatives: bc, or bc -l, or let (for internal integer arithmetic). } # Calculate the new value from temperature and settings; quadratic function. # Sets variables pwmval and func! calc_quadratic() { func="((${tval}-${mint})/(${maxt}-${mint}))^2*(255-${minso})+${minso}" pwmval=`calc "$func"` pwmval="${pwmval%%.*}" } # Calculate the new value from temperature and settings; "quad" function? # Sets variables pwmval and func! calc_quad() { func="((10/(${maxt}-${mint})*(${temp}-${mint}))^2/1000*(${maxt}-${mint})*(255-${minspeed})+${minspeed})" pwmval=`calc "$func"` pwmval="${pwmval%%.*}" } # Calculate the new value from temperature and settings; exponential function. # Sets variables pwmval and func! calc_exponential() { func="..." pwmval=`calc "$func"` pwmval="${pwmval%%.*}" } # main function function UpdateFanSpeeds { local filler filler2 let fcvcount=0 while (( $fcvcount < ${#AFCPWM[@]} )); do # Go through all pwm outputs. #Hopefully shorter vars will improve readability: pwmo=${AFCPWM[$fcvcount]} tsens=${AFCTEMP[$fcvcount]} fan=${AFCFAN[$fcvcount]} mint=${AFCMINTEMP[$fcvcount]} maxt=${AFCMAXTEMP[$fcvcount]} minsa=${AFCMINSTART[$fcvcount]} minso=${AFCMINSTOP[$fcvcount]} thresht=${AFCTHRTEMP[$fcvcount]} minpwm=${AFCMINPWM[$fcvcount]} maxpwm=${AFCMAXPWM[$fcvcount]} read tval < $SENSDIR/${tsens} || { echo "Error reading temperature from $SENSDIR/$tsens" restorefans 1 } if [ "$SYSFS" = "1" ]; then let tval="($tval + 500) / 1000" else tval=`echo ${tval} |cut -d' ' -f3 |cut -d'.' -f1` fi read pwmpval filler < $SENSDIR/${pwmo} || { echo "Error reading PWM value from $SENSDIR/$pwmo" restorefans 1 } if [ "$SYSFS" != "1" ] then pwmpval=`echo ${pwmpval} | cut -d' ' -f1` fi # If fanspeed-sensor output shall be used, do it. if [[ -n ${fan} ]]; then read filler fanval filler2 < $SENSDIR/${fan} || { echo "Error reading Fan value from $SENSDIR/$fan" restorefans 1 } test "$fanval" || fanval=$filler # field 2, or first field if [ "$SYSFS" != "1" ] then fanval=`echo ${fanval} | cut -d' ' -f2` fi else # Set to a non zero value, so the rest of the script still works. fanval=1 fi # debug info if [ -n "$DEBUG" ]; then echo "" echo "pwmo=$pwmo" echo "tsens=$tsens" echo "fan=$fan" echo "mint=$mint" echo "maxt=$maxt" echo "thresht=$thresht" echo "minsa=$minsa" echo "minso=$minso" echo "minpwm=$minpwm" echo "maxpwm=$maxpwm" echo "tval=$tval" echo "pwmpval=$pwmpval" echo "fanval=$fanval" fi if (( $tval <= $mint )) then pwmval=$minpwm # at specified mintemp shut fan off elif (( $tval >= $maxt )) then pwmval=$maxpwm # at specified maxtemp switch to 100% else # Calculate the new value from temperature and settings. # (Sets variables pwmval and func.) calc_quadratic # If fan was stopped, start it using a safe value, # but only start it if temp is at least the threshold over minimum. if [ $pwmpval -eq 0 -o $fanval -eq 0 ]; then if (( $tval >= (( $mint + $thresht )) )); then if [ -n "$DEBUG" ]; then echo "starting fan with pwmval=$minsa" fi echo $minsa > $SENSDIR/$pwmo # ignore possible error #sleep 1 # not killable read < <(exec sleep 1) else pwmval=0 fi fi fi # write new value to pwm output echo $pwmval > $SENSDIR/$pwmo || { echo "Error writing PWM value to $SENSDIR/$pwmo" restorefans 1 } if [ -n "$DEBUG" ]; then echo "func=$func" echo "new pwmval=$pwmval" fi let fcvcount=$fcvcount+1 done } ## MAIN if [ -e "$PIDFILE" ]; then echo 1>&2 "$0: Error: PID file already exists: $PIDFILE" exit 1 fi LoadConfig "${1-/etc/fancontrol}" if [ ! -d $SENSDIR ]; then # For Linux 2.6, detect if config file uses the hwmon class yet. case "${AFCPWM[0]}" in hwmon[0-9]*) SSENSDIR=$HWMONDIR;; esac if [ ! -d $SSENSDIR ]; then echo $0: "No sensors found! (did you load the necessary modules?)" exit 1 else SYSFS=1 SENSDIR=$SSENSDIR fi fi # cd $SENSDIR # Let's not stand in procfs or sysfs! trap "restorefans 0" SIGQUIT SIGTERM trap "restorefans 1" SIGHUP SIGINT echo $$ > "$PIDFILE" echo "Enabling PWM on fans..." let fcvcount=0 while (( $fcvcount < ${#AFCPWM[@]} )); do # go through all pwm outputs pwmo=${AFCPWM[$fcvcount]} pwmenable $SENSDIR/$pwmo if [ $? -ne 0 ]; then echo "Error enabling PWM on $SENSDIR/$pwmo" restorefans 1 fi let fcvcount=$fcvcount+1 done echo "Starting automatic fan control..." # main loop calling the main function at specified intervals while true; do UpdateFanSpeeds # The sleep in the foreground isn't interruptible by kill, which is # unsuitable for system service scripts. # Werner@suse doesn't like sleep $INTERVAL &; wait $! although it works. read < <(exec sleep $INTERVAL) done