#!/bin/sh
# 05-thinkpad - Battery Plugin for ThinkPads using natacpi and/or
# tpacpi-bat/acpi-call for thresholds and forced discharge, i.e. X220/T420 and newer.
#
# Copyright (c) 2022 Thomas Koch <linrunner at gmx.net> and others.
# This software is licensed under the GPL v2 or later.

# Needs: tlp-func-base, 35-tlp-func-batt, tlp-func-stat

# shellcheck disable=SC2086

# --- Hardware Detection
readonly SMAPIBATDIR=/sys/devices/platform/smapi

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|L512|SL[345]00|X121e)$'

readonly MODINFO=modinfo
readonly MOD_TPSMAPI="tp_smapi"
readonly MOD_TPACPI="acpi_call"

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

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

    _tpmodel=""

    if [ -d $TPACPID ]; 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
        # ThinkPad
        echo_debug "bat" "check_thinkpad: tpmodel=$_tpmodel"
        return 0
    else
        # not a ThinkPad
        echo_debug "bat" "check_thinkpad.not_a_thinkpad: model=$pv"
        return 1
    fi
}

# --- Plugin API functions

batdrv_init () {
    # detect hardware and initialize driver
    # rc: 0=matching hardware detected/1=not detected/2=no batteries detected
    # retval: $_batdrv_plugin, $_batdrv_kmod
    #
    # 1. check for native kernel acpi (Linux 4.19 or higher required)
    #    --> retval $_natacpi:
    #       0=thresholds and discharge/
    #       1=thresholds only/
    #       32=disabled/
    #       128=no kernel support/
    #       254=ThinkPad not supported
    #
    # 2. check for acpi-call external kernel module and test with integrated
    #    tpacpi-bat [ThinkPads only]
    #    --> retval $_tpacpi:
    #       0=thresholds and discharge/
    #       1=thresholds only/
    #       32=disabled/
    #       64=acpi_call module not loaded/
    #       127=tpacpi-bat not installed/
    #       128=acpi_call module not installed/
    #       137=kernel error (oops)/
    #       253=tpacpi-bat error/
    #       254=ThinkPad not supported/
    #       255=superseded by natacpi/
    #       256=superseded and kernel module not loaded
    #
    # 3. check for tp-smapi external kernel module
    #    --> retval $_tpsmapi:
    #       1=readonly/
    #       32=disabled/
    #       64=tp_smapi module not loaded/
    #       128=tp_smapi module not installed
    #
    # 4. determine best method for
    #    reading battery data                   --> retval $_bm_read,
    #    reading/writing charging thresholds    --> retval $_bm_thresh,
    #    reading/writing force discharge        --> retval $_bm_dischg:
    #       none/natacpi/tpacpi/tpsmapi
    #
    # 5. determine sysfile basenames for natacpi
    #    start threshold                        --> retval $_bn_start,
    #    stop threshold                         --> retval $_bn_stop,
    #    force discharge                        --> retval $_bn_dischg;
    #
    # 6. determine present batteries
    #    list of batteries (space separated)    --> retval $_batteries;
    #
    # 7. define charge threshold defaults
    #    start threshold                        --> retval $_bt_def_start,
    #    stop threshold                         --> retval $_bt_def_stop;

    _batdrv_plugin="thinkpad"
    _batdrv_kmod="thinkpad_acpi"  # kernel module for natacpi

    if [ -n "$X_BAT_PLUGIN_SIMULATE" ]; then
        if [ "$X_BAT_PLUGIN_SIMULATE" = "$_batdrv_plugin" ]; then
            echo_debug "bat" "batdrv_init.${_batdrv_plugin}.simulate"
        else
            echo_debug "bat" "batdrv_init.${_batdrv_plugin}.simulate_skip"
            return 1
        fi
    elif wordinlist "$_batdrv_plugin" "$X_BAT_PLUGIN_DENYLIST"; then
        echo_debug "bat" "batdrv_init.${_batdrv_plugin}.denylist"
        return 1
    else
        # check if ThinkPad
        if ! check_thinkpad; then
            echo_debug "bat" "batdrv_init.${_batdrv_plugin}.not_a_thinkpad"
            return 1
        elif supports_no_tp_bat_funcs; then
            echo_debug "bat" "batdrv_init.${_batdrv_plugin}.unsupported_model"
            return 1
        fi
    fi

    # presume no features at all
    _natacpi=128
    _tpacpi=255
    _tpsmapi=254
    _bm_read="natacpi"
    _bm_thresh="none"
    _bm_dischg="none"
    _bn_start=""
    _bn_stop=""
    _bn_dischg=""
    _batteries=""
    # shellcheck disable=SC2034
    _bt_def_start=96
    # shellcheck disable=SC2034
    _bt_def_stop=100

    # --- 1. iterate batteries and check for native kernel ACPI
    local bd bs
    local done=0
    for bd in "$ACPIBATDIR"/BAT[01]; do
        if [ "$(read_sysf $bd/present)" = "1" ]; then
            # record detected batteries and directories
            bs=${bd##/*/}
            if [ -n "$_batteries" ]; then
                _batteries="$_batteries $bs"
            else
                _batteries="$bs"
            fi
            # skip natacpi detection for 2nd and subsequent batteries
            [ $done -eq 1 ] && continue

            done=1
            if [ "$NATACPI_ENABLE" = "0" ]; then
                # natacpi disabled in configuration --> skip actual detection
                _natacpi=32
                continue
            fi

            if [ -f $bd/charge_control_start_threshold ] \
                && [ -f $bd/charge_control_end_threshold ]; then
                # sysfiles for thresholds exist (kernel 5.9 and newer)
                _bn_start="charge_control_start_threshold"
                _bn_stop="charge_control_end_threshold"
                _natacpi=254
            elif [ -f $bd/charge_start_threshold ] \
                && [ -f $bd/charge_stop_threshold ]; then
                # sysfiles for thresholds exist (kernel 4.17 and newer)
                _bn_start="charge_start_threshold"
                _bn_stop="charge_stop_threshold"
                _natacpi=254
            else
                # nothing detected
                _natacpi=128
                continue
            fi

            if readable_sysf $bd/$_bn_start \
               && readable_sysf $bd/$_bn_stop; then
                # start/stop thresholds are actually readable
                _natacpi=1
                _bm_thresh="natacpi"

                if readable_sysf $bd/charge_behaviour; then
                    # sysfile for force-discharge exists and is actually readable
                    _natacpi=0
                    _bm_dischg="natacpi"
                    _bn_dischg="charge_behaviour"
                fi
            fi
        fi
    done

    # quit if no battery detected, there is no point in activating the plugin
    if [ -z "$_batteries" ]; then
        echo_debug "bat" "batdrv_init.${_batdrv_plugin}.no_batteries"
        return 2
    fi

    # Consider legacy ThinkPads with Coreboot/natacpi
    if supports_tpsmapi_only && [ $_natacpi -ge 32 ]; then
        # no natacpi --> do not probe acpi_call/tpacpi-bat but quit
        echo_debug "bat" "batdrv_init.${_batdrv_plugin}.no_natacpi: batteries=$_batteries; natacpi=$_natacpi; tpacpi=$_tpacpi; tpsmapi=$_tpsmapi"
        return 1
    fi

    # --- 2. probe acpi_call external kernel module and test with integrated tpacpi-bat
    if [ $_natacpi -gt 0 ]; then
        load_modules $MOD_TPACPI

        if [ ! -e /proc/acpi/call ]; then
        # call API not present
            if $MODINFO $MOD_TPACPI > /dev/null 2>&1; then
                # module installed but not loaded
                _tpacpi=64
            else
                # module neither installed nor builtin
                _tpacpi=128
            fi
        else
            # call API present --> try tpacpi-bat
            if $TPACPIBAT -g ST 1 > /dev/null 2>&1; then
                # thresholds capable
                _tpacpi=1
                if $TPACPIBAT -g FD 1 > /dev/null 2>&1; then
                    # force_discharge capable
                    _tpacpi=0
                fi
            else
                # tpacpi-bat failed
                case $? in
                    255) # acpi call non-existent (AE_NOT_FOUND)
                        _tpacpi=254
                        ;;

                    137) # kernel error (oops)
                        _tpacpi=137
                        ;;

                    * ) # tpacpi-bat error
                        _tpacpi=253
                        ;;
                esac
            fi

            if [ $_tpacpi -le 1 ]; then
                # tpacpi capable
                if [ "$TPACPI_ENABLE" = "0" ]; then
                    # disabled by configuration
                    _tpacpi=32
                else
                    # not disabled
                    case $_natacpi in
                        1) # use for discharge only
                            [ $_tpacpi -eq 0 ] && _bm_dischg="tpacpi"
                            ;;

                        *) # use for thresholds and discharge
                            _bm_thresh="tpacpi"
                            [ $_tpacpi -eq 0 ] && _bm_dischg="tpacpi"
                            ;;
                    esac
                fi
            fi
        fi
    elif [ ! -e /proc/acpi/call ]; then
         # superseded and kernel module not loaded
        _tpacpi=256
    fi

    # --- 3. probe tp-smapi external kernel module (relevant models only)
    if supports_tpsmapi_and_tpacpi; then
        load_modules $MOD_TPSMAPI

        if [ -d $SMAPIBATDIR ]; then
            # module loaded --> tp-smapi available
            if [ "$TPSMAPI_ENABLE" = "0" ]; then
                # tpsmapi disabled by configuration
                _tpsmapi=32
            else
                # reading battery data via tpsmapi is preferred over natacpi
                # because it provides cycle count and more
                _tpsmapi=1
                _bm_read="tpsmapi"
            fi
        elif $MODINFO $MOD_TPSMAPI > /dev/null 2>&1; then
            # module installed but not loaded
            _tpsmapi=64
        else
            # module neither installed nor builtin
            _tpsmapi=128
        fi
    fi

    # shellcheck disable=SC2034
    _batdrv_selected=$_batdrv_plugin
    echo_debug "bat" "batdrv_init.${_batdrv_plugin}: batteries=$_batteries; natacpi=$_natacpi; tpacpi=$_tpacpi; tpsmapi=$_tpsmapi"
    echo_debug "bat" "batdrv_init.${_batdrv_plugin}: read=$_bm_read; thresh=$_bm_thresh; bn_start=$_bn_start; bn_stop=$_bn_stop; dischg=$_bm_dischg; bn_dischg=$_bn_dischg"
    return 0
}

