144 lines
4.2 KiB
Nix
144 lines
4.2 KiB
Nix
# ThinkPad battery charge limiter (native sysfs thresholds).
|
|
#
|
|
# Unlike the HP module (modules/hardware/hp-battery-limit.nix), ThinkPad
|
|
# firmware exposes the battery charge thresholds directly via sysfs:
|
|
#
|
|
# /sys/class/power_supply/BAT0/charge_control_start_threshold (resume point)
|
|
# /sys/class/power_supply/BAT0/charge_control_end_threshold (stop point)
|
|
#
|
|
# Writing a value < 100 to the end threshold stops charging at that capacity;
|
|
# charging resumes once capacity drops to/below the start threshold. The EC
|
|
# persists these across reboots (firmware NVRAM), so unlike the HP /proc magic
|
|
# no periodic poller is required — a one-shot `apply` at boot re-asserts the
|
|
# persisted choice in case the firmware was reset.
|
|
#
|
|
# Control from the shell / CLI (same interface as the HP module, so the
|
|
# QuickShell cog can drive either host):
|
|
# battery-charge-limit on|off|status|apply
|
|
# `on`/`off` persist the choice to /var/lib/battery-charge-limit/state and are
|
|
# applied immediately. petere has NOPASSWD sudo so the QuickShell cog can call
|
|
# `sudo battery-charge-limit on/off` directly.
|
|
{
|
|
config,
|
|
lib,
|
|
pkgs,
|
|
...
|
|
}:
|
|
|
|
let
|
|
cfg = config.my.hardware.thinkpadBatteryLimit;
|
|
|
|
stateDir = "/var/lib/battery-charge-limit";
|
|
stateFile = "${stateDir}/state";
|
|
|
|
sysfs = "/sys/class/power_supply/BAT0";
|
|
|
|
controlScript = pkgs.writeShellScriptBin "battery-charge-limit" ''
|
|
set -eu
|
|
|
|
state_dir=${stateDir}
|
|
state_file=${stateFile}
|
|
start_threshold=${sysfs}/charge_control_start_threshold
|
|
end_threshold=${sysfs}/charge_control_end_threshold
|
|
capacity=${sysfs}/capacity
|
|
|
|
stop_at=${toString cfg.stopAt}
|
|
resume_at=${toString cfg.resumeAt}
|
|
|
|
read_end_threshold() {
|
|
cat "$end_threshold" 2>/dev/null || echo 100
|
|
}
|
|
|
|
ensure_state() {
|
|
mkdir -p "$state_dir"
|
|
if [ ! -f "$state_file" ]; then
|
|
# Infer the current firmware state so the toggle reflects reality on
|
|
# first run (e.g. a limit set from BIOS/Vantage persists in the EC).
|
|
if [ "$(read_end_threshold)" -lt "$stop_at" ]; then
|
|
echo on > "$state_file"
|
|
else
|
|
echo off > "$state_file"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
get_state() {
|
|
ensure_state
|
|
cat "$state_file"
|
|
}
|
|
|
|
apply_policy() {
|
|
local state
|
|
state=$(get_state)
|
|
case "$state" in
|
|
on)
|
|
# Enable the limit: resume charging below resume_at, stop at stop_at.
|
|
echo "$resume_at" > "$start_threshold"
|
|
echo "$stop_at" > "$end_threshold"
|
|
;;
|
|
*)
|
|
# off (or unknown) -> full charge range (0..100).
|
|
echo 0 > "$start_threshold"
|
|
echo 100 > "$end_threshold"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
case "$1" in
|
|
on)
|
|
ensure_state
|
|
echo on > "$state_file"
|
|
apply_policy
|
|
echo on
|
|
;;
|
|
off)
|
|
ensure_state
|
|
echo off > "$state_file"
|
|
apply_policy
|
|
echo off
|
|
;;
|
|
status)
|
|
get_state
|
|
;;
|
|
apply)
|
|
apply_policy
|
|
;;
|
|
*)
|
|
echo "usage: battery-charge-limit {on|off|status|apply}" >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
'';
|
|
in
|
|
{
|
|
options.my.hardware.thinkpadBatteryLimit = {
|
|
enable = lib.mkEnableOption "ThinkPad battery charge limiter (stop charging at stopAt%, re-enable below resumeAt%).";
|
|
stopAt = lib.mkOption {
|
|
type = lib.types.int;
|
|
default = 80;
|
|
description = "Charge capacity % at which charging is inhibited (charge_control_end_threshold).";
|
|
};
|
|
resumeAt = lib.mkOption {
|
|
type = lib.types.int;
|
|
default = 75;
|
|
description = "Charge capacity % below which auto charging is re-enabled (charge_control_start_threshold).";
|
|
};
|
|
};
|
|
|
|
config = lib.mkIf cfg.enable {
|
|
environment.systemPackages = [ controlScript ];
|
|
|
|
# One-shot re-assert at boot (ThinkPad EC persists thresholds, so no
|
|
# poller is needed; this only recovers from firmware resets).
|
|
systemd.services.battery-charge-limit = {
|
|
description = "ThinkPad battery charge limiter (${toString cfg.stopAt}% cap)";
|
|
wantedBy = [ "multi-user.target" ];
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
RemainAfterExit = true;
|
|
ExecStart = "${controlScript}/bin/battery-charge-limit apply";
|
|
};
|
|
};
|
|
};
|
|
}
|