Waarom dit topic?
Aangezien ik van een paar mensen al wat vragen heb over de Marstek sturing die ik in Home Assistant opgezet heb om het capaciteitstarief wat onder controle te houden, maak ik er maar even een apart topic van.Doel van deze sturing
De voornaamste sturing is die van verbruikers. Door het verbruik te spreiden, voorkom je de grootste pieken. Bepaalde verbruikers kan je echter niet of moeilijk spreiden, en je kan ook maar beperkt dingen uitschakelen wanneer er een grote piek aankomt. Die momenten kunnen dan opgevangen worden door de batterij.De implementaties van peak shaving die ik tot nu toe gezien had, laten toe dat je een limiet instelt, en van het moment dat het vermogen daarboven komt, gaat de batterij de overschot bijpassen, zodat er een harde limiet is op het vermogen dat er van het net genomen wordt.
Voor de typisch Belgische situatie met het capaciteitstarief, waarbij het gemiddeld opgenomen vermogen van elk kwartier berekend wordt, en de hoogste kwartierpiek van elke maand meetelt voor een stuk van je netvergoeding, is dat niet zo interessant, want je gaat de batterij zo veel meer gebruiken dan nodig is om je kwartierpiek te beperken, waardoor je verliezen (door de round trip efficiëncy van de batterij) groter zijn, en het misschien niet meer rendabel is.
Ik wilde een sturing die een tijdelijke piekbelasting niet gaat corrigeren, wanneer er nog marge is. Om te bepalen wat die marge is, kijk ik in de eerste 10 minuten van elk kwartier naar het gemiddelde vermogen over de laatste 10 en 15 minuten.
De laatste 5 minuten wordt er effectief gestuurd op het vermogen dat dat kwartier nog gebruikt kan worden zonder over de limiet te gaan. Hiervoor wordt het aantal Watt-seconden dat nog overblijft gedeeld door het aantal seconden dat resteert in het kwartier, rekening houdend met wat marge om de traagheid van de batterij en clock drift van de digitale meter te compenseren.
Componenten en configuratie basis sturing
Minimale benodigdheden
- Home Assistant
- Digitale meter met actieve P1 poort en een module om die uit te lezen, ofwel een andere vermogenmeter die je kan uitlezen
- B2500 Home Assistant add-on (https://github.com/tomquist/b2500-meter)
- Marstek plug-in batterij. De B2500 add-in is enkel bedoeld om deze batterijen voor de gek te houden.
Optionele benodigdheden
Voor een betere sturing, willen we ook het maximum laad- en ontlaadvermogen sturen afhankelijk van de omstandigheden. Hiervoor is de modbus koppeling nodig, ofwel op de Venus V3 via de modbus over TCP (enkel via ethernet), ofwel via een modbus module die verbonden is met de RS485 poort.Hiervoor gebruik ik https://my.home-assistant...tory=marstek_venus_modbus
Home assistant helpers
statistieken en parameters
Omdat we bij de kwartierpiekbeperking de batterij niet naar 0W op de meter willen sturen, maar naar een waarde die een variabele offset heeft ten opzichte van de echte meter, hebben we een paar helpers nodig.- sensor.p1_10_minute_average_power: statistics helper met het lopend gemiddelde over 10 minuten van de p1 meter
- sensor.p1_5_minute_average_power: statistics helper met het lopend gemiddelde over 5 minuten van de p1 meter
- input_number.grid_power_limit_target: slider waarmee je je gewenste grens voor de kwartierpiek instelt
berekening variabele vermogensgrens
We willen een grens bepalen waarboven de batterij moet gaan ingrijpen. Deze grens hangt natuurlijk af van de ingestelde grens, maar om wat headroom te hebben, wordt er de eerste 10 minuten van het kwartier gekeken naar het gemiddelde van de laatste 5 en 10 minuiten. Later in het kwartier weten we wat nauwkeuriger hoeveel vermogen er de rest van het kwartier nog mag gebruikt worden om niet over de piek te gaan, dus voor de laatste 5 minuten wordt die waarde gebruikt.variabele offset: number.grid_power_limit_target_adjusted
Dit is een template helper, die ik in de instellingen begrensd heb tussen 0 en 22.000, maar die nog een eigen begrenzing krijgt, gebaseerd op de ingestelde limiet.
Waarde gesimuleerde p1 metercode:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86{# ----------------------------- Inputs ------------------------------ #} {% set setTarget = states('input_number.grid_power_limit_target') | float(0) %} {% set quarterAvg = states('sensor.p1_meter_average_demand') | float(0) %} {% set avg10m = states('sensor.p1_10_minute_average_power') | float(0) %} {% set avg15m = states('sensor.p1_15_minute_average_power') | float(0) %} {# ----------------------------- Parameters ------------------------------ #} {% set latency_s = 5 %} {# Latency of the battery #} {% set quarterBand_W = 100 %} {# Margin in W for the quarter-hour average. Make sure we end up below the limit #} {% set avgBand_W = 0 %} {# Margin in W for the averages. Optional since we steer towards the effective remaining power budget #} {% set k10_up = 3.50 %} {# How quickly the target power will rise based on the 10 minute average #} {% set k10_down = 1.00 %} {# How quickly the target power will drop based on the 10 minute average #} {% set quarterStartGuard_s = 2 %} {# Prevent strange readings around quarter-hour average reset. 5s should be enough #} {% set p1MeterDrift_s = 4 %} {# How many seconds after the quarter-hour the p1 meter resets the average demand #} {% set downSlopeTarget_s = 60 %} {# Number of seconds before the end of the quarter where the target adjusts down #} {% set downSlopeSpeed_W_s = 0.5 %} {# Number of watts to lower the target by every second into the downslope target #} {# ----------------------------- Time remaining in current quarter-hour, try to compensate for p1 meter clock drift up to 60s ------------------------------ #} {% set secIntoQuarter = (now().minute % 15) * 60 + now().second %} {% if secIntoQuarter <= 60 and quarterAvg > (setTarget / 2) %} {% set secIntoQuarter = 900 %} {% endif %} {% set secLeft = 900 - secIntoQuarter %} {# effective time left after accounting for battery latency #} {% set secLeftEff = [1, p1MeterDrift_s, secLeft + p1MeterDrift_s - latency_s] | max %} {% if secIntoQuarter < quarterStartGuard_s %} {% set quarterAvgEff = 0 %} {% else %} {% set quarterAvgEff = quarterAvg %} {% endif %} {# Gradually aim for a lower target the last minute, but don't go lower than quarterAvg + 10. Hard limit to setTarget - 5 #} {% if secIntoQuarter >= (900 - downSlopeTarget_s) %} {% set t = secIntoQuarter - (900 - downSlopeTarget_s) %} {# seconds into the down-slope window: 0..downSlopeTarget_s #} {% set adjTarget = setTarget - (downSlopeSpeed_W_s * t) %} {% set adjTarget = [adjTarget, quarterAvg + 10] | max %} {% set adjTarget = [adjTarget, setTarget- 5] | min %} {% else %} {% set adjTarget = setTarget %} {% endif %} {# ----------------------------- Quarter-hour constraint (hard limit) quarterAvg is "average if the rest of the quarter-hour would be zero consumption". That implies energy already used this quarter ≈ quarterAvg * 900 (W*s). To finish the quarter at adjTarget, the max constant power for the remainder is: p <= (adjTarget*900 - energy_so_far) / seconds_left, compensated for latency ------------------------------ #} {% set energySoFar = quarterAvgEff * 900 %} {% set powerBudgetInQuarter = ((adjTarget * 900) - energySoFar) / secLeftEff %} {# ----------------------------- 10-minute constraint (soft limit) - If avg10m is comfortably below target, allow some headroom above adjTarget. - If avg10m is near/over target, pull below adjTarget more aggressively. ------------------------------ #} {% set diff10 = adjTarget - (avg10m + avgBand_W) %} {% if diff10 >= 0 %} {% set p10 = adjTarget + diff10 * k10_up %} {% else %} {% set p10 = adjTarget + diff10 * k10_down %} {% endif %} {# ----------------------------- Combine: - The first 10 minutes, base the target on the calculation with the 10 minute power, unless the power budget is higher, or 15 minute average close to setTarget - Next, follow the power budget ------------------------------ #} {% set p = [p10, 2*adjTarget] | min %} {% set p = [p, powerBudgetInQuarter] | max %} {% if secIntoQuarter >= 600 or (avg15m - setTarget) | abs < 50 %} {% set p = powerBudgetInQuarter %} {% endif %} {# Limit p to 2*adjTarget, and round to nearest integer #} {% set p = [p, 2*adjTarget] | min %} {% set p = [0, p] | max %} {{ p | round(1) }}
De offset die in de vorige helper berekend is, wordt gebruikt om een waarde voor de gesimuleerde p1 meter te berekenen. Deze helper gebruik je als input voor de B2500 add-in.
code:
1
2
3
4
5
6
7
8
| {#---------------------
Inputs
-----------------------#}
{% set adjTarget = states('number.grid_power_limit_target_adjusted') | float %}
{% set pInstant = states('sensor.p1_meter_power') | float %}
{% set p = [pInstant, p10s] | max %}
{{ pInstant - adjTarget }} |
Eens je dit toegevoegd hebt, en deze laatse waarde als input voor de B2500 add-in ingesteld hebt, kan je de Marstek koppelen aan de gesimuleerde meter en zal die je piek gaan beperken. Dit is dan het resultaat:
/f/image/AACSpKEqASUN2QBCTzyvznps.png?f=fotoalbum_large)
Uitbreiding
Tekortkoming van de eenvoudige versie
De sturing zoals die hierboven staat werkt om de kwartierpiek te beperken, maar omdat de batterij zichzelf ook onmiddellijk wil opladen wanneer het vermogen wat zakt, krijg je bij sommige belastingen een heel zenuwachtige sturing. Bovendien zorgt dit ervoor dat er een stuk meer vermogen gebruikt wordt dan wanneer de batterij niet zou gaan laden, waardoor de batterij later in het kwartier weer vermogen moet gaan leveren om zijn eigen verbruik van net daarvoor te gaan compenseren.Minimale aanpassing
Een eenvoudige minimale aanpassing, is het laadvermogen via de Modbus integratie te beperken tot een lage waarde, bijvoorbeeld 200W. Hierdoor zal de batterij wel gaan laden, maar wordt het vermogen niet de hele tijd tot tegen de limiet getrokken tot de batterij vol is. Aangezien het mijn doel is om de gebruikte capaciteit voor peak shaving te beperken tot pakweg 1kWh per dag, met hier en daar eens een uitschieter naar 2kWh of zo, is een trage laadsnelheid prima. Daarmee krijg je dan bijvoorbeeld dit:/f/image/zyqPywoHPUJt0wXNgYYW3Bqt.png?f=fotoalbum_large)
betere oplossing
Om dit iets beter te regelen, kan je met een paar automations werken om het laden uit te stellen tot het verbruik een stuk lager ligt:- eentje die het maximum laadvermogen op 0 zet zodra de batterij moet terugleveren
- een andere die het maximum laadvermogen instelt op bijvoorbeeld 500 of 1000W, wanneer het verbruik ver genoeg gezakt is. Hierdoor wordt er wat efficiënter geladen dan aan 200W.
- een laatste die het maximum laadvermogen aanpast op basis van de SOC van de batterij. Wanneer die bijna leeg is, wil je waarschijnlijk sneller laden of dadelijk gaan laden.
[ Voor 0% gewijzigd door GORby op 19-01-2026 11:02 . Reden: groot code blok in quote gezet ]
/f/image/zkwfQHexdCM8PoqJ9sfGYdUU.png?f=fotoalbum_large)