#!/bin/sh
# tlp - power management functions
#
# Copyright (c) 2013 Thomas Koch <linrunner at gmx.net>
# This software is licensed under the GPL v2 or later.
#
# Some concepts and descriptions were adapted from:
# - laptop-mode-tools
# - thinkwiki.org

# ----------------------------------------------------------------------------
# Constants

readonly TLPVER=0.3.8.1

readonly CONFFILE=/etc/default/tlp
readonly RUNDIR=/var/run/tlp

readonly ACPWR=on_ac_power
readonly ETHTOOL=ethtool
readonly HDPARM=hdparm
readonly IWC=iwconfig
readonly IW=iw
readonly MODPRO=modprobe
readonly LOGGER=logger
readonly UDEVADM=udevadm
readonly LAPMODE=laptop_mode
readonly NMCLI=nmcli
readonly NMTOOL=nm-tool
readonly UPWR=upower

readonly TPACPIBAT=$libdir/tpacpi-bat # libdir is initialized by main program

readonly SMAPIDIR=/sys/devices/platform/smapi
readonly ACPIBATDIR=/sys/class/power_supply
readonly NETD=/sys/class/net
readonly PCID=/sys/bus/pci/devices
readonly I915D=/sys/module/i915/parameters
readonly DMID=/sys/class/dmi/id/
readonly CPU_BOOST_ALL_CTRL=/sys/devices/system/cpu/cpufreq/boost

readonly USBD=/sys/bus/usb/devices
readonly USB_TIMEOUT=2
readonly USB_TIMEOUT_MS=2000
readonly USB_DONE=usb_done

readonly LOCK_RDW=lock.rdw

readonly TPMODULES="thinkpad_acpi tp_smapi coretemp acpi_call"


# ----------------------------------------------------------------------------
# Functions

# --- Tests

patinstr () { # test if pattern matches part of target -- $1: pattern, $2: target
    [ -n "$1" -a -n "$2" -a -z "${2##*$1*}" ]
    return $?
}

echo_debug () { # write debug msg if tag matches -- $1: tag; $2: msg;  
    [ "$nodebug" = "1" ] && return 0
    
    if patinstr "$1" "$TLP_DEBUG"; then
        $LOGGER -p debug -t "tlp[$$,$PPID]" "$2"
    fi
}

cmd_exists () { # test if command exists -- $1: command
    command -v $1 > /dev/null 2>&1
}

check_sysfs ()  { # check if sysfile exists -- $1: routine; $2: sysfs path 
    if patinstr "sysfs" "$TLP_DEBUG"; then
        if [ ! -e $2 ]; then
            $LOGGER -p debug -t "tlp[$$]" "$1:$2 nonexistent"
        fi
    fi
}

test_root () { # test root privilege -- rc: 0=root, 1=not root
    [ "$(id -u)" = "0" ]
}

check_root () { # show error message and exit when root privilege missing
    if ! test_root; then
        echo "Error: missing root privilege." 1>&2
        exit 1
    fi
}

check_tlp_enabled () { # check if TLP is enabled in config file
    # rc: 0=disabled/1=enabled
    
    if [ ! "$TLP_ENABLE" = "1" ]; then
        echo "Error: TLP power save is disabled. Set TLP_ENABLE=1 in $CONFFILE." 1>&2
        return 1
    else
        return 0
    fi
}

check_laptop_mode_tools () { # check if lmt installed -- rc: 0=not installed, 1=installed
    if cmd_exists $LAPMODE; then
        echo "Error: TLP power save is disabled because laptop-mode-tools is installed." 1>&2
        echo "       Please uninstall laptop-mode-tools." 1>&2
        echo 1>&2
        echo_debug "pm" "check_laptop_mode_tools: yes" 
        return 1
    else
        return 0
    fi
}

is_thinkpad () { # check if running on a ThinkPad w/ thinkpad_acpi loaded
                 # rc: 0=ThinkPad, 1=other hardware

    [ -d /sys/devices/platform/thinkpad_acpi ]
}

# --- Configuration

read_defaults () { # read config file
    if [ -f $CONFFILE ]; then
        . $CONFFILE --
        return 0
    else
        return 1
    fi
}

# --- Kernel Modules

load_tp_modules () { # load ThinkPad specific kernel modules
    local mod

    # verify module loading is allowed (else explicitly disabled)
    # and possible (else implicitly disabled)
    [ "${TLP_LOAD_MODULES:-y}" = "y" ] && [ -e /proc/modules ] || return 0

    # load modules, ignore any errors
    for mod in $TPMODULES; do
        $MODPRO $mod > /dev/null 2>&1 
    done
    return 0
}

# --- DMI

read_dmi () { # read dmi data -- $*: keywords; retval: $dmirslt
    local ds key

    dmirslt=""
    for key in $*; do
        ds="$( cat ${DMID}/$key 2> /dev/null | \
                egrep -iv 'not available|to be filled|DMI table is broken' )"
        if [ -n "$dmirslt" ]; then
            [ -n "$ds" ] && dmirslt="$dmirslt $ds"
        else
            dmirslt="$ds"
        fi
    done
    
    return 0
}

# --- Power Source

get_power_state () { # get current power source -- rc: 0=ac, 1=battery
    $ACPWR
    return $? 
}

echo_started_mode () { # print operation mode -- $1: 0=ac mode, 1=battery mode
    if [ "$1" = "0" ]; then
        echo "TLP started in ac mode."
    else
        echo "TLP started in battery mode."
    fi
    
    return 0
}

# --- Locking

set_run_flag () { # set flag -- $1: flag name
    local rc
    
    [ -d $RUNDIR ] || mkdir $RUNDIR

    if [ -d $RUNDIR ]; then
        touch $RUNDIR/$1
        rc=$?
        echo_debug "lock" "set_run_flag.touch: $1; rc=$rc"
        
        return $rc
    else
        # mkdir failed
        echo_debug "lock" "set_run_flag.mkdir_failed"
        return 2
    fi
}

reset_run_flag () { # reset flag -- $1: flag name
    if [ -f $RUNDIR/$1 ]; then
        rm $RUNDIR/$1
        echo_debug "lock" "reset_run_flag.remove: $1"
    else
        echo_debug "lock" "reset_run_flag.not_found: $1"
    fi
    
    return 0
}

check_run_flag () { # check flag -- $1: flag name
    local rc
    
    [ -f $RUNDIR/$1 ]
    rc=$?
    echo_debug "lock" "check_run_flag: $1; rc=$rc"
    
    return $rc
}

set_timed_lock () { # create timestamp n seconds in the future
    # $1: lock id, $2: lock duration [s]
    
    set_run_flag ${1}_timed_lock_$(date +%s -d "+${2} seconds")
}

