A minimal, single-file GUI to control laptop battery charge thresholds
(start/stop) via Linux sysfs and TLP.
Why? I normally keep my battery within a conservative charge range to protect its long-term health, but every so often I need to leave the house with a full charge instead. Since I could never remember the exact sysfs/TLP commands for that, I put together this small script to flip between the two in a couple of clicks. At the time I couldn't find an existing tool that did this on Ubuntu 24.04 Noble / Linux Mint 22 (TLPUI, the obvious candidate, is abandoned and broken there) — so this exists mostly for that one recurring need, not as a general-purpose battery manager.
If you want something broader, see Alternatives at the end.
This is a simple, single-file tool with no required pip dependencies — it
runs on plain Tkinter, and looks nicer (flat, dark-mode aware) if
ttkbootstrap + darkdetect happen to be installed.
By Pedro Luis Barrio — github.com/plbarrio
-
Shows the live battery level and charging status (🔋 62% Charging) with a colored bar — 🟩 green while charging, 🟨 yellow when idle (neither charging nor discharging), 🟥 red while discharging — auto-refreshed every 10 seconds without disturbing the sliders if you're mid-adjustment.
-
Shows Current (live sysfs) and Saved (TLP config) values side by side, independently of whatever you have set on the sliders — hover either one for a longer explanation.
-
Two independent sliders — Stop and Start — plus preset buttons (60 / 70 / 80 / 90 / 100%) that set both at once as a shortcut; each can still be fine-tuned separately afterward. Start must be lower than Stop.
-
Apply (temporary) — writes thresholds directly to sysfs. This is the "I need a full charge just for today" button: bump Stop to 100%, Apply, unplug once it's done, and your saved conservative threshold comes back on its own next reboot — no need to remember to set it back.
"Temporary" means TLP puts the saved value back, not that the hardware forgets: on ThinkPads the threshold lives in the embedded controller and survives a full power-off on its own. Measured on a T14 — with both
*_CHARGE_THRESH_BAT0keys commented out so TLP had nothing to apply, an Applied 80 was still 80 after shutting down and powering back on. So if you don't have TLP installed and running, nothing will ever undo an Apply for you, and it is effectively permanent until you change it again. With TLP (the normal case, and what Save writes to) it behaves as the label says. -
Save (persistent) — writes
START_CHARGE_THRESH_BAT0/STOP_CHARGE_THRESH_BAT0to whichever TLP config file actually takes effect —/etc/tlp.confor a drop-in under/etc/tlp.d/*.conf(modern distros commonly use only the latter). Deliberately does not restart TLP itself — that would re-apply everything TLP manages (CPU governor, USB autosuspend, etc.), not just the threshold, which is out of scope for what "Save" promises. The value survives reboots and applies whenever TLP next runs on its own (in practice: at boot); warns iftlp.serviceisn't currently active, since it won't take effect until it is (use Apply for immediate effect instead). -
Its own event log at
~/.local/share/battery-threshold/events.log— every Apply/Save and every charging start/stop/unplug/threshold-reached event is recorded there, independent of whatever the OS notification settings are doing. A History button shows it (newest first); it rolls over toevents.log.1past 1 MiB so it can't grow without bound. It also records thresholds that changed without this app changing them — the EC resetting itself, TLP, a BIOS conservation mode, another tool — with the old and new values and the battery status at the time. If your threshold keeps moving on its own, that log is where you find out when, and what it coincided with.install.sh/uninstall.shlog to the same file (what was created/removed/skipped, and which TLP config file is left active after uninstalling). -
Desktop notifications (
notify-send, with the app's icon) for all of the above. Charging start/stop/unplug notifications can be toggled off (e.g. if your desktop's own power manager already shows these) — Applied/Saved notifications aren't affected by that toggle, and everything keeps being written to the log either way. -
An About button (modal, so it doesn't clutter the main window) with credit and a clickable link — no permanently-visible link/button in the main UI.
-
Optional udev rule (99-battery-threshold.rules) that grants a dedicated group write access to the two sysfs threshold files — once installed, Apply never needs to ask for a password again.
-
Every privileged write is confirmed in a dialog before the password prompt appears, explaining exactly which file and value will be written.
-
Hover Apply or Save for a tooltip telling you whether that action needs a password, before you click anything — Apply's updates live depending on whether the udev rule is installed; Save's is always "yes", by design.
-
Modern flat theme that follows your system's dark/light mode, if
ttkbootstrap+darkdetectare installed — otherwise falls back to plain Tkinter automatically, no crash, no missing features.
See REQUIREMENTS.md for the full spec. In short:
-
Linux with a battery exposing charge thresholds via sysfs, typically:
/sys/class/power_supply/BAT0/charge_control_end_threshold/sys/class/power_supply/BAT0/charge_control_start_threshold(common on ThinkPads and other laptops usingthinkpad_acpior similar)
-
Python 3.8+ with Tkinter (
sudo apt install python3-tk) -
pkexec(polkit) orsudo, since writing thresholds requires root -
TLP installed if you want Save (persistent) — everything else works without it. Apply writes to sysfs, which hands the value to the embedded controller; the EC is what stores and enforces it, not the kernel and not this app, which is why it survives a power-off on its own. Without TLP there is simply nothing that would ever put an old value back, so an Apply stands until you change it. TLP is not special here either:
batctl's systemd unit, a GNOME extension, or your own boot script would overwrite it just the same. If something keeps resetting your threshold, that's what to look for.Measured on a T14, TLP writes the threshold only at boot: it survived plug/unplug, a suspend/resume cycle, and a suspend with the power removed and restored before waking. That fits how TLP documents it — unlike most of its settings, the charge thresholds have no separate AC and battery values, so they aren't part of the profile it switches on power changes; and TLP's per-vendor notes list threshold restoration on resume for ASUS, Huawei and LG, not Lenovo, presumably because the ThinkPad EC never loses it in the first place.
Don't take any of that as universal, though — not even for ThinkPads. TLP's own FAQ hedges it as "persistently stored in the hardware of many vendors", and this thread has two people reporting opposite behavior on the same X1 Carbon Gen 9, one losing the thresholds on every reboot and one keeping them with TLP removed entirely. So firmware revision and what else is running matter, not just the model.
If your threshold keeps getting reset, check for two power managers fighting over it. TLP and
power-profiles-daemonboth want to own this, and having both installed is the usual culprit — that's what the thread above traced it to. Either remove one, or make the conflict explicit intlp.service:[Unit] Conflicts=power-profiles-daemon.service Before=power-profiles-daemon.service -
Optional, for the nicer themed look:
sudo apt install python3-pil.imagetk # ttkbootstrap needs PIL.ImageTk pip install --user ttkbootstrap darkdetect # if pip refuses with "externally-managed-environment": pip install --user --break-system-packages ttkbootstrap darkdetect
Not installed (or
python3-pil.imagetkmissing)? The app silently falls back to plain Tkinter styling — no crash, no missing features.
python3 battery_threshold.pybash install.shThe installer explains and asks for confirmation before each step individually — say no to any one and it's simply skipped, the rest still runs:
- Copies
battery_threshold.pyto~/.local/bin/battery-threshold(no sudo). - Creates a desktop entry under System → Battery Threshold (no sudo),
with
StartupWMClassset so the window is correctly grouped with its icon in the taskbar/dock on KDE, GNOME, XFCE, etc. - Installs a udev rule +
battery-thresholdgroup (requires sudo), and adds your user to that group. After this (and logging back in once), the app can apply thresholds via sysfs without ever asking for a password.
Save (persistent) still requires pkexec/sudo every time, since it
writes /etc/tlp.conf — a shared system file, not something handed out via
group permissions.
Then run:
battery-threshold- Move each slider (or click a percentage preset) to choose Stop and Start.
- Apply (temporary) — takes effect immediately, and goes away on reboot, when TLP re-applies the saved value over it.
- Save (persistent) — writes to the active TLP config file, persists across reboots.
Apply and Save are completely independent — neither implies the other. Clicking Save does not change the live/"Current" threshold at all; the battery keeps using whatever was last Applied (or the system default) until TLP itself next re-applies its config. Clicking Apply does not persist anything; the saved value comes back over it on reboot. If you want a value both active right now and still there after a reboot, click both, in either order.
bash uninstall.shSame "explain before acting" pattern as install.sh — reverses everything
it did (binary, desktop entry, icon, udev rule). The sysfs threshold
files' group is restored to what it actually was before install.sh ever
touched it (recorded at install time, not assumed to be root) — and only
if nothing else changed that group in the meantime; otherwise it's left
alone rather than overwritten. Two things it deliberately
does not touch, since they're your data/config, not this app's
installation footprint: your event log/settings under
~/.local/share/battery-threshold/, and whatever Save wrote to
/etc/tlp.conf or /etc/tlp.d/*.conf — that's your actual battery
threshold, still meaningful after removing the GUI. The script tells you
the commands to remove either, if you really want to.
This app writes to a small, fixed set of paths:
| Path | Purpose |
|---|---|
/sys/class/power_supply/BAT*/charge_control_end_threshold (and _start_threshold) |
Apply threshold immediately |
/etc/tlp.conf and/or /etc/tlp.d/*.conf |
Save threshold persistently — only the file that already defines the threshold (or a new dedicated drop-in if none does) is ever written, never every file |
When does it actually ask for root?
| Action | Needs root? | Why |
|---|---|---|
| Apply (temporary) | No, if install.sh's udev rule is installed |
Your user gets direct group-write access to the two sysfs files — no pkexec/sudo call happens at all. Without the udev rule, it asks every time. |
| Save (persistent) | Always | Writes to shared TLP config — no group-permission shortcut is offered for it, on purpose. |
udev is what removes the password prompt for Apply. It grants the
battery-threshold group write access to exactly the two sysfs threshold
files (99-battery-threshold.rules) — nothing
else.
Whenever elevation genuinely happens, a dialog explains beforehand exactly
which file and value are about to be written — nothing is escalated
silently. Apply never asks twice: STOP and START are batched into a
single pkexec/sudo call, so at most one password prompt appears even if
both files need elevation.
(An AppArmor profile was tried as an extra confinement layer and dropped — see REQUIREMENTS.md for why: an interpreted Python+Tkinter app pulls in too large and too variable a runtime/library surface for path-based confinement to stay maintainable, for a feature that was only defense-in-depth to begin with, never the actual permission gate.)
You can inspect all of this before running anything:
cat battery_threshold.py
cat 99-battery-threshold.rules- If your battery isn't named
BAT0, edit theSYSFS_STOP/SYSFS_STARTpaths at the top ofbattery_threshold.py. - Preset buttons use a default 5% hysteresis (
DEFAULT_HYSTERESISin the script) to pre-fill Start — not enforced afterward, sliders are otherwise independent, so you can widen/narrow the hysteresis band freely. - Multi-battery (
BAT1) support is out of scope for now — see REQUIREMENTS.md. - In TLP's own terms: Apply is
tlp setcharge, Save is editingtlp.conf. TLP's FAQ warns thatsetchargealone won't survive a reboot; what this adds is showing both values at once, so you can see when they've drifted apart.
pytestcovers the pure logic —tlp.conf/tlp.dprecedence, permission checks, regex replace/append, status→color/symbol mappings. No root, no real battery, no GUI needed:pip install --user pytest # or --break-system-packages if required pytest- GitHub Actions (
.github/workflows/tests.yml) runs the same suite on every push/PR. - Everything that depends on real hardware,
sudo, udev, or a real desktop session (Apply/Save actually changing sysfs/TLP config, taskbar icon behavior, notifications, theming fallback, ThinkPad BIOS quirks...) isn't automatable — see MANUAL_TESTING.md for that checklist.
- Linux Mint 22 (Ubuntu 24.04 Noble base), XFCE, ThinkPad T14
This is a single-purpose tool for one recurring need. If you want more, these are better starting points:
- batctl — the closest thing to this one, and a nicer piece of engineering: a Go TUI/CLI covering far more vendors and handling multiple batteries. The difference is what they talk to. batctl drives sysfs directly and persists through its own systemd unit; this one is built around TLP, writing TLP's config file. If TLP already manages your laptop, that matters — TLP re-applies its own configuration at boot over whatever else wrote to sysfs.
- SlimbookBattery — a full, actively maintained power manager, not just thresholds.
- KDE Plasma — native charge-threshold support for a lot of hardware, built into System Settings → Energy. GNOME has been growing its own too.
tlp setchargeand/etc/tlp.conf— if you're comfortable with both, you don't need this at all.
GPL-3.0-or-later — see LICENSE.md.
If you use or reference this project, see CITATION.cff.