batdrv_select_battery () {
    # determine battery sysfiles and tpacpi-bat index
    # $1: BAT0/BAT1/DEF
    # global param: $_bm_read
    # rc: 0=bat exists/1=bat non-existent
    # retval: $_bat_str:   BAT0/BAT1;
    #         $_bat_idx:   1/2;
    #         $_bd_read:   directory with battery data sysfiles;
    #         $_bf_start:  sysfile for start threshold;
    #         $_bf_stop:   sysfile for stop threshold;
    #         $_bf_dischg: sysfile for force discharge
    # prerequisite: batdrv_init()

    # defaults
    _bat_idx=0    # no index
    _bat_str=""   # no bat
    _bd_read=""   # no directories
    _bf_start=""
    _bf_stop=""
    _bf_dischg=""

    # validate battery param
    local bs
    case $1 in
        DEF) # 1st battery is default
            bs="${_batteries%% *}"
            ;;

        *)
            if wordinlist "$1" "$_batteries"; then
                bs=$1
            else
                # battery not present --> quit
                echo_debug "bat" "batdrv.${_batdrv_plugin}.select_battery($1).not_present"
                return 1
            fi
            ;;
    esac

    # determine bat index for tpacpi and main/aux distinction
    case $bs in
        BAT0)
            _bat_str="$bs"
            # BAT0 is always assumed main battery
            _bat_idx=1
            ;;

        BAT1)
            _bat_str="$bs"
            if [ $_tpacpi -le 1 ]; then
                # tpacpi: try to read start threshold for index 2
                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
            else
                # without tpacpi: BAT1 is aux
                _bat_idx=2
            fi
            ;;
    esac

    # determine natacpi sysfiles
    if [ "$_bm_thresh" = "natacpi" ]; then
        _bf_start="$ACPIBATDIR/$bs/$_bn_start"
        _bf_stop="$ACPIBATDIR/$bs/$_bn_stop"
    fi

    if [ "$_bm_dischg" = "natacpi" ]; then
        _bf_dischg="$ACPIBATDIR/$bs/$_bn_dischg"
    fi

    case "$_bm_read" in
        natacpi) _bd_read="$ACPIBATDIR/$bs" ;;
        tpsmapi) _bd_read="$SMAPIBATDIR/$bs" ;;
    esac

    echo_debug "bat" "batdrv.${_batdrv_plugin}.select_battery($1): bat_str=$_bat_str; bat_idx=$_bat_idx; bd_read=$_bd_read; bf_start=$_bf_start; bf_stop=$_bf_stop; bf_dischg=$_bf_dischg"
    return 0
}

