#!/bin/sh
# tlp - Base Functions
#
# Copyright (c) 2020 Thomas Koch <linrunner at gmx.net> and others.
# This software is licensed under the GPL v2 or later.

# shellcheck disable=SC2086

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

readonly TLPVER="1.3.1"

readonly RUNDIR=/run/tlp
readonly VARDIR=/var/lib/tlp

readonly CONF_DEF=/usr/share/tlp/defaults.conf
readonly CONF_DIR=/etc/tlp.d
readonly CONF_USR=/etc/tlp.conf
readonly CONF_OLD=/etc/default/tlp
readonly CONF_RUN="$RUNDIR/run.conf"

readonly FLOCK=flock
readonly HDPARM=hdparm
readonly LAPMODE=laptop_mode
readonly LOGGER=logger
readonly MKTEMP=mktemp
readonly MODPRO=modprobe
readonly READCONFS=/usr/share/tlp/tlp-readconfs
readonly TPACPIBAT=/usr/share/tlp/tpacpi-bat
readonly UDEVADM=udevadm

readonly LOCKFILE=$RUNDIR/lock
readonly LOCKTIMEOUT=2

readonly PWRRUNFILE=$RUNDIR/last_pwr
readonly MANUALMODEFILE=$RUNDIR/manual_mode

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

readonly DMID=/sys/class/dmi/id/
readonly NETD=/sys/class/net

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_TPSMAPI_AND_TPACPI='^(X1|X220[s]?( Tablet)?|T[45]20[s]?|W520)$'
readonly RE_TP_NONE='^(L[45]20|SL[345]00|X121e)$'

readonly RE_PARAM='^[A-Z_]+[0-9]*=[0-9a-zA-Z _\-\:\.]*$'

# power supplies: ignore MacBook Pro 2017 sbs-charger and hid devices
readonly RE_PS_IGNORE='sbs-charger|hidpp_battery|hid-'

readonly DEBUG_TAGS_ALL="arg bat cfg disk lock nm path pm ps rf run sysfs udev usb"

# ----------------------------------------------------------------------------
# Control

_nodebug=0

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

# -- Exit

do_exit () { # cleanup and exit  -- $1: rc
    # remove temporary runconf
    [ -f "$_conf_tmp" ] && rm -rf "$_conf_tmp"

    exit $1
}

# --- Debug

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" --id=$$ -- "$2" > /dev/null 2>&1
    fi
}

# --- Strings

tolower () { # echo string in lowercase -- $1: string
    printf "%s" "$1" | tr "[:upper:]" "[:lower:]"
}

toupper () { # echo string in lowercase -- $1: string
    printf "%s" "$1" | tr "[:lower:]" "[:upper:]"
}

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
}

# --- Sysfiles

check_sysfs () { # debug: check if sysfile exists -- $1: routine; $2: sysfs path
    if wordinlist "sysfs" "$TLP_DEBUG"; then
        if [ ! -e $2 ]; then
            $LOGGER -p debug -t "tlp" --id=$$ -- "$1: $2 nonexistent" > /dev/null 2>&1
        fi
    fi
}

read_sysf () { # read and echo contents of sysfile
    # return 1 and echo default if read fails
    # $1: sysfile, $2: default; rc: 0=ok/1=error
    if ! cat "$1" 2> /dev/null; then
        printf "%s" "$2"
        return 1
    else
        return 0
    fi
}

readable_sysf () { # check if file is actually readable -- $1: file
    # rc: 0=readable/1=read error
    cat "$1" > /dev/null 2>&1
}

read_sysval () { # read and echo contents of sysfile
    # echo '0' if file is non-existent, read fails or is non-numeric
    # $1: sysfile; rc: 0=ok/1=error
    printf "%d" "$(read_sysf $1)" 2> /dev/null
}

write_sysf () { # write string to a sysfile
    # $1: string, $2: sysfile
    # rc: 0=ok/1=error
    { printf '%s\n' "$1" > "$2"; } 2> /dev/null
}

# --- Tests

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

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

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

check_tlp_enabled () { # check if TLP is enabled in config file
    # $1: 1=verbose (default: 0)
    # rc: 0=disabled/1=enabled

    if [ "$TLP_ENABLE" = "1" ]; then
        return 0
    else
        [ "${1:-0}" = "1" ] && echo "Error: TLP power save is disabled. Set TLP_ENABLE=1 in ${CONF_USR}." 1>&2
        return 1
    fi
}

# --- Type and value checking

