# 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"; }; }; }; }