batdrv_read_threshold () {
    # read and print charge threshold
    # $1: start/stop
    # $2: 0=api/1=tlp-stat output
    # global param: $_bm_thresh, $_bf_start, $_bf_stop, $_bat_idx
    # out:
    # - api: 0..100/"" on error
    # - tlp-stat: 0..100/"(not available)" on error
    # rc: 0=ok/4=read error/255=no api
    # prerequisite: batdrv_init(), batdrv_select_battery()

    local bf out="" rc=0

    case $1 in
        start) out="$X_THRESH_SIMULATE_START" ;;
        stop)  out="$X_THRESH_SIMULATE_STOP"  ;;
    esac
    if [ -n "$out" ]; then
        printf "%s" "$out"
        echo_debug "bat" "batdrv.${_batdrv_plugin}.read_threshold($1).simulate: bm_thresh=$_bm_thresh; bf=$bf; bat_idx=$_bat_idx; out=$out; rc=$rc"
        return 0
    fi

    case $_bm_thresh in
        natacpi)
            # read threshold from sysfile
            case $1 in
                start) bf=$_bf_start ;;
                stop)  bf=$_bf_stop  ;;
            esac
            if ! out=$(read_sysf $bf); then
                # not readable/non-existent
                if [ "$2" != "1" ]; then
                    out=""
                else
                    out="(not available)"
                fi
                rc=4
            fi
            # workaround: read threshold sysfile a second time to mitigate
            # the annoying firmware issue on ThinkPad A/E/L/S/X series
            # (refer to issue #369 and FAQ)
            if ! out=$(read_sysf $bf); then
                # not readable/non-existent
                if [ "$2" != "1" ]; then
                    out=""
                else
                    out="(not available)"
                fi
                rc=4
            fi
            ;;

        tpacpi)
            case $1 in
                start)
                    out=$($TPACPIBAT -g ST $_bat_idx 2> /dev/null | cut -f1 -d' ')
                    # workaround: read threshold a second time (see above)
                    out=$($TPACPIBAT -g ST $_bat_idx 2> /dev/null | cut -f1 -d' ')
                    ;;

                stop)
                    out=$($TPACPIBAT -g SP $_bat_idx 2> /dev/null | cut -f1 -d' ')
                    # workaround: read threshold a second time (see above)
                    out=$($TPACPIBAT -g SP $_bat_idx 2> /dev/null | cut -f1 -d' ')
                    ;;
            esac
            # shellcheck disable=SC2181
            if [ $? -eq 0 ] && is_uint "$out"; then
                if [ $out -ge 128 ]; then
                    # remove offset of 128 for Edge S430 et al.
                    out=$((out - 128))
                fi
                if [ "$1" = "stop" ] && [ $out -eq 0 ]; then
                    # stop: 0 (hardware default) means 100
                    out=100
                fi
            else
                if [ "$2" != "1" ]; then
                    out=""
                else
                    out="(not available)"
                fi
                rc=4
            fi
            ;;

        *) # no threshold api
            rc=255
            ;;
    esac

    # "return" threshold
    if [ "$X_THRESH_SIMULATE_READERR" != "1" ]; then
        printf "%s" "$out"
    else
        if [ "$2" = "1" ]; then
            printf "(not available)\n"
        fi
        rc=4
    fi

    echo_debug "bat" "batdrv.${_batdrv_plugin}.read_threshold($1): bm_thresh=$_bm_thresh; bf=$bf; bat_idx=$_bat_idx; out=$out; rc=$rc"
    return $rc
}

