#!/bin/sh
# tlp - power management functions
#
# Copyright (c) 2016 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.9"

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

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 NMD=NetworkManager
readonly NMTOOL=nm-tool
readonly ENERGYPERF=x86_energy_perf_policy
readonly DBUSSEND=dbus-send
readonly SYSTEMD=systemd
readonly SYSTEMCTL=systemctl
readonly INITCTL=initctl
readonly FLOCK=flock

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

readonly TPACPIDIR=/sys/devices/platform/thinkpad_acpi
readonly SMAPIDIR=/sys/devices/platform/smapi
readonly ACPIBATDIR=/sys/class/power_supply

readonly RE_TPSMAPI_ONLY='^(Edge( 13.*)?|G41|R[56][012][eip]?|R[45]00|SL[45]10|T23|T[346][0123][p]?|T[45][01]0[s]?|W[57]0[01]|X[346][012][s]?( Tablet)?|X1[02]0e|X[23]0[01][s]?( Tablet)?|Z6[01][mpt])$'
readonly RE_TPACPI_ONLY='^((Edge )?E[1345][23456][05][s]?|Helix.*|L[45][3456]0|P[75]0|(Edge )?S[245][345][01]*|T[45][3456][01][psu]?|W5[345][01][s]?|X1 Carbon.*|X2[3456]0[s]?( Tablet)?|.*Yoga)$'
readonly RE_TPSMAPI_AND_TPACPI='^(X1|X220[s]?( Tablet)?|T[45]20[s]?|W520)$'
readonly RE_TP_NONE='^(L[45]20|SL[345]00|X121e)$'

readonly NETD=/sys/class/net
readonly BLUETOOTHD=/sys/class/bluetooth
readonly PCID=/sys/bus/pci/devices
readonly PCIDRV=/sys/bus/pci/drivers
readonly I915D=/sys/module/i915/parameters
readonly RADD=/sys/module/radeon
readonly DMID=/sys/class/dmi/id/
readonly CPU_BOOST_ALL_CTRL=/sys/devices/system/cpu/cpufreq/boost
readonly INTEL_PSTATED=/sys/devices/system/cpu/intel_pstate
readonly CPU_MIN_PERF_PCT=$INTEL_PSTATED/min_perf_pct
readonly CPU_MAX_PERF_PCT=$INTEL_PSTATED/max_perf_pct
readonly CPU_TURBO_PSTATE=$INTEL_PSTATED/no_turbo
readonly AHCID=$PCID'/*/ata*'
readonly BLOCKD='/sys/block/sd*'

readonly USBD=/sys/bus/usb/devices
readonly USB_TIMEOUT=2
readonly USB_TIMEOUT_MS=2000
readonly USB_WWAN_VENDORS="0bdb 05c6 1199"
readonly USB_DONE=usb_done

readonly DOCKGLOB="/sys/devices/platform/dock.?"

readonly MOD_MSR="msr"
readonly MOD_TEMP="coretemp"
readonly MOD_TPSMAPI="tp_smapi"
readonly MOD_TPACPI="acpi_call"

readonly PWRRUNFILE=$RUNDIR/last_pwr
readonly LOCKFILE=$RUNDIR/lock
readonly LOCKTIMEOUT=2

readonly RDW_NM_LOCK="rdw_nm"
readonly RDW_DOCK_LOCK="rdw_dock"
readonly RDW_LOCKTIME=2


readonly STATEDIR="/var/lib/tlp"
readonly RFSTATEFILE=$STATEDIR/rfkill-saved
readonly BAYSTATEFILE=$STATEDIR/bay-saved

readonly DISK_NOP_WORDS="_ keep"
readonly DEFAULT_DISK_DEVICES="sda"
readonly DEFAULT_DISK_IO_SCHEDULER="cfq"
readonly DEFAULT_PM_DRIVER_BLACKLIST="radeon nouveau"
readonly DEFAULT_USB_DRIVER_BLACKLIST="usbhid"

# ----------------------------------------------------------------------------
# Control
nodebug=0

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

# --- Tests

wordinlist () { # test if word in list
                # $1: word, $2: whitespace-separated list of words
    local word

    if [ -n "${1-}" ]; then
        for word in ${2-}; do
            [ "${word}" != "${1}" ] || return 0 # exact match
        done
    fi

    return 1 # no match
}

echo_debug () { # write debug msg if tag matches -- $1: tag; $2: msg;
    [ "$nodebug" = "1" ] && return 0

    if wordinlist "$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 wordinlist "sysfs" "$TLP_DEBUG"; then
        if [ ! -e $2 ]; then
            $LOGGER -p debug -t "tlp[$$,$PPID]" "$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 1>&2
        echo "***Warning: laptop-mode-tools detected, this may cause conflicts with TLP." 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
}

check_systemd () { # check if systemd is the active init system (PID 1) and systemctl is installed
                   # rc: 0=yes, 1=no
    [ -d /run/systemd/system ] && cmd_exists $SYSTEMCTL
}

check_upstart () { # check if upstart is active init system (PID 1)
                   # rc: 0=yes, 1=no
    cmd_exists $INITCTL && $INITCTL --version | grep -q upstart
}

check_openrc () { # check if openrc is the active init system (PID 1)
                  # rc: 0=yes, 1=no
    [ -e /run/openrc/softlevel ]
}


# --- PATH
add_sbin2path () { # check if /sbin /usr/sbin in $PATH, otherwise add them
                   # retval: $PATH, $oldpath, $addpath
    local sp

    oldpath="$PATH"
    addpath=""

    for sp in /usr/sbin /sbin; do
        if [ -d $sp ] && [ ! -h $sp ]; then
            # dir exists and is not a symlink
            case ":$PATH:" in
                *":$sp:"*) # $sp already in $PATH
                    ;;

                *) # $sp not in $PATH, add it
                    addpath="$addpath:$sp"
                    ;;
            esac
        fi
    done

    if [ -n "$addpath" ]; then
      export PATH="${PATH}${addpath}"
    fi

    return 0
}

create_rundir () { # make sure $RUNDIR exists
    [ -d $RUNDIR ] || mkdir -p $RUNDIR 2> /dev/null 1>&2
}

# --- Configuration

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

# --- Kernel Modules

load_modules () { # load kernel module(s) -- $*: 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 $*; do
        $MODPRO $mod > /dev/null 2>&1
    done

    return 0
}

# --- DMI

read_dmi () { # read DMI data -- $*: keywords; stdout: dmi strings
    local ds key outr

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

    printf '%s' "$outr"
    return 0
}

# --- ThinkPad

check_thinkpad () { # check for ThinkPad hardware and save model string,
                 # load ThinkPad specific kernel modules
                 # rc: 0=ThinkPad, 1=other hardware
                 # retval: $tpmodel
    local pv

    tpmodel=""

    if [ -d $TPACPIDIR ]; then
        # kernel module thinkpad_acpi is loaded

        # get DMI product string and sanitize it
        pv="$( read_dmi product_version | tr -C -d '[a-zA-Z0-9 ]' )"

        # check DMI product string for occurrence of "ThinkPad"
        if printf '%s' "$pv" | grep -q "ThinkPad"; then
            # it's a real ThinkPad --> save model substring
            tpmodel=${pv#ThinkPad }
        fi
    fi

    if [ -n "$tpmodel" ]; then
        # load tp-smapi when not explicitly unsupported only;
        # prevents kernel error messages
        if ! supports_tpacpi_only && ! supports_no_tp_bat_funcs; then
            load_modules $MOD_TPSMAPI
        fi

        # load acpi-call unconditionally
        load_modules $MOD_TPACPI

        echo_debug "bat" "check_thinkpad: tpmodel=$tpmodel"
        return 0
    fi

    # not a ThinkPad
    echo_debug "bat" "check_thinkpad.not_a_thinkpad"
    return 1
}

is_thinkpad () { # check for ThinkPad by saved model string
                 # rc: 0=ThinkPad, 1=other hardware
    [ -n "$tpmodel" ]
}

# --- Power Source

get_sys_power_supply () { # get current power source
                          # rc: 0=ac, 1=battery, 2=unknown

    local psrc
    local rc=

    for psrc in /sys/class/power_supply/*; do
        # -f $psrc/type not necessary - cat 2>.. handles this
        case "$(cat $psrc/type 2> /dev/null)" in
            Mains)
                # AC detected, check if online
                if [ "$(cat $psrc/online 2> /dev/null)" = "1" ]; then
                    rc=0
                    break
                fi
                # else AC not online => keep $rc as-is
                ;;

            Battery)
                # set rc to battery, but don't stop looking for AC
                rc=1
                ;;

            *)
                echo_debug "pm" "unknown power supply: ${psrc##*/}"
                ;;
        esac
    done

    # set rc to unknown if we haven't seen any AC/battery power source so far
    : ${rc:=2}

    return $rc
}

