#!/bin/sh
# 05-thinkpad - Battery Plugin for ThinkPads w/ thinkpad_acpi driver
# providing thresholds and forced discharge, i.e. X220/T420 and newer.
#
# Copyright (c) 2024 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][01][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"

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 (thresholds require Linux 4.19, discharge needs 5.17)
    #    --> retval $_natacpi:
    #       0=thresholds and discharge/
    #       1=thresholds only/
    #       32=disabled/
    #       128=no kernel support/
    #       254=ThinkPad not supported
    #
    # 2. 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
    #
    # 3. determine method for
    #    reading battery data                   --> retval $_bm_read,
    #       none/natacpi/tpsmapi
    #    reading/writing charging thresholds    --> retval $_bm_thresh,
    #    reading/writing force discharge        --> retval $_bm_dischg:
    #       none/natacpi
    #
    # 4. determine sysfile basenames for natacpi
    #    start threshold                        --> retval $_bn_start,
    #    stop threshold                         --> retval $_bn_stop,
    #    force discharge                        --> retval $_bn_dischg;
    #
    # 5. determine present batteries
    #    list of batteries (space separated)    --> retval $_batteries;
    #
    # 6. 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
    _tpsmapi=254
    _bm_read="natacpi"
    _bm_thresh="none"
    _bm_dischg="none"
    _bn_start=""
    _bn_stop=""
    _bn_dischg="charge_behaviour"
    _batteries=""
    _bt_def_start=96
    _bt_def_stop=100

    # --- 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 [ -f "$bd/$_bn_dischg" ]; then
                    if grep -q "force-discharge" "$bd/$_bn_dischg"; then
                        # sysfile exists and flags the force-discharge capability
                        _natacpi=0
                        _bm_dischg="natacpi"
                    fi
                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 --> try 10-thinkpad-legacy next
        echo_debug "bat" "batdrv_init.${_batdrv_plugin}.no_natacpi: batteries=$_batteries; natacpi=$_natacpi; tpsmapi=$_tpsmapi"
        return 1
    fi

    # 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; 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 params: $_batdrv_plugin, $_batteries, $_bm_read, $_bm_dischg, $_bn_start, $_bn_stop, $_bn_dischg
    # rc: 0=bat exists/1=bat non-existent
    # retval: $_bat_str:    BAT0/BAT1;
    #         $_bt_cfg_bat: config suffix = 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;
    #         $_bf_status:  sysfile for status read
    # prerequisite: batdrv_init()

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

    local bat="$1"

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

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

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

    # determine battery name and index for main/aux distinction in tlp-stat -b
    case $bs in
        BAT0)
            _bat_str="$bs"
            # BAT0 is always assumed main battery
            _bat_idx=1
            ;;

        BAT1)
            _bat_str="$bs"
            if [ -d "$ACPIBATDIR/BAT0" ]; then
                # BAT0 exists, so BAT1 is aux
                _bat_idx=2
            else
                # BAT0 does not exist, so BAT1 is main
                _bat_idx=1
            fi
            ;;
    esac

    # config suffix equals battery name
    _bt_cfg_bat="$_bat_str"

    # 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"
        _bf_status="$ACPIBATDIR/$bs/status"
    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; cfg=$_bt_cfg_bat; 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 params: $_batdrv_plugin, $_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, $2).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
            ;;

        *) # 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, $2): 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: non-empty string indicates thresholds stem from configuration
    # global params: $_batdrv_plugin, $_bm_thresh, $_bat_str, $_bat_idx, $_bt_cfg_bat, $_bf_start, $_bf_stop, $_bt_def_start, $_bt_def_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 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 "$4" ] && [ -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; cfg=$_bt_cfg_bat"
        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; cfg=$_bt_cfg_bat"
        case $verb in
            1)
                if [ -n "$4" ]; then
                    echo_message "Error in configuration at START_CHARGE_THRESH_${_bt_cfg_bat}=\"${new_start}\": not specified, invalid or out of range (0..99). Battery skipped."
                fi
                ;;

            2)
                if [ -n "$4" ]; then
                    cprintf "" "Error in configuration at START_CHARGE_THRESH_%s=\"%s\": not specified, invalid or out of range (0..99). Aborted.\n" "$_bt_cfg_bat" "$new_start" 1>&2
                else
                    cprintf "" "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; cfg=$_bt_cfg_bat"
        case $verb in
            1)
                if [ -n "$4" ]; then
                    echo_message "Error in configuration at STOP_CHARGE_THRESH_${_bt_cfg_bat}=\"${new_stop}\": not specified, invalid or out of range (1..100). Battery skipped."
                fi
                ;;

            2)
                if [ -n "$4" ]; then
                    cprintf "" "Error in configuration at STOP_CHARGE_THRESH_%s=\"%s\": not specified, invalid or out of range (1..100). Aborted.\n" "$_bt_cfg_bat" "$new_stop" 1>&2
                else
                    cprintf "" "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; cfg=$_bt_cfg_bat"
        case $verb in
            1)
                if [ -n "$4" ]; then
                    echo_message "Error in configuration: START_CHARGE_THRESH_${_bt_cfg_bat} >= STOP_CHARGE_THRESH_${_bt_cfg_bat}. Battery skipped."
                fi
                ;;

            2)
                if [ -n "$4" ]; then
                    cprintf "" "Error in configuration: START_CHARGE_THRESH_%s >= STOP_CHARGE_THRESH_%s. Aborted.\n" "$_bt_cfg_bat" "$_bt_cfg_bat" 1>&2
                else
                    cprintf "" "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; cfg=$_bt_cfg_bat"
        case $verb in
            1) echo_message "Warning: could not read current charge threshold(s) for $_bat_str. Battery skipped." ;;
            2) cprintf "" "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" 1>&2
    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 $step in
                start) [ "$X_THRESH_SIMULATE_WRITEERR" != "1" ] && write_sysf "$new_thresh" "$_bf_start" ;;
                stop)  [ "$X_THRESH_SIMULATE_WRITEERR" != "1" ] && write_sysf "$new_thresh" "$_bf_stop"  ;;
            esac
            steprc=$?; [ $steprc -ne 0 ] && [ $rc -eq 0 ] && rc=5
            echo_debug "bat" "batdrv.${_batdrv_plugin}.write_thresholds($1, $2, $3, $4).$step.write: bat=$_bat_str; cfg=$_bt_cfg_bat; 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" 1>&2
                        else
                            printf "  %-5s = %3d\n" "$step" "$new_thresh" 1>&2
                        fi
                    else
                        cprintf "err" "  %-5s = %3d (Error: write failed)\n" "$step" "$new_thresh" 1>&2
                    fi
                    ;;
                1)
                    if [ $steprc -gt 0 ]; then
                        echo_message "Error: writing $step charge 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; cfg=$_bt_cfg_bat; old=$old_thresh; new=$new_thresh"

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

    if [ "$rc" -eq 0 ] && [ "$verb" = "2" ]; then
        soc_gt_stop_notice
    fi

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

