The Someday Project, Part III: The Homelab Paid For Itself In One Push Notification

I built the foundation over a weekend. The thing I actually wanted took an hour.

The Model Y has a bad habit. It sits in the garage, unplugged, while the roof throws 4 kW at an already-full Powerwall and the grid price briefly drops under ten cents. Free energy, one door away, and nobody in the house knows — because the car is in the garage. You don’t walk past it. You don’t see it. There’s no moment in the day where the thought “I should plug that in” is prompted by anything at all.

The obvious fix is a phone reminder at 11am. The obvious fix is wrong, because the answer to “should I plug in right now?” changes minute to minute and depends on four things that live in four different systems. A fixed daily reminder is either noise or it’s silent at exactly the wrong time. Usually both, in that order, until you swipe it away forever.

This is the third part of the series. Part I turned my Home Assistant instance into a proper Grafana data platform. Part II built the Homarr control plane I’d abandoned three times. Same collaborator, same house, same principle: I supply direction, the agent supplies execution.

Here’s the part I didn’t expect. Parts I and II were the point. This one is the payoff — and it’s the first time the homelab stopped being a hobby and started being useful in a way I can put a dollar figure on.

The question is harder than it looks

Here’s what has to be true, simultaneously, for a plug-in alert to be worth sending:

  1. I’m home. Alerting me about free electricity while I’m at the shops teaches me to ignore the alert. One bad push and the channel is dead.
  2. The car is unplugged. Obvious, but it needs a live cable sensor, not an assumption about what I did last night.
  3. There’s genuinely surplus energy. And this is the subtle one — either the solar is producing more than the house is eating and the Powerwall is already full (otherwise the battery should get those electrons first; charging the car from solar while the battery is at 60% is just moving the problem), or the grid price has dropped low enough that it doesn’t matter where the electrons come from.

Four data sources. Tessie for the car’s cable state and battery percentage. Tessie again for the Powerwall charge and live solar output. Amber Electric for the live wholesale price. Home Assistant for presence. Each with its own auth, its own units, its own refresh cadence.

Doing this manually means opening four apps and doing mental arithmetic about whether 2.3 kW of surplus into a 96%-full battery justifies a charge session — and doing it repeatedly, all day, on the off-chance the numbers happen to line up. Nobody does that. That’s the whole reason the car sits unplugged. The task isn’t hard, it’s just impossible to remember to do at the exact moment it matters.

The weekend that made it a one-hour job

Here’s the thing I want to be precise about, because it’s the actual argument of this whole series.

None of the four integrations were built for this.

Tessie was already in Home Assistant because I wanted the car on a dashboard. Amber Electric went in for the Grafana energy graphs — I wanted to look at what power costs, that’s all. ntfy — a self-hosted push server behind Caddy, with a Cloudflare tunnel so it reaches my phone off-LAN — exists because Uptime Kuma and Beszel needed somewhere to shout when a container fell over. Home Assistant was there because of course it was.

Every one of those went in for its own unrelated reason, over a weekend, as part of Parts I and II. And because they did, the plug-in alert cost almost nothing. It was assembly, not construction.

I’d have told you, before this, that the return on a homelab is the services. It isn’t. The return is that the marginal cost of the next idea keeps falling. Once you have a notification bus, a live energy price feed, and a car API all in the same place, speaking the same language, “tell me when charging is free” stops being a project and becomes a config change. The alert itself is one template expression and one push call.

That’s what a foundation is. Not two years of accumulated cruft — a weekend of boring glue, built once, that turns every subsequent someday-idea into an afternoon. I didn’t know that until the third one was trivial.

Technical insights, and the things that bit

Units will lie to you, silently. The Powerwall’s …_solar_power sensor reports in kW, not W. The first threshold written was > 2000 — a perfectly reasonable number that would never, ever fire. It doesn’t error. It doesn’t warn. It just quietly never triggers, and six weeks later you conclude the sun isn’t shining hard enough in Australia. The agent caught it by evaluating the template against live state before trusting it — the same “ask the running system, don’t assume” discipline that ran through Part II.

Fail inert, not loud. The cheap-grid branch reads sensor.amber_general_price | float(999). If Amber’s API is down or the entity goes unavailable, that 999 default makes 999 < 0.10 false and the branch quietly disarms — while the solar branch keeps working. A default of 0 would have turned every Amber outage into a 3am push insisting electricity is free. When you pick a fallback value, pick the one that fails towards silence.