get_power_state () { # get current power source -- rc: 0=ac, 1=battery
    # similar to get_sys_power_supply(),
    # but maps unknown power source to TLP_DEFAULT_MODE
    local psrc

    get_sys_power_supply
    psrc=$?

    if [ $psrc -eq 2 ]; then
        # unknown power supply, apply default mode
        case "$TLP_DEFAULT_MODE" in
            ac|AC)   psrc=0 ;;
            bat|BAT) psrc=1 ;;
            *)       psrc=0 ;; # use AC if no default mode configured
        esac
    fi

    return $psrc
}

compare_and_save_power_state() { # compare $1 to last saved power state,
    # save $1 afterwards when different
    # $1: new state 0=ac, 1=battery
    # rc: 0=different, 1=equal
    local lp

    # read saved state
    lp=$(cat $PWRRUNFILE 2> /dev/null)

    # compare
    if [ -z "$lp" ] || [ "$lp" != "$1" ]; then
        # saved state is nonexistent/empty or is different --> save new state
        create_rundir
        echo "$1" > $PWRRUNFILE 2> /dev/null

        echo_debug "pm" "compare_and_save_power_state($1).different: old=$lp"
        return 0
    else
        echo_debug "pm" "compare_and_save_power_state($1).equal"
        return 1
    fi
}

clear_saved_power_state() { # remove last saved power state

    rm -f $PWRRUNFILE 2> /dev/null

    return 0
}

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 and Semaphores

set_run_flag () { # set flag -- $1: flag name
                  # rc: 0=success/1,2=failed
    local rc

    create_rundir
    touch $RUNDIR/$1; rc=$?
    echo_debug "lock" "set_run_flag.touch: $1; rc=$rc"

    return $rc
}

reset_run_flag () { # reset flag -- $1: flag name
    if rm $RUNDIR/$1 2> /dev/null 1>&2 ; then
        echo_debug "lock" "reset_run_flag($1).remove"
    else
        echo_debug "lock" "reset_run_flag($1).not_found"
    fi

    return 0
}

check_run_flag () { # check flag -- $1: flag name
                    # rc: 0=flag set/1=flag not set
    local rc

    [ -f $RUNDIR/$1 ]; rc=$?
    echo_debug "lock" "check_run_flag($1): rc=$rc"

    return $rc
}

lock_tlp () { # get exclusive lock: blocking with timeout
              # $1: lock id (default: tlp)
              # rc: 0=success/1=failed

    create_rundir
    # open file for writing and attach fd 9
    exec 9> ${LOCKFILE}_${1:-tlp}
    # lock fd 9 exclusive and blocking, wait $LOCKTIME secs to obtain the lock
    if $FLOCK -x -w $LOCKTIMEOUT 9 ; then
        echo_debug "lock" "lock_tlp.locked"
        return 0
    else
        echo_debug "lock" "lock_tlp.failed"
        return 1
    fi
}

lock_tlp_nb () { # get exclusive lock: non-blocking
                 # $1: lock id (default: tlp)
                 # rc: 0=success/1=failed

    create_rundir
    # open file for writing and attach fd 9
    exec 9> ${LOCKFILE}_${1:-tlp}
    # lock fd 9 exclusive and non-blocking
    if $FLOCK -x -n 9 ; then
        echo_debug "lock" "lock_tlp_nb.locked"
        return 0
    else
        echo_debug "lock" "lock_tlp_nb.failed"
        return 1
    fi
}

unlock_tlp () { # free exclusive lock

    # defer unlock for $X_DEFER_UNLOCK seconds -- debugging only
    [ -n "$X_DEFER_UNLOCK" ] && sleep $X_DEFER_UNLOCK

    # free fd 9
    exec 9>&-
    echo_debug "lock" "unlock_tlp"

    return 0
}

echo_tlp_locked () { # print "locked" message
    echo "TLP is locked by another operation."
    return 0
}

set_timed_lock () { # create timestamp n seconds in the future
    # $1: lock id, $2: lock duration [s]

    local rc
    local lock=${1}_timed_lock_$(date +%s -d "+${2} seconds")

    set_run_flag $lock; rc=$?
    echo_debug "lock" "set_timed_lock($1, $2): $lock; rc=$rc"

    return $rc
}

check_timed_lock () { # check if active timestamp exists
    # $1: lock id; rc: 0=locked/1=not locked

    local lockid=$1
    local lockfile 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($1).remove_invalid: ${lockfile#${RUNDIR}/}"
            elif [ $time -lt $locktime ]; then
                # timestamp in the future -> we're locked
                echo_debug "lock" "check_timed_lock($1).locked: $time, $locktime"
                return 0
            else
                # obsolete timestamp -> remove it
                rm $lockfile
                echo_debug "lock" "check_timed_lock($1).remove_obsolete: ${lockfile#${RUNDIR}/}"
            fi
        fi
    done

    echo_debug "lock" "check_timed_lock($1).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"

    local isec

    if [ "$1" = "1" ]; then
        isec=${DISK_IDLE_SECS_ON_BAT:-}
    else
        isec=${DISK_IDLE_SECS_ON_AC:-}
    fi
    # replace with empty string if non-numeric chars are contained
    isec=$(printf '%s' "$isec" | egrep '^[0-9]+$')

    if [ -z "$isec" ]; then
        # do nothing if unconfigured or non numeric value
        echo_debug "pm" "set_laptopmode($1).not_configured"
        return 0
    fi

    echo_debug "pm" "set_laptopmode($1): $isec"
    printf '%s\n' "$isec" > /proc/sys/vm/laptop_mode

    return 0
}

set_dirty_parms () { # set filesystem buffer params
    # $1: 0=ac mode, 1=battery mode
    # concept from laptop-mode-tools

    local age cage df

    check_sysfs "set_dirty_parms" "/proc/sys/vm"

    if [ "$1" = "1" ]; then
        age=${MAX_LOST_WORK_SECS_ON_BAT:-0}
    else
        age=${MAX_LOST_WORK_SECS_ON_AC:-0}
    fi

    # calc age in centisecs, non numeric values result in "0"
    cage=$(($age * 100))

    if [ "$cage" = "0" ]; then
        # do nothing if unconfigured or invalid age
        echo_debug "pm" "set_dirty_parms($1).not_configured"
        return 0
    fi

    echo_debug "pm" "set_dirty_parms($1): $cage"

    for df in /proc/sys/vm/dirty_writeback_centisecs \
             /proc/sys/vm/dirty_expire_centisecs \
             /proc/sys/fs/xfs/age_buffer_centisecs \
             /proc/sys/fs/xfs/xfssyncd_centisecs; do
        [ -w $df ] && echo $cage > $df
    done

    [ -w /proc/sys/fs/xfs/xfsbufd_centisecs ] \
        && echo 3000 > /proc/sys/fs/xfs/xfsbufd_centisecs

    return 0
}

# --- CPU

check_intel_pstate () { # detect intel_pstate driver -- retval: $intel_pstate
    #  Note: intel_pstate requires Linux 3.9 or higher
    intel_pstate=0

    [ -d $INTEL_PSTATED ] && intel_pstate=1
    return 0
}

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 ] && printf '%s\n' "$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 ] && printf '%s\n' "$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 ] && printf '%s\n' "$maxfreq" > $cpu 2> /dev/null
        done
    fi

    return 0
}