batdrv_chargeonce () {
    # charge battery to stop threshold once
    # use pre-determined method and sysfiles from global parms
    # global params: $_batdrv_plugin, $_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
        cprintf "" "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
        cprintf "" "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
        cprintf "" "Error: the %s charge level is %s%%. " "$_bat_str" "$ccharge"  1>&2
        cprintf "err" "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" 1>&2
    write_sysf "$temp_start" "$_bf_start" || rc=5
    if [ $rc -eq 0 ]; then
        printf "  start = %3d\n" "$temp_start" 1>&2
    else
        cprintf "err" "  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 stop thresholds from configuration to all batteries
    # - called for bg tasks tlp init [re]start/auto and tlp start
    # output parameter errors only
    # prerequisite: batdrv_init()

    local bat start_thresh stop_thresh

    for bat in BAT0 BAT1; do
        if batdrv_select_battery "$bat"; then
            eval start_thresh="\$START_CHARGE_THRESH_${_bt_cfg_bat}"
            eval stop_thresh="\$STOP_CHARGE_THRESH_${_bt_cfg_bat}"
            batdrv_write_thresholds "$start_thresh" "$stop_thresh" 1 1
        fi
    done

    return 0
}

batdrv_read_force_discharge () {
    # read and print force-discharge state
    # $1: 0=api/1=tlp-stat output
    # global params: $_batdrv_plugin, $_bm_dischg, $_bat_str, $_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
            ;;

        *) # 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 params: $_batdrv_plugin, $_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

        *) # 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 params: $_batdrv_plugin, $_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 params: $_batdrv_plugin, $_bat_str, $_bm_read, $_bd_read
    # rc: 0=discharging/1=not discharging/2=AC detached/255=no api
    # retval: $_bat_en, $_bat_ef, $_bat_fd, $_bat_pwr, $_bat_soc, $_bat_st, $_bat_volt
    # prerequisite: batdrv_init(), batdrv_select_battery()

    local rc=0 pn vt
    _bat_en=0
    _bat_ef=0
    _bat_pwr=0
    _bat_soc=""
    _bat_st=""
    _bat_volt=""

    _bat_fd="$(batdrv_read_force_discharge 0)"
    if [ "$_bat_fd" = "0" ]; then
        # force_discharge is off --> quit
        echo_debug "bat" "batdrv.${_batdrv_plugin}.force_discharge.reset($_bat_str): fd=$_bat_fd; 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
        _bat_st=$(read_sysf "$_bf_status")
        case "$_bat_st" in
            [Dd]ischarging) rc=0 ;;
            *) rc=1 ;;
        esac

        # battery readings
        case $_bm_read in
            natacpi)
                # 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
                if pn=$(read_sysval "$_bd_read/power_now"); then
                   _bat_pwr=$(perl -e 'printf ("%d", '"$pn"' / 1000.0 );')
                fi
                if vt=$(read_sysval "$_bd_read/voltage_now"); then
                   _bat_volt=$(perl -e 'printf ("%d", '"$vt"' / 1000.0 );')
                fi
                ;;

            tpsmapi)
                # determine soc for log entry
                _bat_soc=$(read_sysf "$_bd_read/remaining_percent")
                _bat_pwr=$(read_sysval "$_bd_read/power_avg")
                _bat_volt=$(read_sysval "$_bd_read/voltage")
                ;;

            *) # no read api
                rc=255
                ;;
        esac
    fi

    if [ "$rc" -gt "0" ]; then
        echo_debug "bat" "batdrv.${_batdrv_plugin}.force_discharge.not_discharging($_bat_str): fd=$_bat_fd; st=$_bat_st; soc=$_bat_soc; pwr=$_bat_pwr; voltage=$_bat_volt; rc=$rc"
    else
        echo_debug "bat" "batdrv.${_batdrv_plugin}.force_discharge.running($_bat_str): fd=$_bat_fd; st=$_bat_st; soc=$_bat_soc; pwr=$_bat_pwr; voltage=$_bat_volt; rc=$rc"
    fi
    return $rc
}