Names are load-bearing. The Amber integration’s site is named “Amber” specifically so its entities land on sensor.amber_general_price. Rename that device in the UI and the automation doesn’t break — it keeps running with the cheap-grid branch permanently disabled, courtesy of that same float(999). Graceful degradation and silent failure turn out to be the same mechanism wearing a different hat. Worth writing down somewhere you’ll find it.

Home Assistant is fully drivable headlessly. All of this went in over REST — the automation via /api/config/automation/config/, and the template helper via /api/config/config_entries/flow (handler template, then a menu step, then a form). No clicking through the UI, which means the whole thing is diffable, reproducible, and re-deployable from the repo.

Revision 1: the version that tested perfectly

The first cut put the whole opportunity expression inline in the automation’s conditions, with two triggers:

  • sustained — a state trigger with for: 00:10:00, so a passing cloud doesn’t fire an alert
  • reminder — a time_pattern: /15 poll that re-pushes every 2 hours while the opportunity holds, throttled against the automation’s own last_triggered

The sustain trigger deliberately bypasses the throttle. That’s intentional: if I plug in, then unplug an hour later while it’s still sunny, that off→on transition should be able to alert me again immediately, even inside the 2-hour reminder window. A feature, not an oversight.

It tested clean. Every gate evaluated correctly against live state. A forced fire landed on the phone with the right numbers. The throttle template correctly blocked the reminder path.

Then it met the real world.

Revision 2: the double-push race

On 15 July, around midday, the conditions genuinely lined up for the first time — the first organic fire, as opposed to the ones we’d triggered by hand.

It pushed twice, 27 seconds apart.

The mechanism, once we went and read the timestamps rather than theorising: the reminder poll fired at 12:00:01, with the opportunity having been true for about nine and a half minutes and last_triggered stale enough to sail straight through the 2-hour throttle. So it pushed. Then 27 seconds later the opportunity crossed its ten-minute mark, the sustained trigger fired — and sustained bypasses the throttle by design — so it pushed again.

Two independently correct rules, racing each other. The reminder path had no idea the initial sustain window was still counting down, because it had no way to know.

The fix was two changes, and only one of them is interesting:

  1. Extract the state. The opportunity expression moved out of the automation and into a template binary sensor helper, binary_sensor.tesla_plug_in_opportunity. “Is this an opportunity?” is now a first-class entity with its own last_changed timestamp, rather than an expression re-evaluated from scratch in two places that can’t see each other. As a bonus, all three thresholds — 2 kW solar, 90% Powerwall, $0.10/kWh — now live in exactly one template instead of being smeared across the conditions.
  2. Give the reminder path a floor. It now additionally requires the sensor’s last_changed to be more than 600 seconds ago. The reminder now physically cannot pre-empt the initial sustain, because both paths are finally reading the same clock.

Lesson: The bug wasn’t the throttle. It was that two triggers were reasoning about the same condition without sharing a source of truth for when that condition started*. The 600-second floor is the fix you’d write in five seconds; making the opportunity a sensor is the fix that made it possible to write, because there was finally something to ask.*

That’s a lesson that generalises well past Home Assistant. When two rules disagree, the first question isn’t “which rule is wrong” — it’s “what do they both need to know that neither can see.”

Where it stands

Live, verified, waiting on the next organic fire to confirm exactly one push. When it goes off, the message says why: solar 0.9 kW · Powerwall 96% · car at 80%, priority 4, zap emoji. A notification that just says “plug in the car” is one you’ll eventually stop believing; a notification that shows its working is one you’ll act on.

The Amber price is also plotted on the Grafana energy dashboard from Part I, with a red threshold line drawn at 10c — the exact number the automation triggers on. If the alert ever behaves oddly, the graph and the trigger tell the same story, because they’re the same number.

Total new infrastructure required: one template helper.

The honest part

Part II ended with me admitting that what unblocked the dashboard wasn’t a tool — it was that the tedious work between “I want this” and “this exists” could be delegated to something that doesn’t get bored.

This one’s different, and I think it’s the more interesting admission. The plug-in alert wasn’t unblocked by delegation. It was unblocked by the fact that the last two projects existed. The agent’s contribution here was the weekend that came before, not the hour that produced the thing I actually wanted.

I’ve been thinking about homelabs wrong for years. I treated each service as the deliverable — Plex is for watching things, Grafana is for looking at graphs, ntfy is for container alerts. But the value compounded somewhere I wasn’t looking: in the adjacency. Four integrations that had nothing to do with each other, sitting in one system, turned out to already contain the answer to a question I’d never asked them.

The car gets plugged in now. Not because I’m more disciplined — because the house finally has enough context to notice something I never could, standing in the kitchen, with the garage door shut.

Build the boring glue once. The third idea is where it pays.