#!/bin/sh
#
# tpbatctl - get or set properties of IBM/Lenovo ThinkPad batteries
#
# Copyright (C) 2018 Guido Berhoerster <guido+tpbatctl@berhoerster.name>
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#

PATH=/usr/sbin:/usr/bin:/sbin:/bin
LC_NUMERIC=C

#
# usage: is_bit_set value bit_pos
# returns: 0 if true, 1 if false
#
is_bit_set () {
    [ $(( $1 & (1 << $2) )) -eq $(( 1 << $2 )) ]
}

#
# usage: is_num str
# returns: 0 if true, 1 if false
#
is_num () {
    case $1 in
        *[!0123456789]*)
            return 1
    esac
}

#
# usage: is_thinkpad
# returns: 0 if true, 1 if false
#
is_thinkpad () {
    case $(head -1 /sys/class/dmi/id/sys_vendor) in
        [Ii][Bb][Mm]|[Ll][Ee][Nn][Oo][Vv][Oo])
            ;;
        *)
            return 1
    esac

    case $(head -1 /sys/class/dmi/id/product_version) in
        ThinkPad*)
            ;;
        *)
            return 1
    esac
}

#
# usage: get_acpi_base_path
# returns: 0 if successful
#
get_acpi_base_path () {
    for sys_device in /sys/class/power_supply/*; do
        if [ ! -d "${sys_device}" ]; then
            printf 'failed to obtain ACPI base path, battery not found\n' >&2
            return 1;
        fi

        # strip trailing underscores from path components and remove the last
        # component
        sed -n -e '1s|_*\.|.|g' -e '1s|\.[^.]*$||' -e '1p' \
                "${sys_device}/device/path" || return 1
        return 0
    done
}

#
# usage: acpi_call_exec acpi_method argument
# returns: 0 if successful
#
acpi_call_exec () {
    printf '%s %s\n' "$1" "$2" >/proc/acpi/call
}

#
# usage: acpi_call_read_int
# returns: 0 if successful
#
acpi_call_read_int () {
    acpi_response="$(head -1 /proc/acpi/call)"
    case $acpi_response in
        0x*)
            # strip non-hexadecimal trailing chars
            acpi_response="${acpi_response#0x}"
            acpi_response="0x${acpi_response%%[!0123456789abcdef]*}"
            printf '%d' "${acpi_response}"
            ;;
        Error:*)
            printf 'ACPI method call failed: %s\n' "${acpi_response}" >&2
            return 1
            ;;
        *)
            printf 'ACPI method call failed, got unexpected response: %s\n' \
                    "${acpi_response}" >&2
            return 1
    esac
}

#
# usage: probe_battery acpi_base_path
# returns: 0 if successful
#
probe_battery () {
    acpi_call_exec "${1}.HKEY.BCTG" "0x1" || return $?
    result="$(acpi_call_read_int)" || return $?

    if is_bit_set "${result}" 31; then
        printf 'failed to probe battery' >&2
        return 1
    fi

    # capabilities
    printf '%d' $(( result >> 8 ))
    return 0
}

#
# usage: get_threshold acpi_method battery
# returns: 0 if successful
#
get_threshold () {
    acpi_call_exec "$1" "$(printf '0x%x' $2)"
    result="$(acpi_call_read_int)" || return $?

    if is_bit_set $result 31; then
        return 1
    fi

    # threshold value
    printf '%d' $(( result & ((1 << 7) - 1) ))
    return 0
}

#
# usage: set_threshold acpi_method battery threshold
# returns: 0 if successful
#
set_threshold () {
    acpi_call_exec "$1" "$(printf '0x%x' $(( $3 | ($2 << 8) )) )"
    result="$(acpi_call_read_int)" || return $?

    if is_bit_set $result 31; then
        return 1
    fi

    return 0
}

#
# usage: get_start_threshold acpi_base_path battery
# returns: 0 if successful
#
get_start_threshold () {
    if ! threshold="$(get_threshold "${1}.HKEY.BCTG" $2)"; then
        printf 'failed to read the start threshold\n' >&2
        return 1
    fi
    printf '%d' "${threshold}"
    return 0
}

#
# usage: get_stop_threshold acpi_base_path battery
# returns: 0 if successful
#
get_stop_threshold () {
    if ! threshold="$(get_threshold "${1}.HKEY.BCSG" $2)"; then
        printf 'failed to read the stop threshold\n' >&2
        return 1
    fi
    printf '%d' "${threshold}"
    return 0
}

#
# usage: set_start_threshold acpi_base_path battery threshold
# returns: 0 if successful
#
set_start_threshold () {
    if ! set_threshold "${1}.HKEY.BCCS" $2 $3; then
        printf 'failed to set the stop threshold\n' >&2
        return 1
    fi
}

#
# usage: set_stop_threshold acpi_base_path battery threshold
# returns: 0 if successful
#
set_stop_threshold () {
    if ! set_threshold "${1}.HKEY.BCSS" $2 $3; then
        printf 'failed to set the stop threshold\n' >&2
        return 1
    fi
}

#
# usage: usage
# returns: nothing
#
usage () {
    printf 'usage: %s [-g property]\n' "${0##*/}"
    printf '    %s [-s property=value]\n' "${0##*/}"
}