batdrv_write_thresholds () {
    # write both charge thresholds for a battery
    # use pre-determined method and sysfiles from global parms
    # $1: new start threshold 0(disabled)..99/DEF(default)
    # $2: new stop threshold 1..100/DEF(default)
    # $3: 0=quiet/1=output parameter errors/2=output progress and errors
    # $4: battery - non-empty string indicates thresholds stem from configuration
    # global param: $_bm_thresh, $_bat_str, $_bat_idx, $_bf_start, $_bf_stop
    # rc: 0=ok/
    #     1=not configured/
    #     2=threshold(s) out of range or non-numeric/
    #     3=minimum start stop diff violated/
    #     4=threshold read error/
    #     5=threshold write error
    # prerequisite: batdrv_init(), batdrv_select_battery()
    local new_start=${1:-}
    local new_stop=${2:-}
    local verb=${3:-0}
    local cfg_bat="$4"
    local old_start old_stop

    # insert defaults
    [ "$new_start" = "DEF" ] && new_start=$_bt_def_start
    [ "$new_stop" = "DEF" ] && new_stop=$_bt_def_stop

    # --- validate thresholds
    local rc

    if [ -n "$cfg_bat" ] && [ -z "$new_start" ] && [ -z "$new_stop" ]; then
        # do nothing if unconfigured
        echo_debug "bat" "batdrv.${_batdrv_plugin}.write_thresholds($1, $2, $3, $4).not_configured: bat=$_bat_str"
        return 1
    fi

    # start: check for 3 digits max, ensure min 0 / max 99
    if ! is_uint "$new_start" 3 || \
       ! is_within_bounds $new_start 0 99; then
        # threshold out of range
        echo_debug "bat" "batdrv.${_batdrv_plugin}.write_thresholds($1, $2, $3, $4).invalid_start: bat=$_bat_str"
        case $verb in
            1)
                if [ -n "$cfg_bat" ]; then
                    echo_message "Error in configuration at START_CHARGE_THRESH_${cfg_bat}=\"${new_start}\": not specified, invalid or out of range (0..99). Battery skipped."
                fi
                ;;

            2)
                if [ -n "$cfg_bat" ]; then
                    printf "Error in configuration at START_CHARGE_THRESH_%s=\"%s\": not specified, invalid or out of range (0..99). Aborted.\n" "$cfg_bat" "$new_start" 1>&2
                else
                    printf "Error: charge start threshold (%s) for %s is not specified, invalid or out of range (0..99). Aborted.\n" "$new_start" "$_bat_str" 1>&2
                fi
                ;;
        esac
        return 2
    fi

    # stop: check for 3 digits max, ensure min 1 / max 100
    if ! is_uint "$new_stop" 3 || \
       ! is_within_bounds $new_stop 1 100; then
        # threshold out of range
        echo_debug "bat" "batdrv.${_batdrv_plugin}.write_thresholds($1, $2, $3, $4).invalid_stop: bat=$_bat_str"
        case $verb in
            1)
                if [ -n "$cfg_bat" ]; then
                    echo_message "Error in configuration at STOP_CHARGE_THRESH_${cfg_bat}=\"${new_stop}\": not specified, invalid or out of range (1..100). Battery skipped."
                fi
                ;;

            2)
                if [ -n "$cfg_bat" ]; then
                    printf "Error in configuration at STOP_CHARGE_THRESH_%s=\"%s\": not specified, invalid or out of range (1..100). Aborted.\n" "$cfg_bat" "$new_stop" 1>&2
                else
                    printf "Error: charge stop threshold (%s) for %s is not specified, invalid or out of range (1..100). Aborted.\n" "$new_stop" "$_bat_str" 1>&2
                fi
                ;;
        esac
        return 2
    fi

    # check if start < stop
    if [ $new_start -ge $new_stop ]; then
        echo_debug "bat" "batdrv.${_batdrv_plugin}.write_thresholds($1, $2, $3, $4).invalid_diff: bat=$_bat_str"
        case $verb in
            1)
                if [ -n "$cfg_bat" ]; then
                    echo_message "Error in configuration: START_CHARGE_THRESH_${cfg_bat} >= STOP_CHARGE_THRESH_${cfg_bat}. Battery skipped."
                fi
                ;;

            2)
                if [ -n "$cfg_bat" ]; then
                    printf "Error in configuration: START_CHARGE_THRESH_%s >= STOP_CHARGE_THRESH_%s. Aborted.\n" "$cfg_bat" "$cfg_bat" 1>&2
                else
                    printf "Error: start threshold >= stop threshold for %s. Aborted.\n" "$_bat_str" 1>&2
                fi
                ;;
        esac
        return 3
    fi

    # read active threshold values
    if ! old_start=$(batdrv_read_threshold start 0) || \
       ! old_stop=$(batdrv_read_threshold stop 0); then
        echo_debug "bat" "batdrv.${_batdrv_plugin}.write_thresholds($1, $2, $3, $4).read_error: bat=$_bat_str"
        case $verb in
            1) echo_message "Warning: could not read current charge threshold(s) for $_bat_str. Battery skipped." ;;
            2) printf "Error: could not read current charge threshold(s) for %s. Aborted.\n" "$_bat_str" 1>&2 ;;
        esac
        return 4
    fi

    if [ $old_start -ge $old_stop ]; then
        # invalid threshold reading, happens on ThinkPad E/L series
        old_start="none"
        old_stop="none"
    fi

    # determine write sequence because driver's intrinsic boundary conditions
    # must be met in all write stages:
    #   - natacpi: start < stop (write fails if not met)
    #   - tpacpi:  nothing (maybe BIOS/ECP enforces something)
    local rc=0 steprc tseq

    if [ "$old_stop" != "none" ] && [ $new_start -gt $old_stop ]; then
        tseq="stop start"
    else
        tseq="start stop"
    fi

    # write new thresholds in determined sequence
    if [ "$verb" = "2" ]; then
        printf "Setting temporary charge thresholds for %s:\n" "$_bat_str"
    fi

    for step in $tseq; do
        local old_thresh new_thresh steprc

        case $step in
            start)
                old_thresh=$old_start
                new_thresh=$new_start
                ;;

            stop)
                old_thresh=$old_stop
                new_thresh=$new_stop
                ;;
        esac

        if [ "$old_thresh" != "$new_thresh" ]; then
            # new threshold differs from effective one --> write it
            case $_bm_thresh in
                natacpi)
                    case $step in
                        start) write_sysf "$new_thresh" $_bf_start ;;
                        stop)  write_sysf "$new_thresh" $_bf_stop  ;;
                    esac
                    steprc=$?; [ $steprc -ne 0 ] && [ $rc -eq 0 ] && rc=5
                    ;;

                tpacpi)
                    case $step in
                        start) $TPACPIBAT -s ST $_bat_idx $new_thresh > /dev/null 2>&1 ;;
                        stop)
                            if [ $new_thresh -eq 100 ]; then
                                # tpacpi-bat won't accept 100
                                $TPACPIBAT -s SP $_bat_idx 0 > /dev/null 2>&1
                            else
                                $TPACPIBAT -s SP $_bat_idx $new_thresh > /dev/null 2>&1
                            fi
                            ;;
                    esac
                    steprc=$?; [ $steprc -ne 0 ] && rc=5
                    ;;

                *) # no threshold api
                    steprc=255
                    rc=5
                    ;;
            esac
            echo_debug "bat" "batdrv.${_batdrv_plugin}.write_thresholds($1, $2, $3, $4).$step.write: bat=$_bat_str; old=$old_thresh; new=$new_thresh; steprc=$steprc"

            case $verb in
                2)
                    if [ $steprc -eq 0 ]; then
                        if [ "$step" = "start" ] && [ $new_thresh -eq 0 ]; then
                            printf "  %-5s = %3d (disabled)\n" $step $new_thresh
                        else
                            printf "  %-5s = %3d\n" $step $new_thresh
                        fi
                    else
                        printf "  %-5s = %3d (Error: write failed)\n" $step $new_thresh 1>&2
                    fi
                    ;;
                1)
                    if [ $steprc -gt 0 ]; then
                        echo_message "Error: writing charge $step threshold for $_bat_str failed."
                    fi
                    ;;
            esac
        else
            echo_debug "bat" "batdrv.${_batdrv_plugin}.write_thresholds($1, $2, $3, $4).$step.no_change: bat=$_bat_str; old=$old_thresh; new=$new_thresh"

            if [ "$verb" = "2" ]; then
                printf "  %-5s = %3d (no change)\n" $step $new_thresh
            fi
        fi
    done # for step

    echo_debug "bat" "batdrv.${_batdrv_plugin}.write_thresholds($1, $2, $3, $4).complete: bat=$_bat_str; rc=$rc"
    return $rc
}

