A Home Assistant integration that calculates the PV surplus and switches loads automatically by priority.
It is the counterpart to the Energy Manager Card: the card displays and allows manual operation, the integration decides and switches. Both use the same formula — otherwise the display would claim something other than what actually happens.
Status: under development. The automation switches, but has not yet been observed over a longer period on a real installation. The main switch is deliberately off after setup — observe first, then arm it.
Requires Home Assistant 2025.2 or newer (config subentries).
- HACS → ⋮ menu → Custom repositories
- Add
https://github.com/eltomato89/EnergyManagerIntegration, category Integration - Install "Energy Manager", restart Home Assistant
- Settings → Devices & Services → Add integration → "Energy Manager"
Copy the folder custom_components/energy_manager to /config/custom_components/ and restart Home
Assistant.
-
Choose the meter source: a bidirectional grid sensor (>0 import, <0 export) or separate sensors for production and house consumption. A home battery is optional.
-
Add loads — one at a time via "Add subentry" on the integration entry. Only the switch entity is mandatory; a power sensor, the rated power and the timing fields make the decision more precise.
If the rated power is missing, the integration estimates it from the recorded statistics of the power sensor: the maximum of the last seven days, refreshed once a day. The mean is unsuitable for this — it would include every hour in which the device was off. If that is not possible either (no power sensor, no recording, sensor without
state_class), a default of 500 W applies, and that is almost always wrong.The
required_sourceattribute on the status sensor tells you where the value comes from:min_power,max_power,measured,estimatedordefault. -
Observe. The surplus sensor should match the installation and the load states should be plausible. If something is off, the configuration is wrong — not the calculation.
-
Only then turn on the main switch.
The Energy Manager Card from v0.4.0 finds the integration on its own and displays everything. Loads are maintained here only.
Card and integration are versioned independently; the numbers are deliberately not kept in lockstep. What the two sides share is the attribute contract, not the release cadence: releases on one side alone are routine, and the card probes for attributes rather than versions, so an older integration means less display, never an error. Where a card feature does require a minimum integration version, the interface contract lists it. That document is the single place where cross-repository decisions are recorded.
The integration intervenes in a real installation. Six mechanisms prevent it from doing so at the wrong moment — each one closes a case that would otherwise show up as oscillation or as a wrong switching decision:
| Protection | Against what |
|---|---|
| At most one switching action per run | Switching on three devices at once would mean spending the same budget three times |
| Anticipation | Seconds pass between switching on and the meter showing it. In that gap the same surplus would be handed out a second time |
| Settling window (default 60 s) | After a switching action that device is left alone, whatever the surplus does. Choose it at least as long as the power sensor needs — sluggish smart plugs report only after minutes |
| Smoothing (default 60 s) | A passing cloud is no reason to switch off. While the averaging window is only half filled, nothing is switched at all |
| Nothing during startup | While HA boots, not every entity reports a state — the installation would look as if everything had failed |
| Nothing with an unusable sensor | A kWh meter or a failed sensor is a configuration error, not a surplus of 0 W |
In addition, each load has the four timing fields (turn_on_delay, turn_off_delay,
min_runtime, min_off_time) and its own automation switch.
It determines two things:
- Who is served first when surplus becomes available.
- Who has to give way to whom if they would otherwise never get a turn.
The second point is the important one. An example: 800 W of surplus and three loads requiring 1000 W, 500 W and 200 W, in that order of rank. The first one does not fit, so the second and third turn on. If the surplus rises to 1100 W, only 400 W are free — the most important load would never run, even though the installation has long been delivering enough.
Therefore: if available surplus + the load of the running, lower-priority consumers is enough for a more important one, those give way. As few as possible give way, the least important first, and only if it is actually sufficient in the end — otherwise you would have switched things off and gained nothing.
Not displaced is anything working through a minimum runtime (a wash cycle in progress is not
aborted), anything not participating in the automation, anything under a forced run, and anything
just switched. The displaces attribute on the status sensor shows how many would give way for a
given load.
Enter a maximum charge power for the battery (in its settings) and it joins the priority order
as a load of its own. Loads ranked above the battery are served first and stay on; the battery then
reserves up to its charge power at its rank; only what is left goes to loads ranked below it. When
the battery is full it reserves nothing. This is pure scheduling — the battery is not commanded,
so no control entity is needed; a number.…_battery_priority holds its rank, draggable in the card
like any load. Leave the charge power empty (the default) and the battery stays what it was: a
correction term in the surplus, with every load ranked ahead of it.
The most common question — and without an answer, an absent switching action cannot be told apart from a fault. Two places provide it:
The automation status sensor (sensor.…_status) for the whole:
| Value | Meaning |
|---|---|
starting |
HA is still booting, or the averaging window is too sparsely filled |
sensor_error |
A meter sensor is missing, has failed, or does not measure power |
paused |
Main switch off |
running |
Everything ready |
The status sensor of each load in its blocked_by attribute for the individual case:
| Value | Meaning |
|---|---|
not_managed |
The automation switch of that load is off |
unavailable |
The switch entity reports no state |
settling |
The settling window after the last switching action is running |
forced |
A forced run is active — the automation keeps away |
turn_on_delay / turn_off_delay |
The condition has not held long enough yet |
min_runtime / min_off_time |
A lockout is running, see "locked until" |
If nothing is listed there, the surplus is simply not sufficient — which the load state then says
(off_close, off_insufficient).
One hub device plus one device per load:
Hub
| Entity | Purpose |
|---|---|
switch.…_automation |
Main switch. Off means: nothing is switched |
sensor.…_surplus |
Available surplus after reserve and state-of-charge rule |
sensor.…_surplus_unsmoothed |
Unsmoothed raw value, for diagnosis |
sensor.…_status |
State of the automation |
number.…_battery_priority |
Rank of the battery as a shiftable load — only when a maximum charge power is set |
Per load
| Entity | Purpose |
|---|---|
switch.<name>_automation |
Does this load participate in the automation? |
number.<name>_priority |
Rank, 1 = highest |
sensor.<name>_status |
Load state, same values as in the card |
sensor.<name>_locked_until |
When the lockout ends — exact, not estimated |
No helper variables are required. The card reads and operates these entities directly.
The entity IDs above follow the language of your Home Assistant instance, because HA derives them
from the translated entity name. On a German instance the same sensors are called
sensor.…_ueberschuss and sensor.<name>_gesperrt_bis. The card does not rely on the IDs; it
identifies the entities by their role.
Four services for what cannot be expressed through entities alone: a duration. The main switch only knows on and off.
| Service | Purpose |
|---|---|
energy_manager.force_on |
Switches a load on immediately and keeps it running for the given time, regardless of surplus. Works even with the main switch off, since this is an operation rather than an automation decision |
energy_manager.clear_force |
Ends a forced run early. Nothing is switched off; from then on the surplus decides again |
energy_manager.pause |
Stops the automation, optionally for a set time ("two hours of quiet", during maintenance for instance) |
energy_manager.resume |
Arms it again |
force_on and clear_force target a load device; an entity of the same device works just as
well, since an automation usually has an entity_id at hand.
action: energy_manager.force_on
target:
device_id: <wallbox>
data:
duration: "01:30:00"uv venv --python 3.13
uv pip install -r requirements_test.txt
.venv/bin/python -m pytest tests/ -q
.venv/bin/python -m ruff check . && .venv/bin/python -m ruff format --check .The calculation core (surplus.py, smoothing.py, engine.py) is a port of the card logic and is
verified against the same test cases. A differing result is a bug — not an acceptable deviation.
MIT