set_cpu_perf_pct () { # set Intel P-state performance
                      # $1: 0=ac mode, 1=battery mode
    local min max

    check_intel_pstate
    if [ "$intel_pstate" != "1" ]; then
        echo_debug "pm" "set_cpu_perf_pct($1).no_intel_pstate"
        return 0
    fi

    if [ "$1" = "1" ]; then
        min="${CPU_MIN_PERF_ON_BAT:-}"
        max="${CPU_MAX_PERF_ON_BAT:-}"
    else
        min="${CPU_MIN_PERF_ON_AC:-}"
        max="${CPU_MAX_PERF_ON_AC:-}"
    fi

    if [ ! -f $CPU_MIN_PERF_PCT ]; then
        echo_debug "pm" "set_cpu_perf_pct($1).min.not_supported"
    elif [ -n "$min" ]; then
        { printf '%s\n' "$min" > $CPU_MIN_PERF_PCT; } 2> /dev/null
        echo_debug "pm" "set_cpu_perf_pct($1).min: $min"
    else
        echo_debug "pm" "set_cpu_perf_pct($1).min.not_configured"
    fi

    if [ ! -f $CPU_MAX_PERF_PCT ]; then
        echo_debug "pm" "set_cpu_perf_pct($1).max.not_supported"
    elif [ -n "$max" ]; then
        { printf '%s\n' "$max" > $CPU_MAX_PERF_PCT; } 2> /dev/null
        echo_debug "pm" "set_cpu_perf_pct($1).max: $max"
    else
        echo_debug "pm" "set_cpu_perf_pct($1).max.not_configured"
    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 ival

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

    if [ -z "$val" ]; then
        # do nothing if unconfigured
        echo_debug "pm" "set_cpu_boost_all($1).not_configured"
        return 0
    fi

    check_intel_pstate

    if [ $intel_pstate -eq 1 ]; then
        # use intel_pstate sysfiles
        if [ -f $CPU_TURBO_PSTATE ]; then
            ival=$(($val ^ 1))
            printf '%s\n' "$ival" > $CPU_TURBO_PSTATE 2> /dev/null
            echo_debug "pm" "set_cpu_boost_all($1).intel_pstate: $val"
        else
            echo_debug "pm" "set_cpu_boost_all($1).intel_pstate.cpu_not_supported"
        fi
    elif [ -f $CPU_BOOST_ALL_CTRL ]; then
        # use acpi_cpufreq sysfiles
        # simple test for attribute "w" doesn't work, so actually write
        if ( printf '%s\n' "$val" > $CPU_BOOST_ALL_CTRL ) 2> /dev/null; then
            echo_debug "pm" "set_cpu_boost_all($1).acpi_cpufreq: $val"
        else
            echo_debug "pm" "set_cpu_boost_all($1).acpi_cpufreq.cpu_not_supported"
        fi
    else
        echo_debug "pm" "set_cpu_boost_all($1).not_available"
    fi

    return 0
}

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:-}
    else
        pwr=${SCHED_POWERSAVE_ON_AC:-}
    fi

    if [ -z "$pwr" ]; then
        # do nothing if unconfigured
        echo_debug "pm" "set_sched_powersave($1).not_configured"
        return 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"
            printf '%s\n' "$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:-}

    if [ -z "$nmiwd" ]; then
        # do nothing if unconfigured
        echo_debug "pm" "set_nmi_watchdog.not_configured"
        return 0
    fi

    if [ -f /proc/sys/kernel/nmi_watchdog ]; then
        printf '%s\n' "$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"

    phc_controls=${PHC_CONTROLS:-}

    if [ -z "$phc_controls" ]; then
        # do nothing if unconfigured
        echo_debug "pm" "set_phc_controls.not_configured"
        return 0
    fi

    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"
            printf '%s\n' "$phc_controls" > $control
            ctrl_avail="1"
        fi
    done

    [ "$ctrl_avail" = "0" ] && echo_debug "pm" "set_phc_controls.not_available"

    return 0
}

set_energy_perf_policy () { # set performance versus energy savings policy
    # $1: 0=ac mode, 1=battery mode

    local perf rc

    if [ "$1" = "1" ]; then
        perf=${ENERGY_PERF_POLICY_ON_BAT:-}
    else
        perf=${ENERGY_PERF_POLICY_ON_AC:-}
    fi

    if [ -z "$perf" ]; then
        echo_debug "pm" "set_energy_perf_policy($1).not_configured"
    elif ! cmd_exists $ENERGYPERF; then
        # x86_energy_perf_policy not installed
        echo_debug "pm" "set_energy_perf_policy($1).not_available"
    else
        # x86_energy_perf_policy needs kernel module 'msr'
        load_modules $MOD_MSR
        $ENERGYPERF $perf > /dev/null 2>&1
        rc=$?
        case $rc in
            0) echo_debug "pm" "set_energy_perf_policy($1): $perf" ;;
            1) echo_debug "pm" "set_energy_perf_policy($1): $perf -- unsupported cpu" ;;
            2) echo_debug "pm" "set_energy_perf_policy($1): $perf -- kernel specific x86_energy_perf_policy missing" ;;
            *) echo_debug "pm" "set_energy_perf_policy($1): $perf -- unknown rc=$rc " ;;
        esac
        return $rc
    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|No such device')" ]; 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/^ *//' )
    printf '%s\n' "$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/^ *//' )
    printf '%s\n' "$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 > /dev/null 2>&1

    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 [ -h /dev/disk/by-id/$1 ]; then
        # $1 is disk id
        disk_id=$1
        disk_dev=$(printf '%s' "$disk_id" | sed -r 's/-part[1-9][0-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=$(printf '%s' "$disk_dev" | sed -r 's/[1-9][0-9]*$//')
}

