# HG changeset patch # User Guido Berhoerster # Date 1533220259 -7200 # Node ID fe9ad91c9f6507bcd2e5a1aff4bf44f17639bdfa Initial revision diff -r 000000000000 -r fe9ad91c9f65 tpbatctl.sh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tpbatctl.sh Thu Aug 02 16:30:59 2018 +0200 @@ -0,0 +1,438 @@ +#!/bin/sh +# +# tpbatctl - get or set properties of IBM/Lenovo ThinkPad batteries +# +# Copyright (C) 2018 Guido Berhoerster +# +# 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