My laptop sits on a desk for 90% of its life with the lid open and the AC plugged in. I leave the house with it maybe twice a week. The battery's job, almost all the time, is to do nothing. It is a backup power supply for the rare unplug, and that is fine.
Lithium-ion cells age in two ways. They age calendar-wise faster when held near 100%, and they age cycle-wise faster the more often they are charged and discharged, even by small amounts. The standard advice for a laptop that lives on AC is to keep the battery somewhere in the middle, around 50-70%, and to avoid cycling it.
My first attempt was the obvious one. I set TLP's charge thresholds: start at 55%, stop at 60%, done. After a while I noticed the battery was consistently warm to the touch. The charge logs showed why. The cell was being topped up every two to three hours: charge to 60%, charger off, slow self-discharge to 55%, charger back on, repeat. That is what start-and-end thresholds describe. They define a band the battery cycles inside, which is better than letting it sit pegged at 100% but it is still a quiet stream of cycle aging.
I wanted a third state above thresholds: stop charging entirely while AC is connected, and hold the battery exactly where it is.
Since Linux 5.17 there has been a sysfs attribute called charge_behaviour at /sys/class/power_supply/BAT0/charge_behaviour. On supported hardware (ThinkPads via thinkpad_acpi, ChromeOS-EC laptops via cros_ec, recent Frameworks, and some System76 and ASUS models) one of the available options is literally inhibit-charge. Set it, and the charger stops. AC powers the system, the cell sits idle and self-discharges at maybe a percent or two per day. Switch back to auto on unplug and the battery discharges normally.
The feature was sitting there. The widely-used userspace tools (TLP, GNOME's power panel, KDE's, framework-tool) mostly stick to start-and-end thresholds because that is the most universally supported interface across vendors. charge_behaviour is newer and less universal, so the higher-level tools largely leave it alone. You can flip the sysfs attribute by hand, but if anything reboots, restarts the daemon, or switches AC state, you fall back to auto and the cell drifts.
I wrote inhibit-charge to put a small, persistent layer on top. It is roughly 150 lines of bash. The daemon spends its time waiting on udev events and writing one byte to sysfs, so there is no hot path that would justify a compiled binary. Bash is also auditable in a way that compiled code is not, which matters more to me for a tool that writes to a battery management interface.
The daemon watches udevadm monitor --subsystem-match=power_supply and reacts to plug, unplug, capacity, and CLI signals instantly. The CLI writes the desired state (home mode at 60% by default, or travel mode at 100%) to /var/lib/inhibit-charge/ and signals the daemon over SIGHUP. Changes apply with no polling delay. A slow 5-minute fallback timer handles the case where the cell self-discharges past the recharge band while parked.
The recharge band scales with the target: max(2, target/10). So a target of 60 parks in 54..60, a target of 25 parks in 23..25. As a safety net, the same band is also written to the firmware-level start and end thresholds. If the daemon dies for any reason, firmware still holds the battery near the target. Belt and suspenders.
The interface is two modes, because a laptop has two relevant states: "I am at my desk" and "I am leaving the house." Anything richer (schedules, time-of-day rules, calendar integration) is overengineering for what is, in practice, a binary. inhibit-charge home [target] parks at the target when plugged in. inhibit-charge travel charges to 100% before you leave. inhibit-charge status shows what is happening. journalctl -u inhibit-charged -f shows why. That is the entire interface.
This tool is not universal. charge_behaviour is exposed by maybe a third of laptop drivers right now. ThinkPads are the safest bet. ChromeOS-EC machines work well. Recent Frameworks work on current EC firmware. System76 and ASUS vary by model. Most Dell, HP, MSI, and Razer laptops do not expose it, and the daemon refuses to start with a clear error on those. The README has a tier table.
If your hardware does not support it, you fall back to TLP's thresholds, which is what you would have had anyway. Nothing breaks.
The battery is no longer warm. It sits around 60% for weeks at a time. When I unplug for a meeting it discharges normally. When I plug back in, the daemon decides what to do based on where the level landed: above 60% it ignores AC and lets the pack idle; between 54% and 60% it also does nothing, holding the charge as it is; only if the level dipped below 54% does it allow a short top-up back to 60%, then stop again. The hysteresis band keeps the charger from flapping on and off near the target. The result is the most boring battery behavior I have ever observed, which is exactly what I want from a battery.
I will not know the long-term impact for a few years. Calendar aging at a parked state of charge is well-documented in the literature, but it is not zero. The honest question is whether parking is better than a tight cycling band, and that depends on your battery chemistry, ambient temperature, and how often you actually unplug. For a desk-bound laptop with rare unplugs, parking is the right strategy. For someone who unplugs every day, the math may go the other way.
The kernel had the feature. Userspace just needed to use it.