is_uint () { # check for unsigned integer -- $1: string; $2: max digits
    printf "%s" "$1" | grep -E -q "^[0-9]{1,$2}$" 2> /dev/null
}

is_within_bounds () { # check condition min <= value <= max
    # $1: value; $2: min; $3: max (all unsigned int)
    # rc: 0=within/1=below/2=above/255=invalid
    #
    # value, min or max undefined/non-numeric means that this branch of the
    # condition is fulfilled

    is_uint $1 || return 255
    if is_uint $2; then
        [ $1 -ge $2 ] || return 1
    fi
    if is_uint $3; then
        [ $1 -le $3 ] || return 2
    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
    # when successful lock fd 9 exclusive and blocking
    # wait $LOCKTIMEOUT secs to obtain the lock
    if { exec 9> ${LOCKFILE}_${1:-tlp} ; } 2> /dev/null && $FLOCK -x -w $LOCKTIMEOUT 9 ; then
        echo_debug "lock" "lock_tlp($1).success"
        return 0
    else
        echo_debug "lock" "lock_tlp($1).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
    # when successful lock fd 9 exclusive and non-blocking
    if { exec 9> ${LOCKFILE}_${1:-tlp} ; } 2> /dev/null && $FLOCK -x -n 9 ; then
        echo_debug "lock" "lock_tlp_nb($1).success"
        return 0
    else
        echo_debug "lock" "lock_tlp_nb($1).failed"
        return 1
    fi
}

unlock_tlp () { # free exclusive lock
                # $1: lock id (default: tlp)

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

    # free fd 9 and scrap lockfile
    { exec 9>&- ; } 2> /dev/null
    rm -f ${LOCKFILE}_${1:-tlp}
    echo_debug "lock" "unlock_tlp($1)"

    return 0
}

lockpeek_tlp () { # check for pending lock (by looking for the lockfile)
                  # $1: lock id (default: tlp)
    if [ -f ${LOCKFILE}_${1:-tlp} ]; then
        echo_debug "lock" "lockpeek_tlp($1).locked"
        return 0
    else
        echo_debug "lock" "lockpeek_tlp($1).not_locked"
        return 1
    fi
}

echo_tlp_locked () { # print "locked" message
    echo "Error: TLP is locked by another operation." 1>&2
    return 0
}

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

    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"

    # cleanup obsolete locks
    time=$(date +%s)
    for lockfile in $RUNDIR/${1}_timed_lock_*; do
        if [ -f "$lockfile" ]; then
            locktime=${lockfile#${RUNDIR}/${1}_timed_lock_}
            if [ $time -ge $locktime ]; then
                rm -f $lockfile
                echo_debug "lock" "set_timed_lock($1, $2).remove_obsolete: ${lockfile#${RUNDIR}/}"
            fi
        fi
    done

    return $rc
}

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

    time=$(date +%s)
    for lockfile in $RUNDIR/${1}_timed_lock_*; do
        if [ -f "$lockfile" ]; then
            locktime=${lockfile#${RUNDIR}/${1}_timed_lock_}
            if [ $time -lt $(( locktime - 120 )) ]; then
                # timestamp is more than 120 secs in the future,
                # something weird has happened -> remove it
                rm -f $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 -f $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
}

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

    # shellcheck disable=SC2034
    _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_config () { # read all config files and write temporary runconf file
    # $1: 0=continue/1=quit on error
    # $2: 1=no trace
    # rc: 0=ok/5=tlp.conf missing/6=defaults.conf missing/7=file creation error
    # retval: config parameters;
    #         _conf_tmp: runconf
    local rc=0
    local tmpdir

    if test_root; then
        tmpdir=$RUNDIR
        create_rundir
    else
        tmpdir=${TMPDIR:-/tmp}
    fi
    if _conf_tmp=$($MKTEMP -p "$tmpdir" "tlp-run.conf_tmpXXXXXX"); then
        # external perl script: merge all config files to $cf
        if [ "$2" = "1" ]; then
            $READCONFS --outfile "$_conf_tmp" --notrace; rc=$?
        else
            $READCONFS --outfile "$_conf_tmp"; rc=$?
        fi
        # shellcheck disable=SC1090
        [ $rc -eq 0 ] && . "$_conf_tmp"
    else
        rc=7
    fi

    if [ $rc -ne 0 ]; then
        case $rc in
            5) echo "Error: cannot read user configuration from $CONF_USR or $CONF_OLD." 1>&2 ;;
            6) echo "Error: cannot read default configuration from $CONF_DEF." 1>&2 ;;
            7) echo "Error: cannot write runtime configuration to $_conf_tmp." 1>&2 ;;
        esac
        if [ "$1" = "1" ]; then
            do_exit $rc
        fi
    fi

    return 0
}

