Feed the Bees

Teaching my laptop when to pause charging

I’m currently working on a predictive charging model for Linux laptops. I’ve gone through a few iterations. It’s inevitably going to change. But I’d like to write here about how the maths works. The weights and parameters and broad ideas are true at the time of writing, but they may drift, change or be completely replaced in future deployments.

You can download the deb, and find the code here: https://github.com/hamishfor/predictive-battery-charging

The daemon

The daemon’s (pbcd) job is to try to estimate, given a plug in time, when am I likely to unplug (I’d like to eventually extended to session criticallity, which means when I unplug how likely is the next session going to require a full battery, and then use that to weight how cautious the model is). Given that information, the daemon will set charge limits at certain times to keep the battery from sitting at 100% during periods of unlikely usage.

To do this, I use a method called the Kaplan-Meier estimator. You can read more about it here. It’s also known as a survival estimator, but here I’m using to how long the charging sessions lasts before the laptop is unplugged.

Building the survival curve

The survival curve, written as S(t), is the estimated share of comparable sessions still plugged in after t hours. It starts at 1 and steps down as plug in events are observed along the time series.

At an unplug event, the estimate is multiplied by the share that made it through that point:

S = S * (1 - unplugged_weight / at_risk_weight)

We can think of this as the history of a set of weighted sessions moving along a timeline. At any duration, some sessions have already unplugged, some are still being observed, and some have stopped being observed while still plugged in. The fraction unplugged_weight / at_risk_weight estimates the share of the at-risk group that unplugs at that point. So (1 - unplugged_weight / at_risk_weight) is the opposite fo that, it’s the share that survives the unplug event.

The at-risk weight is the total weight of sessions that lasted at least that long and were still being observed just before the unplug.

Here are some example sessions from my laptop and what happened to survival

# Plugged in Lasted What happened At risk Unplugged Survival
1 12/10, 12:30 0h 20m Unplugged 5 1 1 × (1 − 1/5) = 0.80
2 14/10, 12:56 0h 25m Unplugged 4 1 0.80 × (1 − 1/4) = 0.60
3 19/10, 14:58 1h 42m+ Still plugged in (censored) 3 0 stays at 0.60
4 13/10, 12:39 2h 05m Unplugged 2 1 0.60 × (1 − 1/2) = 0.30
5 8/10, 12:00 5h 11m Unplugged 1 1 0.30 × (1 − 1/1) = 0.00

survival is the estimated chance that a session like these is still plugged in after that long. From the second row, survival is 0.60 at 25 minutes, so about 60% of midday sessions are expected to last longer than 25 minutes.

In normal use, it’s worth noting that the weights aren’t static for each session; they deteriorate with time, so more recent sesisons are weighted more highly. Currently the sessions have a half life of about 90 days (this is largely an arbitrary number I have selected, I’m still collecting data). So the sessions are about half as relevant after 3 months.

Being cautious

I’m not the most… schedulely driven person. I might unplug my laptop at 1630, or 1830. A to-the-minute estimate would be next to useless for me, and basically any other real-world person I suspect! I tried to come up with some reasonable spread time that the model selected when the device should reach full charge. The model reveals when the estimated share of unplugged sessions reaches 20%, the 20th percentile of session duration.

Basically, having the laptop fully charged for a bit longer, vs having less battery is a trade off I’m willing to make.

Using the output

The Kaplan-Meier quantile supplies pbcd with a full_by time. Separately, the app estimates how much to trust it by multiplying four factors:

confidence = support * consistency * share_long * accuracy

support grows with the total recency weight of similar sessions. consistency is based on the spread of observed unplug durations, a standard deviation of one hour or less scores 1, while three hours or more scores 0. share_long is the share of similar sessions lasting at least three hours. accuracy reflects whether earlier predictions left the battery ready when it was unplugged.

This gives the app a practical score for how much the controller should rely on the estimate. If history is sparse or erratic, confidence stays low and the charging policy becomes more conservative.

Closing thoughts

I’m still testing this, it appears to be working “ok” for now, I’ve not got much data to work with. I’m thinking of adding an opt-in telemetery flag. But I’d need a place to store that telemetry. I’ve not had any expereince with tracking data like that, so would need to think of some way of not exposing the end point to poisoning without adding authentication. And also of course be privacy respecting.

You’re welcome to give it a go. Installation instructions on the github page linked at the start. Currently, the builds are only targeted at Gnome Debian-based operating systems. It’s been tested on both Ubuntu 26.04 and Debian 13. You can install it on other deb based distros and it should still work, just without the Gnome extension. Everything the Gnome extension does can be controlled by the pbc controller (pbcctl) and via config.