check_timed_lock () { # check if active timestamp exists
    # $1: lock id
    
    local lockid=$1
    local locktime
    local time=$(date +%s)
    
    for lockfile in $RUNDIR/${lockid}_timed_lock_*; do
        if [ -f $lockfile ]; then
            locktime=${lockfile#${RUNDIR}/${lockid}_timed_lock_}
            if [ $time -lt $(( $locktime - 120 )) ]; then
                # timestamp is more than 120 secs in the future,
                # something weird has happened -> remove it
                rm $lockfile
                echo_debug "lock" "check_timed_lock.remove_invalid: ${lockfile#${RUNDIR}/}"
            elif [ $time -lt $locktime ]; then
                # timestamp in the future -> we're locked
                echo_debug "lock" "check_timed_lock.locked: $time, $locktime"
                return 0
            else
                # obsolete timestamp -> remove it
                rm $lockfile
                echo_debug "lock" "check_timed_lock.remove_obsolete: ${lockfile#${RUNDIR}/}"
            fi
        fi
    done
    
    echo_debug "lock" "check_timed_lock.not_locked: $time"
    return 1
}

# --- Filesystem

set_laptopmode () { # set kernel laptop mode -- $1: 0=ac mode, 1=battery mode
    check_sysfs "set_laptopmode" "/proc/sys/vm/laptop_mode"
    
    DISK_IDLE_SECS_ON_AC=${DISK_IDLE_SECS_ON_AC:-0}
    DISK_IDLE_SECS_ON_BAT=${DISK_IDLE_SECS_ON_BAT:-2}
    
    if [ "$1" = "1" ]; then
        echo_debug "pm" "set_laptopmode($1): $DISK_IDLE_SECS_ON_BAT"
        echo $DISK_IDLE_SECS_ON_BAT > /proc/sys/vm/laptop_mode
    else
        echo_debug "pm" "set_laptopmode($1): $DISK_IDLE_SECS_ON_AC"
        echo $DISK_IDLE_SECS_ON_AC > /proc/sys/vm/laptop_mode
    fi
    
    return 0
}

set_dirty_parms () { # set filesystem buffer params
    # $1: 0=ac mode, 1=battery mode
    # concept from laptop-mode-tools
    
    local age
    
    check_sysfs "set_dirty_parms" "/proc/sys/vm"
    
    if [ "$1" = "1" ]; then
        age=$((${MAX_LOST_WORK_SECS_ON_BAT:-15} * 100))
    else
        age=$((${MAX_LOST_WORK_SECS_ON_AC:-5} * 100))
    fi
            
    echo_debug "pm" "set_dirty_parms($1): $age"
        
    echo $age > /proc/sys/vm/dirty_writeback_centisecs
    echo $age > /proc/sys/vm/dirty_expire_centisecs
        
    if [ -d /proc/sys/fs/xfs ]; then
        echo $age > /proc/sys/fs/xfs/age_buffer_centisecs
        echo $age > /proc/sys/fs/xfs/xfssyncd_centisecs
        echo 3000 > /proc/sys/fs/xfs/xfsbufd_centisecs
    fi

    echo 60 > /proc/sys/vm/dirty_ratio
    echo 1 > /proc/sys/vm/dirty_background_ratio
    
    return 0
}

# --- CPU

set_scaling_governor () { # set scaling governor -- $1: 0=ac mode, 1=battery mode
    local gov cpu

    if [ "$1" = "1" ]; then
        gov=$CPU_SCALING_GOVERNOR_ON_BAT
    else
        gov=$CPU_SCALING_GOVERNOR_ON_AC
    fi

    if [ -n "$gov" ]; then
        echo_debug "pm" "set_scaling_governor($1): $gov"
        for cpu in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do
            [ -f $cpu ] && echo $gov > $cpu 2> /dev/null
        done
    fi
    
    return 0
}

set_scaling_min_max_freq () { # set scaling limits -- $1: 0=ac mode, 1=battery mode
    local minfreq maxfreq cpu

    if [ "$1" = "1" ]; then
        minfreq=$CPU_SCALING_MIN_FREQ_ON_BAT
        maxfreq=$CPU_SCALING_MAX_FREQ_ON_BAT
    else
        minfreq=$CPU_SCALING_MIN_FREQ_ON_AC
        maxfreq=$CPU_SCALING_MAX_FREQ_ON_AC
    fi

    if [ -n "$minfreq" ]; then
        echo_debug "pm" "set_scaling_min_max_freq($1).min: $minfreq"
        for cpu in /sys/devices/system/cpu/cpu*/cpufreq/scaling_min_freq; do
            [ -f $cpu ] && echo $minfreq > $cpu 2> /dev/null
        done
    fi
    
    if [ -n "$maxfreq" ]; then
        echo_debug "pm" "set_scaling_min_max_freq($1).max: $maxfreq"
        for cpu in /sys/devices/system/cpu/cpu*/cpufreq/scaling_max_freq; do
            [ -f $cpu ] && echo $maxfreq > $cpu 2> /dev/null
        done
    fi
    
    return 0
}

set_cpu_boost_all () { # $1: 0=ac mode, 1=battery mode
    # global cpu boost behavior control based on the current power mode
    #
    # Relevant config option(s): CPU_BOOST_ON_{AC,BAT} with values {'',0,1}
    #
    # Note:
    #  * needs commit #615b7300717b9ad5c23d1f391843484fe30f6c12
    #     (linux-2.6 tree), "Add support for disabling dynamic overclocking",
    #    => requires linux 3.7 or later
   
    local val

    if [ "$1" = "1" ]; then
        val="${CPU_BOOST_ON_BAT:-}"
    else
        val="${CPU_BOOST_ON_AC:-}"
    fi

    if [ -n "$val" ]; then
        if [ -f $CPU_BOOST_ALL_CTRL ]; then
            # simple test for attribute "w" doesn't work, so actually write
            if ( echo "$val" > $CPU_BOOST_ALL_CTRL ) 2> /dev/null; then 
                echo_debug "pm" "set_cpu_boost_all($1): $val"
            else
                echo_debug "pm" "set_cpu_boost_all($1).cpu_not_supported"
            fi
        else
            echo_debug "pm" "set_cpu_boost_all($1).not_available"
        fi
    fi
}

set_sched_powersave () { # set multi-core/-thread powersave policy
    # $1: 0=ac mode, 1=battery mode
    
    local pwr pool sdev 
    local avail=0

    if [ "$1" = "1" ]; then
        pwr=${SCHED_POWERSAVE_ON_BAT:-1}
    else
        pwr=${SCHED_POWERSAVE_ON_AC:-0}
    fi
    
    for pool in mc smp smt; do
        sdev="/sys/devices/system/cpu/sched_${pool}_power_savings"
        if [ -f $sdev ]; then
            echo_debug "pm" "set_sched_powersave($1): ${sdev##/*/} $pwr"
            echo $pwr > "$sdev"
            avail=1
        fi
    done
    
    [ "$avail" = "1" ] || echo_debug "pm" "set_sched_powersave($1).not_available"
    
    return 0
}

set_nmi_watchdog () { # enable/disable nmi watchdog
    local nmiwd=${NMI_WATCHDOG:-0}
    
    if [ -f /proc/sys/kernel/nmi_watchdog ]; then
        echo "$nmiwd" > /proc/sys/kernel/nmi_watchdog 2> /dev/null
        if [ $? = 0 ]; then
            echo_debug "pm" "set_nmi_watchdog: $nmiwd"
        else
            echo_debug "pm" "set_nmi_watchdog.disabled_by_kernel: $nmiwd"
        fi
    else
        echo_debug "pm" "set_nmi_watchdog.not_available"
    fi
    
    return 0
}

set_phc_controls () { # set core voltages 
    local control 
    local ctrl_avail="0"
    
    check_sysfs "set_phc_controls" "/sys/devices/system/cpu/cpu0/cpufreq"
    
    PHC_CONTROLS=${PHC_CONTROLS:-}
    
    if [ -n "$PHC_CONTROLS" ]; then 
        for control in /sys/devices/system/cpu/cpu*/cpufreq/phc_controls; do 
            if [ -f $control ]; then
                echo_debug "pm" "set_phc_controls: $control $PHC_CONTROLS"
                echo $PHC_CONTROLS > $control
                ctrl_avail="1"
            fi
        done
        
        [ "$ctrl_avail" = "0" ] && echo_debug "pm" "set_phc_controls.not_available"
    fi
    
    return 0
}

# --- Storage Devices

check_disk_hdparm_cap () { # check if relevant disk device
    # $1: dev; rc: 0=yes/1=no

    if [ -z "$($HDPARM -I /dev/$1 2>&1 | egrep 'Invalid argument|Invalid exchange|missing sense data')" ]; then  
        return 0
    else
        return 1
    fi
}

echo_disk_model () { # print disk model -- $1: dev
    local model
    
    model=$($HDPARM -I /dev/$1 2>&1 | grep 'Model Number' | \
      cut -f2 -d: | sed -r 's/^ *//' )
    echo "$model"
    
    return 0
}

echo_disk_firmware () { # print firmware version --- $1: dev
    local firmware
    
    firmware=$($HDPARM -I /dev/$1 2>&1 | grep 'Firmware Revision' | \
      cut -f2 -d: | sed -r 's/^ *//' )
    echo "$firmware"
    
    return 0
}

get_disk_state () { # get disk power state -- $1: dev; retval: $disk_state
    disk_state=$($HDPARM -C /dev/$1 2>&1 | awk -F ':' '/drive state is/ { gsub(/ /,"",$2); print $2; }')
    [ -z "$disk_state" ] && disk_state="(not available)"
    
    return 0
}

spindown_disk () { # stop spindle motor -- $1: dev
    $HDPARM -y /dev/$1 2>&1 > /dev/null
    
    return 0
}

get_disk_apm_level () { # get disk apm level -- $1: dev; rc: apm
    local apm
    
    apm=$($HDPARM -I /dev/$1 2>&1 | grep 'Advanced power management level' | \
          cut -f2 -d: | egrep "^ *[0-9]+ *$")
    if [ -n "$apm" ]; then
        return $apm
    else 
        return 0
    fi
    
}

get_disk_trim_capability () { # check for trim capability
    # $1: dev; rc: 0=no, 1=yes, 254=no ssd device
    
    local trim

    if [ -n "$($HDPARM -I /dev/$1 2>&1 | grep 'Solid State Device')" ]; then
        if [ -n "$($HDPARM -I /dev/$1 2>&1 | grep 'TRIM supported')" ]; then
            trim=1
        else
            trim=0
        fi
    else
        trim=255
    fi
    
    return $trim
}

get_disk_dev () { # translate disk id to device (sdX)
    # $1: id or dev; retval: $disk_dev, $disk_id
    
    if [ -L /dev/disk/by-id/$1 ]; then
        # $1 is disk id
        disk_id=$1
        disk_dev=$(echo $disk_id | sed -r 's/-part[1-9]+$//')
        disk_dev=$(readlink /dev/disk/by-id/$disk_dev)
        disk_dev=${disk_dev##*/}
    else
        # $1 is disk dev
        disk_dev=$1
        disk_id=""
    fi
    # strip partition number
    disk_dev=$(echo $disk_dev | sed -r 's/[1-9]+$//')
}

set_disk_apm_level () { # set disk apm level
    # $1: 0=ac mode, 1=battery mode
    
    local dev apm apmlist
    
    DISK_DEVICES=${DISK_DEVICES:=sda}
    if [ $1 = "1" ]; then
        apmlist=$DISK_APM_LEVEL_ON_BAT
    else
        apmlist=$DISK_APM_LEVEL_ON_AC
    fi
    
    # strip excess blanks from list
    apmlist=$(echo -n $apmlist | sed -r 's/ +/ /g; s/^ | $//g') 
    
    [ -z "$apmlist" ] && return 0 
    
    for dev in $DISK_DEVICES; do
        get_disk_dev $dev
        
        if [ -b /dev/$disk_dev ]; then
            check_disk_hdparm_cap $disk_dev
            if [ $? = 0 ]; then
                # parse list
                apm=$(echo -n $apmlist | sed -r 's/^(\w+) .*/\1/')
                apmlist=$(echo -n $apmlist | sed -r 's/^\w+ (.*)/\1/')
                
                echo_debug "pm" "set_disk_apm_level($1): $disk_dev [$disk_id] $apm"
                $HDPARM -B $apm /dev/$disk_dev > /dev/null 2>&1
            fi
        fi
    done
    
    return 0
} 

set_disk_spindown_timeout () { # set disk spindown timeout
    # $1: 0=ac mode, 1=battery mode
    
    local dev timeout timeoutlist
    
    DISK_DEVICES=${DISK_DEVICES:=sda}
    if [ $1 = "1" ]; then
        timeoutlist=$DISK_SPINDOWN_TIMEOUT_ON_BAT
    else
        timeoutlist=$DISK_SPINDOWN_TIMEOUT_ON_AC
    fi
    
    # strip excess blanks from list
    timeoutlist=$(echo -n $timeoutlist | sed -r 's/ +/ /g; s/^ | $//g') 
    
    [ -z "$timeoutlist" ] && return 0 
    
    for dev in $DISK_DEVICES; do
        get_disk_dev $dev
        
        if [ -b /dev/$disk_dev ]; then
            check_disk_hdparm_cap $disk_dev
            if [ $? = 0 ]; then
                # parse list
                timeout=$(echo -n $timeoutlist | sed -r 's/^(\w+) .*/\1/')
                timeoutlist=$(echo -n $timeoutlist | sed -r 's/^\w+ (.*)/\1/')
                
                echo_debug "pm" "set_disk_spindown_timeout($1): $disk_dev [$disk_id] $timeout"
                $HDPARM -S $timeout /dev/$disk_dev > /dev/null 2>&1
            fi
        fi
    done
    
    return 0
} 

set_disk_io_sched () { # set disk io scheduler 
    local dev sched schedlist schedctrl
    
    [ -n "$DISK_IOSCHED" ] || return 0
    
    DISK_DEVICES=${DISK_DEVICES:=sda}
    # strip excess blanks from list
    schedlist=$(echo -n $DISK_IOSCHED | sed -r 's/ +/ /g; s/^ | $//g')
    
    for dev in $DISK_DEVICES; do
        get_disk_dev $dev
        
        if [ -b /dev/$disk_dev ]; then
            # parse list, add cfq as default when list is too short
            sched=$(echo -n $schedlist | sed -r 's/^(\w+) .*/\1/')
            schedlist=$(echo -n "$schedlist cfq"| sed -r 's/^\w+ (.*)/\1/')
            schedctrl="/sys/block/$disk_dev/queue/scheduler"
            
            if [ -f $schedctrl ]; then
                echo_debug "pm" "set_disk_io_sched: $disk_dev [$disk_id] $sched"
                echo -n $sched > $schedctrl
            fi
        fi
    done
    
    return 0

}

# --- Device Power Management 

set_sata_link_power () { # set sata link power management
    # $1: 0=ac mode, 1=battery mode
    
    local i pwr
    local ctrl_avail="0"
    
    if [ "$1" = "1" ]; then
        pwr=${SATA_LINKPWR_ON_BAT:-min_power}
    else
        pwr=${SATA_LINKPWR_ON_AC:-max_perfomance}
    fi
    echo_debug "pm" "set_sata_link_power($1): $pwr"
    
    for i in /sys/class/scsi_host/host*/link_power_management_policy ; do
        if [ -f $i ]; then
            echo "$pwr" > $i
            ctrl_avail="1"
        fi
    done
    
    [ "$ctrl_avail" = "0" ] && echo_debug "pm" "set_sata_link_power($1).not_available"
    
    return 0
} 

set_pcie_aspm () { # set pcie active state power management
    # $1: 0=ac mode, 1=battery mode
    
    local pwr
    
    if [ "$1" = "1" ]; then
        pwr=${PCIE_ASPM_ON_BAT:-powersave}
    else
        pwr=${PCIE_ASPM_ON_AC:-performance}
    fi
    
    if [ -f /sys/module/pcie_aspm/parameters/policy ]; then
        echo "$pwr" > /sys/module/pcie_aspm/parameters/policy 2> /dev/null
        if [ $? = 0 ]; then
            echo_debug "pm" "set_pcie_aspm($1): $pwr"
        else
            echo_debug "pm" "set_pcie_aspm($1).disabled_by_kernel"
        fi
    else
        echo_debug "pm" "set_pcie_aspm($1).not_available"
    fi

    return 0
}

set_radeon_profile () { # set radeon power profile
    # $1: 0=ac mode, 1=battery mode
    
    local pwr card
    
    if [ "$1" = "1" ]; then
        pwr=${RADEON_POWER_PROFILE_ON_BAT:-low}
    else
        pwr=${RADEON_POWER_PROFILE_ON_AC:-high}
    fi
    
    for card in /sys/class/drm/card[0-9]/device ; do
        if [ -f $card/power_method ] && [ -f $card/power_profile ]; then
            echo_debug "pm" "set_radeon_profile($1): $pwr $card"
            echo "profile" > $card/power_method
            echo "$pwr" > $card/power_profile
            return 0
        fi
    done
    
    echo_debug "pm" "set_radeon_profile($1).not_available"
    return 0    
}


set_sound_power_mode () { # set sound chip power mode
    SOUND_POWER_SAVE=${SOUND_POWER_SAVE:-1}
    SOUND_POWER_SAVE_CONTROLLER=${SOUND_POWER_SAVE_CONTROLLER:-Y}
    
    check_sysfs "set_sound_power_mode" "/sys/module"

    if [ -d /sys/module/snd_hda_intel ]; then
        echo_debug "pm" "set_sound_power_mode.hda: $SOUND_POWER_SAVE $SOUND_POWER_SAVE_CONTROLLER"
        echo "$SOUND_POWER_SAVE" > /sys/module/snd_hda_intel/parameters/power_save
        echo "$SOUND_POWER_SAVE_CONTROLLER" > /sys/module/snd_hda_intel/parameters/power_save_controller 
    fi
        
    if [ -d /sys/module/snd_ac97_codec ]; then
        echo_debug "pm" "set_sound_power_mode.ac97: $SOUND_POWER_SAVE"
        echo "$SOUND_POWER_SAVE" > /sys/module/snd_ac97_codec/parameters/power_save
    fi
    
    return 0
}

set_runtime_pm () { # set runtime power management 
    # $1: 0=ac mode, 1=battery mode
    # concept from Ubuntu Precise's implementation of pm-utils power.d/pci_devices
    
    local type device control doall
    
    if [ "$1" = "1" ]; then
        control=${RUNTIME_PM_ON_BAT:-on}
    else
        control=${RUNTIME_PM_ON_AC:-on}
    fi

    doall=${RUNTIME_PM_ALL:-0}
    
    for type in $PCID; do
        for device in $type/*; do
            if [ -f $device/class ] && [ -f $device/power/control ]; then
                class=$(cat $device/class)
                case $class in
                    0x020000) # ethernet
                        echo $control > $device/power/control;
                        echo_debug "pm" "set_runtime_pm($1).$control: $device [$class Ethernet controller]"
                        ;;
                
                    0x028000) # wireless
                        echo $control > $device/power/control;
                        echo_debug "pm" "set_runtime_pm($1).$control: $device [$class Wireless]"
                        ;;
              
                    0x040300) # audio device
                        echo $control > $device/power/control;
                        echo_debug "pm" "set_runtime_pm($1).$control: $device [$class Audio device]"
                        ;;
				
                    0x060000) # host bridge
                        echo $control > $device/power/control;
                        echo_debug "pm" "set_runtime_pm($1).$control: $device [$class Host Bridge]"
                        ;;
 				
                    0x080500) # SD card reader
                        echo $control > $device/power/control;
                        echo_debug "pm" "set_runtime_pm($1).$control: $device [$class SD Card Reader]"
                        ;;
 				
                    0x088000|0x088001) # card reader
                        echo $control > $device/power/control;
                        echo_debug "pm" "set_runtime_pm($1).$control: $device [$class Card Reader]"
                        ;;
 				
                    0x0c0000|0x0c0010) # firewire
                        echo $control > $device/power/control;
                        echo_debug "pm" "set_runtime_pm($1).$control: $device [$class Firewire]"
                        ;;

                    *) # other
                        if [ "$doall" = "1" ]; then
                            echo $control > $device/power/control;
                            echo_debug "pm" "set_runtime_pm($1).$control: $device [$class]"
                        else
                            echo on > $device/power/control;
                            echo_debug "pm" "set_runtime_pm($1).on: $device [$class]"
                        fi
                        ;;
                esac 
            fi
        done
    done
    
    return 0
}

# --- Wifi Power Management 

get_wifi_ifaces () { # get all wifi devices -- retval: $wifaces
    wifaces=""
    
    for wi in /sys/class/net/*/phy80211; do
        if [ -d $wi ]; then
            wi=${wi%/phy80211}; wi=${wi##*/}
            
            [ -n "$wifaces" ] && wifaces="$wifaces "
            wifaces="$wifaces$wi"
        fi
    done
    
    return 0
}

get_wifi_driver () { # get driver associated with interface -- $1: iface; retval: $wifidrv
    local drvl
    
    wifidrv=""
    if [ -d $NETD/$1 ]; then
        drvl=$(readlink $NETD/$1/device/driver)
        [ -n "$drvl" ] && wifidrv=${drvl##*/}
    fi
    
    return 0
}

set_wifi_power_mode () { # set wifi power save mode -- $1: 0=ac mode, 1=battery mode
    local pwr iface
    local iwrc=1
    
    if [ "$1" = "1" ]; then
        pwr=${WIFI_PWR_ON_BAT:-5}
    else
        pwr=${WIFI_PWR_ON_AC:-0}
    fi
    case $pwr in
        0|1|N)          pwr="off" ;;
        2|3|4|5|6|Y)    pwr="on"  ;;
    esac
    
    get_wifi_ifaces
    
    for iface in $wifaces; do
        if [ -n "$iface" ]; then
            if cmd_exists $IW; then
                $IW dev $iface set power_save $pwr > /dev/null 2>&1
                iwrc=$?
                if [ $iwrc = 0 ]; then
                    echo_debug "pm" "set_wifi_power_mode($1, $iface).iw: $pwr"
                fi
            fi
            if [ $iwrc != 0 ]; then
                $IWC $iface power $pwr > /dev/null 2>&1
                if [ $? = 0 ]; then
                    echo_debug "pm" "set_wifi_power_mode($1, $iface).iwconfig: $pwr"
                else
                    echo_debug "pm" "set_wifi_power_mode($1, $iface).iwconfig.not_supported"
                fi
            fi
        fi
    done
    
    return 0
}

# --- LAN

disable_wake_on_lan () {  # disable WOL
    WOL_DISABLE=${WOL_DISABLE:-N}
    
    if [ "$WOL_DISABLE" = "Y" ]; then
        echo_debug "pm" "disable_wake_on_lan: y"
        $ETHTOOL -s eth0 wol d > /dev/null 2>&1
    fi
    
    return 0
}

# --- USB Autosuspend

set_usb_suspend () { # activate usb autosuspend for all devices except input and blacklisted
    # $1: 0=silent/1=report result; $2: on/auto
    
    local devices usbdev subdev control hid usbid busdev
    local ctrlf="control"
    local autof="autosuspend_delay_ms"
        
    check_sysfs "set_usb_suspend" "$USBD"
    
    if [ "$USB_AUTOSUSPEND" = "1" ]; then
        devices=$(ls $USBD | grep -v ':')
        for usbdev in $devices; do
            if [ -f $USBD/$usbdev/power/autosuspend ] || [ -f $USBD/$usbdev/power/autosuspend_delay_ms ]; then
                usbid="$(cat $USBD/$usbdev/idVendor):$(cat $USBD/$usbdev/idProduct)"
                busdev="Bus $(cat $USBD/$usbdev/busnum) Dev $(cat $USBD/$usbdev/devnum)"
            
                control="$2"
                hid=""

                if [ "$control" != "on" ]; then
                    if patinstr "$usbid" "$USB_BLACKLIST"; then
                        # device is in blacklist
                        control="on"
                    else
                        # check for hid subdevices
                        for subdev in $USBD/$usbdev/*:*; do
                            if [ "$(cat $subdev/bInterfaceClass)" = "03" ]; then
                                control="on"
                                hid="_hid"
                                break
                            fi
                        done
                    fi
                fi
                    
                if [ -f $USBD/$usbdev/power/control ]; then
                    echo "$control" > $USBD/$usbdev/power/control
                else
                    # level is deprecated
                    echo "$control" > $USBD/$usbdev/power/level
                    ctrlf="level"
                fi
                
                if [ -f $USBD/$usbdev/power/autosuspend_delay_ms ]; then
                    echo $USB_TIMEOUT_MS > $USBD/$usbdev/power/autosuspend_delay_ms 2> /dev/null
                    if [ $? != 0 ]; then
                        # openSUSE 11.4/2.6.37: writing to autosuspend_delay_ms fails -> fallback to autosuspend
                        echo_debug "usb" "set_usb_suspend.autosuspend_delay_ms_not_writable: $busdev ID $usbid $USBD/$usbdev"
                        echo $USB_TIMEOUT > $USBD/$usbdev/power/autosuspend
                        autof="autosuspend"
                    fi
                else
                    # autosuspend is deprecated
                    echo $USB_TIMEOUT > $USBD/$usbdev/power/autosuspend
                    autof="autosuspend"
                fi
                echo_debug "usb" "set_usb_suspend.$control$hid: $busdev ID $usbid $USBD/$usbdev [$ctrlf $autof]"
            fi
        done
        [ "$1" = "1" ] && echo "usb autosuspend settings applied."
    else
        [ "$1" = "1" ] && echo "Error: usb autosuspend is disabled. Set USB_AUTOSUSPEND=1 in $DEFAULT_FILE."
    fi
    
    # set "startup completion" flag for tlp-usb-udev
    set_run_flag $USB_DONE
    
    return 0
}

# --- ThinkPad Battery Functions

check_tpsmapi () {  # check if tp_smapi is supported and loaded
    # rc: 0=supported/2=module tp_smapi not loaded/127=not installed
    # retval: $tpsmapi

    if [ -d $SMAPIDIR ]; then
        # module loaded
        tpsmapi=0
    else
        if [ -n "$(modinfo tp_smapi 2> /dev/null)" ]; then
            # module installed but not loaded
            tpsmapi=2
        else
            # module not installed
            tpsmapi=127
        fi
    fi

    echo_debug "bat" "check_tp_smapi: rc=$tpsmapi"
    return $tpsmapi
}

check_tpacpi () { # check if tpacpi-bat is supported
    # rc: 0=supported/2=acpi_call not loaded/4=disabled/
    #     127=acpi_call not installed/255=not supported
    # retval: $tpacpi

    if [ -z "$(modinfo acpi_call 2> /dev/null)" ]; then
        # module not installed
        tpacpi=127
    else
        $TPACPIBAT -g FD 1 > /dev/null 2>&1
        tpacpi=$?

        if [ $tpacpi -eq 0 ] && [ "$DISABLE_TPACPIBAT" = "1" ]; then
            tpacpi=4
        fi
    fi

    echo_debug "bat" "check_tpacpi: rc=$tpacpi"
    return $tpacpi
}

do_threshold () { # $1: start/stop, $2: BAT0/BAT1, $3: new value
    # global param: $tpacpi, $tpsmapi 
    # rc: 0=ok/1=read error/2=thresh not present/255=no thresh api
    
    local bati bsys ts
    local old_thresh=-1
    local new_thresh=$3
    local rc=0

    [ $3 -eq -1 ] && return 0 # -1 = do not set threshold

    if [ $tpacpi -eq 0 ]; then # use tpacpi-bat
        case $2 in
            BAT0) bati=1 ;;
            BAT1) bati=2 ;;
            *) bati=255; rc=2 ;; # invalid bat argument
        esac

        if [ $bati -ne 255 ]; then
            # replace factory default values with 0 for tpacpi
            case $1 in
                start)
                    [ $new_thresh -eq  96 ] && new_thresh=0
                    ts="ST"
                    ;;
                stop)
                    [ $new_thresh -eq 100 ] && new_thresh=0
                    ts="SP"
                    ;;
            esac

            old_thresh=$($TPACPIBAT -g $ts $bati 2> /dev/null | cut -f1 -d' ')

            if [ $new_thresh -ne $old_thresh ]; then
                $TPACPIBAT -s $ts $bati $new_thresh > /dev/null 2>&1
                rc=$?
            fi
        fi
    elif [ $tpsmapi -eq 0 ]; then # use tp_smapi
        bsys=$SMAPIDIR/$2/${1}_charge_thresh
        
        if [ -f $bsys ]; then
            old_thresh=$(cat $bsys 2> /dev/null)
            if [ -z "$old_thresh" ]; then
                rc=1
            elif [ "$old_thresh" -ne "$new_thresh" ]; then
                echo $new_thresh > $bsys 2> /dev/null
                rc=$?
            fi
        else
            rc=2 # invalid bat argument
        fi        
    else
        # no threshold API available
        rc=255
    fi
    
    echo_debug "bat" "do_threshold($1, $2): tpacpi-bat=$tpacpi; tp_smapi=$tpsmapi; old=$old_thresh; new=$new_thresh; rc=$rc"    
    return $rc
}

normalize_thresholds () { # check values and enforce start < stop - 3
    # $1: start threshold; $2: stop_threshold
    # global param: $tpacpi, $tpsmapi
    # rc: 0
    # retval: $start_thresh, $stop_thresh
                          
    local type thresh

    for type in start stop; do
        case $type in
            start) thresh=$1 ;;
            stop)  thresh=$2 ;;
        esac
        
        thresh=$(echo "$thresh" | egrep '^[0-9]{1,3}$') # check for 1..3 digits
        [ -z "$thresh" ] && thresh=-1 # replace empty value with -1

        # ensure min/max values; replace 0 with defaults 96/100
        case $type in 
            start)
                [ $thresh -eq 0 ] || [ $thresh -gt 96 ] && thresh=96
                start_thresh=$thresh
                ;;
                
            stop)
                [ $thresh -eq 0 ] || [ $thresh -gt 100 ] && thresh=100
                [ $thresh -ne -1 ] && [ $thresh -lt 5 ] && thresh=5
                stop_thresh=$thresh
                ;;
        esac
    done

    # enforce start < stop - 3
    if [ $start_thresh -ne -1 ] && [ $stop_thresh -ne -1 ]; then
        [ $start_thresh -ge $(($stop_thresh - 3)) ] && start_thresh=$(($stop_thresh - 4))
    fi

    echo_debug "bat" "normalize_thresholds($1, $2): start=$start_thresh; stop=$stop_thresh"

    return 0
}

bat_exists () { # check battery presence
    # $1: BAT0/BAT1; retval: 0=bat exists/1=bat nonexistent/255=no thresh api available
    # global param: $tpacpi, $tpsmapi
    # rc: 0=present/1=absent/255=no thresh api
    
    local rc

    if [ $tpacpi -eq 0 ]; then # use tpacpi-bat
        [ -d $ACPIBATDIR/$1 ] && [ "$(cat $ACPIBATDIR/$1/present)" = "1" ]
        rc=$?        
    elif [ $tpsmapi -eq 0 ]; then # use tp_smapi
        [ -d $SMAPIDIR/$1 ] && [ "$(cat $SMAPIDIR/$1/installed)" = "1" ]
        rc=$?
    else # no threshold API available
        rc=255
    fi
    
    echo_debug "bat" "bat_exists($1): rc=$rc"
    return $rc
}

set_charge_thresholds () { # write all charge thresholds from configuration
    # global param: $tpacpi, $tpsmapi
    # rc: 0
    
    local rc
    
    check_tpacpi; check_tpsmapi
    echo_debug "bat" "set_charge_thresholds: tpacpi-bat=$tpacpi; tp_smapi=$tpsmapi"
    
    if bat_exists BAT0; then
        normalize_thresholds "$START_CHARGE_THRESH_BAT0" "$STOP_CHARGE_THRESH_BAT0"
        
        if [ $start_thresh -ne -1 ]; then
            do_threshold start BAT0 $start_thresh; rc=$?
            echo_debug "bat" "set_charge_thresholds.start(BAT0): $start_thresh; rc=$rc"
        else
            echo_debug "bat" "set_charge_thresholds.start(BAT0).not_set"
        fi
        
        if [ $stop_thresh -ne -1 ]; then
            do_threshold stop BAT0 $stop_thresh; rc=$?
            echo_debug "bat" "set_charge_thresholds.stop(BAT0): $stop_thresh; rc=$rc"
        else
            echo_debug "bat" "set_charge_thresholds.stop(BAT0).not_set"
        fi
    fi
    
    if bat_exists BAT1; then
        normalize_thresholds "$START_CHARGE_THRESH_BAT1" "$STOP_CHARGE_THRESH_BAT1"
        
        if [ $start_thresh -ne -1 ]; then
            do_threshold start BAT1 $start_thresh; rc=$?
            echo_debug "bat" "set_charge_thresholds.start(BAT1): $start_thresh; rc=$rc"
        else
            echo_debug "bat" "set_charge_thresholds.start(BAT1).not_set"
        fi
        
        if [ $stop_thresh -ne -1 ]; then
            do_threshold stop BAT1 $stop_thresh; rc=$?
            echo_debug "bat" "set_charge_thresholds.stop(BAT1): $stop_thresh; rc=$rc"
        else
            echo_debug "bat" "set_charge_thresholds.stop(BAT1).not_set"
        fi
    fi
    
    return 0
}

do_force_discharge () { # write force discharge state
    # $1: BAT0/BAT1, $2: 0=off/1=on
    # global param: $tpacpi, $tpsmapi
    # rc: 0=done/1=write error/2=discharge not present/255=no thresh api
    
    local bati bsys rc=0

    if [ $tpacpi -eq 0 ]; then # use tpacpi-bat
        case $1 in
            BAT0) bati=1 ;;
            BAT1) bati=2 ;;
        esac

        $TPACPIBAT -s FD $bati $2 > /dev/null 2>&1; rc=$?
        echo_debug "bat" "do_force_discharge.tpacpi-bat($1, $2): rc=$rc"
    elif [ $tpsmapi -eq 0 ]; then # use tp_smapi
        bsys=$SMAPIDIR/$1/force_discharge
 
        if [ -f $bsys ]; then
            echo $2 > $bsys 2> /dev/null; rc=$?
        else 
            rc=2
        fi
        echo_debug "bat" "do_force_discharge.tp_smapi($1, $2): rc=$rc"
    else # no threshold API available
        rc=255
        echo_debug "bat" "do_force_discharge.noapi($1, $2)"
    fi
    
    return $rc
}

get_force_discharge () { # $1: BAT0/BAT1, 
    # global param: $tpacpi, $tpsmapi
    # rc: 0=done/1=read error/2=discharge not present/255=no thresh api
                         
    local bati bsys rc=0

    if [ $tpacpi -eq 0 ]; then # use tpacpi-bat
        case $1 in
            BAT0) bati=1 ;;
            BAT1) bati=2 ;;
        esac

        case $($TPACPIBAT -g FD $bati 2> /dev/null) in
            yes) rc=1 ;;
            no)  rc=0 ;;
            *)   rc=2 ;;
        esac
    elif [ $tpsmapi -eq 0 ]; then # use tp_smapi
        bsys=$SMAPIDIR/$1/force_discharge
 
        if [ -f $bsys ]; then
            rc=$(cat $bsys 2> /dev/null)
        else 
            rc=2
        fi
    else # no threshold API available
        rc=255
    fi
 
    echo_debug "bat" "get_force_discharge($1): rc=$rc"
    return $rc
}

cancel_setcharge () { # called from trap -- global param: $bat
    do_force_discharge $bat 0
    echo_debug "bat" "setcharge_battery.cancelled($bat)"
    echo "Cancelled."
    
    exit 0
}

setcharge_battery () { # write charge thresholds (called from cmd line)
    # $1: start charge threshold, $2: stop charge threshold, $3: battery
    # global param: $tpacpi, $tpsmapi
    # rc: 0=ok/1=error
    
    local rc
    # $bat is global for cancel_setcharge () trap
    local act=0

    # check prerequisites: must be on ac power
    if ! get_power_state ; then
        echo_debug "bat" "setcharge_battery.no_ac_power"
        echo "Error: setting charge thresholds is possible on ac power only."
        return 1
    fi  

    # check prerequisites: tpacpi-bat or tp-smapi 
    check_tpacpi; check_tpsmapi
    echo_debug "bat" "setcharge_battery($1, $2, $3): tpacpi-bat=$tpacpi; tp_smapi=$tpsmapi"

    if [ $tpacpi -ne 0 ] && [ $tpsmapi -ne 0 ]; then
        echo_debug "bat" "setcharge_battery.noapi"
        echo "Error: ThinkPad extended battery functions not available."
        return 1
    fi

    # check params
    if [ $# -gt 0 ]; then 
        # some parameters given, check them
        
        # get battery arg
        bat=${3:-BAT0} # default is BAT0
        bat=$(echo $bat | tr "[:lower:]" "[:upper:]")
        
        # get and check thresholds args
        normalize_thresholds "$1" "$2"
    else 
        # no parameters given, assume BAT0 and take thresholds from config file
        bat=BAT0
        normalize_thresholds "$START_CHARGE_THRESH_BAT0" "$STOP_CHARGE_THRESH_BAT0"
    fi

    # check if selected battery is present
    if ! bat_exists $bat ; then
        echo "Error: battery $bat not present."
        echo_debug "bat" "setcharge_battery.not_present($bat)"
        return 1
    fi
        
    # write threshold values
    echo "Setting temporary charge thresholds for $bat:"
  
    if [ $stop_thresh -ne -1 ]; then
        do_threshold stop $bat $stop_thresh; rc=$?  
        
        echo_debug "bat" "setcharge_battery.stop($bat): $stop_thresh; rc=$rc"
        if [ $rc -eq 0 ]; then
            echo "  stop  = $stop_thresh"
            act=1
        else
            echo "  stop  => Error: cannot set threshold. Aborting."
            return 1
        fi
    else
        echo_debug "bat" "setcharge_battery.stop($bat).not_configured"
        echo "  stop = not configured"
    fi
 
    if [ $start_thresh -ne -1 ]; then
        do_threshold start $bat $start_thresh; rc=$?  

        echo_debug "bat" "setcharge_battery.start($bat): $start_thresh; rc=$rc"
        if [ $rc -eq 0 ]; then
            echo "  start = $start_thresh"
            act=1
        else
            echo "  start => Warning: cannot set threshold."
            return 1
        fi
    else
        echo_debug "bat" "setcharge_battery.start($bat).not_configured"
        echo "  start = not configured"
    fi
    
    # trigger charging via discharge
    if [ $act -eq 1 ]; then
        trap cancel_setcharge INT # enable ^C hook
    
        do_force_discharge $bat 1; rc=$?
        if [ $rc -eq 0 ]; then
            echo -n "Activating..."
            sleep 2
            do_force_discharge $bat 0
            echo_debug "bat" "setcharge_battery.initiated($bat)"
            sleep 2
            echo "done."
        else
            trap INT # remove ^C hook
            
            echo_debug "bat" "setcharge_battery.force_discharge_not_available($bat): rc=$rc"
            echo "Cannot activate immediately on this ThinkPad model. Please power off."
            return 1
        fi

        trap INT  # remove ^C hook
    fi
    
    return 0
}

get_sysval () { # $1: file; rc: sysfile value
    local sysf="$1"
    local val=""

    [ -f $sysf ] && val=$(cat $sysf 2> /dev/null)
    val=$(echo "$val" | egrep '^[0-9]+$') # check for non-numeric chars 
    [ -z "$val" ] && val=0 # empty string
    
    return $val
}

chargeonce_battery () { # charge battery to upper threshold once
    # $1: battery
    # global param: $tpacpi, $tpsmapi
    # rc: 0=ok/1=error

    local bat bdir temp_start_thresh
    local start_thresh=""
    local stop_thresh=""
    local efull=0
    local enow=0
    local ccharge=0

    # check params
    if [ $# -gt 0 ]; then 
        # some parameters given, check them
        
        # get battery arg
        bat=${1:-BAT0} # default is BAT0
        bat=$(echo $bat | tr "[:lower:]" "[:upper:]")
    else 
        # no parameters given, assume BAT0
        bat=BAT0
    fi

    # check prerequisites: tpacpi-bat or tp-smapi 
    check_tpacpi; check_tpsmapi
    echo_debug "bat" "chargeonce_battery($bat): tpacpi-bat=$tpacpi; tp_smapi=$tpsmapi"
    if [ $tpacpi -ne 0 ] && [ $tpsmapi -ne 0 ]; then
        echo_debug "bat" "chargeonce_battery.noapi"
        echo "Error: ThinkPad extended battery functions not available."
        return 1
    fi

    # check if selected battery is present
    if ! bat_exists $bat ; then 
        echo_debug "bat" "chargeonce_battery($bat).not_present"
        echo "Error: battery $bat not present."
        return 1
    fi

    # get and check thresholds from configuration
    case $bat in
        BAT0)
            stop_thresh=$STOP_CHARGE_THRESH_BAT0
            start_thresh=$START_CHARGE_THRESH_BAT0
            ;;
            
        BAT1)
            stop_thresh=$STOP_CHARGE_THRESH_BAT1
            start_thresh=$START_CHARGE_THRESH_BAT1
            ;;
    esac
    [ -z "$stop_thresh" ] && stop_thresh=100
    if [ -z "$start_thresh" ] ; then
        echo_debug "bat" "chargeonce_battery($bat).start_threshold_not_configured"
        echo "Error: no start charge threshold configured for $bat."
        return 1
    fi

    # get current charge level (in %)
    if [ $tpsmapi -eq 0 ]; then
        # use tp-smapi
        bdir="$SMAPIDIR/$bat"
        get_sysval $bdir/remaining_percent; ccharge=$?
    else
        # use ACPI data
        bdir="$ACPIBATDIR/$bat"
        if [ -f $bdir/energy_full ]; then 
            get_sysval $bdir/energy_full; efull=$?
            get_sysval $dir/energy_now; enow=$?
        fi
        
        if [ $efull -ne 0 ]; then
            ccharge=$(( 100 * $enow / $efull ))
        else
            ccharge=-1
        fi
    fi
    
    if [ $ccharge -eq -1 ] ; then
        echo_debug "bat" "chargeonce_battery($bat).charge_level_unknown: enow=$enow; efull=$efull; ccharge=$ccharge"
        echo "Error: cannot determine charge level for $bat."
        return 1
    else
        echo_debug "bat" "chargeonce_battery($bat).charge_level: enow=$enow; efull=$efull; ccharge=$ccharge"
    fi

    temp_start_thresh=$(( $stop_thresh - 4 ))
    if [ $temp_start_thresh -le $ccharge ] ; then
        echo_debug "bat" "chargeonce_battery($bat).charge_level_too_high: $temp_start_thresh $stop_thresh"
        echo "Error: current charge level ($ccharge) of $bat is higher than stop charge threshold - 4 ($temp_start_thresh)."
        return 1
    else
        echo_debug "bat" "chargeonce_battery($bat).setcharge: $temp_start_thresh $stop_thresh"
    fi
    
    setcharge_battery $temp_start_thresh $stop_thresh $bat
    return $?
}

cancel_discharge () { # called from trap -- global param: $bat
    do_force_discharge $bat 0
    echo_debug "bat" "discharge_battery.cancelled($bat)"
    echo
    echo "Discharging of battery $bat cancelled."
    
    exit 0
}

discharge_battery () { # discharge battery 
    # $1: battery
    # global param: $tpacpi, $tpsmapi
    # rc: 0=ok/1=error
    
    local bdir ef pn rc 
    # $bat is global for cancel_setcharge () trap
    
    # check prerequisites: must be on ac power
    if ! get_power_state ; then
        echo_debug "bat" "discharge_battery.no_ac_power"
        echo "Error: discharge is possible on ac power only."
        return 1
    fi

    # check prerequisites: tpacpi-bat or tp-smapi 
    check_tpacpi; check_tpsmapi
    echo_debug "bat" "discharge_battery($1, $2, $3): tpacpi-bat=$tpacpi; tp_smapi=$tpsmapi"

    if [ $tpacpi -ne 0 ] && [ $tpsmapi -ne 0 ]; then
        echo_debug "bat" "setcharge_battery.noapi"
        echo "Error: ThinkPad extended battery functions not available."
        return 1
    fi

    # check params
    bat=$1
    bat=${bat:=BAT0} # default is BAT0
    bat=$(echo $bat | tr "[:lower:]" "[:upper:]")
 
    # check if selected battery is present
    if ! bat_exists $bat ; then
        echo_debug "bat" "discharge_battery.not_present($bat)"
        echo "Error: battery $bat not present."
        return 1
    fi

    # start discharge
    do_force_discharge $bat 1; rc=$?
    if [ $rc -ne 0 ]; then
        echo_debug "bat" "discharge_battery.force_discharge_not_available($bat)"
        echo "Error: discharge function not available for this ThinkPad model."
        return 1
    fi
   
    trap cancel_discharge INT # enable ^C hook

    # wait for start
    while get_force_discharge $bat; do :; done
    echo_debug "bat" "discharge_battery.running($bat)"

    # wait for completion
    while ! get_force_discharge $bat; do
        clear
        echo "Currently discharging battery $bat:"

        # show current battery state
        if [ $tpsmapi -eq 0 ]; then # use tp_smapi
            bdir=$SMAPIDIR/$bat
            
            printf "voltage            = %6s [mv]\n"  $(cat $bdir/voltage)
            printf "remaining capacity = %6s [mWh]\n" $(cat $bdir/remaining_capacity)
            printf "remaining percent  = %6s [%%]\n"  $(cat $bdir/remaining_percent)
            printf "remaining time     = %6s [min]\n" $(cat $bdir/remaining_running_time_now)
            printf "power              = %6s [mW]\n"  $(cat $bdir/power_avg)
        else # use acpi
            bdir=$ACPIBATDIR/$bat

            if [ -d $bdir ]; then
                printf "voltage            = %6s [mV]\n"  $(cat $bdir/voltage_now | sed 's/\(.*\).../\1/')
                printf "remaining capacity = %6s [mWh]\n" $(cat $bdir/energy_now | sed 's/\(.*\).../\1/')
                
                ef=$(cat $bdir/energy_full)
                if [ "$ef" != "0" ]; then
                    printf "remaining percent  = %6s [%%]\n" \
                        $(( 100 * $(cat $bdir/energy_now) / $(cat $bdir/energy_full) ))
                else
                    printf "remaining percent  = not available [%]\n"
                fi
    
                pn=$(cat $bdir/power_now)
                if [ "$pn" != "0" ]; then
                    printf "remaining time     = %6s [min]\n" \
                        $(( 60 * $(cat $bdir/energy_now) / $(cat $bdir/power_now) ))
                    printf "power              = %6s [mW]\n" $(( $pn / 1000 ))
                else
                    printf "remaining time     = not discharging [min]\n"
                fi
                    
            fi 
        fi
        echo "Press Ctrl+C to cancel."
        sleep 2
    done

    trap INT # remove ^C hook

    echo
    echo "Done: battery $bat was completely discharged."
    echo_debug "bat" "discharge_battery.complete($bat)"
    return 0
}

# --- Drive Bay

poweroff_drivebay () { # power off optical drive in drive bay
    # $1: 0=conditional+quiet mode, 1=force+verbose mode
    # Some code adapted from http://www.thinkwiki.org/wiki/How_to_hotswap_UltraBay_devices
    
    local dock optdrv syspath
    
    # Run only if either explicitly enabled or forced 
    [ "$BAY_POWEROFF_ON_BAT" = "1" ] || [ "$1" = "1" ] || return 0

    # Find generic dock interface for bay
    dock=$(grep -l ata_bay /sys/devices/platform/dock.?/type)
    dock=${dock%%/type}
    if [ -z "$dock" ] || [ ! -d "$dock" ]; then
        echo_debug "pm" "poweroff_drivebay.no_bay_device"
        [ "$1" = "1" ] && echo "Error: cannot locate bay device."
        return 1
    fi
    echo_debug "pm" "poweroff_drivebay: dock=$dock"

    # Check if bay is occupied
    if [ $(cat $dock/docked) = "0" ]; then
        echo_debug "pm" "poweroff_drivebay.drive_already_off"
        [ "$1" = "1" ] && echo "No drive in bay (or power already off)."
    else 
        # Check for optical drive
        optdrv=/dev/${BAY_DEVICE:=sr0}
        if [ ! -b "$optdrv" ]; then
            echo_debug "pm" "poweroff_drivebay.no_opt_drive: $optdrv"
            [ "$1" = "1" ] && echo "No optical drive in bay ($optdrv)."
            return 0
        else
            echo_debug "pm" "poweroff_drivebay: optdrv=$optdrv"
            
            # Unmount media 
            umount -l $optdrv > /dev/null 2>&1
            
            # Power off drive
            $HDPARM -Y $optdrv > /dev/null 2>&1
            sleep 1
    
            # Unregister scsi device
            syspath="/sys$($UDEVADM info --query=path --name=$optdrv | perl -pe 's!/block/...$!!')"
            echo_debug "pm" "poweroff_drivebay: syspath=$syspath"
            [ -n "$syspath" ] && echo 1 > $syspath/delete

            # Turn power off
            echo 1 > $dock/undock
            [ "$1" = "1" ] && echo "Bay is powered off now."
            echo_debug "pm" "poweroff_drivebay.bay_powered_off"
        fi
    fi
    
    return 0
}
