#!/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) 2023 Thomas Koch <linrunner at gmx.net> and others.
# SPDX-License-Identifier: GPL-2.0-or-later

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

# --- 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_version 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

        # stock BIOS: check DMI product_version 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 //')
        elif [ -z "$X_SIMULATE_MODEL" ]; then
            # Libreboot uses DMI product_name, check it too
            pv="$(read_dmi product_name | tr -C -d 'a-zA-Z0-9 ')"
            if printf '%s' "$pv" | grep -E -q 'Think[Pp]ad'; then
                # it's a librebooted' ThinkPad --> save model substring
                _tpmodel=$(printf '%s\n' "$pv" | sed -r 's/^Think[Pp]ad //')
            fi
        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: start charge 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: stop charge 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" -ge "$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 stop charge 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"

    if [ "$rc" -gt 0 ]; then
        # log output in the error case only
        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"
    fi
    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
    # retval: $_bat_soc, $_bat_en, $_bat_ef
    # prerequisite: batdrv_init(), batdrv_select_battery()

    local bsys rc=0 st=""
    _bat_soc=""
    _bat_en=0
    _bat_ef=0

    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 too
        case $_bm_read in
            natacpi)
                st=$(read_sysf "$_bd_read/status")
                # determine soc for log entry
                _bat_en=$(read_sysval "$_bd_read/energy_now")
                _bat_ef=$(read_sysval "$_bd_read/energy_full")
                if [ "$_bat_ef" != "0" ]; then
                   _bat_soc=$(perl -e 'printf ("%d", 100.0 * '"$_bat_en"' / '"$_bat_ef"' );')
                fi
                ;;

            tpsmapi)
                st=$(read_sysf "$_bd_read/state")
                # determine soc for log entry
                _bat_soc=$(read_sysf "$_bd_read/remaining_percent")
                ;;

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

        # evaluate battery state
        case "$st" in
            [Dd]ischarging) rc=0 ;;
            *) rc=1 ;;
        esac
    fi

    echo_debug "bat" "batdrv.${_batdrv_plugin}.force_discharge_active($_bat_str): bm_read=$_bm_read; bf=$bsys; st=$st; soc=$_bat_soc; 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);'
                    perl -e 'printf ("remaining capacity = %6d [mWh]\n", '"$_bat_en"' / 1000.0);'
                    if [ -n "$_bat_soc" ]; then
                        perl -e 'printf ("remaining percent  = %6d [%%]\n", '"$_bat_soc"' );'
                    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 * '"$_bat_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")"
                    printf "remaining percent  = %6s [%%]\n"  "$_bat_soc"
                    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
}