set_disk_apm_level () { # set disk apm level
    # $1: 0=ac mode, 1=battery mode

    local pwrmode="$1"
    local dev log_message

    # when undefined use default
    : ${DISK_DEVICES:=${DEFAULT_DISK_DEVICES}}

    # set @argv := apmlist (blanks removed - relying on a sane $IFS)
    if [ "$pwrmode" = "1" ]; then
        set -- $DISK_APM_LEVEL_ON_BAT
    else
        set -- $DISK_APM_LEVEL_ON_AC
    fi

    # exit if empty apmlist
    [ $# -gt 0 ] || return 0

    # pairwise iteration DISK_DEVICES[1,n], apmlist[1,m]; m > 0
    #  for j in [1,n]: disk_dev[j], apmlist[min(j,m)]
    #
    for dev in $DISK_DEVICES; do
        : ${1:?BUG: broken DISK_APM_LEVEL list handling}

        get_disk_dev $dev
        log_message="set_disk_apm_level($pwrmode): $disk_dev [$disk_id] $1"

        if [ ! -b /dev/$disk_dev ]; then
            echo_debug "disk" "${log_message} -- missing"
        elif ! check_disk_hdparm_cap $disk_dev; then
            echo_debug "disk" "${log_message} -- not supported"
        elif wordinlist "$1" "$DISK_NOP_WORDS"; then
            echo_debug "disk" "${log_message} -- keep as is"
        else
            echo_debug "disk" "${log_message}"
            $HDPARM -B $1 /dev/$disk_dev > /dev/null 2>&1
        fi

        # last entry in apmlist applies to all remaining disks
        [ $# -lt 2 ] || shift
    done

    return 0
}

set_disk_spindown_timeout () { # set disk spindown timeout
    # $1: 0=ac mode, 1=battery mode

    local pwrmode="$1"
    local dev log_message

    # when undefined use default
    : ${DISK_DEVICES:=${DEFAULT_DISK_DEVICES}}

    # set @argv := timeoutlist
    if [ "$pwrmode" = "1" ]; then
        set -- $DISK_SPINDOWN_TIMEOUT_ON_BAT
    else
        set -- $DISK_SPINDOWN_TIMEOUT_ON_AC
    fi

    # exit if empty timeoutlist
    [ $# -gt 0 ] || return 0

    # pairwise iteration DISK_DEVICES[1,n], timeoutlist[1,m]; m > 0
    #  for j in [1,n]: disk_dev[j], timeoutlist[min(j,m)]
    #
    for dev in $DISK_DEVICES; do
        : ${1:?BUG: broken DISK_SPINDOWN_TIMEOUT list handling}

        get_disk_dev $dev
        log_message="set_disk_spindown_timeout($pwrmode): $disk_dev [$disk_id] $1"

        if [ ! -b /dev/$disk_dev ]; then
            echo_debug "disk" "${log_message} -- missing"
        elif ! check_disk_hdparm_cap $disk_dev; then
            echo_debug "disk" "${log_message} -- not supported"
        elif wordinlist "$1" "$DISK_NOP_WORDS"; then
            echo_debug "disk" "${log_message} -- keep as is"
        else
            echo_debug "disk" "${log_message}"
            $HDPARM -S $1 /dev/$disk_dev > /dev/null 2>&1
        fi

        # last entry in timeoutlist applies to all remaining disks
        [ $# -lt 2 ] || shift
    done

    return 0
}

set_disk_io_sched () { # set disk io scheduler
    local dev sched schedctrl log_message

    # when undefined use default
    : ${DISK_DEVICES:=${DEFAULT_DISK_DEVICES}}

    # set @argv := schedlist
    set -- $DISK_IOSCHED

    # exit if empty timeoutlist
    [ $# -gt 0 ] || return 0

    # pairwise iteration DISK_DEVICES[1,n], schedlist[1,m]; m > 0
    #  for j in [1,min(n,m)]   : disk_dev[j], schedlistj]
    #  for j in [min(n,m)+1,n] : disk_dev[j], %DEFAULT_DISK_IO_SCHEDULER
    for dev in $DISK_DEVICES; do
        get_disk_dev $dev

        # get sched from argv, use default scheduler when list is too short
        sched=${1:-${DEFAULT_DISK_IO_SCHEDULER}}
        schedctrl="/sys/block/$disk_dev/queue/scheduler"
        log_message="set_disk_io_sched: $disk_dev [$disk_id] $sched"

        if [ ! -b /dev/$disk_dev ]; then
            echo_debug "disk" "${log_message} -- missing"
        elif [ ! -f $schedctrl ]; then
            echo_debug "disk" "${log_message} -- not supported"
        elif wordinlist "$sched" "$DISK_NOP_WORDS"; then
            echo_debug "disk" "${log_message} -- keep as is"
        else
            echo_debug "disk" "${log_message}"
            printf '%s' "$sched" > $schedctrl
        fi

        # using %DEFAULT_DISK_IO_SCHEDULER when argv is empty
        [ $# -eq 0 ] || shift
    done

    return 0
}

# --- Device Power Management

set_sata_link_power () { # set sata link power management
    # $1: 0=ac mode, 1=battery mode

    local host host_bl hostid linkpol
    local pwr=""
    local ctrl_avail="0"

    if [ "$1" = "1" ]; then
        pwr=${SATA_LINKPWR_ON_BAT:-}
    else
        pwr=${SATA_LINKPWR_ON_AC:-}
    fi

    if [ -z "$pwr" ]; then
        # do nothing if unconfigured
        echo_debug "pm" "set_sata_link_power($1).not_configured"
        return 0
    fi

    # ALPM blacklist
    host_bl=${SATA_LINKPWR_BLACKLIST:-}

    # iterate SATA hosts
    for host in /sys/class/scsi_host/host* ; do
        linkpol=$host/link_power_management_policy
        if [ -f $linkpol ]; then
            hostid=${host##*/}
            if wordinlist "$hostid" "$host_bl"; then
                # host blacklisted
                echo_debug "pm" "set_sata_link_power($1).black: $host"
                ctrl_avail="1"
            else
                # host not blacklisted
                printf '%s\n' "$pwr" > $linkpol
                echo_debug "pm" "set_sata_link_power($1).$pwr: $host"
                ctrl_avail="1"
            fi
        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:-}
    else
        pwr=${PCIE_ASPM_ON_AC:-}
    fi

    if [ -z "$pwr" ]; then
        # do nothing if unconfigured
        echo_debug "pm" "set_pcie_aspm($1).not_configured"
        return 0
    fi

    if [ -f /sys/module/pcie_aspm/parameters/policy ]; then
        printf '%s\n' "$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 card level pwr rc1 rc2
    local sdone=0 # 1=radeon present

    if [ ! -d $RADD ]; then
        # No card present --> exit
        echo_debug "pm" "set_radeon_profile($1).no_card"
        return 0
    fi

    for card in /sys/class/drm/card[0-9]/device ; do
        if [ -f $card/power_dpm_state ] && [ -f $card/power_dpm_force_performance_level ]; then
            # Use new radeon dynamic power management method (dpm)
            if [ "$1" = "1" ]; then
                pwr=${RADEON_DPM_STATE_ON_BAT:-}
                level=${RADEON_DPM_PERF_LEVEL_ON_BAT:-auto}
            else
                pwr=${RADEON_DPM_STATE_ON_AC:-}
                level=${RADEON_DPM_PERF_LEVEL_ON_AC:-auto}
            fi

            if [ -z "$pwr" ]; then
                # do nothing if unconfigured
                echo_debug "pm" "set_radeon_profile($1).not_configured: $card"
                return 0
            fi

            if [ -n "$pwr" ]; then
                printf '%s\n' "$pwr" > $card/power_dpm_state 2> /dev/null; rc1=$?
                printf '%s\n' "$level" > $card/power_dpm_force_performance_level 2> /dev/null; rc2=$?
                echo_debug "pm" "set_radeon_profile($1): $card state=$pwr [rc=$rc1] perf=$level [rc=$rc2]"
            fi

            sdone=1

        elif [ -f $card/power_method ] && [ -f $card/power_profile ]; then
            # Use old radeon power profile method
            if [ "$1" = "1" ]; then
                pwr=${RADEON_POWER_PROFILE_ON_BAT:-}
            else
                pwr=${RADEON_POWER_PROFILE_ON_AC:-}
            fi

            if [ -z "$pwr" ]; then
                # do nothing if unconfigured
                echo_debug "pm" "set_radeon_profile($1).not_configured: $card"
                return 0
            fi

            if [ -n "$pwr" ]; then
                echo_debug "pm" "set_radeon_profile($1): $card profile=$pwr"
                printf '%s\n' "profile" > $card/power_method 2> /dev/null
                printf '%s\n' "$pwr" > $card/power_profile 2> /dev/null
            fi

            sdone=1
        fi
    done

    if [ $sdone -eq 0 ]; then
        echo_debug "pm" "set_radeon_profile($1).not_available"
    fi

    return 0
}

set_sound_power_mode () { # set sound chip power modes
    # $1: 0=ac mode, 1=battery mode

    local pwr cpwr

    # new config param
    if [ "$1" = "1" ]; then
        pwr=${SOUND_POWER_SAVE_ON_BAT:-}
    else
        pwr=${SOUND_POWER_SAVE_ON_AC:-}
    fi

    # when unconfigured consider legacy config param
    [ -z "$pwr" ] && pwr=${SOUND_POWER_SAVE:-}

    if [ -z "$pwr" ]; then
        # do nothing if unconfigured
        echo_debug "pm" "set_sound_power_mode($1).not_configured"
        return 0
    fi

    cpwr=${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($1).hda: $pwr controller=$cpwr"
        printf '%s\n' "$pwr" > /sys/module/snd_hda_intel/parameters/power_save

        if [ "$pwr" = "0" ]; then
            echo "N" >  /sys/module/snd_hda_intel/parameters/power_save_controller
        else
            printf '%s\n' "$cpwr" > /sys/module/snd_hda_intel/parameters/power_save_controller
        fi
    fi

    if [ -d /sys/module/snd_ac97_codec ]; then
        echo_debug "pm" "set_sound_power_mode($1).ac97: $pwr"
        printf '%s\n' "$pwr"  > /sys/module/snd_ac97_codec/parameters/power_save
    fi

    return 0
}

get_pci_class_descr () { # get long descr of pci device class
    # $1: class; retval: lclass

    case $1 in
        0x020000) lclass="Ethernet controller" ;;
        0x028000) lclass="Wireless" ;;
        0x040300) lclass="Audio device" ;;
        0x060000) lclass="Host Bridge" ;;
        0x080500) lclass="SD Card Reader" ;;
        0x088000|0x088001) lclass="Card Reader" ;;
        0x0c0000|0x0c0010) lclass="Firewire" ;;
        *) lclass="" ;;
    esac

    return 0
}

