I have it working on my Framework laptop running Gentoo, first loaded this kernel module:
Then I put this at /usr/share/tlp/bat.d/41-framework
(it’s a hacked up version of the template that TLP provides):
#!/bin/sh
# Copyright (c) 2023 Thomas Koch <linrunner at gmx.net> and others.
# SPDX-License-Identifier: GPL-2.0-or-later
# Needs: tlp-func-base, 35-tlp-func-batt, tlp-func-stat
# --- Hardware Detection
readonly BATDRV_FRAMEWORK_MD=/sys/module/framework_laptop
batdrv_is_framework () {
# check if vendor specific kernel module is loaded
# rc: 0=ok, 1=other hardware
[ -d $BATDRV_FRAMEWORK_MD ]
}
# --- Plugin API functions
batdrv_init () {
# detect hardware and initialize driver
# rc: 0=matching hardware detected/1=not detected/2=no batteries detected
# retval: $_batdrv_plugin, $_batdrv_kmod
#
# 1. check for native kernel acpi (Linux 5.4 or higher required)
# --> retval $_natacpi:
# 0=thresholds/
# 32=disabled/
# 128=no kernel support/
# 254=laptop 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
#
# 3. define sysfile basenames for natacpi
# stop threshold --> retval $_bn_stop,
#
# 4. determine present batteries
# list of batteries (space separated) --> retval $_batteries;
#
# 5. define charge threshold defaults
# stop threshold --> retval $_bt_def_stop;
_batdrv_plugin="framework"
_batdrv_kmod="framework_laptop" # kernel module for natacpi
# 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 ! batdrv_is_framework; then
echo_debug "bat" "batdrv_init.${_batdrv_plugin}.no_match"
return 1
fi
fi
# presume no features at all
_natacpi=128
# shellcheck disable=SC2034
_bm_read="natacpi"
_bm_thresh="none"
# shellcheck disable=SC2034
_bm_dischg="none"
_bn_stop=""
_batteries=""
# framework doesn't support charge start threshold
_bt_def_stop=60
# 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_end_threshold" ]; then
# threshold sysfiles exist
_bn_stop="charge_control_end_threshold"
_natacpi=254
else
# nothing detected
_natacpi=254
continue
fi
if readable_sysf "$bd/$_bn_stop"; then
# threshold sysfiles are actually readable
_natacpi=0
_bm_thresh="natacpi"
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
# shellcheck disable=SC2034
_batdrv_selected=$_batdrv_plugin
echo_debug "bat" "batdrv_init.${_batdrv_plugin}: batteries=$_batteries; natacpi=$_natacpi; thresh=$_bm_thresh; bn_stop=$_bn_stop;"
return 0
}
batdrv_select_battery () {
# determine battery acpidir and sysfiles
# $1: BAT0/BAT1/DEF
# # rc: 0=bat exists/1=bat non-existent
# retval: $_bat_str: BAT0/BAT1/BATT;
# $_bd_read: directory with battery data sysfiles;
# $_bf_stop: sysfile for stop threshold;
# prerequisite: batdrv_init()
# defaults
_bat_str="" # no bat
_bd_read="" # no directory
_bf_stop=""
# validate battery param
local bs
case $1 in
DEF) # 1st battery is default
_bat_str="${_batteries%% *}"
;;
*)
if wordinlist "$1" "$_batteries"; then
_bat_str=$1
else
# battery not present --> quit
echo_debug "bat" "batdrv.${_batdrv_plugin}.select_battery($1).not_present"
return 1
fi
;;
esac
# determine natacpi sysfiles
_bd_read="$ACPIBATDIR/$_bat_str"
if [ "$_bm_thresh" = "natacpi" ]; then
_bf_stop="$ACPIBATDIR/$_bat_str/$_bn_stop"
fi
echo_debug "bat" "batdrv.${_batdrv_plugin}.select_battery($1): bat_str=$_bat_str; bd_read=$_bd_read; bf_stop=$_bf_stop"
return 0
}
batdrv_read_threshold () {
# read and print charge threshold
# $1: start/stop
# $2: 0=api/1=tlp-stat output
# global param: $_bm_thresh, $_bf_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) printf "(not available)\n"; return 0 ;;
stop) out="$X_THRESH_SIMULATE_STOP" ;;
esac
if [ -n "$out" ]; then
printf "%s" "$out"
echo_debug "bat" "batdrv.${_batdrv_plugin}.read_threshold($1).simulate: bm_thresh=$_bm_thresh; bf=$bf; out=$out; rc=$rc"
return 0
fi
if [ "$_bm_thresh" = "natacpi" ]; then
# read threshold from sysfile
case $1 in
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
else
# no threshold api
if [ "$2" = "1" ]; then
out="(not available)"
fi
rc=255
fi
# "return" threshold
if [ "$X_THRESH_SIMULATE_READERR" != "1" ]; then
printf "%s" "$out"
else
if [ "$2" = "1" ]; then
printf "(not available)\n"
fi
rc=4
fi
echo_debug "bat" "batdrv.${_batdrv_plugin}.read_threshold($1): bm_thresh=$_bm_thresh; bf=$bf; 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)..100/DEF(default)
# $2: new stop threshold 0..100/DEF(default)
# $3: 0=quiet/1=output parameter errors/2=output progress and errors
# $4: config battery - non-empty string indicates thresholds stem from configuration
# global param: $_bm_thresh, $_bat_str, $_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_stop=${2:-}
local verb=${3:-0}
local cfg_bat="$4"
local old_stop
# insert defaults
[ "$new_stop" = "DEF" ] && new_stop=$_bt_def_stop
# --- validate thresholds
local rc
# TEMPLATE: customize boundaries to vendor specs
# stop: check for 3 digits max, ensure min 0 / max 100
if ! is_uint "$new_stop" 3 || \
! is_within_bounds "$new_stop" 0 100; then
# threshold out of range
echo_debug "bat" "batdrv.${_batdrv_plugin}.write_thresholds($1, $2, $3, $4).invalid_stop: bat=$_bat_str"
case $verb in
1)
if [ -n "$cfg_bat" ]; then
echo_message "Error in configuration at STOP_CHARGE_THRESH_${cfg_bat}=\"${new_stop}\": not specified, invalid or out of range (0..100). Battery skipped."
fi
;;
2)
if [ -n "$cfg_bat" ]; then
printf "Error in configuration at STOP_CHARGE_THRESH_%s=\"%s\": not specified, invalid or out of range (0..100). Aborted.\n" "$cfg_bat" "$new_stop" 1>&2
else
printf "Error: stop charge threshold (%s) for %s is not specified, invalid or out of range (0..100). Aborted.\n" "$new_stop" "$_bat_str" 1>&2
fi
;;
esac
return 2
fi
# read active threshold values
if ! old_stop=$(batdrv_read_threshold stop 0); then
echo_debug "bat" "batdrv.${_batdrv_plugin}.write_thresholds($1, $2, $3, $4).read_error: bat=$_bat_str"
case $verb in
1) echo_message "Error: could not read current charge threshold(s) for $_bat_str. Battery skipped." ;;
2) printf "Error: could not read current charge threshold(s) for %s. Aborted.\n" "$_bat_str" 1>&2 ;;
esac
return 4
fi
# TEMPLATE: customize assertion to vendor specs
# determine write sequence too meet boundary condition start < stop
# disclaimer: the driver doesn't enforce it but we don't know about the
# firmware and it's reasonable anyway
local rc=0 steprc tseq="stop"
# write new thresholds in determined sequence
if [ "$verb" = "2" ]; then
printf "Setting temporary charge thresholds for %s:\n" "$_bat_str"
fi
for step in $tseq; do
local old_thresh new_thresh steprc
case $step in
start)
continue
;;
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
stop) write_sysf "$new_thresh" "$_bf_stop" ;;
esac
steprc=$?; [ $steprc -ne 0 ] && rc=5
echo_debug "bat" "batdrv.${_batdrv_plugin}.write_thresholds($1, $2, $3, $4).$step.write: bat=$_bat_str; old=$old_thresh; new=$new_thresh; steprc=$steprc"
case $verb in
2)
if [ $steprc -eq 0 ]; then
printf " %-5s = %3d\n" "$step" "$new_thresh"
else
printf " %-5s = %3d (Error: write failed)\n" "$step" "$new_thresh" 1>&2
fi
;;
1)
if [ $steprc -gt 0 ]; then
echo_message "Error: writing charge $step threshold for $_bat_str failed."
fi
;;
esac
else
echo_debug "bat" "batdrv.${_batdrv_plugin}.write_thresholds($1, $2, $3, $4).$step.no_change: bat=$_bat_str; old=$old_thresh; new=$new_thresh"
if [ "$verb" = "2" ]; then
printf " %-5s = %3d (no change)\n" "$step" "$new_thresh"
fi
fi
done # for step
echo_debug "bat" "batdrv.${_batdrv_plugin}.write_thresholds($1, $2, $3, $4).complete: bat=$_bat_str; rc=$rc"
return $rc
}
batdrv_chargeonce () {
# function not implemented
echo_debug "bat" "batdrv.${_batdrv_plugin}.charge_once.not_implemented"
return 255
}
batdrv_apply_configured_thresholds () {
# apply configured stop thresholds from configuration to all batteries
# output parameter errors only
if batdrv_select_battery BAT0; then
batdrv_write_thresholds 0 "$STOP_CHARGE_THRESH_BAT0" 1 "BAT0"; rc=$?
fi
if batdrv_select_battery BAT1; then
batdrv_write_thresholds 0 "$STOP_CHARGE_THRESH_BAT1" 1 "BAT1"; rc=$?
fi
return 0
}
batdrv_read_force_discharge () {
# function not implemented
echo_debug "bat" "batdrv.${_batdrv_plugin}.read_force_discharge.not_implemented"
return 255
}
batdrv_write_force_discharge () {
# function not implemented
echo_debug "bat" "batdrv.${_batdrv_plugin}.write_force_discharge.not_implemented"
return 255
}
batdrv_cancel_force_discharge () {
# function not implemented
echo_debug "bat" "batdrv.${_batdrv_plugin}.cancel_force_discharge.not_implemented"
return 255
}
batdrv_force_discharge_active () {
# function not implemented
echo_debug "bat" "batdrv.${_batdrv_plugin}.force_discharge_active.not_implemented"
return 255
}
batdrv_discharge () {
# function not implemented
# Important: release lock from caller
unlock_tlp tlp_discharge
echo_debug "bat" "batdrv.${_batdrv_plugin}.discharge.not_implemented"
return 255
}
batdrv_show_battery_data () {
# output battery status
# $1: 1=verbose
# global param: $_batteries
# prerequisite: batdrv_init()
local verbose=${1:-0}
printf "+++ Battery Care\n"
printf "Plugin: %s\n" "$_batdrv_plugin"
if [ "$_bm_thresh" != "none" ]; then
printf "Supported features: charge thresholds\n"
else
printf "Supported features: none available\n"
fi
printf "Driver usage:\n"
# native kernel ACPI battery API
case $_natacpi in
0) printf "* natacpi (%s) = active (charge thresholds)\n" "$_batdrv_kmod" ;;
32) printf "* natacpi (%s) = inactive (disabled by configuration)\n" "$_batdrv_kmod" ;;
128) printf "* natacpi (%s) = inactive (no kernel support)\n" "$_batdrv_kmod" ;;
254) printf "* natacpi (%s) = inactive (laptop not supported)\n" "$_batdrv_kmod" ;;
*) printf "* natacpi (%s) = unknown status\n" "$_batdrv_kmod" ;;
esac
# TEMPLATE: customize to vendor specs
if [ "$_bm_thresh" != "none" ]; then
printf "Parameter value ranges:\n"
printf "* START_CHARGE_THRESH_BAT1/1: 0 (not available)\n"
printf "* STOP_CHARGE_THRESH_BAT1/1: 0..100(default)\n"
fi
printf "\n"
# -- show battery data
local bat
local bcnt=0
local ed ef en
local efsum=0
local ensum=0
for bat in $_batteries; do # iterate batteries
batdrv_select_battery "$bat"
printf "+++ Battery Status: %s\n" "$bat"
printparm "%-59s = ##%s##" "$_bd_read/serial_number"
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
printparm "%-59s = ##%6d## [mAh]" "$_bd_read/charge_full_design" "" 000
printparm "%-59s = ##%6d## [mAh]" "$_bd_read/charge_full" "" 000
printparm "%-59s = ##%6d## [mAh]" "$_bd_read/charge_now" "" 000
printparm "%-59s = ##%6d## [mA]" "$_bd_read/current_now" "" 000
# 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
# --- show battery features: thresholds
if [ "$_bm_thresh" = "natacpi" ]; then
printf "%-59s = %6s [%%]\n" "$_bf_stop" "$(batdrv_read_threshold stop 1)"
printf "\n"
fi
# --- show charge level (SOC) and capacity
lf=0
if [ "$ef" -ne 0 ]; then
perl -e 'printf ("%-59s = %6.1f [%%]\n", "Charge", 100.0 * '"$en"' / '"$ef"');'
lf=1
fi
if [ "$ed" -ne 0 ]; then
perl -e 'printf ("%-59s = %6.1f [%%]\n", "Capacity", 100.0 * '"$ef"' / '"$ed"');'
lf=1
fi
[ "$lf" -gt 0 ] && printf "\n"
bcnt=$((bcnt+1))
done # for bat
if [ $bcnt -gt 1 ] && [ $efsum -ne 0 ]; then
# more than one battery detected --> show charge total
perl -e 'printf ("%-59s = %6.1f [%%]\n", "+++ Charge total", 100.0 * '"$ensum"' / '"$efsum"');'
printf "\n"
fi
return 0
}
batdrv_recommendations () {
# no recommendations
return 0
}