if ! is_thinkpad; then
    printf 'device not supported\n' >&2
    exit 1
fi

if [ ! -f /proc/acpi/call ]; then
    if ! modprobe acpi_call; then
        printf 'failed to load acpi_call kernel module\n' >&2
        exit 1
    fi
fi

if [ ! -r /proc/acpi/call ] || [ ! -w /proc/acpi/call ]; then
    printf 'insufficient privilege to access /proc/acpi/call\n' >&2
    exit 1
fi

selected_batteries=
get_mode=false
set_mode=false
get_start_threshold=false
get_stop_threshold=false
assigned_start_threshold=
assigned_stop_threshold=

while getopts 'g:hs:' opt; do
    case $opt in
        g)
            if $set_mode; then
                usage
                exit 1
            fi

            get_mode=true

            case $OPTARG in
                start_threshold)
                    get_start_threshold=true
                    ;;
                stop_threshold)
                    get_stop_threshold=true
                    ;;
                *)
                    printf 'unknown property: %s\n' "${OPTARG}"
                    exit 1
                    ;;
            esac
            ;;
        h)
            usage
            exit 0
            ;;
        s)
            if $get_mode; then
                usage
                exit 1
            fi

            set_mode=true

            case $OPTARG in
                start_threshold=*)
                    assigned_start_threshold="${OPTARG#*=}"
                    if ! is_num "${assigned_start_threshold}"; then
                        printf 'invalid threshold: %s\n' \
                                "${assigned_start_threshold}" >&2
                        exit 1
                    elif [ $assigned_start_threshold -gt 100 ]; then
                        printf 'invalid threshold: %s\n' \
                                "${assigned_start_threshold}" >&2
                        exit 1
                    elif [ $assigned_start_threshold -eq 100 ]; then
                        assigned_start_threshold=0
                    fi
                    ;;
                stop_threshold=*)
                    assigned_stop_threshold="${OPTARG#*=}"
                    if ! is_num "${assigned_stop_threshold}"; then
                        printf 'invalid threshold: %s\n' \
                                "${assigned_stop_threshold}" >&2
                        exit 1
                    elif [ $assigned_stop_threshold -gt 100 ]; then
                        printf 'invalid threshold: %s\n' \
                                "${assigned_stop_threshold}" >&2
                        exit 1
                    elif [ $assigned_stop_threshold -eq 100 ]; then
                        assigned_stop_threshold=0
                    fi
                    ;;
                *)
                    printf 'unknown property: %s\n' "${OPTARG}"
                    exit 1
                    ;;
            esac
            ;;
        *)
            usage
            exit 1
    esac
