How My Charge Planner Grew Up

How My Charge Planner Grew Up

A few months ago I wrote about the script that decides when my battery charges — how every half hour it compares today's cheapest slot against tomorrow's, and only buys when the gap is worth it. That's still the heart of it.

But a clever buyer that occasionally lets the battery run flat, or freezes when the network hiccups, isn't much use. The last few months have been about two less glamorous things: teaching it to protect itself, and moving it out of my blog into a house of its own.

Chapter one: it learned not to get caught out

The original logic was tuned to spend as little as possible — exactly the sort of thing that talks itself into deferring a charge on a grey afternoon, then watches the battery slide toward empty. Cheap is no good if the lights go out. So the fix came in two layers.

The first is predictive. Before the planner commits to a run of cheap slots, it simulates where the battery's charge will actually go between now and the first of them. If that path would dip below a safety margin — five points above the hard floor — it quietly inserts a bridging charge to carry the gap. It plans not to need rescuing.

The second is reactive, for when the plan meets reality. If the charge actually falls into the danger band, the code that drives the charger overrides everything else and forces it on, as long as the rate is under a sensible ceiling. Belt, and braces.

The failure that actually bit me was subtler. The planner wakes on the hour and half-hour; if Home Assistant is restarting at that exact moment, the tick is skipped and the charger stays in whatever state it was last left in. One May evening that left it charging at 28.6p for twenty-two minutes before I noticed.

Now a separate five-minute watchdog checks the switch against what the plan wants and corrects it. It's idempotent — it only writes when the state is wrong — so it can't fight the main planner, it just covers for it when the network drops a beat.

The proof is in the year. The battery never once fell through its floor across twelve months. The lowest it ever reached was 22.3%, the reactive override stepped in eight times, and the planner deferred a charge 226 times betting a cheaper slot was coming — almost always rightly. Safe first, cheap second.

The safety record — one year
Neverthe battery fell through its floor
22.3%the lowest it ever reached
the reactive override forced a charge
226×a charge was deferred for a cheaper slot

Chapter two: it moved out of the blog

The second change is architectural, and I'm quieter about it because nothing visible moved. The planner was born inside this blog's codebase — same repository, same database file, the reporting and the decision-making tangled together.

That always sat wrong. A blog is a blog; the thing that decides whether to pull a kilowatt into a live battery is not — and it shouldn't be able to break because I fumbled an edit to a travel post.

So I pulled them apart. The planner now lives in its own repository, with its own database of rates and decisions. The blog keeps a database that knows only about blog things. Neither can reach into the other's tables.

They talk over a message bus instead. The planner broadcasts a compact summary of its latest decision and its next 48 hours; the blog subscribes. When my solar dashboard loads, none of it is reading the planner's database live — it's reading the last thing the planner published. The blog could fall over entirely and the battery would carry on charging exactly as planned, none the wiser.

That's the version of grown-up I actually wanted. The interesting logic hasn't changed much — it still compares, it still counts the pennies. It's just harder to break now, and when it does break it fails in the right direction. A year in, the little script that runs my flat's electricity is finally something I'd trust to run unattended — which was the whole point of building it.

Chapter three: the record that lasted a day

I would love to end there. I could not. This post was a day old when the sentence above — trust it to run unattended — was put to the test, and failed. At two the next morning the battery slid to 18.6%, straight through the floor I had just sworn it never crossed.

The cause was Chapter two's sting in the tail. Moving the planner into its own house left a stale copy of the old code behind — and the five-minute watchdog was still reading it. That copy predated the emergency override, so every five minutes it switched the charger back off, cancelling the rescue the live code kept ordering. My safety net was throttling the thing it was built to save.

The fix was a single line — point the watchdog at its own code, never a neighbour's — plus two tests that break the build if it ever drifts again, and a sweep closing the same trap in two other scripts. I wrote the whole night up here. The lesson stuck: “we moved it” and “we moved all of it” are different sentences, and a safety net I have never watched fail is only decoration.

Thinking of switching energy supplier? We both get £50.

Switch to Octopus Energy →