batdrv_chargeonce () {
    # charge battery to stop threshold once
    # use pre-determined method and sysfiles from global parms
    # global param: $_bm_thresh, $_bat_str, $_bat_idx, $_bf_start, $_bf_stop
    # rc: 0=ok/
    #     2=charge level read error
    #     3=charge level too high/
    #     4=threshold read error/
    #     5=threshold write error/
    # prerequisite: batdrv_init(), batdrv_select_battery()

    local ccharge cur_stop efull enow temp_start
    local rc=0

    if ! cur_stop=$(batdrv_read_threshold stop 0); then
        printf "Error: reading charge stop threshold for %s failed. Aborted.\n" $_bat_str 1>&2
        echo_debug "bat" "batdrv.${_batdrv_plugin}.chargeonce($_bat_str).thresh_unknown: stop=$cur_stop; rc=4"
        return 4
    fi

    # get current charge level (in %)
    case $_bm_read in
        natacpi) # use ACPI sysfiles
            if [ -f $_bd_read/energy_full ]; then
                efull=$(read_sysval $_bd_read/energy_full)
                enow=$(read_sysval $_bd_read/energy_now)
            fi

            if is_uint "$enow" && is_uint "$efull" && [ $efull -ne 0 ]; then
                # calculate charge level rounded to integer
                ccharge=$(perl -e 'printf("%.0f\n", 100.0 * '$enow' / '$efull')')
            else
                ccharge=-1
            fi
            ;;

        tpsmapi) # use tp-smapi sysfile
            ccharge=$(read_sysval $_bd_read/remaining_percent) || ccharge=-1
            ;;
    esac

    if [ $ccharge -eq -1 ]; then
        printf "Error: cannot determine charge level for %s.\n" "$_bat_str" 1>&2
        echo_debug "bat" "batdrv.${_batdrv_plugin}.chargeonce($_bat_str).charge_level_unknown: enow=$enow; efull=$efull; rc=2"
        return 2
    fi

    temp_start=$(( cur_stop - 1 ))
    if [ $ccharge -gt $temp_start ]; then
        printf "Error: the %s charge level is %s%%. " "$_bat_str" "$ccharge"  1>&2
        printf "For this command to work, it must not exceed %s%% (stop threshold - 1).\n" "$temp_start" 1>&2
        echo_debug "bat" "batdrv.${_batdrv_plugin}.chargeonce($_bat_str).charge_level_too_high: soc=$ccharge; stop=$cur_stop; rc=3"
        return 3
    fi

    printf "Setting temporary charge threshold for %s:\n" "$_bat_str"
    case $_bm_thresh in
        natacpi)
            write_sysf "$temp_start" $_bf_start || rc=5
            ;;

        tpacpi)
            $TPACPIBAT -s ST $_bat_idx $temp_start > /dev/null 2>&1 || rc=5
            ;;
    esac

    if [ $rc -eq 0 ]; then
        printf "  start = %3d\n" $temp_start
    else
        printf "  start = %3d (Error: write failed)\n" $temp_start 1>&2
    fi
    echo_debug "bat" "batdrv.${_batdrv_plugin}.chargeonce($_bat_str): soc=$ccharge; start=$temp_start; stop=$cur_stop; rc=$rc"

    return $rc
}

batdrv_apply_configured_thresholds () {
    # apply configured thresholds from configuration to all batteries
    # output parameter errors only

    if batdrv_select_battery BAT0; then
        batdrv_write_thresholds "$START_CHARGE_THRESH_BAT0" "$STOP_CHARGE_THRESH_BAT0" 1 "BAT0"; rc=$?
    fi
    if batdrv_select_battery BAT1; then
        # write configured thresholds, output parameter errors
        batdrv_write_thresholds "$START_CHARGE_THRESH_BAT1" "$STOP_CHARGE_THRESH_BAT1" 1 "BAT1"; rc=$?
    fi

    return 0
}

batdrv_read_force_discharge () {
    # read and print force-discharge state
    # $1: 0=api/1=tlp-stat output
    # global param: $_bm_dischg, $_bf_dischg, $_bat_idx
    # out:
    # - api: 0=off/1=on/"" on error
    # - tlp-stat: status text/"(not available)" on error
    # rc: 0=ok/4=read error/255=no api
    # prerequisite: batdrv_init(), batdrv_select_battery()

    local rc=0 out=""

    case $_bm_dischg in
        natacpi)
            # read state from sysfile
            if out=$(read_sysf $_bf_dischg); then
                if [ "$1" != "1" ]; then
                    if echo "$out" | grep -q "\[force-discharge\]"; then
                        out=1
                    else
                        out=0
                    fi
                fi
            else
                # not readable/non-existent
                if [ "$1" != "1" ]; then
                    out=""
                else
                    out="(not available)"
                fi
                rc=4
            fi
            ;;

        tpacpi) # read via tpacpi-bat
            case $($TPACPIBAT -g FD $_bat_idx 2> /dev/null) in
                yes) out=1 ;;
                no)  out=0 ;;
                *)
                    if [ "$1" != "1" ]; then
                        out=""
                    else
                        out="(not available)"
                    fi
                    rc=4
                    ;;
            esac
            ;;

        *) # no discharge api
            if [ "$1" = "1" ]; then
                out="(not available)"
            fi
            rc=255
            ;;
    esac
    printf "%s" "$out"

    echo_debug "bat" "batdrv.${_batdrv_plugin}.read_force_discharge($_bat_str): bm_dischg=$_bm_dischg; bf_dischg=$_bf_dischg; bat_idx=$_bat_idx; out=$out; rc=$rc"
    return $rc
}