set_runtime_pm () { # set runtime power management
    # $1: 0=ac mode, 1=battery mode

    local address class ccontrol control device doall driver drv_bl pci_bl type

    if [ "$1" = "1" ]; then
        ccontrol=${RUNTIME_PM_ON_BAT:-}
    else
        ccontrol=${RUNTIME_PM_ON_AC:-}
    fi

    if [ -z "$ccontrol" ]; then
        # do nothing if unconfigured
        echo_debug "pm" "set_runtime_pm($1).not_configured"
        return 0
    fi

    # enable for all devices
    doall=${RUNTIME_PM_ALL:-1}

    # driver specific blacklist
    drv_bl=${RUNTIME_PM_DRIVER_BLACKLIST:-$DEFAULT_PM_DRIVER_BLACKLIST}

    # pci id blacklist
    pci_bl=${RUNTIME_PM_BLACKLIST:-}

    # add devices assigned to blacklisted drivers to the pci id blacklist
    for driver in $drv_bl; do # iterate list
        if [ -n "$driver" ] && [ -d $PCIDRV/$driver ]; then
            # driver is active --> iterate over assigned devices
            for device in $PCIDRV/$driver/0000:*; do
                # get short device address
                address=${device##/*/0000:}

                # add to list when not already contained
                if ! wordinlist "$address" "$pci_bl"; then
                    pci_bl="$pci_bl $address"
                fi
            done
        fi
    done

    # iterate pci(e) devices
    for type in $PCID; do
        for device in $type/*; do
            class="none"
            lclass=""

            # get short device address
            address=${device##/*/0000:}

            if wordinlist "$address" "$pci_bl"; then
                # device is in blacklist
                control="black"

            elif [ -f $device/class ] && [ -f $device/power/control ]; then
                class=$(cat $device/class)
                get_pci_class_descr $class
                control=$ccontrol                

                if [ -z "$lclass" ] && [ $doall -ne 1 ]; then
                    # device has no long descr and all flag is not set
                    control="class"
                fi

                case $control in
                    auto|on) echo $control > $device/power/control ;;

                    class|none) ;; # do nothing
                esac
            fi

            [ -n "$lclass" ] && lclass=" $lclass"
            echo_debug "pm" "set_runtime_pm($1).$control: $device [$class$lclass]"
        done
    done

    return 0
}

set_ahci_runtime_pm () { # set ahci runtime power management
    # $1: 0=ac mode, 1=battery mode

    local control device timeout rc

    if [ "$1" = "1" ]; then
        control=${AHCI_RUNTIME_PM_ON_BAT:-}
    else
        control=${AHCI_RUNTIME_PM_ON_AC:-}
    fi

    # calc timeout in millisecs, default to 15000
    timeout=$((${AHCI_RUNTIME_PM_TIMEOUT:-15} * 1000))
    [ "$timeout" != "0" ] || timeout=15000

    # check values
    case "$control" in
        on|auto)       ;;
        *) control="" ;; # invalid input --> unconfigured
    esac

    if [ -z "$control" ]; then
        # do nothing if unconfigured
        echo_debug "pm" "set_ahci_runtime_pm($1).not_configured"
        return 0
    fi

    # iterate ahci devices
    for device in $AHCID; do
        if [ -f ${device}/power/control ]; then
            echo $control > ${device}/power/control
            echo_debug "pm" "set_ahci_runtime_pm($1).$control: host=$device"
        fi
    done

    # iterate block devices
    for device in $BLOCKD; do
        if [ -f ${device}/device/power/control ]; then
            # write timeout first because writing "auto" with the default
            # timeout -1 still active will lockup the machine!
            rc=0

            if echo $timeout > ${device}/device/power/autosuspend_delay_ms 2> /dev/null; then
                # writing timeout was successful --> proceed with activation;
                # rc=2 when unsuccessful
                echo $control > ${device}/device/power/control 2> /dev/null || rc=2
            else
                # writing timeout was successful
                rc=1
            fi

            echo_debug "pm" "set_ahci_runtime_pm($1).$control: disk=$device timeout=$timeout; rc=$rc"
        fi
    done

    return 0
}

# --- Wifi Power Management

get_wifi_ifaces () { # get all wifi devices -- retval: $wifaces
    local wi wiu
    wifaces=""

    for wiu in $NETD/*/uevent; do
        if grep -q -s "DEVTYPE=wlan" $wiu ; then
            wi=${wiu%/uevent}; wi=${wi##*/}
            wifaces="$wifaces $wi"
        fi
    done

    wifaces="${wifaces# }"
    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 rc=0
    local cmdx=0

    if [ "$1" = "1" ]; then
        pwr=${WIFI_PWR_ON_BAT:-}
    else
        pwr=${WIFI_PWR_ON_AC:-}
    fi

    # check values, translate obsolete syntax
    case "$pwr" in
        off|on)      ;;
        0|1|N)       pwr="off" ;;
        2|3|4|5|6|Y) pwr="on"  ;;
        *)           pwr=""    ;; # invalid input --> unconfigured
    esac

    if [ -z "$pwr" ]; then
        # do nothing if unconfigured
        echo_debug "pm" "set_wifi_power_mode($1).not_configured"
        return 0
    fi

    get_wifi_ifaces

    for iface in $wifaces; do
        if [ -n "$iface" ]; then
            if  [ "$X_DONT_USE_IW" != "1" ] && cmd_exists $IW; then
                # try with iw first
                $IW dev $iface set power_save $pwr > /dev/null 2>&1
                rc=$?
                echo_debug "pm" "set_wifi_power_mode($1, $iface).iw: $pwr; rc=$rc"
                cmdx=1 # set flag: iw found and called
            fi
            if cmd_exists $IWC; then
                if [ $rc -ne 0 ] || [ $cmdx -eq 0 ]; then
                    # iw did not succeed or iw not installed -> try with iwconfig
                    $IWC $iface power $pwr > /dev/null 2>&1
                    rc=$?
                    echo_debug "pm" "set_wifi_power_mode($1, $iface).iwconfig: $pwr; rc=$rc"
                    cmdx=1 # set flag: iwconfig found and called
                fi
            fi
            if [ $cmdx -eq 0 ]; then
                # flag not set: neither iw nor iwconfig installed --> no way
                echo_debug "pm" "set_wifi_power_mode($1, $iface).no_tool"
            fi
        fi
    done

    return 0
}

wireless_in_use () { # check if wifi or wwan device is in use -- $1: iface
    if [ -f $NETD/$1/carrier ]; then
        if [ "$(cat $NETD/$1/carrier 2>/dev/null)" = "1" ]; then
            return 0
        fi
    fi
    return 1
}

any_wifi_in_use () { # check if any wifi device is in use
    local iface

    get_wifi_ifaces
    for iface in $wifaces; do
        wireless_in_use $iface && return 0
    done

    return 1
}

# --- WWAN Power Management

get_wwan_ifaces () { # get all wwan devices -- retval: $wanifaces
    local wi wiu
    wanifaces=""

    for wiu in $NETD/*/uevent; do
        if grep -q -s "DEVTYPE=wwan" $wiu ; then
            wi=${wiu%/uevent}; wi=${wi##*/}
            wanifaces="$wanifaces $wi"
        fi
    done

    wanifaces="${wanifaces# }"
    return 0
}

any_wwan_in_use () { # check if any wwan device is in use
    local iface

    get_wwan_ifaces
    for iface in $wanifaces; do
        wireless_in_use $iface && return 0
    done

    return 1
}