done
# default to get mode
if ! $get_mode && ! $set_mode; then
    get_mode=true
    get_start_threshold=true
    get_stop_threshold=true
fi

shift $(( OPTIND - 1 ))

if [ $# -gt 1 ]; then
    usage
    exit 1
elif [ $# -eq 1 ]; then
    if ! is_num "$1" || [ $1 -lt 1 ] || [ $1 -gt 2 ]; then
        printf 'invalid battery number: %s\n' "$1"
        exit 1
    fi
    selected_batteries=$1
    if [ ! -d "/sys/class/power_supply/BAT$(( selected_batteries - 1))" ]; then
        printf 'battery %d does not exist\n' $selected_batteries
        exit 1
    fi
else
    # no explicitly selected battery means all batteries
    for battery in 1 2; do
        if [ -d "/sys/class/power_supply/BAT$(( battery - 1))" ]; then
            selected_batteries="${selected_batteries} ${battery}"
        fi
    done
fi

if ! acpi_base_path="$(get_acpi_base_path)"; then
    printf 'failed to obtain ACPI base path\n' >&2
    exit 1
fi

if ! capabilities=$(probe_battery "${acpi_base_path}"); then
    printf 'failed to obtain battery capabilities\n' >&2
    exit 1
fi

if $get_mode; then
    for battery in $selected_batteries; do
        printf 'Battery %d\n' $battery
        if $get_start_threshold; then
            start_threshold="$(get_start_threshold \
                    $acpi_base_path $battery)" || exit $?
            printf '    Charge Start Threshold: %d %%\n' $start_threshold
        fi

        if $get_stop_threshold; then
            stop_threshold="$(get_stop_threshold $acpi_base_path $battery)" || \
                    exit $?
            printf '    Charge Stop Threshold:  %d %%\n' $stop_threshold
        fi
    done
else
    for battery in $selected_batteries; do
        if ! is_bit_set $capabilities 0; then
            printf 'setting thresholds is not supported\n' >&2
            exit 1
        fi

        start_threshold="$(get_start_threshold $acpi_base_path $battery)" || \
                exit $?
        stop_threshold="$(get_stop_threshold $acpi_base_path $battery)" || \
                exit $?

        if [ -n "${assigned_start_threshold}" ] && \
                [ -n "${assigned_stop_threshold}" ]; then
            if [ $assigned_start_threshold -gt $assigned_stop_threshold ]; then
                printf 'the start threshold must be smaller than the stop threshold\n' >&2
                exit 1
            fi

            if [ $assigned_start_threshold -gt $stop_threshold ]; then
                # the new start threshold is greather then the old stop
                # threshold, thus change the stop threshold first so that the
                # start threshold always remains smaller than the stop
                # threshold
                set_stop_threshold $acpi_base_path $battery \
                        $assigned_stop_threshold || exit $?
                set_start_threshold $acpi_base_path $battery \
                        $assigned_start_threshold || exit $?
            else
                # otherwise the new stop threshold is smaller than or equal to
                # the old start threshold, thus change the start threshold
                # first
                set_start_threshold $acpi_base_path $battery \
                        $assigned_start_threshold || exit $?
                set_stop_threshold $acpi_base_path $battery \
                        $assigned_stop_threshold || exit $?
            fi
        elif [ -n "${assigned_start_threshold}" ]; then
            if [ $assigned_start_threshold -gt $stop_threshold ]; then
                printf 'the start threshold must be smaller than the stop threshold\n' >&2
                exit 1
            fi

            set_start_threshold $acpi_base_path $battery \
                    $assigned_start_threshold || exit $?
        else
            if [ $assigned_stop_threshold -lt $start_threshold ]; then
                printf 'the start threshold must be smaller than the stop threshold\n' >&2
                exit 1
            fi

            set_stop_threshold $acpi_base_path $battery \
                    $assigned_stop_threshold || exit $?
        fi
    done
fi