batdrv_discharge () {
    # discharge battery
    # global params: $_batdrv_plugin, $_bm_dischg, $_bat_str, $_bat_idx, $_bd_read, $_bf_dischg, $_bat_en, $_bat_soc
    # 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 soc st vn wt

    # start discharge
    if ! batdrv_write_force_discharge 1; then
        echo_debug "bat" "batdrv.${_batdrv_plugin}.discharge.force_discharge_write_error($_bat_str)"
        cecho "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; soc=0

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

    if batdrv_force_discharge_active; then
        # discharge initiated sucessfully --> wait for completion == while status "discharging"
        while batdrv_force_discharge_active; do
            clear
            printf "Currently discharging battery %s:\n" "$_bat_str" 1>&2

            # show current battery state
            if [ -n "$_bat_volt" ]; then
                printf "voltage            = %6s [mV]\n"  "$_bat_volt" 1>&2
            else
                printf "voltage            = not available [mV]\n" 1>&2
            fi

            case $_bm_read in
                natacpi) # use ACPI sysfiles
                    perl -e 'printf ("remaining capacity = %6d [mWh]\n", '"$_bat_en"' / 1000.0);' 1>&2
                    if [ -n "$_bat_soc" ]; then
                        perl -e 'printf ("remaining percent  = %6d [%%]\n", '"$_bat_soc"' );'  1>&2
                    else
                        printf "remaining percent  = not available [%%]\n" 1>&2
                    fi

                    if [ "$_bat_pwr" != "0" ]; then
                        perl -e 'printf ("remaining time     = %6d [min]\n", 0.06 * '"$_bat_en"' / '"$_bat_pwr"');' 1>&2
                        printf "power              = %6s [mW]\n"  "$_bat_pwr" 1>&2
                    else
                        printf "remaining time     = not discharging [min]\n" 1>&2
                    fi
                    ;; # natacpi, tpsmapi

                tpsmapi) # use tp-smapi sysfiles
                    printf "remaining capacity = %6s [mWh]\n" "$(read_sysf "$_bd_read/remaining_capacity")" 1>&2
                    printf "remaining percent  = %6s [%%]\n"  "$_bat_soc" 1>&2
                    printf "remaining time     = %6s [min]\n" "$(read_sysf "$_bd_read/remaining_running_time_now")" 1>&2
                    printf "power              = %6s [mW]\n"  "$_bat_pwr" 1>&2
                    ;; # tpsmapi

            esac

            printf "status             = %s\n" "$_bat_st" 1>&2
            printf "force-discharge    = %s\n" "$_bat_fd" 1>&2
            echo "Press Ctrl+C to cancel." 1>&2
            sleep 5
        done
        unlock_tlp tlp_discharge

        # read SOC and more one last time
        fd="$(batdrv_read_force_discharge 0)"
        case $_bm_read in
            natacpi) # use ACPI sysfiles
                en=$(read_sysval "$_bd_read/energy_now")
                ef=$(read_sysval "$_bd_read/energy_full")
                if [ "$ef" != "0" ]; then
                    soc=$(perl -e 'printf ("%d", 100.0 * '"$en"' / '"$ef"' );')
                else
                    soc=0
                fi

                pn=$(perl -e 'printf ("%d", '"$(read_sysval "$_bd_read/power_avg")"' / 1000.0 );')
                vn=$(perl -e 'printf ("%d", '"$(read_sysval "$_bd_read/voltage_now")"' / 1000.0 );')
                ;; # natacpi

            tpsmapi) # use tp-smapi sysfiles
                soc=$(read_sysf "$_bd_read/remaining_percent")

                pn=$(read_sysval "$_bd_read/power_now")
                vn=$(read_sysval "$_bd_read/voltage")
                ;;
        esac
        st=$(read_sysf "$_bf_status")

        if [ "$soc" -gt 0 ]; then
            # SOC > 0% = 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): fd=$fd; st=$st; soc=$soc; pwr=$pn; voltage=$vn"
                cecho "Warning: battery $_bat_str was not discharged completely -- AC/charger removed." 1>&2
                rc=3
            elif [ "$soc" -gt 1 ]; then
                # system on AC, SOC > 1% --> discharging terminated by unknown reason
                echo_debug "bat" "batdrv.${_batdrv_plugin}.discharge.not_emptied($_bat_str): fd=$fd; st=$st; soc=$soc; pwr=$pn; voltage=$vn"
                cecho "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.init_error($_bat_str): fd=$_bat_fd; st=$_bat_st; soc=$_bat_soc; pwr=$_bat_pwr; voltage=$_bat_volt; rc=$rc"

        cecho "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 1>&2
        cecho "Done: battery $_bat_str was completely discharged." "success"  1>&2
        echo_debug "bat" "batdrv.${_batdrv_plugin}.discharge.complete($_bat_str)"
    fi
    return $rc
}