parse_args4config () { # parse command-line arguments: everything after the
                       # delimiter '--' is interpreted as a config parameter
    # retval: config parameters
    local dflag=0 param value

    echo_debug "arg" "parse_args4config: ${0##/*/} $*"
    # iterate arguments
    while [ $# -gt 0 ]; do
        if [ $dflag -eq 1 ]; then
            # delimiter was passed --> sanitize and parse argument:
            #   quotes stripped by the shell calling tlp
            #   format is PARAMETER=value
            #   PARAMETER allows 'A'..'Z' and '_' only, may end in a number (_BAT0)
            #   value allows  'A'..'Z', 'a'..'z', '0'..'9', ' ', '_', '-', ':'
            #   value may be an empty string
            if printf "%s" "$1" | grep -E -q "$RE_PARAM"; then
                param="${1%%=*}"
                value="${1#*=}"
                if [ -n "$param" ]; then
                    echo_debug "cfg" "parse_args4config.param: $param=$value"
                    eval "$param='$value'" 2> /dev/null
                fi
            fi
        elif [ "$1" = "--" ]; then
            # delimiter reached --> begin interpretation
            dflag=1
        fi
        shift # next argument
    done # while arguments

    return 0
}

print_config () { # print runtime conf based on all config files
    # external perl script: merge all config files to $cf
    $READCONFS --notrace
}

save_runconf () { # copy temporary to final runconf
    create_rundir
    if cp --preserve=timestamps "$_conf_tmp" $CONF_RUN > /dev/null 2>&1; then
        chmod 664 "$_conf_tmp" $CONF_RUN > /dev/null 2>&1
        echo_debug "run" "save_runconf.ok: $_conf_tmp -> $CONF_RUN"
    else
        echo_debug "run" "save_runconf.failed: $_conf_tmp -> $CONF_RUN"
    fi
}

# --- Kernel

kernel_version_ge () { # check if running kernel version >= $1: minimum version

    [ "$1" = "$(printf "%s\n%s\n" "$1" "$(uname -r)" | sort -V | head -n 1)" ]
}

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
    # shellcheck disable=SC2048
    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=""
    # shellcheck disable=SC2048
    for key in $*; do
        ds="$(read_sysf ${DMID}/$key | \
                grep -E -v -i '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 Checks

supports_tpsmapi_only () {
    # rc: 0=ThinkPad supports tpsmapi only/1=false
    # prerequisite: check_thinkpad()
    printf '%s' "$_tpmodel" | grep -E -q "${RE_TPSMAPI_ONLY}"
}

supports_tpsmapi_and_tpacpi () {
    # rc: 0=ThinkPad supports tpsmapi, tpacpi-bat, natacpi/1=false
    # prerequisite: check_thinkpad()
    printf '%s' "$_tpmodel" | grep -E -q "${RE_TPSMAPI_AND_TPACPI}"
}

supports_no_tp_bat_funcs () {
    # rc: 0=ThinkPad doesn't support battery features/1=false
    # prerequisite: check_thinkpad()
    printf '%s' "$_tpmodel" | grep -E -q "${RE_TP_NONE}"
}

supports_tpacpi () {
    # rc: 0=ThinkPad does support tpacpi-bat, natacpi/1=false
    # prerequisite: check_thinkpad()
    # assumption: all newer models support tpapaci-bat/natacapi except
    # explicitly unsupported or older tpsmapi only models
    ! supports_no_tp_bat_funcs && ! supports_tpsmapi_only
}

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

        if [ -z "$X_SIMULATE_MODEL" ]; then
            # get DMI product string and sanitize it
            pv="$(read_dmi product_version | tr -C -d 'a-zA-Z0-9 ')"
        else
            # simulate arbitrary model
            pv="$X_SIMULATE_MODEL"
        fi

        # check DMI product string for occurrence of "ThinkPad"
        if printf '%s' "$pv" | grep -E -q 'Think[Pp]ad'; then
            # it's a real ThinkPad --> save model substring
            _tpmodel=$(printf '%s\n' "$pv" | sed -r 's/^Think[Pp]ad //')
        fi
    else
        # not a ThinkPad: get DMI product string
        pv="$(read_dmi product_version)"
    fi

    if [ -n "$_tpmodel" ]; then
        # load tp-smapi for supported models only; prevents kernel messages
        if supports_tpsmapi_only || supports_tpsmapi_and_tpacpi; then
            load_modules $MOD_TPSMAPI
        fi

        # load acpi-call for supported models only; prevents kernel messages
        if supports_tpacpi; then
            load_modules $MOD_TPACPI
        fi

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

    # not a ThinkPad
    echo_debug "bat" "check_thinkpad.not_a_thinkpad: model=$pv"
    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 supply
                          # retval/rc: $_syspwr (0=ac, 1=battery, 2=unknown)

    # _syspwr is determined by iterating all power sources:
    #
    #             AC status:  | none  | 1       | 0       |
    #                         | found | online  | offline |
    #           +------------ +-------+---------+---------+
    # battery   | none found  | 2     | 0 *s    | *o      |
    # status    | Charging    | *r    | 0 *s    | *r      |
    #           | Discharging | *d    | 0 *s    | *d      |
    #           | Full        | *r    | 0 *s    | *r      |
    #           | Unknown     | *r    | 0 *s    | *r      |
    #
    # special cases:
    #   *o) if battery status "Unknown" was seen previously then _syspwr=1;
    #       else remember AC offline state
    #   *r) re-check battery status after 0.5 sec -> if battery status
    #       "Discharging" (not forced) then _syspwr=1 and stop looking;
    #       else _syspwr=0
    #   *d) if forced discharge in progress then _syspwr=0; else _syspwr=1;
    #       stop looking
    #   *s) stop looking (for other power sources)

    local bs psrc psrc_name
    local ac0seen=
    _syspwr=

    for psrc in /sys/class/power_supply/*; do
        # -f $psrc/type not necessary - read_sysf() handles this
        psrc_name="${psrc##*/}"

        # ignore atypical power supplies and batteries
        printf '%s\n' "$psrc_name" | grep -E -q "$RE_PS_IGNORE" && continue

        case "$(read_sysf $psrc/type)" in
            Mains|USB)
                # AC detected
                # skip device to ignore incorrect AC status
                if [ "$TLP_PS_IGNORE" = "AC" ]; then
                    echo_debug "ps" "get_sys_power_supply(${psrc_name}).ac_ignored: syspwr=$_syspwr"
                    continue
                fi

                # check AC status
                if [ "$(read_sysf $psrc/online)" = "1" ]; then
                    # AC online --> end iteration
                    _syspwr=0
                    echo_debug "ps" "get_sys_power_supply(${psrc_name}).ac_online: syspwr=$_syspwr"
                    break
                else
                    # AC offline could mean battery, but quirky hardware exists
                    # --> remember and continue looking (for batteries)
                    ac0seen=1
                    echo_debug "ps" "get_sys_power_supply(${psrc_name}).ac_offline_remembered"
                fi
                ;;

            Battery)
                # battery detected
                # skip device to ignore incorrect battery status
                if [ "$TLP_PS_IGNORE" = "BAT" ]; then
                    echo_debug "ps" "get_sys_power_supply(${psrc_name}).bat_ignored: syspwr=$_syspwr"
                    continue
                fi

                # check battery status
                bs="$(read_sysf $psrc/status)"
                case "$bs" in
                    Discharging)
                        if ! lockpeek_tlp tlp_discharge; then
                            # battery status "Discharging" means battery mode ...
                            _syspwr=1
                            echo_debug "ps" "get_sys_power_supply(${psrc_name}).bat_discharging: syspwr=$_syspwr"
                        else
                            # ... unless forced discharge is in progress, which means AC
                            _syspwr=0
                            echo_debug "ps" "get_sys_power_supply(${psrc_name}).forced_discharge: syspwr=$_syspwr"
                        fi
                        break # --> end iteration
                        ;;

                    *) # everything else - e.g. "Charging", "Full", "Unknown" - might be caused by lagging
                       # battery status updates --> re-check after a short delay
                        echo_debug "ps" "get_sys_power_supply(${psrc_name}).bat_not_discharging_check1: bs=$bs; syspwr=$_syspwr"
                        sleep 0.8
                        bs="$(read_sysf $psrc/status)"
                        if [ "$bs" = "Discharging" ] && ! lockpeek_tlp tlp_discharge; then
                            # battery status changed to "Discharging" (not forced) means battery mode
                            # --> end iteration
                            _syspwr=1
                            echo_debug "ps" "get_sys_power_supply(${psrc_name}).bat_discharging_check2: bs=$bs; syspwr=$_syspwr"
                            break
                        else
                            # otherwise assume AC mode
                            # --> continue looking
                            _syspwr=0
                            echo_debug "ps" "get_sys_power_supply(${psrc_name}).bat_not_discharging_check2: bs=$bs; syspwr=$_syspwr"
                        fi
                        ;;

                esac
                ;;

            *) # unknown power source type --> ignore
                ;;
        esac
    done

    if [ -z "$_syspwr" ]; then
        # _syspwr result yet undecided
        if [ "$ac0seen" = "1" ]; then
            # AC offline remembered --> battery mode
            _syspwr=1
            echo_debug "ps" "get_sys_power_supply(${ac0seen##/*/}).ac_offline: syspwr=$_syspwr"
        else
            # we have seen neither a AC nor a battery power source --> unknown mode
            _syspwr=2
            echo_debug "ps" "get_sys_power_supply.none_found: syspwr=$_syspwr"
        fi
    fi

    return $_syspwr
}

