I ran into an architectural problem in Home Assistant — calculation logic slowly spreading across dashboards, automations and template sensors — and this is the solution I ended up with. This is an architecture write-up, not a “look at my dashboard” post.
The short version
Over time my energy management system (EMS) accumulated business logic — calculations, aggregations, comparisons — scattered across dashboard templates, template sensors and automations. It worked, but it was impossible to reuse, hard to maintain, and recalculated on every render.
To fix that, I built a layer of what I call Compute Units (CUs): small, Home-Assistant-native calculation units built with templates, YAML and the built-in scheduler. They are not external services, containers or plugins. The entire runtime lives inside Home Assistant — no Node-RED, no AppDaemon, no external services.
The whole design turns on two principles, which became the backbone of the architecture.
The first decides where logic belongs:
Does this operation create a new truth, or present an existing one?
If it creates a new truth (an aggregate, a trend, a percentage), it belongs in a Compute Unit. If it merely presents an existing one (age, color, sorting, filtering), it belongs in the dashboard.
The second guarantees each truth exists only once:
Every truth has exactly one owner.
A calculation is performed exactly once, by exactly one Compute Unit. Everything else — dashboards, automations, and even other Compute Units — consumes only that canonical result and never recomputes the same truth. This is what stops business logic from quietly leaking back into dashboards and automations, and what prevents duplicate calculations from drifting into conflicting answers.
Everything below follows from these two rules.
Here’s the layer model I ended up with:
L1 Collect raw data from devices, feeds, sensors
L2 Stabilize handle dropouts, spikes, renames
L3 Normalize consistent naming / aliasing
L4 Present short-term dashboards — the here and now
L5 Compute condensed values in historical perspective
L6 Decide cross-domain, rule-based decisions
Part 1 — How it grew (the accidental architecture)
My EMS didn’t start as a grand design. It started, like most Home Assistant projects, with one question: can I see my energy data? Everything after grew organically — each layer solving a problem the layer below created.
- L1 — Collect. Raw measurements from devices, inverters, batteries, price feeds, weather.
- L2 — Stabilize. Real-world data is messy: sensors drop out, spikes happen, integrations rename things. This layer turns unreliable input into a consistent, trustworthy stream.
- L3 — Normalize. Naming and aliasing, so a sensor’s meaning survives a firmware update or an integration change.
- L4 — Present. The here and now — short-term dashboards. This is where most HA setups live, and for a long time mine did too.
- L5 — Compute. Here the stream gets distilled and placed in historical perspective. Not “what’s the value now” but “what’s the pattern, how does today compare to the last 30 days.” Data starts to become insight.
For a long time, L1–L5 was a complete, usable system. I could have stopped there and had a mature platform — many people do, and that’s perfectly fine.
Then I wanted it to decide things. And I hit a wall.
Part 2 — The dilemma, and the choice
A Decision Layer — a system that doesn’t just show data but acts on it — needs data. Not raw data, but reliable, structured, continuously available data.
(To be clear about a loaded word: when I later call this “intelligence,” I mean rule-based reasoning — not AI, not ML, not an LLM. Deterministic decisions from clean inputs.)
When I looked at where my calculations actually lived, I found them scattered: buried in dashboard templates, tangled with presentation logic, recalculated on every render, impossible to reuse. My L5 insights existed, but they weren’t served anywhere a decision layer could reliably consume them.
So the real problem wasn’t “how do I build decisions.” It was: how do I feed that layer, 24/7, without melting my system?
Why not Node-RED, AppDaemon, or a pile of scripts?
The obvious answer many people reach for is an external runtime. I deliberately didn’t. My reasoning was accessibility, not sophistication:
- It demands a different skill set. Not everyone runs Node-RED or wants to learn a new paradigm on top of HA. I wanted a solution that stays inside HA, using tools HA users already know — templates, YAML, sensors.
- It would re-scatter the logic. A wall of flows or a folder of scripts becomes just as opaque as the dashboard templates I was trying to escape. That’s trading one mess for another.
- It adds a runtime to babysit. Another moving part, another dependency outside HA’s own lifecycle.
I wanted something you build once, that any Home Assistant user can understand, and that scales up or down trivially afterward.
What a Compute Unit is
Each CU performs one calculation and publishes its result as a canonical value with a contract — standardized metadata: status, domain, owner, cadence, last run, dependencies, quality. From the outside a CU is read-only: it creates truth; dashboards only present it (that first backbone question again).
And critically, that truth has exactly one owner. The CU is the single place its value is computed. Every consumer — a dashboard, an automation, another CU that needs it as input — reads the published result and never recomputes it. No duplicated logic, no two versions of the same number slowly drifting apart.
“Isn’t this just a template sensor?” — A Compute Unit may internally use one or more Home Assistant template sensors, triggers and helpers, but conceptually it is treated as a single architectural component with one responsibility, one owner and one published contract. The value isn’t the template — it’s the discipline around it.
The scheduler — why not event-driven
This is the part that makes CUs more than “a pile of template sensors,” so it’s worth spelling out.
Every Compute Unit owns its cadence. Fast-changing calculations run every few minutes; daily statistics run once a day. Combined with skip-if-unchanged — a CU only recomputes when its source data actually changed — this keeps CPU usage predictable while still providing continuously available canonical results.
I deliberately chose not to execute every calculation directly from every source event. Home Assistant itself stays event-driven, of course — but my runtime processes those events in a controlled way, on each Compute Unit’s own cadence, instead of recomputing straight from every incoming change. Event-driven recalculation on a busy HA instance is unpredictable: a chatty sensor can trigger storms of recomputation exactly when you least want it. A fixed cadence per CU makes the load flat, boring and plannable — which is exactly what a 24/7 feed needs to be.
The payoff: scale by metadata
Because every CU is self-describing and publishes a contract, the dashboard that monitors them uses auto-discovery: it simply lists every CU it finds.
I recently rolled out several new CUs across multiple domains. They appeared on the monitoring dashboard automatically:
🟢 Battery — MT1 🟢 PV — GoodWe Roof
🟢 Battery — MT2 🟢 PV — GoodWe Shed
🟢 Battery — MTALL 🟢 Climate — Airco
🟢 PV — Hoymiles 🟢 Wallbox — Live cost
🟢 PV — Solcast 🟢 Dynamic — Spot / Day-avg price
No dashboard changes were required. New Compute Units appear automatically because they publish the same contract.
You build the pattern once. After that it’s fill-in-the-blanks, not reinvention — as long as the metadata is correct, scaling up or down is trivial.
Part 3 — What’s next: the Decision Layer (L6)
A Compute Unit is a pattern, not a layer
Before the layer question, one clarification that took me a while to see myself: the Compute Unit is an architectural pattern, not a fixed layer. Nothing forces CUs to live at L5 and L6. You could skip L5 entirely and feed L4 dashboards directly from CUs, or use CUs only to drive automations. You apply the pattern wherever your design needs reusable, single-owner truth — the layer numbers just describe where that happens to be in my system.
In my case they land at L5 and L6 because of what I actually want to do: connect data across domains and have the system act on those connections. That goal needs values that can be reliably linked together (standardized, contract-bearing output — you can’t connect apples and pears), a place where those links become decisions (L6), and a system that acts on them. Someone whose goal is just cleaner dashboards would apply the exact same pattern at L4. Same pattern, different placement.
Why the Decision Layer sits at L6
So why does my Decision Layer sit at L6, and not L5 or L3? Because each layer is a different kind of work:
- L3 and below prepare data. Storing and naming isn’t decision-making.
- L4 shows the present moment — a snapshot.
- L5 condenses and adds historical perspective — patterns become visible. Groundwork, but it still describes rather than decides.
- L6 is where the actual decisions live — reasoning across domains. Not “is the battery healthy” (a single-domain L5 judgment), but “given the PV forecast and the battery state and the price trend, what should happen?”
The Compute Unit layer runs vertically through all of this: it’s the 24/7 feed for both L5 and L6, delivering reliable, structured data continuously — without extra load on the system, thanks to the scheduler and skip-if-unchanged design.
That last point is the whole reason it works. A decision layer that constantly queried a database or recalculated on demand would grind a home server to a halt. By having CUs compute quietly on their own cadence and publish canonical results, L6 can simply read what it needs, instantly, from data that’s already there.
That’s where I’m headed: developing L6 as an always-on, rule-based decision layer — cross-domain choices, fed continuously by the Compute Unit layer beneath it. The foundation is built. Now the interesting part begins.
Why this might matter even if you don’t build an EMS
Whether you’re building an EMS, a climate controller, or a smart home in general, the underlying problem is often the same: calculations gradually spread across dashboards, automations and template sensors until they’re impossible to reuse or reason about. The Compute Unit layer is one possible architectural answer — giving every calculation a single owner, a published contract, and a clear place in the system.
I’m not posting this as “here’s my project.” I’m posting it as: I ran into an architectural problem in Home Assistant, and this is the solution I ended up with. I’d genuinely like to hear how others have tackled the same question — where does calculation logic actually belong?
Happy to go deeper on any part — the CU contract, the scheduler design, or the two principles above — if there’s interest.