batdrv_write_force_discharge () {
    # write force discharge state
    # $1: 0=off/1=on
    # global param: $_bat_str, $_bat_idx, $_bm_dischg, $_bf_dischg
    # rc: 0=done/5=write error/255=no api
    # prerequisite: batdrv_init(), batdrv_select_battery()

    local rc=0

    case $_bm_dischg in
        natacpi)
            # write force_discharge
            case "$1" in
                0) write_sysf "auto" $_bf_dischg || rc=5 ;;
                1) write_sysf "force-discharge" $_bf_dischg || rc=5 ;;
            esac
            ;; # natacpi

        tpacpi) # use tpacpi-bat
            $TPACPIBAT -s FD $_bat_idx $1 > /dev/null 2>&1 || rc=5
            ;; # tpcpaci

        *) # no discharge api
            rc=255
            ;;
    esac

    echo_debug "bat" "batdrv.${_batdrv_plugin}.write_force_discharge($_bat_str, $1): bm_dischg=$_bm_dischg; bf_dischg=$_bf_dischg; bat_idx=$_bat_idx; rc=$rc"
    return $rc
}

batdrv_cancel_force_discharge () {
    # trap: called from batdrv_discharge
    # global param: $_bat_str
    # prerequisite: batdrv_discharge()

    batdrv_write_force_discharge 0
    unlock_tlp tlp_discharge
    echo_debug "bat" "batdrv.${_batdrv_plugin}.discharge.cancelled($_bat_str)"
    printf " Cancelled.\n"

    do_exit 0
}

batdrv_force_discharge_active () { # check if battery is in 'force_discharge' state
    # global param: $_bat_str, $_bm_read, $_bd_read
    # rc: 0=discharging/1=not discharging/2=AC detached/255=no api
    # prerequisite: batdrv_init(), batdrv_select_battery()

    local bsys rc=0 st

    if [ "$(batdrv_read_force_discharge 0)" = "0" ]; then
        # force_discharge is off --> quit
        echo_debug "bat" "batdrv.${_batdrv_plugin}.force_discharge_active($_bat_str): rc=1"
        return 1
    fi

    if ! get_sys_power_supply; then
        # AC detached --> cancel discharge
        rc=2
    else
        # force_discharge is still on, but quirky firmware (e.g. ThinkPad E-series)
        # may keep force_discharge on --> check battery status sysfile too
        case $_bm_read in
            natacpi)
                bsys=$_bd_read/status # use ACPI sysfile
                ;;

            tpsmapi)
                bsys=$_bd_read/state # use tpsmapi sysfile
                ;;

            *) # no read api
                bsys=""
                rc=255
                ;;
        esac

        # read battery state
        if [ -f "$bsys" ]; then
            st="$(read_sysf $bsys)"
            case "$st" in
                [Dd]ischarging) rc=0 ;;
                *) rc=1 ;;
            esac
        fi
    fi

    echo_debug "bat" "batdrv.${_batdrv_plugin}.force_discharge_active($_bat_str): bm_read=$_bm_read; bf=$bsys; st=$st; rc=$rc"
    return $rc
}

batdrv_discharge () {
    # discharge battery
    # global param: $_bm_dischg, $_bat_idx, $_bf_dischg
    # rc: 0=done/1=malfunction/2=not emptied/3=ac removed/255=no api
    # prerequisite: batdrv_init(), batdrv_select_battery()

    local en ef pn rc rp wt

    # start discharge
    if ! batdrv_write_force_discharge 1; then
        echo_debug "bat" "batdrv.${_batdrv_plugin}.discharge.force_discharge_malfunction($_bat_str)"
        echo "Error: discharge $_bat_str malfunction -- check your hardware (battery, charger)." 1>&2
        return 1
    fi

    trap batdrv_cancel_force_discharge INT # enable ^C hook
    rc=0; rp=0

    # wait for start == while status not "discharging" -- 15.0 sec timeout
    printf "Initiating discharge of battery %s " $_bat_str
    wt=15
    while ! batdrv_force_discharge_active && [ $wt -gt 0 ] ; do
        sleep 1
        printf "."
        wt=$((wt - 1))
    done
    printf "\n"

    if batdrv_force_discharge_active; then
        # discharge initiated sucessfully --> wait for completion == while status "discharging"
        echo_debug "bat" "batdrv.${_batdrv_plugin}.discharge.running($_bat_str)"

        while batdrv_force_discharge_active; do
            clear
            printf "Currently discharging battery %s:\n" "$_bat_str"

            # show current battery state
            case $_bm_read in
                natacpi|tpacpi) # use ACPI sysfiles
                    perl -e 'printf ("voltage            = %6d [mV]\n", '"$(read_sysval $_bd_read/voltage_now)"' / 1000.0);'

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

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

                    pn=$(read_sysval $_bd_read/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
                    printf "state              = %s\n"  "$(read_sysf $_bd_read/status)"
                    ;; # natacpi, tpsmapi

                tpsmapi) # use tp-smapi sysfiles
                    printf "voltage            = %6s [mV]\n"  "$(read_sysf $_bd_read/voltage)"
                    printf "remaining capacity = %6s [mWh]\n" "$(read_sysf $_bd_read/remaining_capacity)"
                    rp=$(read_sysf $_bd_read/remaining_percent)
                    printf "remaining percent  = %6s [%%]\n"  "$rp"
                    printf "remaining time     = %6s [min]\n" "$(read_sysf $_bd_read/remaining_running_time_now)"
                    printf "power              = %6s [mW]\n"  "$(read_sysf $_bd_read/power_avg)"
                    printf "state              = %s\n"  "$(read_sysf $_bd_read/state)"
                    ;; # tpsmapi

            esac
            printf "force-discharge    = %s\n"  "$(batdrv_read_force_discharge 0)"

            echo "Press Ctrl+C to cancel."
            sleep 5
        done
        unlock_tlp tlp_discharge

        # read charge level one last time
        case $_bm_read in
            natacpi|tpacpi) # use ACPI sysfiles
                en=$(read_sysval $_bd_read/energy_now)
                ef=$(read_sysval $_bd_read/energy_full)
                if [ "$ef" != "0" ]; then
                    rp=$(perl -e 'printf ("%d", 100.0 * '$en' / '$ef' );')
                else
                    rp=0
                fi
                ;; # natacpi, tpsmapi

            tpsmapi) # use tp-smapi sysfiles
                rp=$(read_sysf $_bd_read/remaining_percent)
                ;;

        esac

        if [ $rp -gt 0 ]; then
            # battery not emptied --> determine cause
            get_sys_power_supply
            # shellcheck disable=SC2154
            if [ $_syspwr -eq 1 ]; then
                # system on battery --> AC power removed
                echo_debug "bat" "batdrv.${_batdrv_plugin}.discharge.ac_removed($_bat_str)"
                echo "Warning: battery $_bat_str was not discharged completely -- AC/charger removed." 1>&2
                rc=3
            else
                # discharging terminated by unknown reason
                echo_debug "bat" "batdrv.${_batdrv_plugin}.discharge.not_emptied($_bat_str)"
                echo "Error: battery $_bat_str was not discharged completely i.e. terminated by the firmware -- check your hardware (battery, charger)." 1>&2
                rc=2
            fi
        fi
    else
        # discharge malfunction --> cancel discharge and abort
        batdrv_write_force_discharge 0
        echo_debug "bat" "batdrv.${_batdrv_plugin}.discharge.malfunction($_bat_str)"
        echo "Error: discharge $_bat_str malfunction -- check your hardware (battery, charger)." 1>&2
        rc=1
    fi

    trap - INT # remove ^C hook

    # ThinkPad E-series firmware may keep force_discharge active --> cancel it
    [ "$(batdrv_read_force_discharge 0)" = "1" ] && batdrv_write_force_discharge 0

    if [ $rc -eq 0 ]; then
        echo
        echo "Done: battery $_bat_str was completely discharged."
        echo_debug "bat" "batdrv.${_batdrv_plugin}.discharge.complete($_bat_str)"
    fi
    return $rc
}