get_persist_mode () { # get persistent operation mode
    # rc: 0=persistent/1=not persistent
    # retval: $_persist_mode (0=ac, 1=battery, none)
    local rc=1
    _persist_mode="none"

    if [ "$TLP_PERSISTENT_DEFAULT" = "1" ]; then
        # persistent mode = configured default mode
        case "$TLP_DEFAULT_MODE" in
            ac|AC)   _persist_mode=0; rc=0 ;;
            bat|BAT) _persist_mode=1; rc=0 ;;
        esac
    fi

    return $rc
}

get_power_mode () { # get current operation mode -- rc: 0=AC, 1=battery
    # similar to get_sys_power_supply(),
    # but maps unknown power source to TLP_DEFAULT_MODE or returns
    # persistent mode when enabled

    get_sys_power_supply
    local rc=$?

    if get_persist_mode; then
        # persistent mode
        rc=$_persist_mode
    else
        # non-persistent mode, use current power source
        if [ $rc -eq 2 ]; then
            # unknown power supply, use configured default mode
            case "$TLP_DEFAULT_MODE" in
                ac|AC)   rc=0 ;;
                bat|BAT) rc=1 ;;
                *)       rc=0 ;; # use AC if none or invalid mode
            esac
        fi
    fi

    return $rc
}

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

    # intercept invalid states
    case $1 in
        0|1) ;; # valid state
        *) # invalid new state --> return "different"
            echo_debug "ps" "compare_and_save_power_state($1).invalid"
            return 0
            ;;
    esac

    # read saved state
    lp=$(read_sysf $PWRRUNFILE)

    # compare
    if [ -z "$lp" ] || [ "$lp" != "$1" ]; then
        # saved state is nonexistent/empty or is different --> save new state
        create_rundir
        write_sysf "$1" $PWRRUNFILE
        echo_debug "ps" "compare_and_save_power_state($1).different: old=$lp"
        return 0
    else
        # touch file for last run
        touch $PWRRUNFILE
        echo_debug "ps" "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
}

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
}