get_wwan_driver () { # get driver associated with interface
                     # $1: iface; retval: $wwandrv
    local drvl

    wwandrv=""
    if [ -d $NETD/$1 ]; then
        drvl=$(readlink $NETD/$1/device/driver)
        [ -n "$drvl" ] && wwandrv=${drvl##*/}
    fi

    return 0
}

# --- Bluetooth Power Management

get_bluetooth_ifaces () { # get all bluetooth devices -- retval: $bifaces
    # enumerate symlinks only
    bifaces="$(for i in $BLUETOOTHD/*; do [ -h $i ] && echo ${i##/*/}; done | grep -v ':')"
    return 0
}

get_bluetooth_driver () { # get driver associated with interface -- $1: iface; retval: $bluetoothdrv
    local drvl

    bluetoothdrv=""
    if [ -d $BLUETOOTHD/$1 ]; then
        drvl=$(readlink $BLUETOOTHD/$1/device/driver)
        [ -n "$drvl" ] && bluetoothdrv=${drvl##*/}
    fi

    return 0
}

dbus_call () { # call method via system dbus -- $1: dest; $2: $path; $3: interface, $4: $method
    $DBUSSEND --system --type=method_call --print-reply --dest="$1" "$2" "$3.$4" 2> /dev/null
}

bluez_call () { # call bluez method via dbus -- $1: path; $2: interface; $3: $method
    dbus_call "org.bluez" "$1" "org.bluez.$2" "$3"
}

bluez_bluetooth_in_use () { # check if bluetooth device (specified as dbus path) is in use $1: dbus path
    local devices device connected

    devices=$(bluez_call $1 Adapter ListDevices | sed -n 's/\s*object path "\(.*\)"\s*/\1/p')
    for device in $devices; do
        connected="$(bluez_call $device Device GetProperties | grep -A 1 Connected | sed -n 's/.*boolean //p')"
        test "$connected" = "true" && return 0
    done

    return 1
}

bluetooth_in_use () { # check if bluetooth device is in use -- $1: iface
    local paths path

    paths="$(bluez_call / Manager ListAdapters | sed -n 's/\s*object path "\(.*\)"\s*/\1/p' | grep /$1\$)"
    for path in $paths; do
        bluez_bluetooth_in_use "$path" && return 0
    done

    return 1
}

any_bluetooth_in_use () { # check if some bluetooth device is in use
    local paths path

    paths="$(bluez_call / Manager ListAdapters | sed -n 's/\s*object path "\(.*\)"\s*/\1/p')"
    for path in $paths; do
        bluez_bluetooth_in_use "$path" && return 0
    done

    return 1
}

# --- LAN

get_eth_ifaces () { # get all eth devices -- retval: $ethifaces
    local ei eic
    ethifaces=""

    for eic in $NETD/*/device/class; do
        if [ -f $eic ] \
            && [ "$(cat $eic)" = "0x020000" ] \
            && [ ! -d "${eic%/class}/ieee80211" ]; then

            ei=${eic%/device/class}; ei=${ei##*/}
            ethifaces="$ethifaces $ei"
        fi
    done

    ethifaces="${ethifaces# }"
    return 0
}

disable_wake_on_lan () {  # disable WOL
    local ei

    WOL_DISABLE=${WOL_DISABLE:-N}

    if [ "$WOL_DISABLE" = "Y" ]; then
        get_eth_ifaces

        for ei in $ethifaces; do
            echo_debug "pm" "disable_wake_on_lan: $ei"
            $ETHTOOL -s $ei wol d > /dev/null 2>&1
        done
    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 busdev control devices exc usbdev usbid subdev
    local ctrlf="control"
    local autof="autosuspend_delay_ms"

    check_sysfs "set_usb_suspend" "$USBD"

    if [ "$USB_AUTOSUSPEND" = "1" ]; then
        # autosuspend is configured

        # iterate devices
        devices=$(ls $USBD 2> /dev/null | 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:-auto}"
                exc=""
                chg=0

                if [ "$control" != "on" ]; then
                    if wordinlist "$usbid" "$USB_WHITELIST"; then
                        # device is in whitelist -- whitelist always wins
                        control="auto"
                        exc="_dev_white"
                    elif wordinlist "$usbid" "$USB_BLACKLIST"; then
                        # device is in blacklist
                        control="on"
                        exc="_dev_black"
                    else
                        # check for hid subdevices
                        for subdev in $USBD/$usbdev/*:*; do
                            if [ -d $subdev ] && [ "$(cat $subdev/bInterfaceClass)" = "03" ]; then
                                control="on"
                                exc="_hid_black"
                                break
                            fi
                        done

                        if [ -z "$exc" ]; then
                            # check for wwan vendor ids
                            USB_BLACKLIST_WWAN=${USB_BLACKLIST_WWAN:-1} # default is exclude

                            if [ $USB_BLACKLIST_WWAN = "1" ]; then
                                vendor="$(cat $USBD/$usbdev/idVendor)"
                                if wordinlist "$vendor" "$USB_WWAN_VENDORS"; then
                                    control="on"
                                    exc="_wwan_black"
                                fi
                            fi
                        fi
                    fi
                fi

                if [ -f $USBD/$usbdev/power/control ]; then
                    if [ "$(cat $USBD/$usbdev/power/control)" != "$control" ]; then
                        # Write actual changes only
                        echo "$control" > $USBD/$usbdev/power/control
                        chg=1
                    fi
                else
                    # level is deprecated
                    if [ "$(cat $USBD/$usbdev/power/level)" != "$control" ]; then
                        # Write actual changes only
                        echo "$control" > $USBD/$usbdev/power/level
                        chg=1
                    fi
                    ctrlf="level"
                fi
                if [ "$X_TLP_USB_SET_AUTOSUSPEND_DELAY" = "1" ]; then
                    # set autosuspend_delay
                    if [ -f $USBD/$usbdev/power/autosuspend_delay_ms ]; then
                        echo $USB_TIMEOUT_MS > $USBD/$usbdev/power/autosuspend_delay_ms
                    else
                        # autosuspend is deprecated
                        echo $USB_TIMEOUT > $USBD/$usbdev/power/autosuspend
                        autof="autosuspend"
                    fi
                    echo_debug "usb" "set_usb_suspend.$control$exc: $busdev ID $usbid $USBD/$usbdev [$ctrlf $autof]"
                elif [ $chg -eq 1 ]; then
                    # default: change control but not autosuspend_delay, i.e. keep kernel default setting
                    echo_debug "usb" "set_usb_suspend.$control$exc: $busdev ID $usbid $USBD/$usbdev [$ctrlf]"
                else
                    # we didn't change anything actually
                    echo_debug "usb" "udev_usb_suspend.$control$exc: $busdev ID $usbid $USBD/$usbdev"
                fi

            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." 1>&2
    fi

    # set "startup completion" flag for tlp-usb-udev
    set_run_flag $USB_DONE

    return 0
}

# --- ThinkPad Battery Functions

supports_tpsmapi_only () {
    # rc: 0=ThinkPad supports tpsmapi only/1=false
    printf '%s' "$tpmodel" | egrep -q "${RE_TPSMAPI_ONLY}"
}

supports_tpacpi_only () {
    # rc: 0=ThinkPad supports tpacpi-bat only/1=false
    printf '%s' "$tpmodel" | egrep -q "${RE_TPACPI_ONLY}"
}

supports_tpsmapi_and_tpacpi () {
    # rc: 0=ThinkPad supports tpsmapi and tpacpi-bat/1=false
    printf '%s' "$tpmodel" | egrep -q "${RE_TPSMAPI_AND_TPACPI}"
}

supports_no_tp_bat_funcs () {
    # rc: 0=ThinkPad doesn't support extended battery functions/1=false
    printf '%s' "$tpmodel" | egrep -q "${RE_TP_NONE}"
}

check_tpsmapi () {  # check if tp_smapi is supported and loaded
    # rc: 0=supported/2=module tp_smapi not loaded/
    #     127=not installed//255=not supported
    # global param: $tpmodel
    # 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
            if supports_tpacpi_only || supports_no_tp_bat_funcs; then
                # not tp-smapi capable models
                tpsmapi=255
            else
                tpsmapi=2
            fi
        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 supports_tpsmapi_only || supports_no_tp_bat_funcs; then
        # not tpacpi-bat capable models
        tpacpi=255
    elif [ ! -e /proc/acpi/call ] && [ -z "$(modinfo acpi_call 2> /dev/null)" ]; then
        # module not installed
        tpacpi=127
    else
        # module loaded --> try tpacpi-bat
        $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
}

check_tp_battery () { # check ThinkPad battery presence and return index
    # $1: BAT0/BAT1/DEF
    # global param: $tpacpi, $tpsmapi
    # rc: 0=bat exists/1=bat nonexistent/255=no thresh api available
    # retval: $bat_str: BAT0/BAT1; $bat_idx: 1/2

    # defaults
    local rc=255 # no threshold API available
    bat_idx=0    # no tpacpi-bat index
    bat_str=""   # no default bat found
    local blist bs

    # load modules and check prerequisites: tpacpi-bat or tp-smapi
    check_thinkpad
    check_tpacpi
    check_tpsmapi

    # validate param
    case $1 in
        BAT0|BAT1) blist="$1" ;;
        DEF) blist="BAT0 BAT1" ;;

        *)         return 1 ;;
    esac

    if [ $tpsmapi -eq 0 ]; then # tp-smapi available
        for bs in $blist; do
            # check tp-smapi name space
            [ -d $SMAPIDIR/$bs ] && [ "$(cat $SMAPIDIR/$bs/installed)" = "1" ]
            rc=$?
            if [ $rc -eq 0 ]; then
                case $bs in
                    BAT0) bat_idx=1; bat_str="$bs" ;;
                    BAT1) bat_idx=2; bat_str="$bs" ;;
                esac
            fi
            [ $rc -ne 0 ] || break # exit loop on first battery detected
        done
    elif [ $tpacpi -eq 0 ]; then # tpacpi-bat available
        for bs in $blist; do
            # check acpi name space
            [ -d $ACPIBATDIR/$bs ] && [ "$(cat $ACPIBATDIR/$bs/present)" = "1" ]
            rc=$?
            if [ $rc -eq 0 ]; then
                # determine tpacpi-bat index
                case $bs in
                    BAT0)
                        bat_idx=1 # BAT0 is always assumed main battery
                        bat_str="$bs"
                        ;;

                    BAT1)
                        # check with tpacpi-bat(2) if BAT1 is main or aux battery
                        if $TPACPIBAT -g ST 2 2> /dev/null 1>&2 ; then
                            bat_idx=2 # BAT1 is aux
                        else
                            bat_idx=1 # BAT1 is main
                        fi
                        bat_str="$bs"
                        ;;
                esac
            fi
            [ $rc -ne 0 ] || break # exit loop on first battery detected
        done
    fi

    echo_debug "bat" "check_tp_battery($1): idx=$bat_idx; str=$bat_str; tpacpi=$tpacpi; tpsmapi=$tpsmapi; rc=$rc"
    return $rc
}

read_tpacpi_threshold () { # $1: ST/SP (start/stop); $2: 0/1 (battery)
                           # rc: threshold (1..99, 0=default, 255=error)
    local thresh rc

    thresh=$($TPACPIBAT -g $1 $2 2> /dev/null | cut -f1 -d' ')
    rc=$?

    if [ $rc -eq 0 ] && [ -n "$thresh" ]; then
        [ $thresh -ge 128 ] && thresh=$(($thresh - 128)) # Remove offset of 128 for Edge S430
        return $thresh
    else
        return 255
    fi
}

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

    local 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
        if [ $bat_idx -ne 0 ]; 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

            read_tpacpi_threshold $ts $bat_idx
            old_thresh=$?

            if [ $new_thresh -ne $old_thresh ]; then
                $TPACPIBAT -s $ts $bat_idx $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): bat_idx=$bat_idx; tpacpi=$tpacpi; tpsmapi=$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

        # check for 1..3 digits, replace with empty string if non-numeric chars are contained
        thresh=$(echo "$thresh" | egrep '^[0-9]{1,3}$')
        # replace empty string with -1
        [ -z "$thresh" ] && thresh=-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
}

set_charge_thresholds () { # write all charge thresholds from configuration
    # global param: $tpacpi, $tpsmapi
    # rc: 0

    local rc

    if check_tp_battery BAT0; then
        normalize_thresholds "$START_CHARGE_THRESH_BAT0" "$STOP_CHARGE_THRESH_BAT0"

        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

        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
    fi

    if check_tp_battery BAT1; then
        normalize_thresholds "$START_CHARGE_THRESH_BAT1" "$STOP_CHARGE_THRESH_BAT1"

        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

        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
    fi

    return 0
}

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

    local bsys rc=0

    if [ $tpacpi -eq 0 ]; then # use tpacpi-bat
        $TPACPIBAT -s FD $bat_idx $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: $bat_idx, $tpacpi, $tpsmapi
    # rc: 0=off/1=on/2=discharge not present/255=no thresh api

    local bsys rc=0

    if [ $tpacpi -eq 0 ]; then # use tpacpi-bat
        case $($TPACPIBAT -g FD $bat_idx 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_force_discharge () { # called from trap -- global param: $bat_str
    do_force_discharge $bat_str 0
    echo_debug "bat" "force_discharge.cancelled($bat_str)"
    echo " Cancelled."

    exit 0
}

bat_discharging () { # check if battery is discharging -- $1: BAT0/BAT1,
    # global param: $bat_idx, $bat_str, $tpacpi, $tpsmapi
    # rc: 0=discharging/1=not discharging/255=no battery api

    local bsys rc=255

    # determine battery api
    if [ $tpsmapi -eq 0 ]; then # use tp-smapi
        bsys=$SMAPIDIR/$bat_str/state
    elif [ $tpacpi -eq 0 ]; then # use tpacpi-bat
        bsys=$ACPIBATDIR/$bat_str/status
    fi

    # get battery state
    if [ -f $bsys ]; then
        case "$(cat $bsys 2> /dev/null)" in
            [Dd]ischarging) rc=0 ;;
            *) rc=1 ;;
        esac
    fi

    echo_debug "bat" "bat_discharging($1): rc=$rc"
    return $rc
}

check_ac_power () { # check if ac power connected -- $1: function

    if ! get_sys_power_supply ; then
        echo_debug "bat" "check_ac_power($1).no_ac_power"
        echo "Error: $1 is possible on AC power only." 1>&2
        return 1
    fi

    return 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 bat rc st sp
    local use_cfg=0
    # $bat_str is global for cancel_force_discharge() trap

    # check params
    case $# in
        0) # no args
            bat=DEF   # use default(1st) battery
            use_cfg=1 # use configured values
            ;;

        1) # assume $1 is battery
            bat=$1
            use_cfg=1 # use configured values
            ;;

        2) # assume $1,$2 are thresholds
            st=$1
            sp=$2
            bat=DEF # use default(1st) battery
            ;;

        3) # assume $1,$2 are thresholds, $3 is battery
            st=$1
            sp=$2
            bat=$3
            ;;
    esac

    # convert bat to uppercase
    bat=$(printf '%s' "$bat" | tr "[:lower:]" "[:upper:]")

    # check bat presence and/or get default(1st) battery
    check_tp_battery $bat
    case $? in
        0) # battery present
            # get configured values if requested
            if [ $use_cfg -eq 1 ]; then
                eval st="\$START_CHARGE_THRESH_$bat_str"
                eval sp="\$STOP_CHARGE_THRESH_$bat_str"
            fi
            ;;

        255) # no api
            echo "Error: ThinkPad extended battery functions not available." 1>&2
            echo_debug "bat" "setcharge_battery.noapi"
            return 1
            ;;

        *) # not present
            echo "Error: battery $bat not present." 1>&2
            echo_debug "bat" "setcharge_battery.not_present($bat)"
            return 1
            ;;
    esac

    # validate thresholds
    normalize_thresholds $st $sp

    # write threshold values
    echo "Setting temporary charge thresholds for $bat_str:"

    if [ $stop_thresh -ne -1 ]; then
        do_threshold stop $bat_str $stop_thresh; rc=$?

        echo_debug "bat" "setcharge_battery.stop($bat_str): $stop_thresh; rc=$rc"
        if [ $rc -eq 0 ]; then
            echo "  stop  = $stop_thresh"
        else
            echo "  stop  => Error: cannot set threshold. Aborting." 1>&2
            return 1
        fi
    else
        echo_debug "bat" "setcharge_battery.stop($bat_str).not_configured"
        echo "  stop = not configured"
    fi

    if [ $start_thresh -ne -1 ]; then
        do_threshold start $bat_str $start_thresh; rc=$?

        echo_debug "bat" "setcharge_battery.start($bat_str): $start_thresh; rc=$rc"
        if [ $rc -eq 0 ]; then
            echo "  start = $start_thresh"
        else
            echo "  start => Warning: cannot set threshold." 1>&2
            return 1
        fi
    else
        echo_debug "bat" "setcharge_battery.start($bat_str).not_configured"
        echo "  start = not configured"
    fi

    return 0
}

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

    # read sysval when it exists
    [ -f $sysf ] && val=$(cat $sysf 2> /dev/null)

    # replace with 0 if empty string or non-numeric chars are contained
    [ -z "$(printf '%s' "$val" | egrep '^[0-9]+$')" ] && val=0

    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:-DEF}
        bat=$(printf '%s' "$bat" | tr "[:lower:]" "[:upper:]")
    else
        # no parameters given, use default(1st) battery
        bat=DEF
    fi

    # check if selected battery is present
    check_tp_battery $bat
    case $? in
        0) ;; # battery present

        255) # no api
            echo "Error: ThinkPad extended battery functions not available." 1>&2
            echo_debug "bat" "chargeonce_battery.noapi"
            return 1
            ;;

        *) # not present
            echo "Error: battery $bat_str not present." 1>&2
            echo_debug "bat" "chargeonce_battery.not_present($bat_str)"
            return 1
            ;;
    esac

    # get and check thresholds from configuration)
    eval stop_thresh=\$STOP_CHARGE_THRESH_$bat_str
    eval start_thresh=\$START_CHARGE_THRESH_$bat_str

    [ -z "$stop_thresh" ] && stop_thresh=100
    if [ -z "$start_thresh" ] ; then
        echo_debug "bat" "chargeonce_battery($bat_str).start_threshold_not_configured"
        echo "Error: no start charge threshold configured for $bat_str." 1>&2
        return 1
    fi

    # get current charge level (in %)
    if [ $tpsmapi -eq 0 ]; then
        # use tp-smapi
        bdir="$SMAPIDIR/$bat_str"
        get_sysval $bdir/remaining_percent; ccharge=$?
    else
        # use ACPI data
        bdir="$ACPIBATDIR/$bat_str"
        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_str).charge_level_unknown: enow=$enow; efull=$efull; ccharge=$ccharge"
        echo "Error: cannot determine charge level for $bat_str." 1>&2
        return 1
    else
        echo_debug "bat" "chargeonce_battery($bat_str).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_str).charge_level_too_high: $temp_start_thresh $stop_thresh"
        echo "Error: current charge level ($ccharge) of $bat_str is higher than stop charge threshold - 4 ($temp_start_thresh)." 1>&2
        return 1
    else
        echo_debug "bat" "chargeonce_battery($bat_str).setcharge: $temp_start_thresh $stop_thresh"
    fi

    setcharge_battery $temp_start_thresh $stop_thresh $bat_str
    return $?
}

discharge_battery () { # discharge battery
    # $1: battery
    # global param: $tpacpi, $tpsmapi
    # rc: 0=ok/1=error

    local bat bdir en ef pn rc
    # $bat_str is global for cancel_force_discharge() trap

    # check params
    bat=$1
    bat=${bat:=DEF}
    bat=$(printf '%s' "$bat" | tr "[:lower:]" "[:upper:]")

    # check if selected battery is present
    check_tp_battery $bat
    case $? in
        0) ;; # battery present

        255) # no api
            echo "Error: ThinkPad extended battery functions not available." 1>&2
            echo_debug "bat" "discharge_battery.noapi"
            return 1
            ;;

        *) # not present
            echo "Error: battery $bat not present." 1>&2
            echo_debug "bat" "discharge_battery.not_present($bat)"
            return 1
            ;;
    esac

    # start discharge
    do_force_discharge $bat_str 1; rc=$?
    if [ $rc -ne 0 ]; then
        echo_debug "bat" "discharge_battery.force_discharge_not_available($bat_str)"
        echo "Error: discharge function not available for this ThinkPad model." 1>&2
        return 1
    fi

    trap cancel_force_discharge INT # enable ^C hook

    # wait for start == while status not "discharging"
    while ! bat_discharging $bat_str; do :; done
    echo_debug "bat" "discharge_battery.running($bat_str)"

    # wait for completion == while status "discharging"
    while bat_discharging $bat_str; do
        clear
        echo "Currently discharging battery $bat_str:"

        # show current battery state
        if [ $tpsmapi -eq 0 ]; then # use tp_smapi
            bdir=$SMAPIDIR/$bat_str

            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_str

            if [ -d $bdir ]; then
                perl -e 'printf ("voltage            = %6d [mV]\n", '$(cat $bdir/voltage_now)' / 1000.0);'

                en=$(cat $bdir/energy_now)
                perl -e 'printf ("remaining capacity = %6d [mWh]\n", '$en' / 1000.0);'

                ef=$(cat $bdir/energy_full)
                if [ "$ef" != "0" ]; then
                    perl -e 'printf ("remaining percent  = %6d [%%]\n", 100.0 * '$en' / '$ef' );'
                else
                    printf "remaining percent  = not available [%]\n"
                fi

                pn=$(cat $bdir/power_now)
                if [ "$pn" != "0" ]; then
                    perl -e 'printf ("remaining time     = %6d [min]\n", 60.0 * '$en' / '$pn');'
                    perl -e 'printf ("power              = %6d [mW]\n", '$pn' / 1000.0);'
                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

    # crappy ThinkPad E-series firmware may keep force_discharge active --> cancel it
    ! get_force_discharge $bat_str && do_force_discharge $bat_str 0

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

# --- Drive Bay

get_drivebay_device () { # Find generic dock interface for drive bay
                         # rc: 0; retval: $dock
	dock=$(grep -l ata_bay $DOCKGLOB/type 2> /dev/null)
	dock=${dock%%/type}
	if [ ! -d "$dock" ]; then
		dock=""
	fi

    return 0
}

check_is_docked() { # check if $dock is docked;
                    # rc: 0 if docked, else 1

   local dock_status dock_info_file

   # return 0 if any sysfs file indicates "docked"
   for dock_info_file in docked firmware_node/status; do
        if [ -f $dock/$dock_info_file ] && \
            read -r dock_status < $dock/$dock_info_file 2>/dev/null; then
            # catch empty $dock_status (safety check, unlikely case)
            [ "${dock_status:-0}" != "0" ] && return 0
        fi
   done

   # otherwise assume "not docked"
   return 1
}

suspend_drivebay () { # Save power state of drive bay before suspend
                      # $1: 0=ac mode, 1=battery mode

    if [ "$1" = "1" ] && [ "$BAY_POWEROFF_ON_BAT" = "1" ]; then
        # bat power and setting is active -> save state
        get_drivebay_device

        if [ -n "$dock" ]; then
            mkdir -p $STATEDIR 2> /dev/null 1>&2

            if ! check_is_docked; then
                echo "off" > $BAYSTATEFILE
                echo_debug "pm" "suspend_drivebay: bay=off"
            else
                echo "on" > $BAYSTATEFILE
                echo_debug "pm" "suspend_drivebay: bay=on"
            fi
        fi
    else
        # not on bat or setting not active -> remove state file
        rm -f $BAYSTATEFILE 2> /dev/null
    fi

    return 0
}

resume_drivebay () { # Restore power state of drive bay after resume

	local cnt rc

	[ -f $BAYSTATEFILE ] || return 0 # no state file -> do nothing

    if [ "$(cat $BAYSTATEFILE)" = "off" ]; then
        get_drivebay_device

        if [ -n "$dock" ]; then
            if check_is_docked; then
                # device active -> deactivate
                if [ -e $dock/undock ]; then
                    cnt=5
                    rc=1
                    until [ $rc = 0 -o $cnt = 0 ]; do
                        cnt=$((cnt - 1))
                        echo 1 > $dock/undock
                        rc=$?
                        [ $rc = 0 ] || sleep 0.5
                    done
                    echo_debug "pm" "resume_drivebay.bay_off: rc=$rc"
                fi
            else
                echo_debug "pm" "resume_drivebay.already_off"
            fi
        fi
    fi

    rm -f $BAYSTATEFILE 2> /dev/null

    return 0
}

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 optdrv syspath

    # Run only if either explicitly enabled or forced
    [ "$BAY_POWEROFF_ON_BAT" = "1" ] || [ "$1" = "1" ] || return 0

    get_drivebay_device
    if [ -z "$dock" ] || [ ! -d "$dock" ]; then
        echo_debug "pm" "poweroff_drivebay.no_bay_device"
        [ "$1" = "1" ] && echo "Error: cannot locate bay device." 1>&2
        return 1
    fi
    echo_debug "pm" "poweroff_drivebay: dock=$dock"

    # Check if bay is occupied
    if ! check_is_docked; 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"

            echo -n "Powering off drive bay..."

            # Unmount media
            umount -l $optdrv > /dev/null 2>&1

            # Sync drive
            sync
            sleep 1

            # Power off drive
            $HDPARM -Y $optdrv > /dev/null 2>&1
            sleep 5

            # Unregister scsi device
            if syspath="$($UDEVADM info --query=path --name=$optdrv)"; then
                syspath="/sys${syspath%/block/*}"

                if [ "$syspath" != "/sys" ]; then
                    echo_debug "pm" "poweroff_drivebay: syspath=$syspath"
                    echo 1 > $syspath/delete
                else
                    echo_debug "pm" "poweroff_drivebay: got empty/invalid syspath for $optdrv"
                fi
            else
                echo_debug "pm" "poweroff_drivebay: failed to get syspath (udevadm returned $?)"
            fi

            # Turn power off
            echo 1 > $dock/undock
            [ "$1" = "1" ] && echo "done."
            echo_debug "pm" "poweroff_drivebay.bay_powered_off"
        fi
    fi

    return 0
}