batdrv_show_battery_data () {
    # output battery status
    # $1: 1=verbose
    # global param: $_batteries
    # prerequisite: batdrv_init()
    local verbose=${1:-0}

    printf "+++ Battery Care\n"
    printf "Plugin: %s\n" "$_batdrv_plugin"

    local fs=""
    [ "$_bm_thresh" != "none" ] && fs="charge thresholds"
    if [ "$_bm_dischg" != "none" ]; then
        [ -n "$fs" ] && fs="${fs}, "
        fs="${fs}recalibration"
    fi
    [ -n "$fs" ] || fs="none available"
    printf "Supported features: %s\n" "$fs"

    printf "Driver usage:\n"
    # native kernel ACPI battery API
    case $_natacpi in
        0|1) printf "* natacpi (%s) = active " "$_batdrv_kmod"; print_methods_per_driver "natacpi" ;;
        32)  printf "* natacpi (%s) = inactive (disabled by configuration)\n" "$_batdrv_kmod" ;;
        128) printf "* natacpi (%s) = inactive (no kernel support)\n" "$_batdrv_kmod" ;;
        254) printf "* natacpi (%s) = inactive (ThinkPad not supported)\n" "$_batdrv_kmod" ;;
        *)   printf "* natacpi (%s) = unknown status\n" "$_batdrv_kmod" ;;
    esac

    # ThinkPad-specific battery APIs
    case $_tpacpi in
        0)   printf "* tpacpi-bat (acpi_call)  = active "; print_methods_per_driver "tpacpi" ;;
        1)   printf "* tpacpi-bat (acpi_call)  = inactive (none)\n" ;;
        32)  printf "* tpacpi-bat (acpi_call)  = inactive (disabled by configuration)\n" ;;
        64)  printf "* tpacpi-bat (acpi_call)  = inactive (kernel module 'acpi_call' load error)\n" ;;
        127) printf "* tpacpi-bat (acpi_call)  = inactive (program 'tpacpi-bat' not installed)\n" ;;
        128) printf "* tpacpi-bat (acpi_call)  = inactive (kernel module 'acpi_call' not installed)\n" ;;
        137) printf "* tpacpi-bat (acpi_call)  = inactive (kernel error)\n" ;;
        253) printf "* tpacpi-bat (acpi_call)  = inactive (tpacpi-bat error)\n" ;;
        254) printf "* tpacpi-bat (acpi_call)  = inactive (ThinkPad not supported)\n" ;;
        255) printf "* tpacpi-bat (acpi_call)  = inactive (superseded by natacpi)\n" ;;
        256) ;; # superseded and kernel module not loaded--> be quiet
        *)   printf "* tpacpi-bat = unknown status" ;;
    esac
    case $_tpsmapi in
        1)   printf "* tp-smapi (tp_smapi)     = readonly "; print_methods_per_driver "tpsmapi" ;;
        32)  printf "* tp-smapi (tp_smapi)     = inactive (disabled by configuration)\n" ;;
        64)  printf "* tp-smapi (tp_smapi)     = inactive (kernel module 'tp_smapi' load error)\n" ;;
        128) printf "* tp-smapi (tp_smapi)     = inactive (kernel module 'tp_smapi' not installed)\n" ;;
    esac

    if [ "$_bm_thresh" != "none" ]; then
        printf "Parameter value ranges:\n"
        printf "* START_CHARGE_THRESH_BAT0/1:  0(off)..96(default)..99\n"
        printf "* STOP_CHARGE_THRESH_BAT0/1:   1..100(default)\n"
    fi
    printf "\n"

    # -- show battery data
    local bat
    local bcnt=0
    local ed ef en
    local efsum=0
    local ensum=0

    for bat in $_batteries; do # iterate detected batteries
        batdrv_select_battery $bat

        case $_bat_idx in
            1) printf "+++ ThinkPad Battery Status: %s (Main / Internal)\n" "$bat" ;;
            2) printf "+++ ThinkPad Battery Status: %s (Ultrabay / Slice / Replaceable)\n" "$bat" ;;
            0) printf "+++ ThinkPad Battery Status: %s\n" "$bat" ;;
        esac

        # --- show basic data
        case $_bm_read in
            natacpi) # use ACPI data
                printparm "%-59s = ##%s##" $_bd_read/manufacturer
                printparm "%-59s = ##%s##" $_bd_read/model_name

                print_battery_cycle_count "$_bd_read/cycle_count" "$(read_sysf $_bd_read/cycle_count)"

                if [ -f $_bd_read/energy_full ]; then
                    printparm "%-59s = ##%6d## [mWh]" $_bd_read/energy_full_design "" 000
                    printparm "%-59s = ##%6d## [mWh]" $_bd_read/energy_full "" 000
                    printparm "%-59s = ##%6d## [mWh]" $_bd_read/energy_now "" 000
                    printparm "%-59s = ##%6d## [mW]"  $_bd_read/power_now "" 000

                    # store values for charge / capacity calculation below
                    ed=$(read_sysval $_bd_read/energy_full_design)
                    ef=$(read_sysval $_bd_read/energy_full)
                    en=$(read_sysval $_bd_read/energy_now)
                    efsum=$((efsum + ef))
                    ensum=$((ensum + en))
                else
                    ed=0
                    ef=0
                    en=0
                fi

                print_batstate $_bd_read/status
                printf "\n"

                if [ $verbose -eq 1 ]; then
                    printparm "%-59s = ##%6s## [mV]" $_bd_read/voltage_min_design "" 000
                    printparm "%-59s = ##%6s## [mV]" $_bd_read/voltage_now "" 000
                    printf "\n"
                fi
                ;; # natacpi

            tpsmapi) # ThinkPad with active tp-smapi
                printparm "%-59s = ##%s##" $_bd_read/manufacturer
                printparm "%-59s = ##%s##" $_bd_read/model
                printparm "%-59s = ##%s##" $_bd_read/manufacture_date
                printparm "%-59s = ##%s##" $_bd_read/first_use_date
                printparm "%-59s = ##%6d##" $_bd_read/cycle_count

                if [ -f $_bd_read/temperature ]; then
                    # shellcheck disable=SC2046
                    perl -e 'printf ("%-59s = %6d [°C]\n", "'$_bd_read/temperature'", '$(read_sysval $_bd_read/temperature)' / 1000.0);'
                fi

                printparm "%-59s = ##%6d## [mWh]" $_bd_read/design_capacity
                printparm "%-59s = ##%6d## [mWh]" $_bd_read/last_full_capacity
                printparm "%-59s = ##%6d## [mWh]" $_bd_read/remaining_capacity
                printparm "%-59s = ##%6d## [%%]" $_bd_read/remaining_percent
                printparm "%-59s = ##%6s## [min]" $_bd_read/remaining_running_time_now
                printparm "%-59s = ##%6s## [min]" $_bd_read/remaining_charging_time
                printparm "%-59s = ##%6d## [mW]" $_bd_read/power_now
                printparm "%-59s = ##%6d## [mW]" $_bd_read/power_avg
                print_batstate $_bd_read/state
                printf "\n"
                if [ $verbose -eq 1 ]; then
                    printparm "%-59s = ##%6s## [mV]" $_bd_read/design_voltage
                    printparm "%-59s = ##%6s## [mV]" $_bd_read/voltage
                    printparm "%-59s = ##%6s## [mV]" $_bd_read/group0_voltage
                    printparm "%-59s = ##%6s## [mV]" $_bd_read/group1_voltage
                    printparm "%-59s = ##%6s## [mV]" $_bd_read/group2_voltage
                    printparm "%-59s = ##%6s## [mV]" $_bd_read/group3_voltage
                    printf "\n"
                fi

                # store values for charge / capacity calculation below
                ed=$(read_sysval $_bd_read/design_capacity)
                ef=$(read_sysval $_bd_read/last_full_capacity)
                en=$(read_sysval $_bd_read/remaining_capacity)
                efsum=$((efsum + ef))
                ensum=$((ensum + en))
                ;; # tp-smapi

        esac # $_bm_read

        # --- show battery features: thresholds, force_discharge
        local lf=0
        case $_bm_thresh in
            natacpi)
                printf "%-59s = %6s [%%]\n" "$_bf_start" "$(batdrv_read_threshold start 1)"
                printf "%-59s = %6s [%%]\n" "$_bf_stop"  "$(batdrv_read_threshold stop 1)"
                lf=1
                ;;

            tpacpi)
                printf "%-59s = %6s [%%]\n" "tpacpi-bat.${_bat_str}.startThreshold" "$(batdrv_read_threshold start 1)"
                printf "%-59s = %6s [%%]\n" "tpacpi-bat.${_bat_str}.stopThreshold"  "$(batdrv_read_threshold stop 1)"
                lf=1
                ;;
        esac
        case $_bm_dischg in
            natacpi)
                printf "%-59s = %6s\n" "$_bf_dischg" "$(batdrv_read_force_discharge 1)"
                lf=1
                ;;

            tpacpi)
                printf "%-59s = %6s\n" "tpacpi-bat.${_bat_str}.forceDischarge" "$(batdrv_read_force_discharge 1)"
                lf=1
                ;;
        esac
        [ $lf -gt 0 ] && printf "\n"

        # --- show charge level (SOC) and capacity
        lf=0
        if [ $ef -ne 0 ]; then
            perl -e 'printf ("%-59s = %6.1f [%%]\n", "Charge",   100.0 * '$en' / '$ef');'
            lf=1
        fi
        if [ $ed -ne 0 ]; then
            perl -e 'printf ("%-59s = %6.1f [%%]\n", "Capacity", 100.0 * '$ef' / '$ed');'
            lf=1
        fi
        [ $lf -gt 0 ] && printf "\n"

        bcnt=$((bcnt+1))

    done # for bat

    if [ $bcnt -gt 1 ] && [ $efsum -ne 0 ]; then
        # more than one battery detected --> show charge total
        perl -e 'printf ("%-59s = %6.1f [%%]\n", "+++ Charge total",   100.0 * '$ensum' / '$efsum');'
        printf "\n"
    fi

    return 0
}

batdrv_recommendations () {
    # output ThinkPad specific recommendations
    # prerequisite: batdrv_init()

    local missing

    case "$_tpacpi" in
        127) missing="tpacpi-bat program" ;;
        128) missing="acpi_call kernel module" ;;
        *)   missing="" ;;
    esac
    if [ -n "$missing" ]; then
        case "$_natacpi" in
            "") ;;  # undefined
            0) ;; # natacpi covers it all
            1) printf "Install %s for ThinkPad battery recalibration\n" "$missing" ;;
            *) printf "Install %s for ThinkPad battery thresholds and recalibration\n" "$missing";;
        esac
    fi
    if [ "$_tpsmapi" = "128" ]; then
      printf "Install tp-smapi kernel modules for extended battery status (e.g. the cycle count)\n"
    fi

    return 0
}