batdrv_show_battery_data () {
    # output battery status
    # $1: 1=verbose
    # global params: $_batdrv_plugin, $_batteries, $_batdrv_kmod, $_natacpi, $_tpsmapi,
    #                $_bm_thresh, $_bm_dischg, $_bd_read, $_bf_start, $_bf_stop, $_bf_dischg
    # prerequisite: batdrv_init()
    local verbose=${1:-0}

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

    local fs=""
    [ "$_bm_thresh" = "natacpi" ] && fs="charge thresholds"
    if [ "$_bm_dischg" = "natacpi" ]; then
        [ -n "$fs" ] && fs="${fs}, "
        fs="${fs}recalibration"
    fi
    if [ -n "$fs" ]; then
        cprintf "success" "Supported features: %s\n" "$fs"
    else
        cprintf "warning" "Supported features: none available\n"
    fi

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

    # Legacy-ThinkPad battery API
    case $_tpsmapi in
        1)   cprintf "success" "* tp-smapi (tp_smapi)     = readonly (%s)\n" "$(print_methods_per_driver "tpsmapi")" ;;
        32)  cprintf "notice"  "* tp-smapi (tp_smapi)     = inactive (disabled by configuration)\n" ;;
        64)  cprintf "err"     "* tp-smapi (tp_smapi)     = inactive (kernel module 'tp_smapi' load error)\n" ;;
        128) cprintf "notice"  "* tp-smapi (tp_smapi)     = inactive (kernel module 'tp_smapi' not installed)\n" ;;
    esac

    if [ "$_bm_thresh" = "natacpi" ]; 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
    local coreboot=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 (Removable / Ultrabay / Slice)\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))

                elif [ -f "$_bd_read/charge_full" ]; then
                    # workaround: coreboot (Skulls) provides ACPI mW(h) values incorrectly:
                    # -  using sysfiles for mA(h)
                    # -  one decimal place too little
                    # refer to https://github.com/linrunner/TLP/issues/657
                    coreboot=1

                    printparm "%-59s = ##%6d## [mWh] *" "$_bd_read/charge_full_design" "" 00
                    printparm "%-59s = ##%6d## [mWh] *" "$_bd_read/charge_full" "" 00
                    printparm "%-59s = ##%6d## [mWh] *" "$_bd_read/charge_now" "" 00
                    printparm "%-59s = ##%6d## [mW] *" "$_bd_read/current_now" "" 00

                    # store values for charge / capacity calculation below
                    ed=$(read_sysval "$_bd_read/charge_full_design")
                    ef=$(read_sysval "$_bd_read/charge_full")
                    en=$(read_sysval "$_bd_read/charge_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
        if [ "$_bm_thresh" = "natacpi" ]; then
            printf "%-59s = %6s [%%]\n" "$_bf_start" "$(batdrv_read_threshold start 1)"
            printf "%-59s = %6s [%%]\n" "$_bf_stop"  "$(batdrv_read_threshold stop 1)"
            lf=1
        fi
        if [ "$_bm_dischg" = "natacpi" ]; then
            printf "%-59s = %s\n" "$_bf_dischg" "$(batdrv_read_force_discharge 1)"
            lf=1
        fi
        [ "$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

    if [ "$coreboot" = "1" ]; then
        printf "*) Converted coreboot charge readings may differ.\n\n"
    fi

    return 0
}

batdrv_check_soc_gt_stop () {
    # check if battery charge level (SOC) is greater than the stop threshold
    # rc: 0=greater/¹=less or equal (or thresholds not supported)
    # global params: $_bm_thresh, $_bat_str
    # prerequisite: batdrv_init(), batdrv_select_battery()

    local soc stop

    if [ "$_bm_thresh" = "natacpi" ] && soc=$(read_sysval "$ACPIBATDIR/$_bat_str/capacity"); then
        stop="$(batdrv_read_threshold stop 0)"
        if [ -n "$stop" ] && [ "$soc" -gt "$stop" ]; then
            return 0
        fi
    fi

    return 1
}

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

    soc_gt_stop_recommendation

    if [ "$_natacpi" = "1" ] && [ -f "$_bd_read/energy_full" ]; then
        # natacpi degraded and no coreboot
        printf "Install kernel 5.17 (or later) for battery recalibration support\n"
    fi
    if [ "$_tpsmapi" = "128" ]; then
        printf "Install tp-smapi kernel modules for extended battery status (e.g. the cycle count)\n"
    fi

    return 0
}
