#!/bin/sh
# 10-thinkpad-legacy - Battery Plugin for ThinkPads using tp_smapi
# for thresholds and forced discharge,
# i.e. X201/T410 and older.
#
# 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, 05-thinkpad

# --- 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 tp_smapi external kernel module
    #    --> retval $_tpsmapi
    #       0=supported/
    #       32=disabled/
    #       64=tp_smapi module not loaded/
    #       128=tp_smapi module not installed/
    #       254=ThinkPad not supported
    #
    # 2. determine 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
    #
    # 3. determine sysfile basenames for tpsmapi
    #    start threshold                        --> retval $_bn_start,
    #    stop threshold                         --> retval $_bn_stop,
    #    force discharge                        --> retval $_bn_dischg;
    #
    # 4. determine present batteries
    #    list of batteries (space separated)    --> retval $_batteries;
    #
    # 5. define charge threshold defaults
    #    start threshold                        --> retval $_bt_def_start,
    #    stop threshold                         --> retval $_bt_def_stop;

    _batdrv_plugin="thinkpad-legacy"
    _batdrv_kmod="tp_smapi"

    # check plugin simulation override and denylist
    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 hardware matches
        if ! check_thinkpad; then
            echo_debug "bat" "batdrv_init.${_batdrv_plugin}.not_a_thinkpad"
            return 1
        elif ! supports_tpsmapi_only; then
            echo_debug "bat" "batdrv_init.${_batdrv_plugin}.unsupported_model"
            return 1
        fi
    fi

    # presume no features at all
    _tpsmapi=254
    _bm_read="natacpi"
    _bm_thresh="none"
    _bm_dischg="none"
    _bn_start="start_charge_thresh"
    _bn_stop="stop_charge_thresh"
    _bn_dischg="force_discharge"
    _batteries=""
    _bt_def_start=96
    _bt_def_stop=100

    local bs bd done=0

    if [ "$TPSMAPI_ENABLE" = "0" ]; then
        # tp-smapi disabled by configuration
        _tpsmapi=32
    else
        # probe tp_smapi external kernel module
        load_modules "$MOD_TPSMAPI"

        if [ -d "$SMAPIBATDIR" ]; then
            # module loaded --> tp-smapi available
            # enumerate tp_smapi batteries
            # note: tp_smapi battery names may diverge from ACPI names,
            #   refer to https://github.com/linrunner/TLP/issues/714
            for bd in "$SMAPIBATDIR"/BAT[01]; do
                if [ "$(read_sysf "$bd/installed")" = "1" ]; then
                    # record detected batteries and directories
                    bs=${bd##/*/}
                    if [ -n "$_batteries" ]; then
                        _batteries="$_batteries $bs"
                    else
                        _batteries="$bs"
                    fi
                    # skip tp-smapi detection for 2nd battery
                    [ $done -eq 1 ] && continue

                    done=1
                    if readable_sysf "$bd/$_bn_start" \
                       && readable_sysf "$bd/$_bn_stop" \
                       && readable_sysf "$bd/$_bn_dischg"; then
                        # tp-smapi sysfiles are actually readable
                        _tpsmapi=0
                        _bm_read="tpsmapi"
                        _bm_thresh="tpsmapi"
                        _bm_dischg="tpsmapi"
                    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
        fi
    fi

    if [ "$_tpsmapi" -ne 0 ]; then
        # tp-smapi unavailable or disabled
        # enumerate ACPI batteries so that at least tlp-stat -b can show their data
        _batteries=""
        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
            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

        # remaining tp-smapi inavailability cases
        if [ "$_tpsmapi" -eq 32 ]; then
            : # tp-smapi disabled
        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; 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 bat index
    # $1: BAT0/BAT1/DEF
    # global params: $_batdrv_plugin, $_batteries, $_bm_thresh, $_bm_dischg
    # 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=""

    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 bat index for main/aux distinction
    _bat_str="$bs"
    case $bs in
        BAT0) _bat_idx=1 ;;
        BAT1) _bat_idx=2 ;;
    esac

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

    # determine tp-smapi sysfiles
    if [ "$_bm_thresh" = "tpsmapi" ]; then
        _bf_start="$SMAPIBATDIR/$bs/$_bn_start"
        _bf_stop="$SMAPIBATDIR/$bs/$_bn_stop"
    fi

    if [ "$_bm_dischg" = "tpsmapi" ]; then
        _bf_dischg="$SMAPIBATDIR/$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; 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
    # 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; out=$out; rc=$rc"
        return 0
    fi

    case $_bm_thresh in
        tpsmapi)
            # 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
            ;;

        *) # 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, $_bt_cfg_bat, $_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 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 2 / max 96
    if ! is_uint "$new_start" 3 || \
       ! is_within_bounds "$new_start" 2 96; 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 (2..96). 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 (2..96). 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 (2..96). Aborted.\n" "$new_start" "$_bat_str" 1>&2
                fi
                ;;
        esac
        return 2
    fi

    # stop: check for 3 digits max, ensure min 6 / max 100
    if ! is_uint "$new_stop" 3 || \
       ! is_within_bounds "$new_stop" 6 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 (6..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 (6..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 (6..100). Aborted.\n" "$new_stop" "$_bat_str" 1>&2
                fi
                ;;
        esac
        return 2
    fi

    # check minimum start - stop difference
    if [ $((new_start + 4)) -gt "$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} - 4. Battery skipped."
                fi
                ;;

            2)
                if [ -n "$4" ]; then
                    cprintf "" "Error in configuration: START_CHARGE_THRESH_%s > STOP_CHARGE_THRESH_%s - 4. Aborted.\n" "$_bt_cfg_bat" "$_bt_cfg_bat" 1>&2
                else
                    cprintf "" "Error: start threshold > stop threshold - 4 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 "Error: 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

    # determine write sequence because driver's intrinsic boundary conditions
    # must be met in all write stages:
    #   - tp-smapi: start <= stop - 4 (changes value for compliance)
    local rc=0 steprc tseq

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

    # write new thresholds in determined sequence
    if [ "$verb" = "2" ]; then
        printf "Setting temporary charge thresholds for %s:\n" "$_bat_str" 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) write_sysf "$new_thresh" "$_bf_start" ;;
                stop)  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
                        printf "  %-5s = %3d\n" "$step" "$new_thresh" 1>&2
                    else
                        cprintf "err" "  %-5s = %3d (Error: write failed)\n" "$step" "$new_thresh" 1>&2
                        rc=5
                    fi
                    ;;
                1)
                    if [ $steprc -gt 0 ]; then
                        echo_message "Error: writing $step charge threshold for $_bat_str failed."
                        rc=5
                    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, $_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/
    #     6=stop threshold too low
    # 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
    elif [ "$cur_stop" -lt 6 ]; then
        cprintf "Error: the %s stop charge threshold is %s. " "$_bat_str" "$ccharge"  1>&2
        cprintf "err" "For this command to work, it must be 6 at least.\n"  1>&2
        echo_debug "bat" "batdrv.${_batdrv_plugin}.chargeonce($_bat_str).stop_thresh_too_low: stop=$cur_stop; rc=6"
        return 6
    fi

    # get current charge level (in %)
    case $_bm_read in
        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 - 4 ))
    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 - 4).\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:" "$_bat_str" 1>&2
    case $_bm_thresh in
        tpsmapi)
            write_sysf "$temp_start" "$_bf_start" || rc=5
            ;;
    esac

    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
    # - api: 0=off/1=on/"" on error
    # - tlp-stat: 0=off/1=on/"(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
        tpsmapi)
            # read force discharge from sysfile
            out=$(read_sysf "$_bf_dischg");
            if ! out=$(read_sysf "$_bf_dischg"); then
                # not readable/non-existent
                if [ "$1" = "1" ]; then
                    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; out=$out; rc=$rc"
    fi
    return $rc
}

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

    local rc=0

    case $_bm_dischg in
        tpsmapi)
            # write force_discharge
            write_sysf "$1" "$_bf_dischg" || rc=5
            ;;

        *) # 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 or read error
    # prerequisite: batdrv_init(), batdrv_select_battery()

    local rc=1 soc

    # check if force_discharge is on
    [ "$(batdrv_read_force_discharge 0)" = "1" ] && rc=0

    if [ $rc -eq 0 ] && ! get_sys_power_supply; then
        # AC unplugged --> cancel discharge
        batdrv_write_force_discharge 0
        rc=1
    fi

    soc=$(read_sysf "$_bd_read/remaining_percent")
    echo_debug "bat" "batdrv.${_batdrv_plugin}.force_discharge_active($_bat_str): bm_read=$_bm_read; soc=$soc; rc=$rc"
    return $rc
}

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

    local rc rp wt

    # start discharge
    if ! batdrv_write_force_discharge 1; then
        echo_debug "bat" "batdrv.${_batdrv_plugin}.discharge.force_discharge_malfunction($_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; rp=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"
        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"  1>&2

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

            esac
            printf "force discharge    = %s\n"  "$(batdrv_read_force_discharge 0)" 1>&2

            printf "Press Ctrl+C to cancel.\n" 1>&2
            sleep 5
        done
        unlock_tlp tlp_discharge

        # read charge level one last time
        case $_bm_read in
            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)"
                cecho "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)"
                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.malfunction($_bat_str)"
        cecho "Error: discharge $_bat_str malfunction -- check your hardware (battery, charger)" 1>&2
        rc=1
    fi

    trap - INT # remove ^C hook

    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 data
    # $1: 1=verbose
    # global params: $_batdrv_plugin, $_batteries, $_batdrv_kmod, $_tpsmapi, $_bm_thresh, $_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" = "tpsmapi" ] && fs="charge thresholds"
    if [ "$_bm_dischg" = "tpsmapi" ]; 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"
    # ThinkPad-specific battery API
    case $_tpsmapi in
        0)   cprintf "success" "* tp-smapi (%s) = active (%s)\n" "$_batdrv_kmod" "$(print_methods_per_driver "tpsmapi")" ;;
        32)  cprintf "notice"  "* tp-smapi (%s) = inactive (disabled by configuration)\n" "$_batdrv_kmod" ;;
        64)  cprintf "err"     "* tp-smapi (%s) = inactive (kernel module 'tp_smapi' load error)\n" "$_batdrv_kmod" ;;
        128) cprintf "err"     "* tp-smapi (%s) = inactive (kernel module 'tp_smapi' not installed)\n" "$_batdrv_kmod" ;;
        254) cprintf "warning" "* tp-smapi (%s) = inactive (ThinkPad not supported)\n" "$_batdrv_kmod" ;;
        *)   cprintf "err"     "* tp-smapi (%s) = unknown status\n" "$_batdrv_kmod" ;;
    esac

    if [ "$_bm_thresh" = "tpsmapi" ]; then
        printf "Parameter value ranges:\n"
        printf "* START_CHARGE_THRESH_BAT0/1:  2..96(default)\n"
        printf "* STOP_CHARGE_THRESH_BAT0/1:   6..100(default)\n"
    fi
    printf "\n"

    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) # no tp-smapi --> 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) # tp-smapi active
                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" = "tpsmapi" ]; 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" = "tpsmapi" ]; then
            printf "%-59s = %6s\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

    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" = "tpsmapi" ] && soc=$(read_sysval "$_bd_read/remaining_percent"); then
        stop="$(batdrv_read_threshold stop 0)"
        if [ -n "$stop" ] && [ "$soc" -gt "$stop" ]; then
            return 0
        fi
    fi

    return 1
}

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

    soc_gt_stop_recommendation

    if [ "$_tpsmapi" = "128" ]; then
       printf "Install tp-smapi kernel modules for ThinkPad battery thresholds and recalibration\n"
    fi

    return 0
}
