Charging an EV under the Belgian capacity tariff
Keeping the whole house under 2.5 kW while the car charges, with one evcc setting, a budget per quarter hour, and the ceilings and floors that hid whether it was working.
In this post 16 sections
In Flanders, part of the electricity bill is the capaciteitstarief: you pay for your monthly peak, measured as the highest quarter-hour average of the month, with a floor of 2.5 kW. Stay under the floor and you pay the minimum. Run the oven, the dishwasher and an EV charger together for a quarter of an hour and you pay for that quarter hour all month.
So the goal: while the car charges normally, the whole house stays under 2.5 kW. When I press “fast”, the car charges three-phase and unlimited, and the cap gets out of the way.
The cap itself took one setting. Making it hold took most of September: two ceilings I didn’t know about made a broken setup look like a working one, a cap that followed the monthly peak kept pushing that peak up, and an evcc update put back a floor I had switched off.
Updates:
- 1 October: Back at 2.5 kW, the first evening at the 2.5 kW floor, with two charts.
- 28 September: The budget held and An update put the floor back. The rest of the post was rewritten for readability and got three diagrams.
Where it ended up
The version that runs now has four parts:
- evcc’s
residualPoweris set to minus the limit, so Smart mode charges from the grid up to it and Fast mode ignores it. - The limit is 2.5 kW, or the month’s peak minus 100 W if the household has already set a higher one.
- Home Assistant turns the limit into a budget per quarter hour, the unit the tariff bills in, and pushes the resulting target to evcc.
- Always charge is off, with no mode stored on the car to override it, and Home Assistant switches it back off if anything turns it on.
From 18 September, when the budget went live, no charging session moved September’s peak. On 1 October the month started over at 2.5 kW, and on its first evening the charger paused when the house took the room and came back when it left. Two things still leaked: a gap in Home Assistant’s measurement, and the charger’s disable delay while the house alone was over the limit. October’s peak now stands at 3.63 kW, so the cap runs at 3,530 W until something sets a higher one.
The setup
evcc runs as a Home Assistant add-on with one loadpoint: an Alfen Eve Single Pro-line 3PH that can switch phases. Grid measurements come from a HomeWizard P1. There is no PV and no battery, and the control loop runs every 30 seconds. All the configuration lives in evcc’s database, set through the web UI, not in evcc.yaml.
The code quoted below is from evcc 0.315.0, which is what I read when I built this. The add-on runs 0.316.1 now, which renamed the charge modes after what they do instead of what powers them: Solar became Smart (“Slim” in the Dutch UI), and Min+Solar became an “Always charge” switch on the Smart button. Fast is still Fast.
| Before 0.316 | Since 0.316 | API since 0.316 |
|---|---|---|
Solar (pv) |
Smart, Always charge off | smart, alwaysCharge: off |
Min+Solar (minpv) |
Smart, Always charge on | smart, alwaysCharge: on |
| - | Smart, Always charge once | smart, alwaysCharge: once, this session only |
I use the new names throughout, also for what happened before the update.
No PV makes Smart mode a dead button out of the box. Surplus is always 0, so the target current is max(0 - 2.3 A, 0) = 0 and the enable check never becomes true. That’s why the loadpoint ran with Always charge on: it was the only setting that did anything.
One number, in the right branch
The setting is residualPower, which shifts the working point of the control loop:
sitePower := state.gridPower + batteryPower + excessDCPower + residualPower - ...A negative value is therefore allowed grid import: -2500 means regulate so the total grid draw converges on 2.5 kW. The setter has no lower bound, so negative values are accepted.
What makes this work is where sitePower is used. Only the ModeMinPV || ModePV branch reads it, which is Smart with or without Always charge. The ModeNow branch next to it, Fast, calls fastCharging() and never looks at it. So the cap applies only where I want it, without any extra logic to switch it off for fast charging.
It also gives Smart mode something to regulate against without PV: the -2500 is an artificial surplus.
Always charge puts a floor under it
Setting residualPower alone isn’t enough, because of two rules in pvMaxCurrent:
// floor, always
if (mode == api.ModeMinPV || battery) && targetCurrent < minCurrent {
return minCurrent
}
// Min+Solar may never switch off
mayDisable := mode == api.ModePV && !batteryWith Always charge on, the loop regulates down and then sits at the minimum current, which puts a floor under the ceiling:
With Always charge off, the car pauses instead. I give up “always charging at least 1.4 kW” to get “never above 2.5 kW”, which is what a cap means. With one phase, a 6 A minimum and the measured ~240 V, that looks like this:
| Situation | sitePower | Result |
|---|---|---|
| House 523 W, not charging | 523 - 2500 = -1977 | 8.6 A, above the 6 A minimum, so on after the 60 s enable delay |
| Charging steadily | (523+1977) - 2500 = 0 | Holds, grid at 2.5 kW |
| House rises to 2,000 W | (2000+1977) - 2500 = 1477 | Current drops below 6 A, so off after the 180 s disable delay |
Why not load balancing
A circuit with maxPower: 2500 looks like the obvious answer, and it breaks the requirement I care about most:
phases := 3
if !lp.circuitAllowsPhases(3, lp.effectiveMinCurrent()) {
phases = 1
}circuitAllowsPhases(3, 6A) needs 4,140 W of free circuit headroom, which a 2,500 W circuit never has, so fast charging falls back to one phase for good. Without a circuit the check always passes. A roomier circuit doesn’t help either: with the grid meter attached, the house load eats the headroom, and whether fast charging gets three phases depends on what else is switched on.
A circuit does have one property nothing else has: it’s hard in every mode. setLimit clamps after the mode logic and sets enabled = current >= effMinCurrent, so a clamp below the minimum current switches the charger off immediately, with no disable delay. Worth remembering if I ever need a ceiling that Fast mode can’t escape.
The first night: the loop worked, the car didn’t
First run in Smart mode, the night of 10 to 11 September: 14.0 kWh in 10 hours and 20 minutes, a flat 1.36 kW. Disappointing at first glance.
Prometheus scrapes the Home Assistant sensors every minute, so the night can be reconstructed. The control loop did its job:
| Measurement | Value |
|---|---|
| House | median 447 W, mean 603 W |
| Total at the meter | median 1,796 W, max 2,407 W |
| Minutes above 2.5 kW | 0 out of 571 |
| Current evcc offered | 10.4 to 11.5 A, modulating continuously |
With the house around 450 W, evcc saw roughly 2,050 W of room and offered about 11 A. The car drew 5.8 to 6.0 A all night, 1,362 W, which is 0.54 of what it was offered.
That’s a hard 6 A ceiling somewhere between evcc and the battery, and it only showed that night because evcc had never asked for more: with Always charge on and no PV, it sits at the 6 A minimum. From 5 to 10 September it offered 6.00 A and the car drew 5.97 A. The ceiling had been there all along.
The ceiling was in the charger, in the third place I looked
The Alfen has three current limits, and the interesting one isn’t among the two you check first:
| Setting | Was | Relevant? |
|---|---|---|
| Station maximum current | 18 A | No, never reached |
| Active Load Balancing, Safe current | 16 A | No, never reached |
| Connector 1, Max current | 6 A | This was it |
evcc commanded 11 A, the Alfen clamped to the connector limit, and the car followed the pilot signal as it always had. The meter’s phase currents agree: all night the car charged on L2 only, at 6.0 to 6.5 A, while L1 and L3 stayed at house base load.
Safe current is the fallback for an expired EMS connection, not a ceiling. At 16 A against a measured 6 A it was ruled out at once, and that should have pointed me at the connector sooner.
That 6 A cost about 11 kWh that night: 14.0 kWh instead of the roughly 25 kWh that 11 A would have given. The connector limit is an installation setting, not a software knob, so it gets checked against the breaker and the cable of the charging circuit first. It went up to 16 A the next day, in line with evcc’s max current and under the station’s 18 A. The sessions longer than fifteen minutes show the difference:
| Session | Duration | Energy | Charge power, median |
|---|---|---|---|
| 8 Sep | 17.9 h | 24.1 kWh | 1.35 kW |
| 9 Sep | 12.7 h | 17.1 kWh | 1.35 kW |
| 10 Sep | 10.2 h | 14.0 kWh | 1.36 kW |
| 11 Sep | 1.7 h | 8.1 kWh | 4.85 kW |
| 13 Sep | 6.7 h | 29.5 kWh | 4.59 kW |
| 17 Sep | 7.7 h | 38.0 kWh | 5.22 kW |
The first three are the 6 A ceiling. From 11 September the charger modulates between 6 and 16 A over three phases, and the 17 September session took 38 kWh in 7.7 hours where the first night took 14 kWh in 10.2.
The vehicle’s mode wins
The second ceiling was a mode. A charge point’s default mode only applies when a vehicle is unplugged. On identification, the vehicle’s own setting takes over:
// vehicle mode overrides the yaml onIdentify action
mode, ok := v.OnIdentified().GetMode()
if m := vehicle.Settings(lp.log, v).GetMode(); m != "" {
mode, ok = m, true
}
if ok && mode != "" {
lp.SetMode(mode)
}The BMW i4 in evcc’s database had mode: minpv, so every plug-in turned Always charge back on, whatever the loadpoint’s default said. That also explained why the API reported mode: pv while the car was unplugged.
Until I found this, the hard cap had never been active while charging. The first night almost certainly ran with Always charge on, and nothing showed it: above the floor, both settings regulate identically. The fix was clearing the mode on the vehicle, not just on the loadpoint.
Measuring the thing that gets billed
evcc regulates every 30 seconds, but the tariff bills quarter-hour averages, which flatten short spikes, so regulating at 2,500 W is conservative. A miss has a price: one quarter hour at 3 kW instead of 2.5 costs 0.5 kW × €4.45, so €2.23, and only if it ends up being the month’s maximum.
The sensor that counts is the closed quarter hour, which matches what Fluvius bills. My sensor for the running quarter hour was worse than useless. It read p1_qh_energy * 4, but that utility meter resets at :00, :15, :30 and :45, so multiplying by 4 only gives the average at minute 15. Before that it under-reports, which makes a cap look like it holds when it doesn’t. With an oven that starts at minute 5, in a quarter hour that ends at 3.50 kW:
| t | P now | Old (×4) | Average so far | Projection |
|---|---|---|---|---|
| 5 | 5.00 | 0.167 | 0.500 | 3.500 |
| 10 | 5.00 | 1.833 | 2.750 | 3.500 |
| 15 | 5.00 | 3.500 | 3.500 | 3.500 |
At minute 5 the old sensor read 0.167 kW, just as the peak was being set. Two sensors replace it: the real average so far, E × 60/t, and a projection of where the quarter hour lands if the current power holds, 4E + P × (15-t)/15, which needs no division and has no edge case. The peak warnings run on the projection, because a warning should fire on where the quarter hour is heading. Graphs stay on the average, which also got rid of the sawtooth that climbed from zero every quarter hour.
Following the monthly peak
The tariff bills max(2.5 kW, monthly peak). Once the oven and the washing machine have set this month’s peak at 4 kW, anything up to 4 kW costs nothing extra, so the car may as well use that room. At the turn of the month the limit drops back with the peak.
limit = max(2500, monthly peak - margin)
residualPower = -target # the target is the limit until the budget below bitesAt 2.5 kW there’s no margin, because you pay for 2.5 kW either way. Above it, a margin absorbs evcc’s 30-second control lag. Charging at exactly the peak doesn’t raise it, because a quarter hour at the peak averages to the peak. The margin is 100 W, and further down it turns out to be too small.
Home Assistant pushes the value to this endpoint, unchanged between 0.315.0 and 0.316.1:
POST /api/residualpower/{value:-?[0-9.]+}It needs no authentication: ensureAuth covers the config routes, not the site routes under /api. That’s fine on a LAN, and worth knowing before evcc is reachable from anywhere else.
The cap ratchets
On 17 September the meter’s monthly peak, the number that gets billed, went up twice while the car was charging, to 5.73 kW at 20:15 and to 5.83 kW at 21:15, and the target followed each step, from 5,539 to 5,634 to 5,730 W.
The margin is where it leaks. evcc regulates every 30 seconds, so the total overshoots its target between corrections. That session spent 180 of 463 minutes above target, by a median of 27 W, 72 W at the 90th percentile and 1,612 W at worst, and 13 September looked the same. A 100 W margin doesn’t absorb that, so a quarter hour can land above the old peak and set a new one, and the cap follows it up.
That cost €0.85 this month: 0.19 kW at €4.45. The direction bothers me more than the amount. A cap that follows a peak it can push up doesn’t settle until the charger’s own maximum stops it.
My first fix was a 250 W margin, which covers the 90th percentile of the overshoot. It only damps the problem: a margin wide enough for the 1,612 W spike would throw away most of the room the cap is supposed to use.
A budget per quarter hour
The limit applies to an average over fifteen minutes, so the target doesn’t have to be a fixed line. It can be what’s left to spend:
target = (limit × 0.25 kWh - E) × 60 / (15 - t)E is the energy used so far in the running quarter hour, from a Home Assistant utility meter that resets every quarter hour, and t is the minutes elapsed. With nothing spent, the target is the limit. Run high for a few minutes and the target drops so the average still lands on budget. Spend the whole budget early and the target becomes 0, below the minimum current, so evcc switches the charger off until the next quarter hour.
The target is clamped at the limit, so an underused quarter hour can’t turn into a catch-up sprint in the last two minutes. For the first thirty seconds it returns the limit, because the utility meter has only just reset and still shows the previous quarter.
I got one detail wrong on the first try. I divided by 15 - t - lag, to account for the last half minute that can’t be steered, but that spreads the remaining budget over less time than actually remains, so it aims too high. Rendering the template with stubbed sensors showed it: with 800 W of overshoot in the first three minutes, a quarter hour with a 5,730 W limit landed on 5,890 W. Without the lag term it lands on 5,730 W exactly. The last half minute belongs in the margin, not in the division.
Early overshoots of 300, 800 and 1,600 W now all end on the limit. Only an overshoot that persists every minute still leaks, because the final minute can’t be corrected: 690 W continuous ends at 5,776 W. That’s one amp on three phases, the finest step the controller has, so it corrects by dropping an amp for part of the quarter hour rather than by shaving watts. The 46 W left over fits inside a 100 W margin, so the margin went back from 250 W to 100 W.
The budget held
The budget went live on 18 September. Five sessions since, 92.8 kWh in total. The last column comes from Home Assistant’s closed-quarter sensor, which holds each closed value for fifteen minutes, so sampling it once a minute catches every one:
| Session | Energy | Minutes the budget cut the target | Lowest target | Highest closed quarter hour |
|---|---|---|---|---|
| 19 Sep | 16.6 kWh | 72 of 199 | 3,422 W | 5.704 kW |
| 21 Sep | 11.3 kWh | 15 of 132 | 3,385 W | 5.697 kW |
| 22 Sep, morning | 2.2 kWh | 0 of 43 | - | 3.684 kW |
| 22 Sep, evening | 35.3 kWh | 109 of 411 | 3,430 W | 5.703 kW |
| 25 Sep | 27.4 kWh | 31 of 325 | 4,505 W | 5.708 kW |
The limit was 5,730 W throughout, the 5.83 kW peak minus 100 W. In four of the five sessions the budget had to act, pulling the target as low as 3.4 kW, and the highest closed quarter hour of each of those sessions landed 22 to 33 W under the limit. Under the fixed target the peak went up twice in one evening. Under the budget, after 227 minutes of corrections, no session moved it from 5.83 kW.
The simulation assumed the total follows the target. With the 30-second loop, the charger and the car in between, it doesn’t quite, but the quarter hours still land just under the limit, so the car gets nearly all the room there is.
An update put the floor back
The update arrived as a Renovate pull request, like every other image in my homelab. The rename in 0.316 was harmless on its own. Always charge on behaves exactly like Min+Solar did, and the floor is the same code with a new condition:
// with always charge or under special conditions return at least minCurrent
if (alwaysCharge || battery) && targetCurrent < minCurrent {
return minCurrent
}The migration is where it went wrong. On its first start, 0.316 turns a stored minpv into Smart with Always charge on, and somewhere in the loadpoint’s settings there was still a minpv. Always charge is also persistent now, so unplugging doesn’t reset it. After the update the cap had a floor again and nothing looked wrong: the mode said Smart, which is just what Solar is called now. I only noticed because the UI had changed, and the new dropdown showed Always charge on.
It cost nothing this time. The floor only bites when the target drops below the house load plus the car’s 1.4 kW minimum, and this month the budget never pushed the target below 3.4 kW. In a month that stays at 2.5 kW, it would have broken the cap the first evening the oven ran.
The vehicle has its own Always charge setting now too, and a vehicle with minpv stored ends up with it on. The BMW’s mode was already cleared, so it has none.
The fix is the switch, plus a guard so the next migration can’t do this unnoticed. Every minute, Home Assistant reads the one value, using evcc’s jq filter so it fetches 6 bytes instead of 19 kB, and sets it back when needed:
GET /api/state?jq=.loadpoints[0].alwaysCharge # "off", "on" or "once"
POST /api/loadpoints/1/alwayscharge/offWhile the dynamic cap is active, an automation turns on back to off and sends a notification. once, for a single session without pauses, is left alone, because that one is a choice. The guard calls evcc directly rather than going through the Home Assistant integration, which had to change for this same release.
Back at 2.5 kW
September ended at 8.63 kW anyway. On its last afternoon the washer-dryer and the cooking set quarter hours of 6.40 and 8.63 kW with the car not plugged in, so there was nothing for the cap to steer. At midnight the meter reset its monthly peak, and the limit fell from 8,529 W to 2,500 W with it.
On the evening of 1 October the car was plugged in at 18:09, and the cap had to hold 2.5 kW against a real house for the first time:
The car charged on one phase at 6 to 9 A, and the total sat on the limit. When the house took the room, evcc switched the charger off. At 19:43 the house was drawing about 1.2 kW, which left less than the car’s 1.4 kW minimum, and the charger stayed off until the house dropped back to about 0.8 kW at 20:02. At 20:16 a hair dryer took the total to 4.3 kW. The budget cut the target to 2.0 kW, the charger switched off three minutes later, after the disable delay, and it came back at 20:23. The narrow dips in the target at the end of each quarter hour are the budget squeezing its last seconds: with half a minute to go, a few watt-hours over plan become a cut of more than a kilowatt.
By Home Assistant’s numbers, the five quarter hours from 18:15 to 19:30, with the car charging throughout, closed between 2.486 and 2.498 kW. The ones with a pause in them closed lower, except the quarter of 20:15, which closed at 2.56 kW: the house went to 3.6 kW in its final minute, and the final minute can’t be corrected. That cost nothing, because the month’s peak was already higher.
That higher peak is the quarter of 18:15. Home Assistant closed it at 2.491 kW, 9 W under the limit. The meter closed it at 2.591 kW, and the meter is what gets billed. The difference is in E, the energy the budget thinks it has already spent. Home Assistant gets it from a sensor that adds up the P1’s power readings, and at 18:22:45 the P1 readings dropped out for about a minute. That sensor skipped the minute, so the quarter hour came out 25 Wh short of the meter’s own counter. The budget thought it had 100 W of room left that wasn’t there, and spent it.
That gap would have cost €0.40 this month, 0.09 kW at €4.45. The fix is to steer on the meter’s own number. The P1 also reports the meter’s running quarter-hour demand D, the value that becomes the monthly peak, and since D is E × 4, the budget becomes target = (limit - D) × 15 / (15 - t). When the P1 drops out, D catches up as soon as it’s back, because the meter kept counting.
The 2.59 kW peak didn’t last the evening. The house stayed at 3.6 kW from 20:30 to 20:58, on its own, over the limit. The budget ran out and the target fell to 0, but the charger kept going at its 1.34 kW minimum until the disable delay ran out at 20:33. The quarter closed at 3.63 kW, a new monthly peak, and about 0.27 kW of that was those three minutes of charging. That’s roughly €1.20 this month, unless the house sets a higher peak on its own later. A shorter disable delay would cut it, at the price of more stops and starts.
The limit followed the new peak to 3,530 W. When the house load ended at 20:59, the car came back at 21:01 with a kilowatt more room than before, charging at about 2.7 kW. Every quarter hour after that, through 22:15, closed between 3.25 and 3.55 kW, under the new peak.
Side effects worth remembering
Smart mode charges from the grid up to the cap, day and night, which is the intent. Before 0.316 it was called Solar, and the name lied.
Solar % stays at 0. Without a PV meter, evcc derives pvPower from the grid and charge power plus residualPower, and with -2500 that’s always 0. Correct, but it looks odd in the UI.
Fast mode skipping the cap is the point, but two other things take the same route to fastCharging(): an active charging plan (or a minimum state of charge that isn’t reached yet), and Smart with a price limit on a dynamic tariff whenever the price drops under it. Neither is set here. The BMW’s 07:00 plan is switched off.
0.316 also added a solar share setting. Below 1, it lets charging import up to (1 - share) × minimum power on top of the target. It stays at 1.
Once there’s a battery, evcc overwrites a residualPower of 0 or less with 100 W whenever the battery is below its priority state of charge. A home battery would silently switch the cap off every time that happens.
TL;DR
- The capaciteitstarief bills the monthly peak with a 2.5 kW floor, so I wanted the whole house under 2.5 kW while the car charges normally.
- The cap took one evcc setting,
residualPower, but a 6 A ceiling in the charger and the car's ownminpvmode made a broken setup look like a working one. - A budget per quarter hour stopped the cap from pushing the peak up, and a guard now catches an update that turns Always charge back on.
- From 18 September no session moved September's peak, and on the first evening back at 2.5 kW the charger paused when the house took the room.