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

# ----------------------------------------------------------------------------
# Constants
readonly TLPVER=0.3.6

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 MODPRO=modprobe
readonly LOGGER=logger
readonly UDEVADM=udevadm
readonly LAPMODE=laptop_mode
readonly NMCLI=nmcli
readonly NMTOOL=nm-tool
readonly IP=ip

readonly SMAPIDIR=/sys/devices/platform/smapi
readonly NETD=/sys/class/net
readonly PCID=/sys/bus/pci/devices
readonly I915D=/sys/module/i915/parameters

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"


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

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

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

check_sysfs ()  { # $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
}

check_root () { 
    if [ "$(id -u)" != "0" ]; then
        echo "Error: missing root privilege." 1>&2
        exit 1
    fi
}

check_tlp_enabled () { # ret: 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 () { # ret: 0=not installed, 1=installed
    local lmt=$(which $LAPMODE 2> /dev/null)
    
    if [ -n "$lmt" ] && [ -x "$lmt" ]; 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
}

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

load_tp_modules () {
    local mod
    
    for mod in $TPMODULES; do
        $MODPRO $mod > /dev/null 2>&1 
    done
    return 0
}

get_power_state () { # ret: 0=ac, 1=battery
    $ACPWR
    return $? 
}

echo_started_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
}

set_run_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 $?
    else
        # mkdir failed
        echo_debug "lock" "set_run_flag.mkdir_failed"
        return 2
    fi
}

reset_run_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 () { # $1: flag name
    local rc
    
    [ -f $RUNDIR/$1 ]
    rc=$?
    echo_debug "lock" "check_run_flag: $1; rc=$rc"
    
    return $rc
}

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

check_timed_lock () { # $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 ]; 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
}

set_laptopmode () { # $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 () { # $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
}

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
            echo $gov > $cpu 2> /dev/null
        done
    fi
    
    return 0
}

set_scaling_min_max_freq () { # $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
            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
            echo $maxfreq > $cpu 2> /dev/null
        done
    fi
    
    return 0
}

set_sched_powersave () { # $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"
        [ -w "$sdev" ] || continue
        
        echo_debug "pm" "set_sched_powersave($1): ${sdev##/*/} $pwr"
        echo $pwr > "$sdev"
        avail=1
    done
    
    [ "$avail" = "1" ] || echo_debug "pm" "set_sched_powersave($1).not_available"
    
    return 0
}

set_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 () { 
    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
}

check_disk_hdparm_cap () { # $1: dev; ret: 0=none/1=available
    if [ -z "$($HDPARM -I /dev/$1 2>&1 | grep 'Invalid exchange')" ]; then
        return 0
    else
        return 1
    fi
}

echo_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 () { # $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_apm_level () { # $1: dev; ret: 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_dev () { # $1: id or dev; ret: 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 () { # $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 () { # $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 () {
    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
        
        # 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
    done
    
    return 0

}

set_sata_link_power () { # $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 () { # $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_i915_power_mgmt () { #$1: 0=ac mode, 1=battery mode
    local rc6 fbc
    
    if [ "$1" = "1" ]; then
        rc6=${I915_ENABLE_RC6_ON_BAT:-}
        fbc=${I915_ENABLE_FBC_ON_BAT:-}
    else
        rc6=${I915_ENABLE_RC6_ON_AC:-}
        fbc=${I915_ENABLE_FBC_ON_AC:-}
    fi

    if [ -n "$rc6" ]; then 
        if [ -f $I915D/i915_enable_rc6 ]; then 
            echo_debug "pm" "set_i915_power_mgmt($1).rc6: $rc6"
            echo "$rc6" > $I915D/i915_enable_rc6
        else
            echo_debug "pm" "set_i915_power_mgmt($1).rc6.not_available"
        fi  
    fi
    
    if [ -n "$fbc" ]; then 
        if [ -f $I915D/i915_enable_fbc ]; then 
            echo_debug "pm" "set_i915_power_mgmt($1).fbc: $fbc"
            echo "$fbc" > $I915D/i915_enable_fbc
        else
            echo_debug "pm" "set_i915_power_mgmt($1).fbc.not_available"
        fi
    fi

    return 0    
}