echo_started_mode () { # print operation mode -- $1: 0=ac mode, 1=battery mode
    if [ "$1" = "0" ]; then
        printf "TLP started in AC mode"
    else
        printf "TLP started in battery mode"
    fi
    if [ "$_manual_mode" != "none" ]; then
        printf " (manual).\n"
    else
        printf " (auto).\n"
    fi

    return 0
}

set_manual_mode () { # set manual operation mode
    # $1: 0=ac mode, 1=battery mode
    # retval: $_manual_mode (0=ac, 1=battery, none=not found)

    create_rundir
    write_sysf "$1" $MANUALMODEFILE
    _manual_mode="$1"

    echo_debug "pm" "set_manual_mode($1)"
    return 0
}

clear_manual_mode () { # remove manual operation mode
    # retval: $_manual_mode (none)

    rm -f $MANUALMODEFILE 2> /dev/null
    _manual_mode="none"

    echo_debug "pm" "clear_manual_mode"
    return 0
}

get_manual_mode () { # get manual operation mode
    # rc: 0=ok/1=not found
    # retval: $_manual_mode (0=ac, 1=battery, none=not found)
    local rc=1
    _manual_mode="none"

    if [ -f $MANUALMODEFILE ]; then
        # read mode file
        _manual_mode=$(read_sysf $MANUALMODEFILE)
        case $_manual_mode in
            0|1) rc=0 ;;
            *) _manual_mode="none" ;;
        esac
    fi
    return $rc
}

# --- Misc Checks

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
}