set_radeon_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    
}

get_wifi_ifaces () { # retval: $WIFACES
    WIFACES=$($IWC 2> /dev/null | grep 'IEEE' | sed -r 's/^(.*)[ \t]+IEEE.*/\1/')
    
    return 0
}

get_wifi_driver () { # $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 () { # $1: 0=ac mode, 1=battery mode; $2 iface
    local pwr iface
    
    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
    
    if [ -n "$2" ]; then
        echo_debug "pm" "set_wifi_power_mode($1, $2): $pwr"     
        $IWC $2 power $pwr > /dev/null 2>&1 
    else
        get_wifi_ifaces
        
        for iface in $WIFACES; do
            echo_debug "pm" "set_wifi_power_mode($1, $iface): $pwr"
            $IWC $iface power $pwr > /dev/null 2>&1 
        done
    fi
    
    return 0
}

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

set_sound_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 () { # $1: 0=ac mode, 1=battery mode
    # Concept from Ubuntu Precise's implementation of pm-utils power.d/pci_devices
    local type device control
    
    if [ "$1" = "1" ]; then
        control=${RUNTIME_PM_ON_BAT:-on}
    else
        control=${RUNTIME_PM_ON_AC:-on}
    fi
    
    for type in $PCID; do
        for device in $type/*; do
            if [ -f $device/class ] && [ -f $device/power/control ]; then
                case $(cat $device/class) in
                    0x020000) # ethernet
                        echo $control > $device/power/control;
                        echo_debug "pm" "set_runtime_pm($1).$control: $device [Ethernet]"
                        ;;
                
                    0x028000) # wireless
                        echo $control > $device/power/control;
                        echo_debug "pm" "set_runtime_pm($1).$control: $device [Wireless]"
                        ;;
              
                    0x040300) # audio
                        echo $control > $device/power/control;
                        echo_debug "pm" "set_runtime_pm($1).$control: $device [Audio]"
                        ;;
				
                    0x060000) # host bridge
                        echo $control > $device/power/control;
                        echo_debug "pm" "set_runtime_pm($1).$control: $device [Host Bridge]"
                        ;;
 				
                    0x080500) # SD card reader
                        echo $control > $device/power/control;
                        echo_debug "pm" "set_runtime_pm($1).$control: $device [SD Card Reader]"
                        ;;
 				
                    0x088000|0x088001) # card reader
                        echo $control > $device/power/control;
                        echo_debug "pm" "set_runtime_pm($1).$control: $device [Card Reader]"
                        ;;
 				
                    0x0c0000|0x0c0010) # firewire
                        echo $control > $device/power/control;
                        echo_debug "pm" "set_runtime_pm($1).$control: $device [Firewire]"
                        ;;
                esac 
            fi
        done
    done
    
    return 0
}

enable_usb_suspend () { # $1: 0=silent/1=report result
    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="auto"
                hid=""
                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
                
                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" "enable_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" "enable_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
}

do_threshold () { # $1: threshold sysfile, $2: new value
    local old_thresh rc=0
    
    if [ -f $1 ]; then
        old_thresh=$(cat $1 2> /dev/null)
        if [ -z "$old_thresh" ]; then
            rc=1
        elif [ "$old_thresh" != "$2" ]; then
            echo $2 > $1 2> /dev/null; rc=$?
        fi
    else
        rc=2
    fi
    
    return $rc
}

check_thresh () { # $1: threshold; ret: 0=invalid/1..100=valid
    local thresh
    
    thresh=$(echo $1 | egrep '^[0-9]{1,3}$') # check for 1..3 digits
    [ -z "$thresh" ] && thresh=0 # replace empty value with 0
    [ $thresh -gt 100 ] && thresh=0 # replace values > 100 with 0
    
    return $thresh
}

set_charge_thresholds () {
    local thresh rc
    
    echo_debug "pm" "set_charge_thresholds.enter"
    [ -d $SMAPIDIR ] || return 0
    echo_debug "pm" "set_charge_thresholds.tp-smapi_present"
    
    if [ -d $SMAPIDIR/BAT0 ] && [ "$(cat $SMAPIDIR/BAT0/installed)" = "1" ]; then
        check_thresh "$START_CHARGE_THRESH_BAT0"; thresh=$?
        if [ $thresh -gt 0 ]; then
            do_threshold $SMAPIDIR/BAT0/start_charge_thresh $thresh; rc=$?
            echo_debug "pm" "set_charge_thresholds.start(BAT0): $thresh; rc=$rc"
        else
            echo_debug "pm" "set_charge_thresholds.start(BAT0).not_set"
        fi
        
        check_thresh "$STOP_CHARGE_THRESH_BAT0"; thresh=$?
        if [ $thresh -gt 0 ]; then
            do_threshold $SMAPIDIR/BAT0/stop_charge_thresh $thresh; rc=$?
            echo_debug "pm" "set_charge_thresholds.stop(BAT0): $thresh; rc=$rc"
        else
            echo_debug "pm" "set_charge_thresholds.stop(BAT0).not_set"
        fi
    fi
    
    if [ -d $SMAPIDIR/BAT1 ] &&  [ "$(cat $SMAPIDIR/BAT1/installed)" = "1" ]; then
        check_thresh "$START_CHARGE_THRESH_BAT1"; thresh=$?
        if [ $thresh -gt 0 ]; then
            do_threshold $SMAPIDIR/BAT1/start_charge_thresh $thresh; rc=$?
            echo_debug "pm" "set_charge_thresholds.start(BAT1): $thresh; rc=$rc"
         else
            echo_debug "pm" "set_charge_thresholds.start(BAT1).not_set"
       fi
        
        check_thresh "$STOP_CHARGE_THRESH_BAT1"; thresh=$?
        if [ $thresh -gt 0 ]; then
            do_threshold $SMAPIDIR/BAT1/stop_charge_thresh $thresh; rc=$?
            echo_debug "pm" "set_charge_thresholds.stop(BAT1): $thresh; rc=$rc"
        else
            echo_debug "pm" "set_charge_thresholds.stop(BAT1).not_set"
        fi
    fi
    
    return 0
}

do_force_discharge () { # $1: bat sysfile; $2: 0=off/1=on
    local rc
    
    if [ -f $1 ]; then
        echo $2 > $1 2> /dev/null; rc=$?
    else 
        rc=2
    fi
    
    return $rc
}

cancel_setcharge () { # called from trap
    do_force_discharge $bdir/force_discharge 0
    echo_debug "pm" "setcharge_battery.cancelled($bat)"
    echo "Cancelled."
    
    exit 0
}

setcharge_battery () { # $1: start charge threshold, $2: stop charge threshold, $3: battery, 
    local bat bdir start_thresh stop_thresh rc

    # check prerequisites: tp-smapi 
    if [ ! -d $SMAPIDIR ]; then
        echo_debug "pm" "fullcharge_battery.nosmapi"
        echo "Error: ThinkPad battery functions not available (missing tp_smapi kernel module)."
        return 1
    fi

    # check prerequisites: must be on ac power
    if ! get_power_state ; then
        echo_debug "pm" "setcharge_battery.no_ac_power"
        echo "Error: setting charge thresholds is possible on ac power only."
        return 1
    fi  
    
    if [ $# -gt 0 ]; then 
        # some parameters given, check them
        
        # get battery arg, check if selected battery is present
        bat=${3:-BAT0} # default is BAT0
        bat=$(echo $bat | tr "[:lower:]" "[:upper:]")
        bdir=$SMAPIDIR/$bat
        
        if [ ! -f $bdir/installed ] || [ "$(cat $bdir/installed)" = "0" ]; then
            echo "Error: battery $bat not present."
            echo_debug "pm" "setcharge_battery.not_present($bat)"
            return 1
        fi
        
        # get thresholds args
        start_thresh=$1
        stop_thresh=${2:-0}
               
    else 
        # no parameters given, assume BAT0 and take thresholds from config file
        bat=BAT0
        bdir=$SMAPIDIR/$bat
        start_thresh=${START_CHARGE_THRESH_BAT0:-0}
        stop_thresh=${STOP_CHARGE_THRESH_BAT0:-0}
        
    fi
 
    # check thresholds 
    check_thresh $start_thresh; start_thresh=$?
    check_thresh $stop_thresh; stop_thresh=$?
  
    # check if thresholds != 0    
    if [ $start_thresh -eq 0 ] && [ $stop_thresh -eq 0 ]; then
        echo "Error: no thresholds configured for $bat."        
        echo_debug "pm" "setcharge_battery.no_thresholds_configured($bat)"
        return 1
    fi
    
    # check if stop > start + 3 
    if [ $(($start_thresh + 3)) -ge $stop_thresh ]; then
        echo "Error: invalid threshold values for $bat (start=$start_thresh, stop=$stop_thresh)."        
        echo_debug "pm" "setcharge_battery.invalid_thresh($bat): $start_thresh $stop_thresh"
        return 1
    fi

    # write threshold values
    echo "Setting temporary charge thresholds for $bat:"
    
    do_threshold $bdir/stop_charge_thresh "$stop_thresh"; rc=$?
    echo_debug "pm" "setcharge_battery.stop($bat): $stop_thresh; rc=$rc"
    if [ $rc -eq 0 ]; then
        echo "  stop  = $stop_thresh"
    else
        echo "  stop  => Error: cannot set threshold. Aborting."
        return 1
    fi

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

cancel_discharge () { # called from trap
    do_force_discharge $bdir/force_discharge 0
    echo_debug "pm" "discharge_battery.cancelled($bat)"
    echo
    echo "Discharging of battery $bat cancelled."
    
    exit 0
}

discharge_battery () { # $1: battery
    local bat bdir rc
    
    if [ ! -d $SMAPIDIR ]; then
        echo_debug "pm" "discharge_battery.no_tp-smapi($bat)"
        echo "Error: ThinkPad battery functions not available (missing tp_smapi kernel module)."
        return 1
    fi
        
    if ! get_power_state ; then
        echo_debug "pm" "discharge_battery.no_ac_power"
        echo "Error: discharge is possible on ac power only."
        return 1
    fi  

    bat=$1
    bat=${bat:=BAT0} # default is BAT0
    bat=$(echo $bat | tr "[:lower:]" "[:upper:]")
    bdir=$SMAPIDIR/$bat
    
    if [ ! -f $bdir/installed ] || [ "$(cat $bdir/installed)" = "0" ]; then
        echo_debug "pm" "discharge_battery.not_present($bat)"
        echo "Error: battery $bat not present."
        return 1
    fi
        
    do_force_discharge $bdir/force_discharge 1; rc=$?
    if [ $rc -ne 0 ]; then
        echo_debug "pm" "discharge_battery.force_discharge_not_available($bat)"
        echo "Error: discharge function not available for this ThinkPad model."
        return 1
    fi
   
    trap cancel_discharge INT
    
    while [ "$(cat $bdir/force_discharge)" = "0" ]; do :; done
    echo_debug "pm" "discharge_battery.running($bat)"

    while [ "$(cat $bdir/force_discharge)" = "1" ]; do
        clear
        echo "Currently discharging battery $bat:"
        echo "voltage            = $(cat $bdir/voltage) [mV]"
        echo "remaining percent  = $(cat $bdir/remaining_percent) [%]"
        echo "remaining capacity = $(cat $bdir/remaining_capacity) [mWh]"
        echo "remaining time     = $(cat $bdir/remaining_running_time_now) [min]"
        echo "Press Ctrl+C to cancel."
        sleep 2
    done

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

poweroff_drivebay () { # $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
}

