Zonneplan/Nexus/Waveshare — Zelfbouw EMS, PID-tuning, YAML

Pagina: 1
Acties:

  • Micronikje
  • Registratie: Januari 2018
  • Laatst online: 18:24
Er is op dit forum al veel kennis over day-ahead optimizers (DAO, EMHASS) en over Zonneplan/Nexus in het algemeen. Dit topic is bedoeld voor een specifiekere niche: mensen die zelf een EMS gebouwd hebben rond Zonneplan (Nexus/Waveshare) en daarbij echt de diepte in zijn gegaan — PID-tuning, SoC-strategieën, financiële resultaten, en het uitwisselen van concrete YAML-structuren in plaats van algemene uitleg.

Mijn setup

Solis S6 EH1P hybride omvormer, 20 kWh LFP-batterij, 8 kWp PV (8x Solax X1 micro-omvormers)

YouLess P1 + S0 voor meting, Zonneplan voor dynamische tarieven en batterijhandel

Volledig zelfgebouwd EMS in Home Assistant, momenteel op v31/v32, PID-gebaseerd

Waar ik nu mee bezig ben

Ontkoppelde PID-parameters voor laden en ontladen (kp_charge/kp_discharge) i.p.v. één gedeelde gain

Dynamische avondontlading die meeschaalt met de piek-SoC van die dag

Eén uniforme soc_min_pct-logica, elke cyclus weggeschreven naar een centrale input_number, zodat alle downstream-automatiseringen (net-laden, ontladen, reserve) uit dezelfde bron putten

Migratie van het net-laadmodule van uur- naar 15-minuten Zonneplan-pricing, moet voor 1 augustus 2026 live staan

Een eerste opzet voor predictive feed-forward sturing (Solcast-verwachting naast de reactieve PID), waar ik nog tegen dubbeltelling van het netvermogen in de feed-forward term aanloop en de Solcast-integratie nog niet compleet is

Vragen aan de rest

Hoe pakken jullie PID-tuning aan: vaste gains of adaptief (bijv. EMA-gebaseerd)?

Wie heeft al ervaring met 15-minuten Zonneplan-pricing i.p.v. uurprijzen — ik zag dat DAO in dit forum een vergelijkbare uur-naar-kwartier-migratie heeft doorgemaakt, dus ervaringen daaruit zijn ook welkom

Combineert iemand een day-ahead planner (DAO/EMHASS-achtig) met een reactieve PID-laag, waarbij de planner het dagschema zet en de PID corrigeert op realtime afwijkingen?

Wat zijn jullie financiële resultaten — vermeden import, Zonneplan-handelsrevenue, en verwachte impact van het einde van salderen per 1-1-2027?

Ik deel graag concrete YAML-blokken en debug-sensoren zodra er animo is. Input, tegenargumenten en eigen configuraties zijn allemaal welkom — het doel is een zo goed mogelijk resultaat voor iedereen die dit soort setup draait richting 2027.

  • Micronikje
  • Registratie: Januari 2018
  • Laatst online: 18:24
Afbeeldingslocatie: https://tweakers.net/i/Z4YTN_kOymGUW-xEKEiYmGr2bxc=/800x/filters:strip_icc():strip_exif()/f/image/WzY2DgACRrzmjhQnrFSAZIWW.jpg?f=fotoalbum_large

  • TKroon
  • Registratie: December 2006
  • Niet online
Yes, druk mee bezig. Nog niet voor de Nexus, maar wel voor de Marstek. Is ook een proof of concept voor de Nexus, mocht de Zonneplan-aansturing me tegen gaan staan. Wat ik nu werkend heb:

1. Omdat de Marstek ook noodstroom kan leveren aan mijn belangrijkste groepen, en directe PV input krijgt, berekent Homey de minimale SoC om 24 uur zonder netspanning te kunnen zitten. Met data uit solar forecast, verwachte tijden van hoger verbruik (verlichting, tv, etc) kom ik bijvoorbeeld vandaag uit op 38% SoC (12% is beveiligd, dus 26% heb ik daadwerkelijk nodig).

2. Met die minimale SoC mag een EPEX-script gaan zoeken naar interessante laadkoppels, waarbij er alleen maar ontladen mag worden als er een later goedkoper prijsuur tegenover staat. Ik zeg wel uur, maar het script werkt inmiddels al met kwartieren.

3. Omdat de aansturing niet vlekkeloos gaat, maar ik wel CT-informatie kan faken, stuur ik daar de Marstek mee aan. Ik kan dus via dezelfde aansturing NOM-draaien (zelfs met een Nexus ernaast die zijn eigen plan trekt), maar ook EPEX-handelen.

4. Homey zelf doet ook aan energiemanagement door prioriteiten toe te kennen aan apparaten. Bijvoorbeeld de auto laden als die nog niet vol is, maar wel bijna weg moet. Of de tank opwarmen als er beperkt warm water is, etc. Op basis van die prioriteiten kunnen apparaten worden beperkt in hun verbruik of zelfs uitgezet. Werkt zowel voor load balancing doeleinden als zonnestroom gebruiken.

5. PID-tuning gaat hier niet te snel en dat is goed genoeg. Anders zit je al te snel op over- of undershoots als de Quooker even 7 seconden aanspringt met 10A. Dat vind ik geen ramp en is gerommel in de marge.

6. Ervaring nog niet, maar voor de batterij met handelen gebruik ik kwartierprijzen, voor zaken als de auto laden, tank opwarmen, etc gebruik ik gewoon nog uurprijzen. Dat zijn langere processen wat niet op een paar cent komt, maar toch meerdere kwartieren beslaat. Als de schommelingen straks te groot blijken pas ik het wellicht nog aan om de blokken in zijn geheel wat te verschuiven, maar ik ga de auto niet elk kwartier laten stoppen en starten met laden.
GaMbiNo schreef op zondag 12 juli 2026 @ 11:35:
[...] Via modbus zorg ik nog wel voor een fatsoenlijke loadbalancing om overbelasting van fases te voorkomen.
Dus je hebt de Zonneplan-dongles er gewoon in, maar je overrulet kortstondig via modbus om direct effect te hebben?

Daikin Altherma 3 LT 8 kW + 16 kWp PV


  • GaMbiNo
  • Registratie: April 2001
  • Nu online

GaMbiNo

1337

TKroon schreef op zondag 12 juli 2026 @ 12:02:
[...]

Dus je hebt de Zonneplan-dongles er gewoon in, maar je overrulet kortstondig via modbus om direct effect te hebben?
Klopt.

Ik bereken eigenlijk continu de
code:
1
number.solis_s6_eh3p10k_h_zp_battery_max_charge_current
en de
code:
1
number.solis_s6_eh3p10k_h_zp_battery_max_discharge_current
Daar zet ik continu (als die geknepen moet worden) de maximale ruimte die er is om te laden en ontladen. Als er niet geknepen hoeft te worden staat die op de standaard 50A. Zo weet ik zeker dat Zonneplan geen fases gaat overbelasten. Verder laat ik de aansturing geheel aan Zonneplan.

[ Voor 12% gewijzigd door GaMbiNo op 12-07-2026 12:48 ]


  • Mauri13
  • Registratie: Juni 2011
  • Nu online
Heb met modbus aansturing en EVCC geprobeerd, lukt alleen niet om opwek overschot te laden. Ligt waarschijnlijk aan de gemodificeerde omvormer. Werkt waarschijnlijk beter met standaard omvormer.

Met HBC meer succes. Dit werkt goed en je hebt een aantal strategieën die je kan gebruiken zoals NOM, zon laden, EPEX handel of een combinatie van de strategieën.

  • Micronikje
  • Registratie: Januari 2018
  • Laatst online: 18:24
quote: @GaMbiNo
GaMbiNo schreef op zondag 12 juli 2026 @ 11:35:
Eigen topic lijkt mij ook prima.

Ik heb in de afgelopen tijden ook al een heleboel verschillende varianten eigen aansturing geprobeerd. Allemaal via modbus/Home Assistant.

Ik heb o.a. een eigen aansturing die vooral nul op de meter doet maar overgaat op dynamisch handelen bij een hoge spread. Daarnaast laadt die ook op de laagste dynamische prijzen als er onvoldoende zonnestroom voorspeld is. Maar ik moet de kwartierprijzen nog even inbouwen. :)

Nu draai ik overigens wel weer op Zonneplan aansturing omdat dit een tijdje ook wel weer aardig liep. Via modbus zorg ik nog wel voor een fatsoenlijke loadbalancing om overbelasting van fases te voorkomen.
Interessant, vooral het punt over forecast-gestuurd laden bij te weinig zon — dat raakt precies waar ik met mijn v33 feed-forward tegenaan loop.

Kleine nuance in mijn insteek: mijn focus ligt minder op spread-gedreven handelen en meer op autonomie. Minder import, minder cycli (batterijgezondheid) en minder export leveren mij ook gewoon rendement op, alleen dan via een andere route dan actief handelen. Vandaar dat ik met een continue PID werk in plaats van discrete modes zoals jouw NOM/dynamisch-handelen-omschakeling — ik wil de batterij zo min mogelijk onnodig laten fietsen, en elke cyclus die puur voor arbitrage is (in plaats van voor eigen verbruik) is voor mij eigenlijk een cyclus die niet per se hoeft.

Een paar dingen die ik daarom graag zou willen weten:

Bij jouw omschakeling naar dynamisch handelen bij hoge spread: hoe weeg je dat tegen extra cycli? Of is dat voor jou geen factor omdat de marge dat compenseert?

Je forecast-gestuurde bijladen bij weinig zon — gebruik je dat puur om exportverlies/import te voorkomen (autonomie-doel), of ook om alvast goedkoop in te kopen voor latere handel? Die twee doelen lopen bij mij nogal uiteen in de logica.

Je load-balancing via modbus terwijl Zonneplan de aansturing doet: zit daar ook een SoC-ondergrens in verwerkt voor noodstroom/autonomie, of is dat puur fase-bescherming?

Voor de kwartierprijzen: mijn net-laadmodule is daar al klaar voor gebouwd, maar gaat pas per 1 augustus live (nu draait dat deel nog op v31/uurprijzen). Zodra die overgang achter de rug is deel ik graag wat ik tegenkwam — vooral qua granulariteit van de PID-regeling, want een kwartier is kort genoeg dat je oscillatie/overshoot-gevoeligheid opnieuw moet doorrekenen t.o.v. uurwaarden.

  • GaMbiNo
  • Registratie: April 2001
  • Nu online

GaMbiNo

1337

Micronikje schreef op zondag 12 juli 2026 @ 14:09:
[...]

Interessant, vooral het punt over forecast-gestuurd laden bij te weinig zon — dat raakt precies waar ik met mijn v33 feed-forward tegenaan loop.

Kleine nuance in mijn insteek: mijn focus ligt minder op spread-gedreven handelen en meer op autonomie. Minder import, minder cycli (batterijgezondheid) en minder export leveren mij ook gewoon rendement op, alleen dan via een andere route dan actief handelen. Vandaar dat ik met een continue PID werk in plaats van discrete modes zoals jouw NOM/dynamisch-handelen-omschakeling — ik wil de batterij zo min mogelijk onnodig laten fietsen, en elke cyclus die puur voor arbitrage is (in plaats van voor eigen verbruik) is voor mij eigenlijk een cyclus die niet per se hoeft.

Een paar dingen die ik daarom graag zou willen weten:

Bij jouw omschakeling naar dynamisch handelen bij hoge spread: hoe weeg je dat tegen extra cycli? Of is dat voor jou geen factor omdat de marge dat compenseert?

Je forecast-gestuurde bijladen bij weinig zon — gebruik je dat puur om exportverlies/import te voorkomen (autonomie-doel), of ook om alvast goedkoop in te kopen voor latere handel? Die twee doelen lopen bij mij nogal uiteen in de logica.

Je load-balancing via modbus terwijl Zonneplan de aansturing doet: zit daar ook een SoC-ondergrens in verwerkt voor noodstroom/autonomie, of is dat puur fase-bescherming?

Voor de kwartierprijzen: mijn net-laadmodule is daar al klaar voor gebouwd, maar gaat pas per 1 augustus live (nu draait dat deel nog op v31/uurprijzen). Zodra die overgang achter de rug is deel ik graag wat ik tegenkwam — vooral qua granulariteit van de PID-regeling, want een kwartier is kort genoeg dat je oscillatie/overshoot-gevoeligheid opnieuw moet doorrekenen t.o.v. uurwaarden.
Ik heb voor de eigen aansturing een template gemaakt die eigenlijk bepaald wat de batterij doet.

Daarin doe ik een aantal dingen zoals o.a.:
1. Als het het duurste uur van de dag is, en het verschil tussen het duurste en goedkoopste uur meer dan 20 cent is, dan gaat hij op volle snelheid ontladen. Daarbij kijk ik niet naar extra cycli omdat de opbrengst dan groot genoeg is.

2. De Forecast.solar integratie laat ik een schatting maken van de solar opbrengst van de dag. Als dat te laag is gaat hij op basis van die schatting op de laagste 1, 2 of 3 uur van de dag laden. Om die goedkope stroom later weer te kunnen gebruiken. Of om deze later weer terug te leveren als het prijsverschil groot genoeg is.

De uitkomst van die template bepaald welk deel van de automatisering op dat moment actief is.

Voor het load balancing verhaal icm Zonneplan aansturing kijk ik niet naar SOC. Dat doe ik puur om de fases te beschermen voor een te hoge load van Zonneplan.

Die load balancing doe ik overigens ook in mijn eigen aansturing. Wanneer hij volle bak wil laden/ontladen doet hij dat ook niet meer dan de fasen aan kunnen.

  • Jasper79
  • Registratie: December 2015
  • Laatst online: 17:40
Ik heb zelf Forecast solar ML al een tijdje draaien om in te leren.

https://github.com/Zara-Toorox/Solar-Forecast-ML

  • Micronikje
  • Registratie: Januari 2018
  • Laatst online: 18:24
@GaMbiNo

Als ik het goed begrijp, gebruik jij een vaste spread van 20 cent als trigger voor een volle ontlading? Dus bij een dag waar de spread bijv. 15 cent is, doe jij niks ook al is dat de beste mogelijkheid van de dag. mijn automatisering past zich iets meer aan, door naar het prijsverschil te kijken en een max prijs van 6 uur.

Heb je overwogen om je spread flexibel/adaptief te maken door bijv. een exponentieel voortschrijdend gemiddelde te gebruiken? Ik gebruik hiervoor een 7 daags gemiddelde van mijn import/export per dagdeel. Zie hier de code ervan.
YAML: 7 daagse
1
2
3
4
5
6
7
8
9
10
11
12
13
14
</max_prijs_6u: |
  {% if not data_missing %}
    {% set ns = namespace(prijzen=[], geteld=0) %}
    {% for e in forecast %}
      {% if e.datetime[:16] >= nu_kwartier_str and ns.geteld < 24 and e.electricity_price > 0 %}
        {% set ns.prijzen = ns.prijzen + [e.electricity_price / 10000000] %}
        {% set ns.geteld = ns.geteld + 1 %}
      {% endif %}
    {% endfor %}
    {{ ns.prijzen | max if ns.prijzen | length > 0 else 0 }}
  {% else %}
    0
  {% endif %}
prijs_verschil: "{{ [max_prijs_6u - tarief_nu, 0] | max | round(4) }}"
ps. forcast en electricity_price zijn Zonneplan sensoren

[ Voor 3% gewijzigd door Micronikje op 12-07-2026 22:17 ]


  • Micronikje
  • Registratie: Januari 2018
  • Laatst online: 18:24
Jasper79 schreef op zondag 12 juli 2026 @ 20:47:
Ik heb zelf Forecast solar ML al een tijdje draaien om in te leren.

https://github.com/Zara-Toorox/Solar-Forecast-ML
Zelf gebruik ik solcast. Ik vind die redelijk accuraat.

  • GaMbiNo
  • Registratie: April 2001
  • Nu online

GaMbiNo

1337

Micronikje schreef op zondag 12 juli 2026 @ 22:15:
@GaMbiNo

Als ik het goed begrijp, gebruik jij een vaste spread van 20 cent als trigger voor een volle ontlading? Dus bij een dag waar de spread bijv. 15 cent is, doe jij niks ook al is dat de beste mogelijkheid van de dag. mijn automatisering past zich iets meer aan, door naar het prijsverschil te kijken en een max prijs van 6 uur.

Heb je overwogen om je spread flexibel/adaptief te maken door bijv. een exponentieel voortschrijdend gemiddelde te gebruiken? Ik gebruik hiervoor een 7 daags gemiddelde van mijn import/export per dagdeel. Zie hier de code ervan.
YAML: 7 daagse
1
2
3
4
5
6
7
8
9
10
11
12
13
14
</max_prijs_6u: |
  {% if not data_missing %}
    {% set ns = namespace(prijzen=[], geteld=0) %}
    {% for e in forecast %}
      {% if e.datetime[:16] >= nu_kwartier_str and ns.geteld < 24 and e.electricity_price > 0 %}
        {% set ns.prijzen = ns.prijzen + [e.electricity_price / 10000000] %}
        {% set ns.geteld = ns.geteld + 1 %}
      {% endif %}
    {% endfor %}
    {{ ns.prijzen | max if ns.prijzen | length > 0 else 0 }}
  {% else %}
    0
  {% endif %}
prijs_verschil: "{{ [max_prijs_6u - tarief_nu, 0] | max | round(4) }}"
ps. forcast en electricity_price zijn Zonneplan sensoren
Ik gebruik idd een 20 cents verschil als criteria voor dynamisch handelen. Ik had de code initieel geschreven voor nul op de meter. Maar ik heb hier later wat zaken aan toegevoegd om geen buitenkansjes op dynamisch handelen te missen en ook bij weinig zon te zorgen voor een gevulde batterij.

Verder is het allemaal nog work in progress. Ik ben er een tijdje mee bezig geweest en het werkte prima maar op dat moment presteerde de onbalans ook wel weer oké waardoor ik het weer even heb gelaten.

Dank voor je code. Ik ga hier op een later moment eens voor zitten. Ik moet dan ook de kwartierprijzen integreren.

Wel leuk om ook andermans ideeen hier op te kunnen doen.

  • hasseroderman
  • Registratie: Oktober 2012
  • Laatst online: 14:57
Heel mooi initiatief. Ik ga dit topic met veel interesse volgen met het oog op mijn eigen Nexus en de gevolgen van het stoppen van salderen. Erg benieuwd welke kansen er zijn en op welke manier we maximaal rendement kunnen behalen. NOM, zelf aansturen of door ZP laten doen, we gaan het zien.

  • Micronikje
  • Registratie: Januari 2018
  • Laatst online: 18:24
Wie met een Nexus en eigen HA-logica aan de slag gaat, loopt volgens mij vroeg of laat tegen dezelfde vraag aan: hoe zorg je dat je eigen aansturing en Zonneplans Powerplay niet tegelijk aan de knoppen zitten?

Zonneplan werkt met 3 4G-dongles: twee zitten aan de BMS van de batterij, één aan de P1-poort. Zolang die BMS-dongles aangesloten zijn, kan Zonneplan altijd meesturen — ook als jij zelf setpoints naar dezelfde registers schrijft, wat tot conflicten kan leiden (setpoints die elkaar afwisselen, onvoorspelbaar laad/ontlaadgedrag).

Twee benaderingen die ik ken

1. BMS-dongles loskoppelen

Definitief en simpel: zonder verbinding kan Zonneplan niet meer sturen. Kost je wel de Powerplay-vergoeding volledig — die vervalt zodra de dongles niet actief zijn. Ook belangrijk: Zonneplan kan zien dat de dongles offline zijn en kan daar contact over opnemen. Dus dit is geen stille work-around, ze weten het.

2. Dongles laten zitten, alleen begrenzen (clamping)

Zonneplan blijft sturen, maar jij schrijft continu een maximale laad-/ontlaadstroom naar de limiet-registers. Dit is exact wat GaMbiNo eerder in dit topic beschreef: dongles blijven erin, en hij begrenst alleen via de max_charge_current/max_discharge_current-registers, terwijl Zonneplan verder de volledige aansturing behoudt. Vergoeding blijft dan intact, omdat je niets blokkeert — alleen begrenst.

Vragen aan de rest:

Heeft iemand die de BMS-dongles heeft losgekoppeld ervaring met wat Zonneplan precies doet als ze contact opnemen — is dat puur informatief, of zijn er consequenties voor je energiecontract als je aangeeft bewust te zijn losgekoppeld?

Voor wie clamping gebruikt zoals GaMbiNo: hoe reageert Zonneplans aansturing als hun gewenste setpoint boven jouw limiet uitkomt — respecteert het systeem dat gewoon stil, of zie je foutmeldingen/pogingen om alsnog te forceren?

  • Jasper79
  • Registratie: December 2015
  • Laatst online: 17:40
Eventuele oplossing is om de dongels van stroom te voorzien maar niet aan de batterij gekoppeld (wellicht een POE switch?)

Dan is de dongle online maar stuurt niet.

  • melvinnie
  • Registratie: April 2011
  • Niet online
Micronikje schreef op maandag 13 juli 2026 @ 17:30:
Wie met een Nexus en eigen HA-logica aan de slag gaat, loopt volgens mij vroeg of laat tegen dezelfde vraag aan: hoe zorg je dat je eigen aansturing en Zonneplans Powerplay niet tegelijk aan de knoppen zitten?

Zonneplan werkt met 3 4G-dongles: twee zitten aan de BMS van de batterij, één aan de P1-poort. Zolang die BMS-dongles aangesloten zijn, kan Zonneplan altijd meesturen — ook als jij zelf setpoints naar dezelfde registers schrijft, wat tot conflicten kan leiden (setpoints die elkaar afwisselen, onvoorspelbaar laad/ontlaadgedrag).

Twee benaderingen die ik ken

1. BMS-dongles loskoppelen

Definitief en simpel: zonder verbinding kan Zonneplan niet meer sturen. Kost je wel de Powerplay-vergoeding volledig — die vervalt zodra de dongles niet actief zijn. Ook belangrijk: Zonneplan kan zien dat de dongles offline zijn en kan daar contact over opnemen. Dus dit is geen stille work-around, ze weten het.

2. Dongles laten zitten, alleen begrenzen (clamping)

Zonneplan blijft sturen, maar jij schrijft continu een maximale laad-/ontlaadstroom naar de limiet-registers. Dit is exact wat GaMbiNo eerder in dit topic beschreef: dongles blijven erin, en hij begrenst alleen via de max_charge_current/max_discharge_current-registers, terwijl Zonneplan verder de volledige aansturing behoudt. Vergoeding blijft dan intact, omdat je niets blokkeert — alleen begrenst.

Vragen aan de rest:

Heeft iemand die de BMS-dongles heeft losgekoppeld ervaring met wat Zonneplan precies doet als ze contact opnemen — is dat puur informatief, of zijn er consequenties voor je energiecontract als je aangeeft bewust te zijn losgekoppeld?

Voor wie clamping gebruikt zoals GaMbiNo: hoe reageert Zonneplans aansturing als hun gewenste setpoint boven jouw limiet uitkomt — respecteert het systeem dat gewoon stil, of zie je foutmeldingen/pogingen om alsnog te forceren?
Clamping werkt goed. Zonneplan vraagt 10kW in RC Force Battery Discharge Power maar de omvormer levert uiteindelijk maar 2.5A (~900W) wat ingesteld staat in Battery Max Discharge Current. Geen fouten gehad sinds ik hiermee begon in Februari.

Die waarde blijft gerespecteerd en hoeft dus niet constant overschreven te worden, dat werkt bij de registers welke Zonneplan gebruikt toch niet.

Bij het daadwerkelijk zelf laden/ontladen via Modbus moet je om die rede de dongles uitschakelen. Het is anders vrijwel onmogelijk om te laden/ontladen omdat de registers vrijwel direct worden overschreven.

Beide dongles gaan trouwens uit als je de deze aan de kabel aan de kant van de omvormer losmaakt.

Ik heb in mei contact opgenomen en volgens mij eerlijk aangegeven wat ik doe. Mijn interpretatie van de reactie hieronder is dat het niet verboden is. Misschien denken jullie er nog anders over?

Afbeeldingslocatie: https://tweakers.net/i/cMjF77ZbexR34tSbjA_1WHdQg-Y=/x800/filters:strip_icc():strip_exif()/f/image/Pz6J85J7FV1Q7RPpPjSoCU84.jpg?f=fotoalbum_large

Afbeeldingslocatie: https://tweakers.net/i/fdsDKGoUoH8mzIDpQHf1W4YW-PE=/800x/filters:strip_icc():strip_exif()/f/image/rIiABavYiQIvCNlSX8RgZmkC.jpg?f=fotoalbum_large


Voor 2027 heb ik nog niet echt een plan. Ik heb maar twee panelen (balkon 5 hoog) en hoop dat het opladen blijft zoals het was. Ik ben aardig gehecht geraakt aan die grote energiebron die vrijwel dagelijks voor 0 euro wordt gevuld :)

  • Micronikje
  • Registratie: Januari 2018
  • Laatst online: 18:24
@melvinnie

Goed om te horen dat clamping bij jou ook al sinds februari stabiel draait zonder fouten — dat bevestigt dat dit geen toevalstreffer is maar een robuust patroon.

Interessant is vooral je bevestiging dat het limiet-register niet herhaald hoeft te worden weggeschreven, terwijl het setpoint-register (RC Force Battery Discharge Power) wél continu overschreven wordt. Zelf schrijf ik mijn max-current-waarden continu weg als voorzorg, maar als dat register inderdaad stabiel blijft staan zonder Zonneplans inmenging, kan dat een stuk simpeler — alleen schrijven bij een daadwerkelijke wijziging in plaats van iedere cyclus.

En mooi dat je de reactie van Zonneplan hebt gedeeld — bevestigt dat er geen actief verbod is, alleen geen officiële ondersteuning.

Vraag: zet jij een vaste max-waarde, of pas je die dynamisch aan (bijvoorbeeld op basis van SoC, tijdstip, of fase-belasting)? En heb je weleens gezien dat Zonneplan zelf zijn setpoint aanpast nadat het tegen jouw limiet aanloopt, of blijft het gewoon "botsen" tegen het plafond zonder dat ze het setpoint corrigeren?

  • Micronikje
  • Registratie: Januari 2018
  • Laatst online: 18:24
Tussenstand: Zonneplan Nexus + eigen aansturing via Modbus:

Kernpunt: twee soorten registers gedragen zich verschillend

Limiet-registers (Battery Max Charge/Discharge Current): worden door Zonneplan gerespecteerd als plafond. Zonneplan kan er niet overheen, maar blijft verder gewoon zelf bepalen wanneer en hoeveel er geladen/ontladen wordt binnen die grens.

Setpoint-registers (RC Force Battery Charge/Discharge Power): worden door Zonneplan vrijwel continu overschreven zodra de dongles actief zijn. Direct zelf sturen via deze registers werkt dus niet zolang Zonneplan actief verbonden is.

Wat dit betekent voor je eigen aansturing:
code: Tabel 1
1
2
3
4
Aanpak         Dongles         Register        Vergoeding     Opbrengst juli   Vermeden import juli   Ervaring
Clamping       Blijven zitten  Limiet-reg.     Blijft intact  ?                ?                      Melville: stabiel sinds feb.
                                                                                                      GaMbiNo: ook fase-load-balancing
Volledig zelf  Losgekoppeld    Setpoint-reg.   Vervalt        16,36 euro       39,01 euro             Ikzelf: eigen PID-aansturing
Is er iemand die 1 of beide aanpakken heeft geprobeerd en dus een eerlijke vergelijking kan maken/delen?

  • GaMbiNo
  • Registratie: April 2001
  • Nu online

GaMbiNo

1337

Micronikje schreef op woensdag 15 juli 2026 @ 10:31:
Tussenstand: Zonneplan Nexus + eigen aansturing via Modbus:

Kernpunt: twee soorten registers gedragen zich verschillend

Limiet-registers (Battery Max Charge/Discharge Current): worden door Zonneplan gerespecteerd als plafond. Zonneplan kan er niet overheen, maar blijft verder gewoon zelf bepalen wanneer en hoeveel er geladen/ontladen wordt binnen die grens.

Setpoint-registers (RC Force Battery Charge/Discharge Power): worden door Zonneplan vrijwel continu overschreven zodra de dongles actief zijn. Direct zelf sturen via deze registers werkt dus niet zolang Zonneplan actief verbonden is.

Wat dit betekent voor je eigen aansturing:
code: Tabel 1
1
2
3
4
Aanpak         Dongles         Register        Vergoeding     Opbrengst juli   Vermeden import juli   Ervaring
Clamping       Blijven zitten  Limiet-reg.     Blijft intact  ?                ?                      Melville: stabiel sinds feb.
                                                                                                      GaMbiNo: ook fase-load-balancing
Volledig zelf  Losgekoppeld    Setpoint-reg.   Vervalt        16,36 euro       39,01 euro             Ikzelf: eigen PID-aansturing
Is er iemand die 1 of beide aanpakken heeft geprobeerd en dus een eerlijke vergelijking kan maken/delen?
Ik gebruik de “clamping” strategie al een hele poos, sowieso sinds eind ‘25 oid. En dat werkte met zowel de oude als de nieuwe ZP aansturing prima.

Maar dat gebruik ik dus alleen samen met de Zonneplan aansturing. Ik neem dmv een template de zwaarst belaste fase, zowel laad als ontlaad en vul aan de hand daarvan de max charge en max discharge. Het is dus niet zo dat ik elke fase afzonderlijk aanstuur.

Een verschil in opbrengst zal er niet/nauwelijks zijn omdat deze aansturing gelukkig niet heel vaak hoeft in te grijpen. Maar ik ben blij dat ik hiermee wel voorkom dat de fases eruit klappen, zoals ik in het verleden wel heb gehad.

  • TKroon
  • Registratie: December 2006
  • Niet online
@GaMbiNo Je hebt me voldoende geïnspireerd om hetzelfde op te tuigen :) ik heb nu goede ervaring met waveshare en de Marstek dat ik het bij de Nexus ook ga doen.

Daikin Altherma 3 LT 8 kW + 16 kWp PV


  • GaMbiNo
  • Registratie: April 2001
  • Nu online

GaMbiNo

1337

TKroon schreef op woensdag 15 juli 2026 @ 12:30:
@GaMbiNo Je hebt me voldoende geïnspireerd om hetzelfde op te tuigen :) ik heb nu goede ervaring met waveshare en de Marstek dat ik het bij de Nexus ook ga doen.
Let wel even op de Amperes waar je mee limiteert. Dit zijn amperes gebaseerd op de Nexus voltages (~425 volt oid). Dit zijn dus andere amperes dan je gewend bent ws.
25A op de max_(dis)charge is dus om en nabij de 10.000 watt.
Zelf gebruik ik de sensor.solis_s6_eh3p10k_h_zp_battery_voltage om mee te rekenen.

Zo bereken ik de max_charge:
code:
1
{{ ((6000 - states('sensor.max_verbruik_op_1_fase') | float + (states('sensor.hw_energymeter_175af2_vermogen_fase_2') | float ) | float(0)) / states('sensor.solis_s6_eh3p10k_h_zp_battery_voltage') | float * 3) | round(1) }}
- 6000 = de door mij gestelde max op een fase.
- sensor.max_verbruik_op_1_fase = een waarde uit een template. De op dat moment zwaarst belaste fase.
- sensor.hw_energymeter_175af2_vermogen_fase_2 = de huidige activiteit van de Nexus. Dit dmv een HomeWizard driefase meter op de Nexus. Dit zou je evt ook via de Waveshare uit de Nexus kunnen halen.
- sensor.solis_s6_eh3p10k_h_zp_battery_voltage = het huidige voltage van de Nexus om mee te rekenen.

De max_discharge bereken ik met:
code:
1
2
3
{{ ((6000 - states('sensor.max_teruglevering_op_1_fase') | float -
    (states('sensor.hw_energymeter_175af2_vermogen_fase_2') | float if states('sensor.hw_energymeter_175af2_vermogen_fase_2') | float < 0 else 0)) /
    states('sensor.solis_s6_eh3p10k_h_zp_battery_voltage') | float * 3) | round(0) }}
Deze lijkt erop maar met iets andere entiteiten. Wijst zichzelf denk ik.

[ Voor 52% gewijzigd door GaMbiNo op 15-07-2026 13:08 ]


  • _PM
  • Registratie: Mei 2003
  • Laatst online: 15:36

_PM

GaMbiNo schreef op woensdag 15 juli 2026 @ 12:05:
[...]

Ik gebruik de “clamping” strategie al een hele poos, sowieso sinds eind ‘25 oid. En dat werkte met zowel de oude als de nieuwe ZP aansturing prima.

Maar dat gebruik ik dus alleen samen met de Zonneplan aansturing. Ik neem dmv een template de zwaarst belaste fase, zowel laad als ontlaad en vul aan de hand daarvan de max charge en max discharge. Het is dus niet zo dat ik elke fase afzonderlijk aanstuur.

Een verschil in opbrengst zal er niet/nauwelijks zijn omdat deze aansturing gelukkig niet heel vaak hoeft in te grijpen. Maar ik ben blij dat ik hiermee wel voorkom dat de fases eruit klappen, zoals ik in het verleden wel heb gehad.
De standaard oplossing hiervoor van Zonneplan werkt voor jou niet?
(voor mij ook een keer niet overigens toen de Zonneplan cloud oplossing eruit lag)

Op basis van jouw clamping oplossing.... Zou het ook mogelijk moeten zijn om in de zonneplan app de aansluiting van 3x25 naar 3x35 te veranderen en de limieten via de max charge en max discharge te regelen, met de potentie dat de batterij meer gebruikt wordt / meer vermogen. Ik heb nu het idee dat hij erg behoudend is als er ook andere verbruikers of de zonnepanelen actief zijn. Zou dit verschil maken?

  • TKroon
  • Registratie: December 2006
  • Niet online
GaMbiNo schreef op woensdag 15 juli 2026 @ 12:38:
[...]

Let wel even op de Amperes waar je mee limiteert. Dit zijn amperes gebaseerd op de Nexus voltages (~425 volt oid). Dit zijn dus andere amperes dan je gewend bent ws.
25A op de max_(dis)charge is dus om en nabij de 10.000 watt.
Zelf gebruik ik de sensor.solis_s6_eh3p10k_h_zp_battery_voltage om mee te rekenen.

Zo bereken ik de max_charge:
code:
1
{{ ((6000 - states('sensor.max_verbruik_op_1_fase') | float + (states('sensor.hw_energymeter_175af2_vermogen_fase_2') | float ) | float(0)) / states('sensor.solis_s6_eh3p10k_h_zp_battery_voltage') | float * 3) | round(1) }}
- 6000 = de door mij gestelde max op een fase.
- sensor.max_verbruik_op_1_fase = een waarde uit een template. De op dat moment zwaarst belaste fase.
- sensor.hw_energymeter_175af2_vermogen_fase_2 = de huidige activiteit van de Nexus. Dit dmv een HomeWizard driefase meter op de Nexus. Dit zou je evt ook via de Waveshare uit de Nexus kunnen halen.
- sensor.solis_s6_eh3p10k_h_zp_battery_voltage = het huidige voltage van de Nexus om mee te rekenen.

De max_discharge bereken ik met:
code:
1
2
3
{{ ((6000 - states('sensor.max_teruglevering_op_1_fase') | float -
    (states('sensor.hw_energymeter_175af2_vermogen_fase_2') | float if states('sensor.hw_energymeter_175af2_vermogen_fase_2') | float < 0 else 0)) /
    states('sensor.solis_s6_eh3p10k_h_zp_battery_voltage') | float * 3) | round(0) }}
Deze lijkt erop maar met iets andere entiteiten. Wijst zichzelf denk ik.
Thanks, dat helpt. Ik gebruik homey en ga eerst eens kijken welke waarden ik allemaal uit kan lezen en hoe ze zich gedragen gedurende een tijdje voordat ik schrijfacties ga doen.

Daikin Altherma 3 LT 8 kW + 16 kWp PV


  • GaMbiNo
  • Registratie: April 2001
  • Nu online

GaMbiNo

1337

_PM schreef op woensdag 15 juli 2026 @ 13:28:
[...]

De standaard oplossing hiervoor van Zonneplan werkt voor jou niet?
(voor mij ook een keer niet overigens toen de Zonneplan cloud oplossing eruit lag)

Op basis van jouw clamping oplossing.... Zou het ook mogelijk moeten zijn om in de zonneplan app de aansluiting van 3x25 naar 3x35 te veranderen en de limieten via de max charge en max discharge te regelen, met de potentie dat de batterij meer gebruikt wordt / meer vermogen. Ik heb nu het idee dat hij erg behoudend is als er ook andere verbruikers of de zonnepanelen actief zijn. Zou dit verschil maken?
De Zonneplan oplossing werkt voor mij niet idd. Die is al eens te traag gebleken in een all electric huis met Nexus, laadpaal, Warmtepomp, enz. Hier vlogen de stoppen er eens uit toen er teveel tegelijk werd gevraagd. De Nexus ging volle bak laden terwijl de warmtepomp net een legionellarun deed op 3x16A.

Wat jij beschrijft is precies wat ik gedaan heb. De Zonneplan configuratie staat idd op 3x35A en mijn eigen aansturing zorgt voor het knijpen van de Nexus wanneer nodig.

[ Voor 5% gewijzigd door GaMbiNo op 15-07-2026 13:34 ]


  • TKroon
  • Registratie: December 2006
  • Niet online
Dat klinkt perfect en lost mijn grootste klacht van de Nexus op. Ik heb dit onlangs nog besproken aan de hand van meerdere grafieken en cijfers met Zonneplan.

Daikin Altherma 3 LT 8 kW + 16 kWp PV


  • elbizarre
  • Registratie: Augustus 2015
  • Laatst online: 10:12
Ik ben nu nog lurker, maar tzt ga ik hier ook mee aan de slag.
Wat betreft Nexus of iets anders: dit maakt de k ik niet zoveel uit toch? De logic en aansturing zal grotendeels hetzelfde zijn. Wellicht andere modbus registers her en der.

Ik heb nu een andere omvormer (van SolarEdge) gekocht waar een batterij output in zit. Daarmee hoop ik wat omvormer verliezen te besparen. (Van zonneopbrengst direct de batterij in, ipv 2 maal door een omvormer.)

Ik vermoed ook dat de mensen in dit topic ook wel kleine verschillen met elkaar zullen hebben. Maar het doel en 80-90% van de opzet zal identiek zijn

  • Micronikje
  • Registratie: Januari 2018
  • Laatst online: 18:24
elbizarre schreef op donderdag 16 juli 2026 @ 15:28:
Ik ben nu nog lurker, maar tzt ga ik hier ook mee aan de slag.
Wat betreft Nexus of iets anders: dit maakt de k ik niet zoveel uit toch? De logic en aansturing zal grotendeels hetzelfde zijn. Wellicht andere modbus registers her en der.

Ik heb nu een andere omvormer (van SolarEdge) gekocht waar een batterij output in zit. Daarmee hoop ik wat omvormer verliezen te besparen. (Van zonneopbrengst direct de batterij in, ipv 2 maal door een omvormer.)

Ik vermoed ook dat de mensen in dit topic ook wel kleine verschillen met elkaar zullen hebben. Maar het doel en 80-90% van de opzet zal identiek zijn
Helder punt, en het brengt me bij een eerlijke constatering: mijn insteek verschilt eigenlijk fundamenteel van de meeste reacties hier. Het overgrote deel van de discussie draaide om hoe je omgaat met Zonneplans actieve aansturing. Bij mij speelt dat niet, aangezien ik de dongles al los heb en volledig zelf stuur. Zonneplan mag mijn batterij ook niet meer aansturen om diverse redenen. Ik moet nog wel 1 correctie aanbrengen op mijn openingspost. Ik heb geen Solis S6 EH1P omvormer maar een Solis S6 EH3P10K omvormer. Dus een 3 fase aansluiting.

  • GaMbiNo
  • Registratie: April 2001
  • Nu online

GaMbiNo

1337

Wat mij betreft niet. Ik heb ook tijden op een eigen aansturing gedraaid, zoals jij nu doet. Zonder de dongles dus.

Ik denk dat dat iets is wat ik vanaf 2027 weer meer ga doen. Of eerder als de onbalans vergoeding weer compleet inzakt. Wat mij betreft is die discussie ook zeer welkom.

[ Voor 9% gewijzigd door GaMbiNo op 16-07-2026 19:03 ]


  • sbar12
  • Registratie: Juli 2026
  • Laatst online: 19-09 16:45
Is er (ook) al ervaring met aansturen van de nieuwe Pylontech omvormers h3X? aan te sturen?

  • Boris2024
  • Registratie: Juli 2026
  • Laatst online: 24-07 21:45
Ik zit er over te denken om mijn zonneplan thuisaccu door mijn openclaw agent aan te laten sturen. Begrijp ik het nu goed dat ik hier rekening mee moet houden bij het berekenen van de marge?
+ 15% laadverlies bij opladen accu
+ 10% energiebelasting (of geld dit pas volgend jaar bij stoppen saldering)

En dat het moment van laden en ontladen daarom dus minimaal 25% verschil moet hebben voor een positief resultaat?
Zijn er nog andere zaken om rekening mee te houden?

Iets anders waar ik aan dacht, interessante onbalans vergoedingen zijn er tegenwoordig weinig. Volgens mij komt dit het vaakst voor op dagen wanneer het windstil is. Iemand die hier wel of geen rekening mee houdt?

[ Voor 18% gewijzigd door Boris2024 op 23-07-2026 11:19 ]


  • Micronikje
  • Registratie: Januari 2018
  • Laatst online: 18:24
Boris2024 schreef op donderdag 23 juli 2026 @ 11:17:
Ik zit er over te denken om mijn zonneplan thuisaccu door mijn openclaw agent aan te laten sturen. Begrijp ik het nu goed dat ik hier rekening mee moet houden bij het berekenen van de marge?
+ 15% laadverlies bij opladen accu
+ 10% energiebelasting (of geld dit pas volgend jaar bij stoppen saldering)

En dat het moment van laden en ontladen daarom dus minimaal 25% verschil moet hebben voor een positief resultaat?
Zijn er nog andere zaken om rekening mee te houden?

Iets anders waar ik aan dacht, interessante onbalans vergoedingen zijn er tegenwoordig weinig. Volgens mij komt dit het vaakst voor op dagen wanneer het windstil is. Iemand die hier wel of geen rekening mee houdt?
Je 15% laadverlies zit binnen de normale bandbreedte (round-trip efficiency van LFP-systemen ligt meestal tussen 85-92%). Zelf meet ik ongeveer 92% (dus 8% verlies) — meet het bij jezelf, want dit verschilt per systeem.

De 10% energiebelasting zou ik niet los optellen. Bij een dynamisch contract zit die belasting al in de prijs die je per uur/kwartier ziet. Als je je marge berekent op het verschil tussen die prijzen, zit de belasting er dus al in — je telt 'm anders dubbel.

Dus geen 25% minimaal verschil nodig, eerder ongeveer je laadverlies (8-15%, zelf meten) als vuistregel.

Over die dubbele belasting: dat is nog niet opgelost. De Tweede Kamer heeft het kabinet gevraagd om hier bij het Belastingplan 2027 iets aan te doen, maar een concrete regeling is er nog niet.

Je punt over de onbalansmarkt klopt: door de toename van thuisbatterijen die allemaal op dezelfde signalen reageren, zijn de opbrengsten in 2026 een stuk lager dan in 2023-2024. Het verband met windstille dagen kan ik niet met harde cijfers bevestigen, maar het is een aannemelijke gedachte — windprognosefouten zijn van oudsher een belangrijke oorzaak van onbalans.

  • _PM
  • Registratie: Mei 2003
  • Laatst online: 15:36

_PM

Boris2024 schreef op donderdag 23 juli 2026 @ 11:17:
Ik zit er over te denken om mijn zonneplan thuisaccu door mijn openclaw agent aan te laten sturen. Begrijp ik het nu goed dat ik hier rekening mee moet houden bij het berekenen van de marge?
+ 15% laadverlies bij opladen accu
+ 10% energiebelasting (of geld dit pas volgend jaar bij stoppen saldering)

En dat het moment van laden en ontladen daarom dus minimaal 25% verschil moet hebben voor een positief resultaat?
Zijn er nog andere zaken om rekening mee te houden?

Iets anders waar ik aan dacht, interessante onbalans vergoedingen zijn er tegenwoordig weinig. Volgens mij komt dit het vaakst voor op dagen wanneer het windstil is. Iemand die hier wel of geen rekening mee houdt?
En je kan rekening houden met de afschrijving van je batterij (aanschaf incl installatie / 6000 cycli) = €1,40 (?) per volledige(!) leeg-vol-leeg cyclus. In praktijk gebruik je 75-80% ofzo..

Zo'n 8 ct per kWh

  • Boris2024
  • Registratie: Juli 2026
  • Laatst online: 24-07 21:45
Ik ben opzoek naar een manier om de omvormer met UTP te kunnen verbinden, wat gebruiken jullie? Het is mij niet helemaal duidelijk of er nu twee versies zijn een USB versie en een 4 pin versie? Welke wordt hier gebruikt, waar is ervaring mee en waar te kan ik deze het beste kopen?
De USB versie lijkt in Nederland niet goed te koop te zijn..

4 pin: https://www.warmteservice.nl/Duurzaam/Zonnepanelen/Omvormer/Solis-Datalogging-stick-Wifi-LAN/p/07751011

USB: https://sunnergie.nl/product/solis-dual-lan-and-wifi-datalogger-usb/

Bij nader inzien, in zie helemaal geen usb poort op de Omvormer.
Ik zie wel een beschikbare UTP ingang (com poort) op de accu. Wat is de manier om verbinding tot stand te brengen?
Ik heb een: S6-EH3P10K-H-ZP omvormer

[ Voor 23% gewijzigd door Boris2024 op 23-07-2026 15:43 ]


  • Boris2024
  • Registratie: Juli 2026
  • Laatst online: 24-07 21:45
_PM schreef op donderdag 23 juli 2026 @ 12:24:
[...]

En je kan rekening houden met de afschrijving van je batterij (aanschaf incl installatie / 6000 cycli) = €1,40 (?) per volledige(!) leeg-vol-leeg cyclus. In praktijk gebruik je 75-80% ofzo..

Zo'n 8 ct per kWh
Bedankt voor de tip!

  • Boris2024
  • Registratie: Juli 2026
  • Laatst online: 24-07 21:45
Ik heb nu de RS485 poort gevonden...
Claude stelt dit voor, is dat ook hoe mensen het hier doen?

Omvormer (RS485-poort, pin 4+5) → UTP-kabel → RS485-naar-TCP/WiFi module → je netwerk (WiFi of Ethernet) → Home Assistant leest via Modbus TCP

Concreet:
  1. Kabel maken: een RJ45-stekker waarbij alleen pin 4 (RS485B, blauw) en pin 5 (RS485A, blauw/wit) zijn aangesloten. Een gewone kant-en-klare UTP-patchkabel werkt hiervoor prima — pin 4 en 5 vormen samen al het gedraaide blauwe paar in de kabel, wat toevallig ideaal is voor een differentieel RS485-signaal. Je hoeft dus niet per se zelf te knippen/krimpen; een standaard Cat5e/6-kabel volstaat, je gebruikt gewoon alleen dat ene aderpaar.
  2. Converter: het andere uiteinde van die kabel (de losse A/B-draden) sluit je aan op de RS485-ingang van bijvoorbeeld een Waveshare RS485 naar ETH/WiFi module, een Elfin EW11, of de eerder genoemde GC-1201 dual-master adapter (die laatste specifiek als je ook Zonneplans eigen RS485-apparaat wilt laten blijven werken op dezelfde bus).
  3. Die converter krijgt zelf stroom (meestal 12V, soms via USB) en een netwerkverbinding (WiFi of ethernetkabel naar je router).
  4. Home Assistant praat vervolgens niet meer rechtstreeks RS485, maar Modbus TCP over je netwerk naar het IP-adres van die converter — en die stuurt het weer door naar de omvormer als RS485. Dat is precies wat de Pho3niX90-integratie verwacht als je "Modbus TCP" als verbindingstype kiest, in plaats van een lokale seriële poort.
Kort gezegd: de UTP-kabel is puur het fysieke transportmedium tussen omvormer en converter; de converter is het apparaat dat het "vertaalwerk" doet zodat Home Assistant er via WiFi/netwerk bij kan.

  • Boris2024
  • Registratie: Juli 2026
  • Laatst online: 24-07 21:45
Bedankt dat is informatie waar ik wat mee kan.
De Waveshare was al besteld, ik ga er binnenkort mee aan de slag, ben benieuwd.
Het zou fijn om wat meer controle te hebben over die accu

  • Micronikje
  • Registratie: Januari 2018
  • Laatst online: 18:24
Update: kwartierprijzen live en het doel van mijn EMS

Kleine update na een tijdje stilte: mijn net-laadmodule draait inmiddels op de kwartierprijzen van Zonneplan (sinds 1 augustus), na wat aanpassingen die nodig bleken om de overstap van uur- naar kwartierdata soepel te laten verlopen. De regellogica reageerde in eerste instantie te gevoelig op de kortere, grillige prijsbewegingen per kwartier — dat vroeg om wat herijking om te voorkomen dat het systeem te nerveus ging schakelen.

Even iets rechtzetten over waar mijn EMS eigenlijk voor bedoeld is, want dat verschilt volgens mij van veel andere aanpakken die hier voorbijkomen. Ik optimaliseer niet voor NOM (Nul Op de Meter — waarbij je probeert om over een bepaalde periode netto niets meer te importeren of exporteren, dus je verbruik en opwek volledig tegen elkaar wegstrepen). Mijn doel is autonomie: zo min mogelijk import, zo min mogelijk export, én zo min mogelijk cycli om de batterij te ontzien. Dat is dus geen streven naar een sluitende jaarbalans, maar puur het minimaliseren van afhankelijkheid van het net en onnodige slijtage.

Concreet resultaat: in juli heb ik door de zelfaansturing 32% minder kWh heb geïmporteerd en daardoor bespaard op mijn importkosten. Ik deel dit vooral om te laten zien dat autonomie-gericht sturen (in plaats van handelsgericht) ook gewoon meetbaar resultaat oplevert, zonder dat je continu op de spread hoeft te jagen zoals bij dynamisch handelen.

Benieuwd of hier meer mensen zitten die bewust voor autonomie kiezen in plaats van NOM of handel — en zo ja, hoe doen jullie dat dan.

Naast de net-laadmodule draait er ook nog een hoofdmodule (voor gebruik zonne-energie) en een leermodule. Alle drie communiceren ze met elkaar.

  • Micronikje
  • Registratie: Januari 2018
  • Laatst online: 18:24
Ik heb hier op Tweakers inmiddels al het een en ander gepost over mijn EMS voor mijn Zonneplan-thuisbatterij, maar daar is tot nu toe weinig reactie op gekomen.

Dat kan natuurlijk verschillende redenen hebben. Ik ben in ieder geval vooralsnog geen andere Tweaker tegengekomen die een vergelijkbare, zelfgebouwde EMS-regeling voor een Zonneplan-thuisbatterij gebruikt.

Daarom ga ik mijn EMS de komende tijd stap voor stap en in delen met jullie delen.

Het doel is niet om te zeggen dat mijn regeling "af" of optimaal is. Integendeel: ik ben juist benieuwd of anderen vanuit hun kennis van Home Assistant, energiebeheer, regeltechniek en thuisbatterijen verbeterpunten zien die ik zelf over het hoofd zie.

De EMS is inmiddels behoorlijk uitgebreid en gebruikt onder andere:

werkelijk netvermogen als feedback;

werkelijk batterijvermogen;

dynamische SOC-reserves;

PV-forecast;

geleerde verbruiksprofielen;

laad- en ontlaadregeling;

PID-achtige regeling;

bescherming tegen overshoot;

detectie van daadwerkelijk PV-surplus.

Ik zal daarom niet meteen honderden regels YAML plaatsen, maar de verschillende onderdelen afzonderlijk uitleggen en vervolgens de bijbehorende code delen.

Mijn doel: niet alleen mijn eigen installatie beter laten werken, maar kijken of we deze regeling samen verder kunnen verbeteren en uiteindelijk een zo robuust mogelijk EMS-concept voor Home Assistant kunnen maken.

Kritische feedback is dus juist welkom. Als je denkt dat een bepaalde berekening, regelstrategie of beveiliging beter kan, hoor ik graag waarom.
Generieke sensoren (EMS-basislaag)
Voordat we naar regels en forecasts gaan, eerst de basis: de generieke sensoren.

Dit is de rekenlaag van het EMS in Home Assistant.
1. Netmeting (grid power)
  • positief = afname
  • negatief = teruglevering
Dit is de belangrijkste “basis”.
2. PV-opwek
  • actuele zonneproductie
  • gebruikt voor surplus en forecast
3. Huisverbruik (afgeleid)
huisverbruik = PV + netafname - batterijvermogen
Geeft het echte verbruik zonder batterij-invloed.
4. Batterijvermogen
  • positief = laden
  • negatief = ontladen
5. State of Charge (SOC)
Basis voor alle grenzen:
  • min/max SOC
  • dynamische buffers
6. Dynamische SOC-reserve
Extra buffer bovenop minimum SOC, afhankelijk van forecast en gedrag.
7. PV-surplus
PV - huisverbruik
Bepaalt wanneer laden mag starten (met filtering tegen oscillaties).
8. Geleerd verbruiksprofiel
Historisch gedrag per tijdstip, gebruikt voor voorspellingen en buffers.
Waarom deze laag belangrijk is
Zonder deze laag:
  • instabiele regeling
  • geen goede feedback
  • moeilijk te tunen
Met deze laag:
  • alles is herleidbaar
  • stabiele regelopbouw
  • combineerbaar met PID/forecast

Volgende stap
In het volgende deel:
  • definitie van “overschot”
  • opbouw van SOC-reserves
  • laad/ontlaad-logica

  • GaMbiNo
  • Registratie: April 2001
  • Nu online

GaMbiNo

1337

Je bent niet de enige. Ik ben sinds een aantal weken ook weer begonnen met volledig zelf aansturen. Ik moest nog even de kwartierprijzen integreren maar dat is voor 1 augustus nog gelukt en sindsdien heb ik de dongle er weer uitgetrokken.

Ik heb geprobeerd een balans te maken tussen nul op de meter en dynamisch handelen. Bij een grote spread tussen de laagste en de hoogste kwartieren zoals de afgelopen dagen handelt hij, en anders gaat hij voor nul op de meter.

En ik maak ook gebruik van PV forecast. Als het erop lijkt dat er onvoldoende opbrengst is van de PV gaat hij in de goedkoopste kwartieren bijladen.

Verder zit er nog load balancing in, houdt hij rekening met EV laden, enz.

En het is idd een hele lap yaml code, allerlei templates, automatiseringen, enz. Ik heb o.a. een template gemaakt die alle kwartierprijzen rangschikt van 1 t/m 96 zodat ik daarmee weer kan aansturen. Bijvoorbeeld bijladen in de uren 1 t/m 5 en ontladen in de uren 92 t/m 96 oid. Dus compleet delen is lastig maar wel leuk om wat te vergelijken. :)

[ Voor 10% gewijzigd door GaMbiNo op 15-08-2026 12:41 ]


  • elbizarre
  • Registratie: Augustus 2015
  • Laatst online: 10:12
Wat is jullie minimale verschil in hoge/lage prijs wanneer jullie gaan handelen? Je zit natuurlijk met kosten en slijtage wat ook kosten met zich meebrengt.
Daarnaast wil je waarschijnlijk 50% oid overhouden om NoM te garanderen?

  • Micronikje
  • Registratie: Januari 2018
  • Laatst online: 18:24
elbizarre schreef op zondag 16 augustus 2026 @ 09:30:
Wat is jullie minimale verschil in hoge/lage prijs wanneer jullie gaan handelen? Je zit natuurlijk met kosten en slijtage wat ook kosten met zich meebrengt.
Daarnaast wil je waarschijnlijk 50% oid overhouden om NoM te garanderen?
Ik stuur de batterij zelf aan en handel er niet mee. Ook nul op de meter vind ik voor mijzelf niet interessant (dus evenveel import als export) vanwege einde salderingsregeling. Mijn doel is een zo groot mogelijke autonomie. Dus zo min mogelijk import, export en batterij cycli (voor de levensduur).

Ik stuur sinds juli zelf aan en heb een besparing van import van 32% gerealiseerd. Dit wil ik nog verder omhoog zien te krijgen naar 40%. Mijn autonomie was in juli 85%. Doel is 90%.

Ook dit levert geld op, maar waarschijnlijk minder dan als Zonneplan de batterij had aangestuurd.

  • Micronikje
  • Registratie: Januari 2018
  • Laatst online: 18:24
Deel 2 — PV-surplus en dynamische SOC-reserve
In het vorige deel heb ik de basislaag van het EMS beschreven: netvermogen, PV-productie, batterijvermogen en SOC.

In dit deel ga ik een stap verder. Twee vragen zijn hierbij essentieel:
  1. Wanneer is er daadwerkelijk sprake van een PV-overschot en mag de batterij gaan laden?
  2. Hoeveel energie moet de batterij bewaren voor later op de dag of voor de nacht?
Dat klinkt eenvoudig, maar juist hier ontstaan bij een EMS snel ongewenste laad- en ontlaadbewegingen.

1. Wanneer is er sprake van PV-surplus?

Je zou kunnen zeggen:
PV-surplus = PV-productie − huisverbruik
Maar in de praktijk gebruik ik daarvoor niet alleen een berekend huisverbruik.

Mijn EMS kijkt primair naar het gemeten netvermogen.

De tekenconventie is:
  • positief netvermogen = import
  • negatief netvermogen = export
Dus bijvoorbeeld:
code:
1
2
3
4
5
PV = 2500 W
Net = -1000 W

→ er wordt 1000 W teruggeleverd
→ er is dus minimaal ongeveer 1000 W aan PV-overschot
Daarmee kan de batterij gaan laden.

Maar er zit een belangrijk probleem aan deze eenvoudige aanpak.
Een negatieve netmeting betekent niet automatisch PV-surplus
Stel dat de batterij zojuist 2000 W aan het ontladen was.

Door een plotselinge verandering in het huisverbruik kan het netvermogen kortstondig omslaan naar bijvoorbeeld:
code:
1
Net = -800 W
De netmeter zegt dan:
"Er wordt 800 W geëxporteerd."
Maar dat hoeft helemaal geen echte zonne-energie te zijn.

Het kan ook een tijdelijke overshoot van de batterijregeling zijn.

Daarom controleert mijn EMS sinds v40.1 ook of de gemeten export fysiek kan worden verklaard door de actuele PV-productie.

In vereenvoudigde vorm:
code:
1
werkelijke export <= PV-productie + marge
Bijvoorbeeld:
code:
1
2
3
4
5
PV = 1000 W
Export = 850 W

→ logisch
→ PV-surplus kan worden toegestaan
Maar:
code:
1
2
3
4
5
PV = 200 W
Export = 850 W

→ fysiek verdacht
→ waarschijnlijk geen echt PV-surplus
In mijn huidige regeling wordt hiervoor een kleine marge gebruikt om meetvertraging en meetruis op te vangen.

Dit voorkomt dat de batterij 's avonds of bij weinig zon ineens gaat laden omdat een tijdelijke batterij-overshoot ten onrechte als PV-overschot wordt gezien.

2. Waarom een deadband?

Ook een perfecte meter geeft niet altijd exact nul aan.

Wanneer het werkelijke doel bijvoorbeeld 0 W netverbruik is, kunnen kleine fluctuaties ontstaan:
code:
1
2
3
4
+12 W
-8 W
+17 W
-14 W
Als de batterij op iedere kleine verandering reageert, krijg je een onrustige regeling.

Daarom gebruik ik een deadband.

Kleine afwijkingen worden genegeerd totdat ze groot genoeg zijn om daadwerkelijk actie te rechtvaardigen.

Dit is vooral belangrijk bij:
  • kleine PV-productie;
  • wisselend huishoudelijk verbruik;
  • meetvertraging;
  • batterijreactietijd.

3. Dynamische SOC-reserve

Het tweede onderdeel is minstens zo belangrijk.

Je zou een thuisbatterij simpel kunnen laten ontladen tot bijvoorbeeld:
code:
1
SOC = 10%
Maar dat is voor een EMS meestal niet optimaal.

Stel:
code:
1
2
15:00
SOC = 45%
Dan zijn er twee compleet verschillende situaties mogelijk.
Situatie A — veel zon verwacht
De volgende dag of later die middag wordt veel PV verwacht.

Dan hoeft de batterij mogelijk niet zoveel energie achter de hand te houden.
Situatie B — weinig zon verwacht
Er komt nauwelijks PV en er wordt nog een lange avond en nacht verwacht.

Dan is diezelfde 45% SOC misschien juist onvoldoende.

Daarom gebruikt mijn EMS een dynamische SOC-minimumgrens.

Die grens wordt niet alleen bepaald door een vaste waarde, maar onder andere door:
  • verwacht avondverbruik;
  • verwacht nachtverbruik;
  • seizoen;
  • huidige tijd;
  • verwachte PV-productie;
  • geleerd historisch verbruik.
Het minimum-SOC kan daardoor gedurende de dag veranderen.

4. Avond- en nachtreserve

De regeling probeert vooraf te bepalen hoeveel energie waarschijnlijk nog nodig is.

Bijvoorbeeld:
code:
1
2
verwacht avondverbruik = 4 kWh
verwacht nachtverbruik = 2 kWh
Dan is de geschatte resterende behoefte:
code:
1
4 + 2 = 6 kWh
Bij een batterij van 20 kWh komt dat grofweg overeen met:
code:
1
6 / 20 × 100 = 30% SOC
Dat betekent niet automatisch dat de batterij exact op 30% moet blijven.

Er worden namelijk nog grenzen en correcties toegepast.

5. Forecast-correctie

De verwachte PV-productie wordt gebruikt om die benodigde reserve verder te corrigeren.

Stel:
code:
1
Benodigde reserve zonder PV = 40% SOC
maar er wordt later nog veel zonneproductie verwacht.

Dan kan een deel van die toekomstige behoefte door PV worden gedekt.

De benodigde reserve kan dan bijvoorbeeld dalen naar:
code:
1
30% SOC
Bij weinig of geen verwachte PV gebeurt het tegenovergestelde:
code:
1
2
meer reserve nodig
→ hogere SOC-minimumgrens
Het EMS probeert dus niet alleen te reageren op het huidige moment, maar ook rekening te houden met wat er later waarschijnlijk gaat gebeuren.

6. Waarom dit belangrijk is voor zelfconsumptie

Het doel is niet:
"Batterij altijd zo leeg mogelijk."
Maar:
"De beschikbare batterijcapaciteit zo inzetten dat er zo weinig mogelijk netimport ontstaat zonder de batterij onnodig vol of leeg te sturen."
Dat betekent soms bewust energie bewaren.

Een batterij die om 18:00 bijna leeg is, kan bijvoorbeeld geen bijdrage meer leveren tijdens een dure nachtelijke periode.

Omgekeerd wil je ook niet onnodig op 70% SOC blijven terwijl je weet dat er de volgende ochtend veel PV beschikbaar komt.

7. De combinatie

De twee onderdelen werken uiteindelijk samen:
code:
1
2
3
4
5
6
7
8
9
10
11
PV-surplus
     ↓
Mag de batterij laden?

SOC-reserve
     ↓
Hoe ver mag de batterij ontladen?

Netvermogen
     ↓
Hoeveel moet de batterij daadwerkelijk sturen?
Daarmee ontstaan drie verschillende functies:
code:
1
2
3
PV-surplus → laadbeslissing
SOC-reserve → ondergrens
Netvermogen → directe regeling
En juist die combinatie vormt de basis voor de volgende stap.
Volgende stap
In het volgende deel ga ik in op de eigenlijke regelkring:
  • waarom ik ben afgestapt van alleen geschat huisverbruik;
  • waarom net_power de belangrijkste feedback is geworden;
  • wat actual_batt_power toevoegt;
  • hoe de PID-achtige regeling hiermee het batterijvermogen bepaalt;
  • en hoe overshoot wordt beperkt.
Daar begint het EMS volgens mij echt interessant te worden.

  • Micronikje
  • Registratie: Januari 2018
  • Laatst online: 18:24
Deel 3 — Van geschat verbruik naar werkelijk netvermogen
In de vorige delen heb ik de basis van het EMS en de berekening van PV-surplus en de SOC-reserve uitgelegd.

In dit deel komt het belangrijkste onderdeel van mijn huidige regeling: hoe het EMS bepaalt hoeveel vermogen de batterij daadwerkelijk moet leveren of opnemen.

Hier heb ik in de ontwikkeling van het EMS een belangrijke wijziging gemaakt.
De oude aanpak
Een eerdere versie van het EMS gebruikte voornamelijk het berekende huisverbruik als uitgangspunt.

Dat werkt op zich, maar er zit een probleem in:
het huisverbruik is een berekende waarde, terwijl het netvermogen daadwerkelijk gemeten wordt.
Voor een batterijregeling is uiteindelijk niet alleen belangrijk hoeveel het huis verbruikt.

Het belangrijkste is:
Wat gebeurt er daadwerkelijk op de aansluiting met het elektriciteitsnet?
Daarom is de regeling in v40 omgebouwd.
1. Netvermogen als feedback
Mijn netmeter geeft het actuele vermogen van de aansluiting weer.

Ik gebruik daarbij:
code:
1
2
positief = import
negatief = export
Bijvoorbeeld:
code:
1
Netvermogen = +800 W
betekent:
het huis haalt op dat moment 800 W uit het net.
De EMS kan vervolgens de batterij opdracht geven om bijvoorbeeld ongeveer 800 W te ontladen.

Maar het interessante komt daarna.

De volgende meting kan bijvoorbeeld zijn:
code:
1
Netvermogen = +250 W
Dan weet de EMS:
de batterij levert wel vermogen, maar nog niet genoeg om de netimport volledig weg te regelen.
De volgende stap wordt daarop aangepast.

Als de meting vervolgens wordt:
code:
1
Netvermogen = -150 W
dan is er juist sprake van teruglevering.

De batterij moet dan minder ontladen.

Het systeem gebruikt het net dus als feedbacksignaal.

2. Waarom ook het werkelijke batterijvermogen?

Alleen het netvermogen gebruiken is nog niet voldoende.

Een omvormer reageert namelijk niet altijd onmiddellijk op een nieuw commando.

Stel dat de EMS zegt:
code:
1
Batterij → 1500 W ontladen
maar de batterij levert op dat moment daadwerkelijk maar:
code:
1
900 W
Dan moet de regeling weten dat er een verschil bestaat tussen:

gevraagd vermogen

en

werkelijk vermogen.

Daarom gebruik ik twee afzonderlijke metingen:
code:
1
2
battery_charge_power
battery_discharge_power
Daaruit wordt het actuele batterijvermogen bepaald.

In vereenvoudigde vorm:
code:
1
2
actual_batt_power =
werkelijk ontladen − werkelijk laden
Hierdoor weet de regelkring wat de batterij daadwerkelijk doet in plaats van wat het EMS eerder heeft opgedragen.

3. Waarom is dit beter?

Een voorbeeld.

Het huis vraagt:
code:
1
2000 W
De batterij wordt ingesteld op:
code:
1
2000 W ontladen
Maar door de reactie van de omvormer is het werkelijke batterijvermogen tijdelijk:
code:
1
1400 W
Het net importeert daardoor nog:
code:
1
600 W
De EMS ziet dat onmiddellijk terug via de netmeter.

Hij hoeft dus niet te veronderstellen dat de opdracht van 2000 W ook daadwerkelijk 2000 W oplevert.

Dat is het fundamentele verschil.
Oude gedachte:
code:
1
2
3
gevraagd batterijvermogen
        ↓
verwacht resultaat
Nieuwe gedachte:
code:
1
2
3
4
5
6
7
8
9
gevraagd batterijvermogen
        ↓
werkelijk batterijvermogen
        ↓
gemeten netvermogen
        ↓
correctie
        ↓
nieuw batterijvermogen
Dit maakt de regeling veel meer een echte closed-loop regeling.

4. De PID-achtige regeling

Om het batterijvermogen te bepalen gebruik ik een PID-achtige regeling.

De drie onderdelen reageren ieder op iets anders.
P — Proportional
De P-component reageert direct op de actuele netafwijking.

Bijvoorbeeld:
code:
1
+1000 W import
geeft een grotere correctie dan:
code:
1
+200 W import
De proportional gain bepaalt hoe sterk de regeling daarop reageert.
I — Integral
De integrale component kijkt naar een afwijking die blijft bestaan.

Stel dat het net gedurende langere tijd:
code:
1
2
3
4
+300 W
+280 W
+320 W
+290 W
aangeeft.

Dan is duidelijk dat de batterij structureel iets te weinig vermogen levert.

De I-component bouwt daarom geleidelijk een correctie op.

In mijn regeling wordt deze component begrensd en bovendien onder bepaalde omstandigheden afgebouwd.

Dat voorkomt dat de integrator onbeperkt blijft oplopen.
D — Derivative
De D-component kijkt naar de verandering.

Bijvoorbeeld:
code:
1
2
3
+200 W
+500 W
+1000 W
De netafwijking loopt snel op.

De D-component reageert daarop sneller dan de P-component alleen zou doen.

Het doel is vooral om de regeling beter te laten reageren op snelle veranderingen en overshoot te beperken.

5. Een belangrijk detail: de batterij zelf zit in de terugkoppeling

Een van de belangrijkste verschillen met mijn eerdere EMS-versies is dat actual_batt_power onderdeel is geworden van de berekening.

Conceptueel ziet de regeling er ongeveer zo uit:
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
                ┌────────────────────┐
                │  Gewenste toestand │
                │   ≈ 0 W net        │
                └─────────┬──────────┘
                          ↓
                     netfout
                          ↓
                  PID-regeling
                          ↓
                 batterijcommando
                          ↓
                    omvormer
                          ↓
                werkelijk batterijvermogen
                          ↓
                    elektriciteitsnet
                          ↓
                  gemeten netvermogen
                          │
                          └────── feedback ──────┐
                                                  ↓
                                             PID-regeling
Het EMS kijkt dus voortdurend naar het resultaat van zijn eigen actie.

6. Waarom niet gewoon import = batterijvermogen?

Dat zou op papier eenvoudiger zijn.

Bijvoorbeeld:
code:
1
2
Import = 1200 W
→ batterij = 1200 W ontladen
In de praktijk werkt dat niet altijd goed.

Het huishoudelijke verbruik verandert voortdurend.

Bijvoorbeeld:
code:
1
2
3
4
01:00  → 400 W
01:02  → 1200 W
01:04  → 600 W
01:06  → 1800 W
Daar komen nog bij:
  • meetvertraging;
  • reactie van de omvormer;
  • veranderingen in huishoudelijke belasting;
  • minimale batterijvermogens;
  • afrondingen;
  • fluctuaties in PV-productie.
Een agressieve regeling kan daardoor gemakkelijk gaan oscilleren:
code:
1
2
3
4
5
6
7
ontladen
→ export
→ minder ontladen
→ import
→ meer ontladen
→ export
→ ...
Daarom zit er naast de PID-regeling ook filtering en hysterese in het EMS.

7. Filtering

Het berekende batterijvermogen wordt niet één-op-één naar de batterij gestuurd.

Er wordt een eenvoudige filtering toegepast.

Het idee is:
code:
1
2
3
4
5
nieuw gevraagd vermogen
+
vorige waarde
        ↓
gefilterd vermogen
De verhouding tussen beide is afhankelijk van de situatie.

Bijvoorbeeld bij ontladen:
  • bij weinig PV wordt sneller gereageerd;
  • bij meer PV wordt de regeling wat rustiger.
Hierdoor wordt geprobeerd om kleine meetfluctuaties niet direct als een nieuwe regelactie te behandelen.

8. Hysterese

Daarnaast gebruikt de regeling verschillende drempels voor inschakelen en uitschakelen.

Bijvoorbeeld conceptueel:
code:
1
2
> 15 W → starten
< 8 W  → stoppen
Daardoor ontstaat een kleine hysterese.

Zonder hysterese kan een systeem rond een grens voortdurend schakelen.

Met hysterese moet de afwijking eerst voldoende groot worden voordat een toestand verandert.

9. Wat probeert deze regeling uiteindelijk te bereiken?

Niet:
"De batterij moet altijd exact het huisverbruik volgen."
Maar:
"Het netvermogen moet zo dicht mogelijk bij nul komen, terwijl rekening wordt gehouden met batterijgrenzen, PV, SOC-reserve en toekomstige energiebehoefte."
Dat is een subtiel maar belangrijk verschil.

De batterij is daarmee niet het doel van de regeling.

Het netvermogen is het belangrijkste feedbacksignaal.

De batterij is het middel waarmee de netfout wordt gecorrigeerd.
Een vereenvoudigd voorbeeld
Stel:
code:
1
2
3
4
PV                 = 2500 W
Huisverbruik       = 3500 W
Batterij           = 0 W
Net                 = +1000 W
De EMS ziet:
code:
1
+1000 W import
en verhoogt het ontlaadvermogen.

Na enige tijd:
code:
1
2
Batterij            = 850 W
Net                  = +150 W
De fout is kleiner geworden.

De regeling hoeft nu dus niet nog eens 1000 W extra te geven.

Vervolgens:
code:
1
2
Batterij            = 1050 W
Net                  = -50 W
Nu is er een kleine export.

De regeling kan het batterijvermogen weer iets terugnemen.

Het uiteindelijke doel is ongeveer:
code:
1
Net ≈ 0 W
zonder dat de regeling voortdurend grote sprongen maakt.
Waarom ik dit deel belangrijk vind
Dit was voor mij een van de belangrijkste stappen in de ontwikkeling van het EMS.

Door over te stappen van voornamelijk geschat huisverbruik naar gemeten netvermogen + werkelijk batterijvermogen, wordt de regeling veel meer gebaseerd op wat er fysiek daadwerkelijk gebeurt.

Dat betekent niet automatisch dat deze aanpak perfect is.

Ik ben juist benieuwd of anderen hier vanuit regeltechniek of energiebeheer anders tegenaan kijken.

Is deze manier van closed-loop regelen volgens jullie logisch opgebouwd?

En vooral:

Waar zouden jullie de grootste risico's op oscillatie, overshoot of verkeerde integratie zien?

Dat is precies het soort feedback waarmee ik deze EMS verder wil verbeteren.
Volgende deel
In deel 4 ga ik daarom dieper in op de PID-regeling zelf: de keuze van Kp, Ki en Kd, de integratorbegrenzing, filtering en de manier waarop de regeling onderscheid maakt tussen laden en ontladen.

  • Micronikje
  • Registratie: Januari 2018
  • Laatst online: 18:24
Deel 4 — De PID-regeling: hoe bepaalt de EMS het batterijvermogen?
In deel 3 heb ik uitgelegd waarom mijn EMS is overgestapt van geschat huisverbruik naar werkelijk gemeten netvermogen en werkelijk batterijvermogen.

In dit deel ga ik in op de regelkring zelf. De vraag is namelijk niet óf de batterij moet reageren, maar hoeveel vermogen hij op dat moment moet leveren of opnemen.

De kern van mijn EMS is een PID-achtige closed-loop regeling.
1. Wat probeert de PID op te lossen?
Het doel is verrassend eenvoudig:
code:
1
Netvermogen ≈ 0 W
Mijn tekenconventie is:
  • positief = import
  • negatief = export
Dus:
code:
1
2
3
+1200 W  → we nemen stroom af
0 W      → vrijwel in balans
-800 W   → we leveren terug
De PID probeert de batterij zó aan te sturen dat deze netafwijking zo klein mogelijk wordt.
2. De P-component: directe reactie
De P staat voor Proportional.

Deze reageert rechtstreeks op de actuele netfout.

Voorbeeld:
code:
1
Net = +300 W
geeft een kleine correctie.
code:
1
Net = +1800 W
geeft een veel grotere correctie.

Conceptueel:
code:
1
correctie = netfout × Kp
Hoe hoger Kp, hoe sneller de batterij reageert.

Maar té hoog geeft een onrustige regeling:
code:
1
2
3
4
5
6
7
8
9
Import
 ↓
Te veel ontladen
 ↓
Export
 ↓
Te weinig ontladen
 ↓
Import
Daarom is Kp begrensd.
3. De I-component: blijvende afwijkingen
De I staat voor Integral.

Deze kijkt niet naar één meting, maar naar een fout die blijft bestaan.

Bijvoorbeeld:
code:
1
2
3
4
+250 W
+240 W
+260 W
+255 W
Blijkbaar levert de batterij structureel nét iets te weinig vermogen.

De integrator bouwt dan langzaam een extra correctie op.

Dat helpt om kleine blijvende afwijkingen uiteindelijk weg te werken.
4. Integrator wind-up voorkomen
Een bekende valkuil van PID-regelingen is wind-up.

Stel dat de batterij maximaal vermogen levert, maar het huis blijft nog steeds importeren:
code:
1
2
3
+900 W
+900 W
+900 W
De integrator zou blijven oplopen en steeds méér correctie willen geven.

Zodra de beperking verdwijnt, ontstaat dan vaak een grote overshoot.

Daarom:
  • is de integrator begrensd;
  • wordt hij afgebouwd zodra de import verdwijnt;
  • wordt hij gereset wanneer de regeling weer in balans is.
Dat maakt de regeling veel stabieler.
5. De D-component: reageren op verandering
De D staat voor Derivative.

Deze kijkt naar hoe snel de netfout verandert.

Bijvoorbeeld:
code:
1
2
3
4
100 W
350 W
800 W
1400 W
De fout loopt snel op.

De D-component reageert daarop eerder dan P alleen.

Omgekeerd:
code:
1
2
3
4
1200 W
900 W
600 W
300 W
Dan helpt D juist om de regeling af te remmen.

Hij kijkt dus niet naar de grootte van de fout, maar naar de snelheid van verandering.
6. Waarom het werkelijke batterijvermogen zo belangrijk is
Hier zit misschien wel het grootste verschil met mijn eerdere EMS-versies.

Stel:
code:
1
2
3
EMS vraagt      : 1500 W ontladen
Batterij levert : 900 W
Net import      : +600 W
Zonder terugkoppeling zou de EMS denken dat de batterij al 1500 W levert.

Mijn huidige regeling ziet juist:
  • gevraagd vermogen;
  • werkelijk batterijvermogen;
  • werkelijk netvermogen.
Daardoor wordt niet gestuurd op de opdracht, maar op het daadwerkelijke resultaat.

Dat maakt de regelkring echt closed-loop.
7. Laden en ontladen zijn niet hetzelfde
Ik gebruik bewust aparte instellingen voor laden en ontladen.

Waarom?

Omdat de dynamiek heel anders is.

Ontladen
  • vaak relatief stabiel huisverbruik;
  • snelle reactie gewenst.
Laden
  • afhankelijk van wisselende PV;
  • veel meer korte fluctuaties;
  • rustiger gedrag gewenst.
Daarom hebben laden en ontladen ieder hun eigen PID-afstelling en filtering.
8. Filtering voorkomt een zenuwachtige batterij
De PID berekent eerst een gewenst vermogen.

Dat vermogen gaat niet direct naar de omvormer.

Eerst wordt het gefilterd.

Conceptueel:
code:
1
2
3
4
5
Nieuw vermogen
      +
Vorige vermogen
      ↓
Gefilterd vermogen
Hierdoor krijg je geen grote sprongen zoals:
code:
1
2
3
4
500 W
1800 W
700 W
2100 W
maar een veel vloeiender verloop.

Dat vermindert slijtage én voorkomt oscillaties.
9. Hysterese: niet schakelen rond nul
Een tweede stabiliteitslaag is hysterese.

In plaats van één drempel gebruik ik twee grenzen.

Bijvoorbeeld:
code:
1
2
3
>15 W  → starten

<8 W   → stoppen
Daardoor blijft de batterij niet voortdurend wisselen tussen laden, stoppen en ontladen wanneer het net rond nul schommelt.
10. De complete regelkring
De volledige logica kun je zien als:
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Gemeten netvermogen
         │
         ▼
      Netfout
         │
         ▼
   P + I + D regeling
         │
         ▼
 Gewenst batterijvermogen
         │
         ▼
      Filtering
         │
         ▼
   Omvormer / batterij
         │
         ▼
 Werkelijk batterijvermogen
         │
         ▼
 Nieuw gemeten netvermogen
         └───────────────↺
De batterij is dus niet het doel van de regeling.

Het netvermogen is de belangrijkste feedbackvariabele; de batterij is het middel om die fout te corrigeren.
Waarom ik deze aanpak gekozen heb
Mijn ervaring was dat een regeling op basis van berekend huisverbruik prima werkt zolang alles ideaal reageert.

In de praktijk heb je echter te maken met:
  • meetvertraging;
  • omvormervertraging;
  • wisselend huishoudelijk verbruik;
  • grillige PV-productie;
  • begrensd batterijvermogen.
Door de regelkring volledig te baseren op werkelijk gemeten netvermogen en werkelijk batterijvermogen reageert het EMS op wat er fysiek gebeurt, niet op wat het verwacht dat er gebeurt.

Dat heeft de stabiliteit van mijn installatie duidelijk verbeterd.
Volgende deel
In deel 5 ga ik in op de beslislaag boven de PID.

De PID kan namelijk berekenen hoeveel vermogen nodig is, maar bepaalt niet of de batterij dat ook mág doen.

Daar komen zaken als:
  • dynamische SOC-reserve;
  • avond- en nachtbuffer;
  • forecast;
  • netladen;
  • veiligheidsgrenzen;
  • maximale laad- en ontlaadvermogens
bij elkaar.

  • Micronikje
  • Registratie: Januari 2018
  • Laatst online: 18:24
Deel 5 — De beslislaag: wanneer mag de batterij eigenlijk laden of ontladen?
In deel 4 ging het over de PID-regeling. Die berekent hoeveel vermogen de batterij idealiter moet leveren of opnemen.

Maar een goede EMS heeft nog een belangrijkere laag daarboven: de beslislaag.

Die bepaalt namelijk of de batterij op dat moment überhaupt mag laden of ontladen.

Je kunt het zien als twee niveaus:
  • PID → hoeveel vermogen is nodig?
  • Beslislaag → mag dat vermogen gebruikt worden?
Juist deze laag voorkomt dat de batterij op het verkeerde moment leegloopt of onnodig vol blijft.
1. Waarom een beslislaag?
Stel dat het huis 1200 W uit het net haalt.

De PID zegt:
code:
1
Ontladen met 1200 W
Dat lijkt logisch.

Maar wat als:
  • de batterij nog maar 18% SOC heeft?
  • er nog een hele avond voor de boeg is?
  • morgen nauwelijks zon wordt verwacht?
Dan is direct ontladen misschien helemaal niet de beste keuze.

De beslislaag mag de PID dus overrulen.
2. Dynamische SOC in plaats van een vaste ondergrens
Veel batterijen werken met één vaste minimumwaarde, bijvoorbeeld:
code:
1
Min SOC = 10%
Mijn EMS gebruikt juist een dynamische minimum-SOC.

Die verandert gedurende de dag op basis van:
  • tijdstip;
  • seizoen;
  • verwacht avondverbruik;
  • verwacht nachtverbruik;
  • PV-forecast;
  • historisch verbruik.
Daardoor is 25% SOC niet altijd hetzelfde.

Voorbeeld:
TijdVerwachte situatieMinimum SOC
09:00Veel PV verwacht10%
15:00Avond nadert20%
20:00Nachtreserve nodig30–40%
De batterij bewaart dus automatisch meer energie naarmate de avond dichterbij komt.
3. De avond- en nachtbuffer
In plaats van een willekeurige reserve berekent de EMS hoeveel energie waarschijnlijk nog nodig is.

Bijvoorbeeld:
code:
1
2
Avondverbruik : 3,8 kWh
Nachtverbruik : 1,9 kWh
Samen:
code:
1
5,7 kWh
Bij een batterij van 20 kWh is dat ongeveer:
code:
1
29% SOC
Dat wordt vervolgens gebruikt als richtwaarde voor de minimale reserve.

De batterij probeert dus niet maximaal leeg te raken, maar voldoende energie beschikbaar te houden voor de resterende uren.
4. Forecast verlaagt of verhoogt de reserve
De reserve is niet statisch.

Stel dat morgen 14 kWh PV wordt verwacht.

Dan hoeft de batterij minder energie te bewaren.

Bijvoorbeeld:
code:
1
2
3
4
5
Reserve zonder forecast : 35%

Forecast-correctie : -10%

Nieuwe reserve : 25%
Omgekeerd werkt het ook.

Bij weinig verwachte zon blijft de reserve juist hoger.

Hierdoor gebruikt de EMS de batterij agressiever op zonnige dagen en conservatiever op donkere dagen.
5. Wanneer mag de batterij laden?
Laden gebeurt niet alleen omdat de netmeter export ziet.

Er moeten meerdere voorwaarden tegelijk waar zijn:
  • daadwerkelijk PV-surplus;
  • voldoende ruimte in de batterij;
  • geen blokkering door netladen;
  • geen veiligheidslimiet bereikt.
Pas dan mag de PID bepalen hoeveel laadvermogen nodig is.

Dus niet:
code:
1
Export → laden
maar:
code:
1
2
3
4
5
6
7
PV-surplus
      +
SOC lager dan maximum
      +
Geen blokkering
      ↓
PID bepaalt laadvermogen
Dat voorkomt veel ongewenste laadacties.
6. Wanneer mag de batterij ontladen?
Ontladen werkt op dezelfde manier.

De PID kan 1800 W willen leveren, maar de beslislaag controleert eerst:
  • ligt SOC boven de dynamische ondergrens?
  • is er geen cooldown actief?
  • is er geen netlaad-interlock?
  • bevinden we ons binnen de veilige grenzen?
Pas daarna wordt het ontlaadvermogen toegestaan.
7. Waarom een cooldown?
Na het beëindigen van netladen wil ik niet dat de batterij direct weer begint te ontladen.

Daarom zit er een korte cooldown in.

Conceptueel:
code:
1
2
3
4
5
Netladen stopt
      ↓
15 minuten wachttijd
      ↓
Normale EMS-regeling actief
Dat voorkomt dat twee verschillende regelingen elkaar onmiddellijk gaan tegenwerken.
8. Veiligheid vóór optimalisatie
De hoogste prioriteit binnen de EMS is niet rendement maar veiligheid.

Daarom worden extreme situaties altijd eerst afgehandeld.

Bijvoorbeeld:
code:
1
2
3
4
5
6
7
8
SOC te laag
→ ontladen uit

SOC extreem hoog
→ laden uit

Override actief
→ alles stoppen
Pas wanneer aan alle veiligheidsvoorwaarden is voldaan, wordt de normale regeling weer actief.
9. De complete beslisboom
Vereenvoudigd ziet de logica er zo uit:
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Meetwaarden
     │
     ▼
Veiligheidscontrole
     │
     ▼
SOC-reserve berekenen
     │
     ▼
PV-surplus bepalen
     │
     ▼
Mag laden of ontladen?
     │
     ▼
PID berekent vermogen
     │
     ▼
Filtering
     │
     ▼
Omvormer
De PID staat dus bewust niet bovenaan.

Eerst wordt bepaald wat verstandig is, daarna pas hoeveel vermogen gebruikt wordt.
Waarom ik deze structuur gekozen heb
Tijdens eerdere versies merkte ik dat een goede PID alleen nog geen goede EMS maakt.

De echte winst zat in het scheiden van twee verantwoordelijkheden:
  • Beslislaag: energie bewaren, forecast meenemen en grenzen bewaken.
  • Regellaag: het netvermogen zo netjes mogelijk naar nul sturen.
Daardoor blijven beide onderdelen veel eenvoudiger te begrijpen én afzonderlijk te tunen.
Volgende deel
In deel 6 ga ik in op de laatste grote bouwsteen: netladen op dynamische kwartierprijzen.

Daar laat ik zien hoe goedkoop tarief, toekomstige prijsstijgingen en PV-forecast samen bepalen wanneer het juist wél verstandig is om bewust stroom uit het net in de batterij op te slaan.

  • Micronikje
  • Registratie: Januari 2018
  • Laatst online: 18:24
Deel 6 — Slim netladen: wanneer is goedkope netstroom wél verstandig?
In de vorige delen ging het vooral over zelfconsumptie: zoveel mogelijk eigen zonnestroom gebruiken en het netvermogen rond nul houden.

Maar een thuisbatterij krijgt na het verdwijnen van de salderingsregeling nog een tweede taak: energie inkopen wanneer die goedkoop is.

Dat klinkt eenvoudig, maar mijn ervaring is dat juist hier veel regelingen te snel of te vaak laden. Daarom staat netladen in mijn EMS volledig los van de normale PID-regeling.
1. Waarom een aparte netlaadmodule?
De PID-regeling reageert op seconden.

Dynamische elektriciteitsprijzen veranderen per kwartier.

Dat zijn twee totaal verschillende tijdschalen.

Daarom heb ik de architectuur bewust gescheiden:
code:
1
2
3
4
5
6
7
8
9
10
Netlaadmodule
      │
      ▼
Beslist OF netladen mag
      │
      ▼
PID wordt tijdelijk geblokkeerd
      │
      ▼
Batterij laadt met ingesteld vermogen
Tijdens actief netladen mag de normale EMS dus niet tegelijkertijd proberen te ontladen.
2. Goedkoop is niet automatisch goedkoop genoeg
Een lage prijs alleen is voor mij geen reden om te laden.

Stel:
TijdTarief
02:00€0,16
03:00€0,15
04:00€0,14
Alle uren zijn goedkoop.

Maar als de ochtendprijs ook rond €0,15 blijft, levert laden nauwelijks voordeel op.

Mijn EMS kijkt daarom niet alleen naar de huidige prijs, maar ook naar de verwachte prijsontwikkeling.

De vraag is eigenlijk:
Wordt stroom later duurder?

3. Het uplift-principe
Ik gebruik hiervoor een eenvoudige uplift-logica.

Conceptueel:
code:
1
2
3
4
5
Toekomstige prijs
      -
Huidige prijs
      =
Prijsverschil
Bijvoorbeeld:
NuLaterVerschil
€0,14€0,28+€0,14
€0,18€0,21+€0,03
€0,22€0,19-€0,03
Alleen een voldoende grote positieve uplift maakt netladen interessant.

Daardoor wordt voorkomen dat de batterij voor een paar cent verschil een volledige laadcyclus maakt.
4. PV-forecast voorkomt onnodig netladen
Een tweede belangrijke vraag:
Komt er morgen veel zon?
Stel dat de prijs vannacht laag is.

Maar morgen verwacht je:
code:
1
PV-forecast = 18 kWh
Dan is de kans groot dat de batterij vanzelf weer vol raakt.

In dat geval laat mijn EMS het netladen vaak helemaal vervallen.

Omgekeerd:
code:
1
PV-forecast = 2 kWh
Dan kan goedkoop laden juist heel logisch zijn.

De forecast werkt dus als een rem op onnodige laadcycli.
5. SOC bepaalt hoeveel er geladen wordt
De batterij hoeft ook niet altijd naar 100%.

Ik gebruik een maximaal laadniveau van ongeveer 95%.

Daarnaast speelt de actuele SOC mee.

Bijvoorbeeld:
SOCBeslissing
18%Laden toegestaan
52%Afhankelijk van uplift
91%Meestal niet meer laden
96%Laden blokkeren
Hierdoor blijft er ruimte voor regeneratie en voorkom ik langdurig op 100% staan.
6. Waarom een interlock?
Tijdens netladen wil ik absoluut voorkomen dat twee regelingen elkaar tegenwerken.

Zonder interlock kan dit gebeuren:
code:
1
2
3
4
5
6
7
8
Netlaadmodule
→ laad 2000 W

PID ziet import
→ ontlaad 600 W

Resultaat:
de batterij laadt én ontlaadt tegelijk.
Daarom activeert de netlaadmodule een aparte status:
code:
1
Grid charging = ON
Zolang die actief is:
  • ontladen wordt geblokkeerd;
  • de PID mag geen ontlaadcommando geven;
  • alleen de netlaadmodule stuurt de batterij.
Dat maakt de regeling veel voorspelbaarder.
7. Wanneer stopt netladen?
Netladen stopt niet alleen wanneer de batterij vol is.

Ook deze situaties beëindigen het laden:
  • prijsvoordeel verdwenen;
  • eind van het goedkope kwartier;
  • maximale SOC bereikt;
  • forecast maakt verder laden onlogisch;
  • handmatige override.
Na het stoppen begint bovendien een korte cooldown, zodat de normale EMS niet direct terugschakelt.
8. De complete beslisboom
Vereenvoudigd ziet de netlaadmodule er zo uit:
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Kwartierprijs
      │
      ▼
Is prijs goedkoop?
      │
      ▼
Is uplift groot genoeg?
      │
      ▼
PV-forecast controleren
      │
      ▼
SOC onder maximum?
      │
      ▼
Netladen starten
      │
      ▼
Interlock activeert
Hierdoor wordt alleen geladen wanneer meerdere voorwaarden tegelijk gunstig zijn.
9. Waarom ik hiervoor heb gekozen
Mijn uitgangspunt is dat een batterij geen handelsrobot is.

Elke laad- en ontlaadcyclus kost efficiëntie en draagt bij aan slijtage.

Daarom probeer ik niet ieder prijsverschil te benutten, maar alleen situaties waarin het verwachte voordeel groot genoeg is én de kans klein is dat de zon hetzelfde gratis oplost.

Dat levert misschien minder laadcycli op, maar hopelijk wel betere laadcycli.
Tot nu toe
In deze reeks heb ik de EMS opgebouwd van onder naar boven:
  • Deel 1: de basislaag en sensoren
  • Deel 2: PV-surplus en dynamische SOC-reserve
  • Deel 3: werkelijk netvermogen als feedback
  • Deel 4: de PID-regeling
  • Deel 5: de beslislaag boven de PID
  • Deel 6: slim netladen op kwartierprijzen
In de volgende delen zal ik de volledige generieke Home Assistant YAML publiceren, zodat iedereen de logica kan aanpassen aan zijn eigen sensoren en omvormer.

Ik ben vooral benieuwd hoe anderen de balans zien tussen prijsoptimalisatie, forecast en het beperken van onnodige batterijcycli. Helaas echter nog geen inhoudelijke reacties ontvangern.

  • Micronikje
  • Registratie: Januari 2018
  • Laatst online: 18:24
Deel 7A.1 — EMS v40.9: Predictive Reserve

In de vorige delen heb ik de basis van mijn EMS uitgelegd. In dit deel komt een belangrijk onderdeel van de regeling: de Predictive Reserve.

Het idee is eigenlijk vrij simpel.

Een thuisbatterij moet niet alleen reageren op wat er op dit moment gebeurt. Als de batterij bijvoorbeeld om 14:00 uur 90% vol zit, betekent dat niet automatisch dat hij maximaal moet gaan ontladen. Misschien komt er 's avonds een periode met veel verbruik en weinig zonnepanelen.

Daarom kijkt deze versie niet alleen naar het actuele netvermogen en de actuele SOC, maar ook naar:

het historische verbruik;

het verwachte PV-aanbod;

het resterende verbruik van de dag;

de huidige SOC;

een minimale SOC die op basis daarvan wordt berekend;

temperatuur van de batterij;

de daadwerkelijke batterijvermogens;

en de reactie van de batterij op eerdere regelcommando's.

Daarnaast zit er een PID-regeling in om het batterijvermogen geleidelijk bij te sturen in plaats van voortdurend grote sprongen te maken.

Belangrijk: dit is geen kant-en-klare YAML die je blind in iedere installatie kunt plaatsen. De namen van sensoren, switches, numbers en helpers verschillen per installatie.

Wat moet je aanpassen?

In onderstaande code zijn mijn oorspronkelijke entiteiten vervangen door generieke namen.

Deze moet je vervangen door de entiteiten uit jouw eigen Home Assistant-installatie:

sensor.grid_power → actueel netvermogen/import-export in watt

sensor.battery_soc → actuele batterij-SOC in %

sensor.pv_power → actueel PV-vermogen in watt

sensor.battery_voltage → batterijspanning

sensor.battery_temperature → batterijtemperatuur

sensor.battery_charge_power → daadwerkelijk laadvermogen van de batterij

sensor.battery_discharge_power → daadwerkelijk ontlaadvermogen van de batterij

Voor de inverterbesturing:

switch.battery_force_charge → force-charge switch van de inverter

switch.battery_force_discharge → force-discharge switch van de inverter

number.battery_force_charge_power → instelbaar laadvermogen

number.battery_force_discharge_power → instelbaar ontlaadvermogen

time.battery_charge_end_slot_1

time.battery_charge_end_slot_2

time.battery_discharge_end_slot_1

time.battery_discharge_end_slot_2

De overige input_* helpers zijn eveneens generiek gemaakt. Deze moet je dus zelf aanmaken of koppelen aan bestaande helpers.

Voor de PV-voorspelling:

sensor.pv_forecast_tomorrow

sensor.pv_forecast_remaining_today

Deze moeten een PV-voorspelling in kWh leveren.

Ik post de code in delen, anders wordt het veel te groot.
code:
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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
alias: EMS v40.9 — Predictive Reserve
description: >
  Generieke EMS v40.9: Predictive Reserve met EMA7/verbruiksprofielen,
  resterend verbruik, PV-forecast, PV-confidence en PID-regeling.

triggers:
  - trigger: time_pattern
    seconds: /8

actions:
  - action: system_log.write
    data:
      message: >
        [TRIGGER v40.9] @{{ now().strftime('%H:%M:%S') }} | State={{
        current_state }}→{{ next_state }} | Net={{ net_power }}W | PV={{ pv_now
        }}W | SOC={{ soc }}% | Flow={{ flow_state }}
      level: info

  - if:
      - condition: state
        entity_id: input_boolean.ems_system_override
        state: 'on'
    then:
      - action: switch.turn_off
        target:
          entity_id:
            - switch.battery_force_charge
            - switch.battery_force_discharge

      - action: input_boolean.turn_off
        target:
          entity_id:
            - input_boolean.ems_battery_charging
            - input_boolean.ems_battery_discharging

      - action: input_number.set_value
        target:
          entity_id: input_number.ems_pid_integral
        data:
          value: 0

      - action: input_number.set_value
        target:
          entity_id: input_number.ems_last_power
        data:
          value: 0

      - action: input_number.set_value
        target:
          entity_id: input_number.ems_last_net_power
        data:
          value: '{{ net_power }}'

      - action: input_text.set_value
        target:
          entity_id: input_text.ems_current_state
        data:
          value: IDLE

      - action: input_text.set_value
        target:
          entity_id: input_text.ems_previous_state
        data:
          value: IDLE

  - if:
      - condition: template
        value_template: '{{ data_missing }}'
    then:
      - action: switch.turn_off
        target:
          entity_id:
            - switch.battery_force_charge
            - switch.battery_force_discharge

      - action: input_boolean.turn_off
        target:
          entity_id:
            - input_boolean.ems_battery_charging
            - input_boolean.ems_battery_discharging

      - action: input_text.set_value
        target:
          entity_id: input_text.ems_current_state
        data:
          value: IDLE

      - action: input_text.set_value
        target:
          entity_id: input_text.ems_previous_state
        data:
          value: IDLE

      - action: input_number.set_value
        target:
          entity_id: input_number.ems_pid_integral
        data:
          value: 0

      - action: system_log.write
        data:
          message: '[EMS v40.9][ABORT] Data missing — battery control disabled'
          level: error

  • Micronikje
  • Registratie: Januari 2018
  • Laatst online: 18:24
Deel 7A.1.2
code:
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
  - if:
      - condition: template
        value_template: |
          {{ system_active and extreme_soc }}
    then:
      - action: switch.turn_off
        target:
          entity_id:
            - switch.battery_force_charge
            - switch.battery_force_discharge

      - action: input_boolean.turn_off
        target:
          entity_id:
            - input_boolean.ems_battery_charging
            - input_boolean.ems_battery_discharging

      - action: input_text.set_value
        target:
          entity_id: input_text.ems_current_state
        data:
          value: IDLE

      - action: input_number.set_value
        target:
          entity_id: input_number.ems_pid_integral
        data:
          value: 0

      - action: system_log.write
        data:
          message: >
            ~~~~~~~[EMS v40.9]~~~~~~~[EXTREME SOC] SOC={{ soc }}% —
            battery control disabled
          level: warning

  - if:
      - condition: template
        value_template: |
          {{ transitioning_now
             and system_active
             and not data_missing
             and not extreme_soc }}
    then:
      - action: input_text.set_value
        target:
          entity_id: input_text.ems_previous_state
        data:
          value: '{{ current_state }}'

      - action: input_text.set_value
        target:
          entity_id: input_text.ems_current_state
        data:
          value: '{{ next_state }}'

      - action: input_datetime.set_datetime
        target:
          entity_id: input_datetime.ems_state_entered_at
        data:
          datetime: '{{ now().strftime(''%Y-%m-%dT%H:%M:%S'') }}'

      - action: input_datetime.set_datetime
        target:
          entity_id: input_datetime.ems_last_transition_cooldown
        data:
          datetime: '{{ now().strftime(''%Y-%m-%dT%H:%M:%S'') }}'

      - action: system_log.write
        data:
          message: >
            ~~~~~~~[EMS v40.9]~~~~~~~[TRANSITION]
            {{ current_state }} → {{ next_state }}
          level: info

  - if:
      - condition: template
        value_template: '{{ do_discharge }}'
    then:
      - action: system_log.write
        data:
          message: >
            ~~~~~~~[EMS v40.9]~~~~~~~[DISCHARGE] Target={{ final_discharge_w }}W |
            Net={{ net_power }}W | Basis={{ basis_ontlading }}W |
            PID={{ pid_output|round(0) }}W
          level: info

    - if:
          - condition: template
            value_template: '{{ not currently_discharging }}'
        then:
          - action: switch.turn_off
            target:
              entity_id:
                - switch.battery_force_charge
                - switch.battery_force_discharge

          - delay:
              seconds: 1

          - action: input_boolean.turn_on
            target:
              entity_id: input_boolean.ems_battery_discharging

          - action: time.set_value
            target:
              entity_id: time.battery_discharge_end_slot_1
            data:
              time: '23:59:00'

          - action: time.set_value
            target:
              entity_id: time.battery_discharge_end_slot_2
            data:
              time: '23:59:00'

          - action: time.set_value
            target:
              entity_id: time.battery_charge_end_slot_1
            data:
              time: '00:00:00'

          - action: time.set_value
            target:
              entity_id: time.battery_charge_end_slot_2
            data:
              time: '00:00:00'

          - action: number.set_value
            target:
              entity_id: number.battery_force_discharge_power
            data:
              value: '{{ final_discharge_w }}'

          - delay:
              seconds: 1

          - action: switch.turn_on
            target:
              entity_id: switch.battery_force_discharge

        else:
          - action: switch.turn_on
            target:
              entity_id: switch.battery_force_discharge

          - action: number.set_value
            target:
              entity_id: number.battery_force_discharge_power
            data:
              value: '{{ final_discharge_w }}'

  • Micronikje
  • Registratie: Januari 2018
  • Laatst online: 18:24
Deel 7A1.3
code:
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
 #
- if:
      - condition: template
        value_template: '{{ do_charge }}'
    then:
      - action: system_log.write
        data:
          message: >
            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~[EMS v40.9]~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~[CHARGE] Target={{ final_charge_w }}W |
            Net={{ net_power }}W | PV={{ pv_now }}W |
            PID={{ pid_output_charge|round(0) }}W
          level: info

      - if:
          - condition: template
            value_template: '{{ not currently_charging }}'
        then:
          - action: switch.turn_off
            target:
              entity_id:
                - switch.battery_force_charge
                - switch.battery_force_discharge

          - delay:
              seconds: 1

          - action: input_boolean.turn_on
            target:
              entity_id: input_boolean.ems_battery_charging

          - action: time.set_value
            target:
              entity_id: time.battery_charge_end_slot_1
            data:
              time: '23:59:00'

          - action: time.set_value
            target:
              entity_id: time.battery_charge_end_slot_2
            data:
              time: '23:59:00'

          - action: time.set_value
            target:
              entity_id: time.battery_discharge_end_slot_1
            data:
              time: '00:00:00'

          - action: time.set_value
            target:
              entity_id: time.battery_discharge_end_slot_2
            data:
              time: '00:00:00'

          - action: number.set_value
            target:
              entity_id: number.battery_force_charge_power
            data:
              value: '{{ final_charge_w }}'

          - delay:
              seconds: 1

          - action: switch.turn_on
            target:
              entity_id: switch.battery_force_charge

        else:
          - action: switch.turn_on
            target:
              entity_id: switch.battery_force_charge

          - action: number.set_value
            target:
              entity_id: number.battery_force_charge_power
            data:
              value: '{{ final_charge_w }}'

  - if:
      - condition: template
        value_template: |
          {{ next_state == 'IDLE'
             and current_state in ['DISCHARGING', 'CHARGING'] }}
    then:
      - action: system_log.write
        data:
          message: >
            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~[EMS v40.9]~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~[STOP] {{ current_state }} → IDLE |
            Net={{ net_power }}W | PV={{ pv_now }}W
          level: info

      - action: switch.turn_off
        target:
          entity_id:
            - switch.battery_force_charge
            - switch.battery_force_discharge

      - action: input_boolean.turn_off
        target:
          entity_id:
            - input_boolean.ems_battery_charging
            - input_boolean.ems_battery_discharging

      - action: time.set_value
        target:
          entity_id: time.battery_charge_end_slot_1
        data:
          time: '00:00:00'

      - action: time.set_value
        target:
          entity_id: time.battery_charge_end_slot_2
        data:
          time: '00:00:00'

      - action: time.set_value
        target:
          entity_id: time.battery_discharge_end_slot_1
        data:
          time: '00:00:00'

      - action: time.set_value
        target:
          entity_id: time.battery_discharge_end_slot_2
        data:
          time: '00:00:00'

  - if:
      - condition: template
        value_template: '{{ system_active and not data_missing }}'
    then:
      - action: input_number.set_value
        target:
          entity_id: input_number.ems_last_power
        data:
          value: '{{ filtered_signal_safe }}'

      - action: input_number.set_value
        target:
          entity_id: input_number.ems_last_net_power
        data:
          value: '{{ net_power }}'

      - action: input_number.set_value
        target:
          entity_id: input_number.ems_pid_integral
        data:
          value: '{{ integral_safe }}'

      - action: input_number.set_value
        target:
          entity_id: input_number.ems_soc_min_current
        data:
          value: '{{ soc_min_pct }}'

  - action: input_text.set_value
    target:
      entity_id: input_text.ems_debug_output_1
    data:
      value: >
        State={{ current_state }}→{{ next_state }} |
        Prev={{ previous_state }} |
        Dur={{ state_duration_sec|round(0) }}s |
        Cool={{ cooldown_active }} |
        SOC={{ soc|round(1) }}% |
        SOCmin={{ soc_min_pct }}%

  - action: input_text.set_value
    target:
      entity_id: input_text.ems_debug_output_2
    data:
      value: >
        Net={{ net_power|round(0) }}W |
        PV={{ pv_now|round(0) }}W |
        WantD={{ want_to_discharge }} |
        WantC={{ want_to_charge }} |
        StopD={{ want_to_stop_from_discharge }} |
        StopC={{ want_to_stop_from_charge }} |
        D={{ do_discharge }} |
        C={{ do_charge }}

  - action: input_text.set_value
    target:
      entity_id: input_text.ems_debug_output_3
    data:
      value: >
        EMA_O={{ ema_ochtend }} |
        EMA_M={{ ema_middag }} |
        EMA_A={{ ema_avond }} |
        EMA_N={{ ema_nacht }} |
        kpD={{ kp_discharge|round(3) }} |
        kpC={{ kp_charge|round(3) }} |
        Ki={{ ki }} |
        I={{ integral_safe|round(1) }}

  - action: input_text.set_value
    target:
      entity_id: input_text.ems_debug_output_4
    data:
      value: >
        Import={{ importing }} |
        Surplus={{ pv_surplus }} |
        NetChg={{ net_charging }} |
        dAllow={{ discharge_allowed }} |
        cAllow={{ charge_allowed }} |
        StrongI={{ strong_import_override }} |
        StrongE={{ strong_export_override }} |
        Ramp={{ ramp_step_w }}W

  - action: input_text.set_value
    target:
      entity_id: input_text.ems_debug_output_5
    data:
      value: >
        Flow={{ flow_state }} |
        aBatt={{ actual_batt_power|round(0) }}W |
        PID={{ pid_output|round(0) }}W |
        PIDc={{ pid_output_charge|round(0) }}W |
        TargetD={{ state_target_discharge|round(0) }}W |
        TargetC={{ state_target_charge|round(0) }}W |
        FinalD={{ final_discharge_w }}W |
        FinalC={{ final_charge_w }}W |
        ΔD={{ limited_delta_discharge }}W |
        ΔC={{ limited_delta_charge }}W

  - action: system_log.write
    data:
      message: '{{ log_msg }}'
      level: info

mode: restart
max_exceeded: silent

variables:

  # ============================================================
  # ACTUELE MEETWAARDEN
  # Vervang deze generieke sensoren door jouw eigen entiteiten.
  # ============================================================

  net_power_raw: '{{ states(''sensor.grid_power'') }}'
  soc_raw: '{{ states(''sensor.battery_soc'') }}'
  pv_now_raw: '{{ states(''sensor.pv_power'') }}'
  batt_voltage_raw: '{{ states(''sensor.battery_voltage'') }}'

  real_charge_power: >
    {{ states('sensor.battery_charge_power')|float(0) }}

  real_discharge_power: >
    {{ states('sensor.battery_discharge_power')|float(0) }}

  real_consumption: >
    {{ net_power_raw|float(0) +
       real_discharge_power|float(0) -
       real_charge_power|float(0) }}

  actual_batt_power: >
    {{ real_discharge_power|float(0) -
       real_charge_power|float(0) }}

  current_inverter_discharge: >
    {{ states('number.battery_force_discharge_power')|float(0) }}

  current_inverter_charge: >
    {{ states('number.battery_force_charge_power')|float(0) }}

  current_state_raw: >
    {{ states('input_text.ems_current_state') }}

  previous_state_raw: >
    {{ states('input_text.ems_previous_state') }}

  current_state: |
    {% if current_state_raw in ['IDLE','DISCHARGING','CHARGING','STOP'] %}
      {{ current_state_raw }}
    {% else %}
      IDLE
    {% endif %}

  previous_state: |
    {% if previous_state_raw in ['IDLE','DISCHARGING','CHARGING','STOP'] %}
      {{ previous_state_raw }}
    {% else %}
      IDLE
    {% endif %}

  system_active: >
    {{ states('input_boolean.ems_system_override') != 'on' }}

  net_charging: >
    {{ is_state('input_boolean.ems_net_charging','on') }}

  currently_charging: >
    {{ is_state('input_boolean.ems_battery_charging','on') }}

  currently_discharging: >
    {{ is_state('input_boolean.ems_battery_discharging','on') }}

  data_missing: |
    {{ net_power_raw in ['unknown','unavailable','none','']
       or soc_raw in ['unknown','unavailable','none','']
       or batt_voltage_raw in ['unknown','unavailable','none',''] }}

  • Micronikje
  • Registratie: Januari 2018
  • Laatst online: 18:24
Deel 7A1.4
code:
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
#
net_power: >
    {{ 0 if data_missing else net_power_raw|float(0) }}

  soc: >
    {{ 50 if data_missing else soc_raw|float(50) }}

  pv_now: >
    {{ pv_now_raw|float(0) }}

  batt_voltage: >
    {{ 48 if data_missing else
       batt_voltage_raw|float(48)|round(1) }}

  batt_temp: >
    {{ states('sensor.battery_temperature')|float(20) }}

  extreme_soc: >
    {{ soc|float(50) < 10 or soc|float(50) > 98 }}

  hour: '{{ now().hour }}'
  month: '{{ now().month }}'

  is_winter: >
    {{ month|int in [11,12,1,2,3] }}

  state_entered_at_ts: >
    {% set ts = states('input_datetime.ems_state_entered_at') %}
    {% if ts in ['unknown','unavailable','none',''] %}
      {{ as_timestamp(now()) }}
    {% else %}
      {{ as_timestamp(ts) }}
    {% endif %}

  state_duration_sec: >
    {{ (as_timestamp(now()) -
        state_entered_at_ts|float(0))|round(1) }}

  cooldown_ts: >
    {% set ts = states('input_datetime.ems_last_transition_cooldown') %}
    {% if ts in ['unknown','unavailable','none',''] %}
      999
    {% else %}
      {{ as_timestamp(now()) - as_timestamp(ts) }}
    {% endif %}

  cooldown_active: >
    {{ cooldown_ts|float(999) < 5 }}

  require_cooldown: >
    {{ current_state != previous_state and
       cooldown_ts|float(999) < 5 }}
#
# ============================================================
  # BASISINSTELLINGEN
  # ============================================================

  threshold_on: 40
  threshold_off: 20
  ramp_step_w: 300
  min_state_duration_sec: 120

  strong_import_threshold: 150
  strong_export_threshold: -150

  import_deadband: 10
  export_deadband: 20
  pv_marge: 30

  importing: >
    {{ net_power|float(0) > import_deadband }}

  exporting: >
    {{ net_power|float(0) < -export_deadband }}

  pv_surplus: |
    {% set net = net_power|float(0) %}
    {% set pv = pv_now|float(0) %}

    {% if pv < 50 %}
      {{ net < -75 and (-net) <= (pv + pv_marge) }}
    {% else %}
      {{ net < -export_deadband and
         (-net) <= (pv + pv_marge) }}
    {% endif %}
#
  # ============================================================
  # PID-INSTELLINGEN
  # ============================================================

  kp_base: >
    {{ states('input_number.ems_kp_base')|float(1.0) }}

  kp_adjustment: >
    {{ states('input_number.ems_kp_adjustment')|float(0) }}

  kp_grid: >
    {{ [[kp_base|float(1.0) +
        kp_adjustment|float(0), 1.5]|min, 0.2]|max }}

  kp_discharge: >
    {{ kp_grid|float(1.0) }}

  kp_charge_base: >
    {{ states('input_number.ems_kp_charge_base')|float(1.0) }}

  kp_charge: >
    {{ [[kp_charge_base|float(1.0), 1.5]|min, 0.2]|max }}

  ki: >
    {{ states('input_number.ems_ki')|float(0.035) }}

  kd: >
    {{ states('input_number.ems_kd')|float(0.08) }}

  kd_charge: >
    {{ states('input_number.ems_kd_charge')|float(0.05) }}

  last_power: >
    {{ states('input_number.ems_last_power')|float(0) }}

  last_net_power: >
    {{ states('input_number.ems_last_net_power')|float(0) }}

  integral_raw: >
    {{ states('input_number.ems_pid_integral')|float(0) }}

  derivative: >
    {{ net_power|float(0) -
       last_net_power|float(0) }}

  integral_candidate: |
    {% set net = net_power|float(0) %}

    {% if importing %}
      {{ [[integral_raw|float(0) +
          (net * 0.8), 1000]|min, -1000]|max|round(2) }}
    {% else %}
      {{ [[integral_raw|float(0) * 0.95,
          1000]|min, -1000]|max|round(2) }}
    {% endif %}

  integral_safe: >
    {{ 0 if state_duration_sec|float(0) < 10
       else integral_candidate|float(0) }}

  pid_output: |
    {% set net = net_power|float(0) %}
    {% set ab = actual_batt_power|float(0) %}

    {{ ([ab, 0]|max)
       + (net * kp_discharge|float(0))
       + (integral_safe|float(0) * ki|float(0))
       + (derivative|float(0) * kd|float(0))
       + (30 if (ab == 0 and importing) else 0) }}

  pid_output_charge: |
    {% set net = net_power|float(0) %}
    {% set ab = actual_batt_power|float(0) %}

    {{ ([ab * -1, 0]|max)
       + ((net * -1) * kp_charge|float(0))
       + (derivative|float(0) *
          kd_charge|float(0) * -1) }}

# ============================================================
  # HISTORISCH VERBRUIK / EMA
  # ============================================================

  ema_avond: >
    {{ states('input_number.ems_import_ema_avond')|float(4.0) }}

  ema_nacht: >
    {{ states('input_number.ems_import_ema_nacht')|float(1.0) }}

  ema_ochtend: >
    {{ states('input_number.ems_import_ema_ochtend')|float(2.0) }}

  ema_middag: >
    {{ states('input_number.ems_import_ema_middag')|float(-3.0) }}

  # ============================================================
  # BATTERIJ
  # ============================================================

  batt_kwh: >
    {{ states('input_number.ems_battery_capacity_kwh')|float(20) }}

  soc_per_kwh: >
    {{ 100 / batt_kwh }}

  verbruik_baseline_kwh: >
    {{ states('input_number.ems_consumption_baseline_kwh')|float(20) }}

  # ============================================================
  # PV-VOORSPELLING
  # ============================================================

  pv_forecast_morgen_kwh: >
    {{ states('sensor.pv_forecast_tomorrow')|float(0) }}

  pv_forecast_resterend_vandaag_kwh: >
    {{ states('sensor.pv_forecast_remaining_today')|float(0) }}

  pv_forecast_relevant_kwh: |
    {% if hour|int < 12 %}
      {{ pv_forecast_resterend_vandaag_kwh|float(0) }}
    {% else %}
      {{ pv_forecast_morgen_kwh|float(0) }}
    {% endif %}

  forecast_pv_missing: >
    {{ states('sensor.pv_forecast_tomorrow') in
       ['unknown','unavailable','none','']
       or
       states('sensor.pv_forecast_remaining_today') in
       ['unknown','unavailable','none',''] }}

  pv_confidence: >
    {{ states('input_number.ems_pv_confidence')|float(0.92) }}

# ============================================================
  # VERWACHT RESTEREND VERBRUIK
  # ============================================================

  verbruik_resterend: |
    {% if hour|int >= 18 %}

      {{ [ema_avond|float(0), 0]|max +
         [ema_nacht|float(0), 0]|max }}

    {% elif hour|int >= 12 %}

      {{ [ema_middag|float(0), 0]|max +
         [ema_avond|float(0), 0]|max +
         [ema_nacht|float(0), 0]|max }}

    {% elif hour|int >= 6 %}

      {{ [ema_ochtend|float(0), 0]|max +
         [ema_middag|float(0), 0]|max +
         [ema_avond|float(0), 0]|max +
         [ema_nacht|float(0), 0]|max }}

    {% else %}

      {{ [ema_nacht|float(0), 0]|max }}

    {% endif %}

# ============================================================
  # PREDICTIVE SOC-CORRECTIE
  # ============================================================

  soc_correctie_hoofdloop: |
    {% if forecast_pv_missing %}
      0
    {% else %}
      {{ [[
          ((pv_forecast_relevant_kwh|float(0) *
            pv_confidence|float(0.92))
            -
            verbruik_resterend|float(0))
          *
          soc_per_kwh|float(5),
          15
        ]|min, 0]|max|round(1) }}
    {% endif %}

# ============================================================
  # DYNAMISCHE MINIMUM SOC
  # ============================================================

  soc_min_pct: |
    {% set corr = soc_correctie_hoofdloop|float(0) %}

    {% if is_winter %}

      {% if hour|int >= 18 %}

        {% set nodig =
          (([ema_avond|float(0), 0]|max +
            [ema_nacht|float(0), 0]|max)
           * soc_per_kwh|float(5))
          - corr %}

        {{ [[nodig, 15]|max, 40]|min|round(0)|int }}

      {% elif hour|int >= 12 %}

        {% set nodig =
          (([ema_middag|float(0), 0]|max +
            [ema_avond|float(0), 0]|max)
           * soc_per_kwh|float(5))
          - corr %}

{{ [[nodig, 20]|max, 50]|min|round(0)|int }}

      {% elif hour|int >= 6 %}

        {% set nodig =
          (([ema_ochtend|float(0), 0]|max +
            [ema_middag|float(0), 0]|max)
           * soc_per_kwh|float(5))
          - corr %}

        {{ [[nodig, 15]|max, 40]|min|round(0)|int }}

      {% else %}

        {% set nodig =
          ([ema_nacht|float(0), 0]|max *
           soc_per_kwh|float(5))
          - corr %}

        {{ [[nodig, 15]|max, 40]|min|round(0)|int }}

      {% endif %}

    {% else %}

      {% if hour|int >= 18 %}

        {% set nodig =
          (([ema_avond|float(0), 0]|max +
            [ema_nacht|float(0), 0]|max)
           * soc_per_kwh|float(5))
          - corr %}

{{ [[nodig, 10]|max, 25]|min|round(0)|int }}

      {% elif hour|int >= 12 %}

        {% set nodig =
          (([ema_middag|float(0), 0]|max +
            [ema_avond|float(0), 0]|max)
           * soc_per_kwh|float(5))
          - corr %}

        {{ [[nodig, 10]|max, 30]|min|round(0)|int }}

      {% elif hour|int >= 6 %}

        {% set nodig =
          (([ema_ochtend|float(0), 0]|max +
            [ema_middag|float(0), 0]|max)
           * soc_per_kwh|float(5))
          - corr %}

        {{ [[nodig, 10]|max, 30]|min|round(0)|int }}

      {% else %}

        {% set nodig =
          ([ema_nacht|float(0), 0]|max *
           soc_per_kwh|float(5))
          - corr %}

        {{ [[nodig, 10]|max, 25]|min|round(0)|int }}

      {% endif %}

    {% endif %}

soc_max_charge_pct: 95

  discharge_allowed: >
    {{ soc|float(50) > soc_min_pct|float(15) }}

  charge_allowed: >
    {{ soc|float(50) < soc_max_charge_pct
       and soc|float(50) < 97.5 }}

  • Micronikje
  • Registratie: Januari 2018
  • Laatst online: 18:24
Deel 7A1.5 Laatste deel van deze automatisering.

Simpel gezegd: deze automatisering laadt de batterij via de zonnepanelen, ontlaadt de batterij wanneer nodig, houdt rekening met de seizoenen, de zon, enz. Daarnaast heeft het een koppeling met de volgende automatiseringen die over netladen gaat en een leermodule. Laat weten hoe jullie er naar kijken, wat misschien beter kan, zodat we tot een versie komen die voor iedereen vanaf 2027 goed werkt.
code:
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
#
# ============================================================
  # AVONDRESERVE
  # ============================================================

  soc_piek_vandaag: >
    {{ states('input_number.ems_soc_peak_today')|float(50) }}

  soc_piek_target: >
    {{ states('input_number.ems_soc_peak_target')|float(90) }}

  soc_piek_ondergrens: >
    {{ states('input_number.ems_soc_peak_lower_limit')|float(50) }}

  avond_schaal: >
    {% set bereik =
      soc_piek_target|float(90) -
      soc_piek_ondergrens|float(50) %}

    {% if bereik <= 0 %}
      1.0
    {% else %}
      {{ [[
          (soc_piek_vandaag|float(50) -
           soc_piek_ondergrens|float(50))
          / bereik,
          1.0
        ]|min, 0.0]|max|round(3) }}
    {% endif %}

  avond_uren: 6
  nacht_uren: 6
  ochtend_uren: 6
  middag_uren: 6

avond_target_raw_w: >
    {{ ([ema_avond|float(0), 0]|max *
        1000 / avond_uren)|round(0) }}

  nacht_target_raw_w: >
    {{ ([ema_nacht|float(0), 0]|max *
        1000 / nacht_uren)|round(0) }}

  ochtend_target_raw_w: >
    {{ ([ema_ochtend|float(0), 0]|max *
        1000 / ochtend_uren)|round(0) }}

  middag_target_raw_w: >
    {{ ([ema_middag|float(0), 0]|max *
        1000 / middag_uren)|round(0) }}

  basis_ontlading: |
    {% if discharge_allowed %}

      {% if hour|int >= 18 %}
        {% set target =
          avond_target_raw_w|float(0) *
          avond_schaal|float(1.0) %}

      {% elif hour|int >= 12 %}
        {% set target =
          middag_target_raw_w|float(0) *
          avond_schaal|float(1.0) %}

      {% elif hour|int >= 6 %}
        {% set target =
          ochtend_target_raw_w|float(0) *
          avond_schaal|float(1.0) %}

      {% else %}
        {% set target =
          nacht_target_raw_w|float(0) *
          avond_schaal|float(1.0) %}
      {% endif %}

      {{ [[target,
          real_consumption|float(0)]|min,
          0]|max|round(0)|int }}

    {% else %}
      0
    {% endif %}

# ============================================================
  # START/STOP-LOGICA
  # ============================================================

  stop_discharge_direction: >
    {{ net_power|float(0) <= threshold_off
       or pv_surplus
       or not discharge_allowed }}

  stop_charge_direction: >
    {{ not pv_surplus
       or importing
       or not charge_allowed }}

  strong_export_override: >
    {{ net_power|float(0) < strong_export_threshold }}

  strong_import_override: >
    {{ net_power|float(0) > strong_import_threshold }}

  want_to_stop_from_discharge: |
    {{ current_state == 'DISCHARGING'
       and (
         (
           state_duration_sec|float(0) >=
           min_state_duration_sec
           and stop_discharge_direction
         )
         or strong_export_override
         or not system_active
         or data_missing
         or extreme_soc
       ) }}

  want_to_stop_from_charge: |
    {{ current_state == 'CHARGING'
       and (
         (
           state_duration_sec|float(0) >=
           min_state_duration_sec
           and stop_charge_direction
         )
         or strong_import_override
         or not system_active
         or data_missing
         or extreme_soc
       ) }}

  want_to_discharge: |
    {{ current_state == 'IDLE'
       and not require_cooldown
       and system_active
       and not data_missing
       and not extreme_soc
       and net_power|float(0) > threshold_on
       and not pv_surplus
       and discharge_allowed
       and not net_charging }}

want_to_charge: |
    {{ current_state == 'IDLE'
       and not require_cooldown
       and system_active
       and not data_missing
       and not extreme_soc
       and pv_surplus
       and not importing
       and charge_allowed
       and not net_charging }}

  next_state: |
    {% if not system_active
       or data_missing
       or extreme_soc %}

      IDLE

    {% elif want_to_stop_from_discharge
       or want_to_stop_from_charge %}

      IDLE

    {% elif current_state == 'DISCHARGING' %}

      DISCHARGING

    {% elif current_state == 'CHARGING' %}

      CHARGING

    {% elif want_to_discharge %}

      DISCHARGING

    {% elif want_to_charge %}

      CHARGING

    {% else %}

      IDLE

    {% endif %}

  transitioning_now: >
    {{ current_state != next_state }}

# ============================================================
  # TEMPERATUUR-DERATING
  # ============================================================

  charge_derate: |
    {% if batt_temp >= 10 %}
      1.0
    {% elif batt_temp <= 0 %}
      0.25
    {% else %}
      {{ (0.25 +
          (batt_temp/10)*0.75)|round(2) }}
    {% endif %}

  discharge_derate: |
    {% if batt_temp >= 0 %}
      1.0
    {% elif batt_temp <= -10 %}
      0.25
    {% else %}
      {{ (0.25 +
          ((batt_temp+10)/10)*0.75)|round(2) }}
    {% endif %}

  # ============================================================
  # MAXIMALE BATTERIJVERMOGENS
  # ============================================================

  max_discharge_w: >
    {{ (9000 *
        discharge_derate|float(1))|round(0)|int }}

  max_charge_w: >
    {{ (9000 *
        charge_derate|float(1))|round(0)|int }}

  # ============================================================
  # PID VRAAG
  # ============================================================

  discharge_demand_raw: >
    {% if
      (current_state == 'DISCHARGING'
       or next_state == 'DISCHARGING')
      and discharge_allowed %}

      {% if importing %}

        {{ [[
            pid_output|float(0),
            max_discharge_w|int
          ]|min, 0]|max|round(0)|int }}

      {% elif basis_ontlading|float(0) > 0 %}

        {{ basis_ontlading|float(0)|round(0)|int }}

      {% else %}

        0

      {% endif %}

    {% else %}

      0

    {% endif %}

  charge_demand_raw: |
    {% if next_state == 'CHARGING'
       and pv_surplus
       and charge_allowed %}

      {{ [[
          pid_output_charge|float(0),
          max_charge_w|int
        ]|min, 0]|max|round(0)|int }}

    {% else %}

      0

    {% endif %}

# ============================================================
  # FILTERING / SMOOTHING
  # ============================================================

  alpha_discharge: |
    {% set pv = pv_now|float(0) %}

    {% if pv == 0 %}
      0.95
    {% elif pv < 100 %}
      0.92
    {% elif pv < 500 %}
      0.88
    {% elif pv < 2000 %}
      0.75
    {% else %}
      0.65
    {% endif %}

  alpha_charge_base: |
    {% set pv = pv_now|float(0) %}

    {% if pv == 0 %}
      0.95
    {% elif pv < 100 %}
      0.92
    {% elif pv < 500 %}
      0.88
    {% elif pv < 2000 %}
      0.68
    {% else %}
      0.60
    {% endif %}

  alpha_charge_offset: >
    {{ states('input_number.ems_alpha_charge_offset')|float(0) }}

  alpha_charge: >
    {{ [[
        alpha_charge_base|float(0) +
        alpha_charge_offset|float(0),
        1.0
      ]|min, 0.1]|max }}

  filtered_signal_raw: |
    {% if discharge_demand_raw|float(0) > 0 %}

      {{ (
        alpha_discharge|float(0) *
        discharge_demand_raw|float(0)
        +
        (
          1 - alpha_discharge|float(0)
        ) *
        ([last_power|float(0), 0]|max)
      )|round(0)|int }}

    {% elif charge_demand_raw|float(0) > 0 %}

      {{ (
        alpha_charge|float(0) *
        charge_demand_raw|float(0) * -1
        +
        (
          1 - alpha_charge|float(0)
        ) *
        ([last_power|float(0) * -1, 0]|max)
        * -1
      )|round(0)|int }}

    {% else %}

      0

    {% endif %}

  state_target_discharge: |
    {% if next_state == 'DISCHARGING' %}

      {{ [[
          filtered_signal_raw|float(0),
          max_discharge_w|int
        ]|min, 0]|max|round(0)|int }}

    {% else %}

      0

    {% endif %}

  state_target_charge: |
    {% if next_state == 'CHARGING' %}

      {{ [[
          filtered_signal_raw|float(0) * -1,
          max_charge_w|int
        ]|min, 0]|max|round(0)|int }}

    {% else %}

      0

    {% endif %}

# ============================================================
  # RAMP LIMITER
  # ============================================================

  delta_discharge: >
    {{ state_target_discharge|float(0) -
       current_inverter_discharge|float(0) }}

  delta_charge: >
    {{ state_target_charge|float(0) -
       current_inverter_charge|float(0) }}

  limited_delta_discharge: >
    {{ [[
        delta_discharge|float(0),
        ramp_step_w
      ]|min,
      ramp_step_w * -1
      ]|max|round(0)|int }}

  limited_delta_charge: >
    {{ [[
        delta_charge|float(0),
        ramp_step_w
      ]|min,
      ramp_step_w * -1
      ]|max|round(0)|int }}

  final_discharge_w: >
    {{ [
        current_inverter_discharge|float(0) +
        limited_delta_discharge|float(0),
        0
      ]|max|round(0)|int }}

  final_charge_w: >
    {{ [
        current_inverter_charge|float(0) +
        limited_delta_charge|float(0),
        0
      ]|max|round(0)|int }}

  # ============================================================
  # DEFINITIEVE ACTIE
  # ============================================================

  do_discharge: |
    {{ next_state == 'DISCHARGING'
       and system_active
       and not data_missing
       and not extreme_soc
       and not require_cooldown
       and discharge_allowed
       and not net_charging }}

  do_charge: |
    {{ next_state == 'CHARGING'
       and system_active
       and not data_missing
       and not extreme_soc
       and not require_cooldown
       and charge_allowed
       and not net_charging }}

  should_reset_filters: >
    {{ transitioning_now and next_state == 'IDLE' }}

  filtered_signal_safe: |
    {% if should_reset_filters %}
      0
    {% elif transitioning_now %}
      0
    {% else %}
      {{ filtered_signal_raw }}
    {% endif %}

# ============================================================
  # STATUS / DEBUG
  # ============================================================

  flow_state: |
    {% if not system_active %}
      OVERRIDE
    {% elif data_missing %}
      DATA-MISSING
    {% elif extreme_soc %}
      EXTREME-SOC
    {% elif net_charging %}
      NET-LADEN
    {% elif next_state == 'DISCHARGING' %}
      DISCHARGING
    {% elif next_state == 'CHARGING' %}
      CHARGING
    {% elif pv_surplus %}
      PV-SURPLUS
    {% elif importing %}
      IMPORT
    {% else %}
      IDLE
    {% endif %}

  log_msg: >
    [EMS v40.9] State={{ current_state }}→{{ next_state }} |
    Net={{ net_power }}W |
    PV={{ pv_now }}W |
    SOC={{ soc }}% |
    SOCmin={{ soc_min_pct }}% |
    Flow={{ flow_state }}

  • Jasper79
  • Registratie: December 2015
  • Laatst online: 17:40
Bedankt, heb er nog niet naar gekeken maar top dat je dit doet.

  • Micronikje
  • Registratie: Januari 2018
  • Laatst online: 18:24
Deel 7B.1 de Net-laad module. Daarna volgt de leermodule.
code:
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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
alias: Solis EMS Net-laad Module v42.8 — Onafhankelijke prijslaadkans
description: >-
  v42.8: gebaseerd op v42.7. Prijsbeslissing herontworpen zodat goedkoop tarief
  en significante toekomstige prijsuplift onafhankelijke laadredenen zijn. Een
  normale huidige prijs blokkeert netladen niet langer wanneer de komende
  beschikbare prijzen aantoonbaar hoger liggen. Korte horizons gebruiken alle
  beschikbare toekomstige kwartieren. De minimale prijsuplift heeft een absolute
  ondergrens van 4 cent/kWh en wordt bij een korte horizon iets hoger. De
  overige veiligheids- en EMS-voorwaarden blijven harde voorwaarden.
triggers:
  - trigger: time_pattern
    minutes: /5
actions:
  - if:
      - condition: template
        value_template: '{{ system_active|string|lower == ''true'' }}'
    then:
      - action: system_log.write
        data:
          message: '{{ log_msg }}'
          level: info
      - action: input_text.set_value
        target:
          entity_id: input_text.ems_debug_output_5
        data:
          value: >
            {% set msg = "NET-LAAD:" + (" Override actief" if not system_active
            else " Data ontbreekt" if data_missing else " Hoofdloop actief -
            wacht" if hoofdloop_actief else " Cooldown actief - wacht" if
            cooldown_active else " START " + net_charge_power_w|string + "W @" +
            tarief_nu|round(3)|string + "€ Piek:" +
            max_prijs_relevant|round(3)|string + "€" + " Uplift:" +
            prijs_uplift|round(3)|string + "€" + " SOC:" + soc|string + "->" +
            soc_net_charge_target|string + " PV:" + net_power|int|string + "W
            Drempel:" + pv_surplus_drempel_adaptief|string + "W" + " PV:" +
            pv_forecast_relevant_kwh|round(1)|string + "/" +
            verbruik_baseline_kwh|round(1)|string + "kWh" if net_charge_gewenst
            and not nu_net_charging else " LADEN " + net_charge_power_w|string +
            "W @" + tarief_nu|round(3)|string + "€ SOC:" + soc|string + "->" +
            soc_net_charge_target|string if nu_net_charging and not
            net_charge_stop else " STOP SOC:" + soc|string + "@" +
            tarief_nu|round(3)|string + "€" if nu_net_charging and
            net_charge_stop else " GEEN ACTIE SOC:" + soc|string + " Groep:" +
            tarief_groep_nu|trim + "@" + tarief_nu|round(3)|string + "€
            Goedkoop:" + tarief_goedkoop|string + " Uplift:" +
            prijs_uplift_significant|string + " Laadkans:" +
            prijs_laadkans|string + " Surplus:" + pv_surplus|string) %} {{
            msg[:250] }}
- if:
      - condition: template
        value_template: >-
          {{ (net_charge_gewenst|string|lower == 'true') and
          (nu_net_charging|string|lower == 'false') and
          (system_active|string|lower == 'true') }}
    then:
      - action: input_boolean.turn_on
        target:
          entity_id:
            - input_boolean.ems_net_charging
            - input_boolean.ems_battery_charging
      - action: input_boolean.turn_off
        target:
          entity_id: input_boolean.ems_battery_discharging
      - action: switch.turn_off
        target:
          entity_id: switch.solis_s6_eh1p_rc_force_battery_charge
      - delay:
          seconds: 2
      - action: number.set_value
        target:
          entity_id: number.solis_s6_eh1p_rc_force_battery_charge_power
        data:
          value: '{{ net_charge_power_w }}'
      - delay:
          seconds: 1
      - action: switch.turn_on
        target:
          entity_id: switch.solis_s6_eh1p_rc_force_battery_charge
      - action: switch.turn_off
        target:
          entity_id: switch.solis_s6_eh1p_rc_force_battery_discharge
      - action: system_log.write
        data:
          message: >
            [NET-LAAD START v42.8] {{ net_charge_power_w }}W @ {{ tarief_nu |
            round(4) }}€/kWh (groep:{{ tarief_groep_nu }}) | Goedkoop:{{
            tarief_goedkoop }} | Uplift:{{ prijs_uplift | round(4) }}€ | Uplift
            significant:{{ prijs_uplift_significant }} | Minimum uplift:{{
            minimale_prijs_uplift | round(4) }}€ | Laadkans:{{ prijs_laadkans }}
            | Piek komende 24u:{{ max_prijs_relevant | round(4) }}€ | P75
            toekomst:{{ toekomstige_prijs_p75 | round(4) }}€ | Volledige horizon
            max:{{ max_prijs_venster | round(4) }}€ | Horizon:{{
            kwartier_prijzen_relevant|length }} kwartieren | SOC:{{ soc }}% →
            target:{{ soc_net_charge_target }}% | PV-drempel:{{
            pv_surplus_drempel_adaptief }}W
          level: warning
- if:
      - condition: template
        value_template: >-
          {{ (nu_net_charging|string|lower == 'true') and
          (net_charge_stop|string|lower == 'false') and
          (system_active|string|lower == 'true') }}
    then:
      - action: switch.turn_off
        target:
          entity_id: switch.solis_s6_eh1p_rc_force_battery_charge
      - delay:
          seconds: 1
      - action: number.set_value
        target:
          entity_id: number.solis_s6_eh1p_rc_force_battery_charge_power
        data:
          value: '{{ net_charge_power_w }}'
      - delay:
          seconds: 1
      - action: switch.turn_on
        target:
          entity_id: switch.solis_s6_eh1p_rc_force_battery_charge
      - action: switch.turn_off
        target:
          entity_id: switch.solis_s6_eh1p_rc_force_battery_discharge
  - if:
      - condition: template
        value_template: >-
          {{ (nu_net_charging|string|lower == 'true') and
          (net_charge_stop|string|lower == 'true') and
          (system_active|string|lower == 'true') }}
    then:
      - action: input_boolean.turn_on
        target:
          entity_id: input_boolean.ems_net_charge_cooldown
      - action: input_datetime.set_datetime
        target:
          entity_id: input_datetime.ems_last_net_charge_stop
        data:
          datetime: '{{ now().isoformat() }}'
      - action: input_boolean.turn_off
        target:
          entity_id:
            - input_boolean.ems_net_charging
            - input_boolean.ems_battery_charging
      - action: switch.turn_off
        target:
          entity_id:
            - switch.solis_s6_eh1p_rc_force_battery_charge
            - switch.solis_s6_eh1p_rc_force_battery_discharge
      - action: system_log.write
        data:
          message: >-
            [NET-LAAD STOP v42.8] SOC:{{ soc }}% | Reden:{% if soc|float(50) >=
            soc_net_charge_target|float(20) -%} Target bereikt {%- elif
            tarief_goedkoop|string|lower == 'false'
               and prijs_uplift_significant|string|lower == 'false' -%}
            Geen prijsvoordeel {%- elif pv_surplus|string|lower == 'true' -%}
            PV-overschot (drempel:{{ pv_surplus_drempel_adaptief }}W) {%- elif
            hoofdloop_discharge|string|lower == 'true' -%} Hoofdloop discharge
            {%- else -%} Andere reden {%- endif %} | Goedkoop:{{ tarief_goedkoop
            }} | Uplift:{{ prijs_uplift_significant }} | Laadkans:{{
            prijs_laadkans }}
          level: warning
mode: single
max_exceeded: silent
variables:
  soc_raw: '{{ states(''sensor.solis_s6_eh1p_battery_soc'') }}'
  net_power_raw: '{{ states(''sensor.energiemeter_huidig_stroomverbruik'') }}'
  batt_voltage_raw: '{{ states(''sensor.solis_s6_eh1p_battery_voltage'') }}'
  tarief_nu_raw: |-
    {{ states(
      'sensor.zonneplan_current_quarter_hourly_electricity_tariff'
    ) }}
  forecast_raw: |-
    {{ state_attr(
      'sensor.zonneplan_current_quarter_hourly_electricity_tariff',
      'forecast'
    ) }}
  forecast: |-
    {% if forecast_raw is string %}
      {{ forecast_raw | from_json(default=[]) }}
    {% elif forecast_raw is iterable %}
      {{ forecast_raw }}
    {% else %}
      []
    {% endif %}
  system_active: '{{ states(''input_boolean.ems_system_override'') != ''on'' }}'
  data_missing: |-
    {{ soc_raw in ['unavailable', 'unknown', 'none']
       or tarief_nu_raw in ['unavailable', 'unknown', 'none']
       or forecast | length == 0 }}
  soc: |-
    {% if data_missing %}
      50
    {% else %}
      {{ soc_raw | float(50) }}
    {% endif %}
  net_power: '{{ net_power_raw | float(0) }}'
  tarief_nu: '{{ tarief_nu_raw | float(0) }}'
  batt_voltage: |-
    {% if data_missing %}
      48
    {% else %}
      {{ [batt_voltage_raw | float(400), 100] | max }}
    {% endif %}
  hour: '{{ now().hour | int }}'
  month: '{{ now().month | int }}'
  nu_kwartier_str: >-
    {{ now().strftime('%Y-%m-%dT%H:') }}{{ '%02d' % ((now().minute // 15) * 15)
    }}
  pv_forecast_1u_kwh: |-
    {{ states(
      'sensor.solcast_pv_forecast_voorspelling_volgend_uur'
    ) | float(0) }}
  pv_forecast_now_kwh: |-
    {{ states(
      'sensor.solcast_pv_forecast_resterende_voorspelling_vandaag'
    ) | float(0) }}
  cloud_proxy_pct: |-
    {% if pv_forecast_1u_kwh | float(0) > 0
       and pv_forecast_now_kwh | float(0) > 0 %}
      {% set raw = 100 -
        ((pv_forecast_1u_kwh | float(0)) /
        (pv_forecast_now_kwh | float(0) + 0.01) * 100) %}
      {{ [[raw, 0] | max, 100] | min | int }}
    {% else %}
      50
    {% endif %}
  pv_now_w: |-
    {{ [0, (-1 * net_power)] | max
       if net_power | float(0) < 0 else 0 }}
  soc_batt: '{{ soc | float(50) }}'
  pv_surplus_drempel_adaptief: |-
    {% set drempel = -120 %}
    {% if net_power | float(0) < 0
       and pv_now_w | float(0) < 100
       and pv_forecast_1u_kwh | float(0) < 0.2 %}
      {% set drempel = -50 %}

    {% elif net_power | float(0) < 0
       and pv_now_w | float(0) > 500
       and pv_forecast_1u_kwh | float(0) > 0.4
       and soc_batt | float(50) < 30 %}
      {% set drempel = -180 %}

    {% elif cloud_proxy_pct | float(50) > 70 %}
      {% set drempel = -40 %}

    {% elif net_power | float(0) < 0
       and pv_now_w | float(0) < 50
       and soc_batt | float(50) < 20 %}
      {% set drempel = -60 %}
    {% endif %}
    {{ drempel }}
pv_surplus: |-
    {{ net_power | float(0) <
       pv_surplus_drempel_adaptief | float(-120) }}
  kwartier_prijzen: |
    {% if not (data_missing|string|lower == 'true') %}
      {% set ns = namespace(lijst=[]) %}

      {% set geldig = forecast
           | select('mapping')
           | selectattr('start_date', 'defined')
           | list
           | sort(attribute='start_date') %}

      {% for e in geldig %}
        {% if as_datetime(e.start_date) >= now()
           and (ns.lijst|length) < 300 %}

          {% set prijs =
            (e.price_tax_included.get('amount', 0) | float(0))
            / 10000000 %}

          {% set ns.lijst = ns.lijst + [prijs] %}
        {% endif %}
      {% endfor %}

      {{ ns.lijst }}

    {% else %}
      []
    {% endif %}
  max_prijs_venster: |-
    {{ kwartier_prijzen | max
       if kwartier_prijzen | length > 0 else 0 }}
  kwartier_prijzen_relevant: '{{ kwartier_prijzen[:96] }}'
  relevante_horizon_lengte: '{{ kwartier_prijzen_relevant | length }}'
  max_prijs_relevant: |-
    {{ kwartier_prijzen_relevant | max
       if kwartier_prijzen_relevant | length > 0
       else tarief_nu | float(0) }}
  toekomstige_prijzen: '{{ kwartier_prijzen_relevant[1:] }}'
  toekomstige_horizon_lengte: '{{ toekomstige_prijzen | length }}'
  toekomstige_prijs_p75: |-
    {% if toekomstige_prijzen | length >= 1 %}
      {% set gesorteerd = toekomstige_prijzen | sort %}
      {% set index =
        ((gesorteerd | length - 1) * 0.75)
        | round(0, 'floor') | int %}
      {{ gesorteerd[index] | float(0) }}
    {% else %}
      {{ tarief_nu | float(0) }}
    {% endif %}
  prijs_uplift: |-
    {{ [toekomstige_prijs_p75|float(0) -
        tarief_nu|float(0), 0] | max | round(4) }}
  prijs_p33: |-
    {{ (kwartier_prijzen_relevant | sort)
       [(kwartier_prijzen_relevant|length // 3)]
       if kwartier_prijzen_relevant | length >= 3
       else tarief_nu | float(0) }}
  prijs_p67: |-
    {{ (kwartier_prijzen_relevant | sort)
       [(2 * (kwartier_prijzen_relevant|length) // 3)]
       if kwartier_prijzen_relevant | length >= 3
       else tarief_nu | float(0) }}
  tarief_groep_nu: |-
    {% if tarief_nu | float(0) <= prijs_p33 | float(0) %}
      low
    {% elif tarief_nu | float(0) >= prijs_p67 | float(0) %}
      high
    {% else %}
      normal
    {% endif %}
  drempel_schaal: |-
    {% set fractie =
      relevante_horizon_lengte | float(0) / 96 %}
    {{ 0.75 +
       (0.25 * ([[fractie, 0] | max, 1.0] | min)) }}
  minimale_prijs_uplift: |-
    {% set basis = 0.04 %} {% set horizon_factor =
      1.0 + (0.25 * (1.0 - drempel_schaal|float(0.75))) %}
    {{ (basis * horizon_factor) | round(4) }}
  prijs_uplift_significant: |-
    {{ toekomstige_prijzen | length >= 1
       and prijs_uplift | float(0) >=
       minimale_prijs_uplift | float(0.04) }}
  tarief_goedkoop: '{{ tarief_groep_nu | trim == ''low'' }}'
  prijs_laadkans: |-
    {{ (tarief_goedkoop|string|lower == 'true')
       or
       (prijs_uplift_significant|string|lower == 'true') }}
  soc_min_pct_actueel_raw: '{{ states(''input_number.solis_soc_min_pct_actueel'') }}'
  soc_min_pct_fallback: |-
    {% if month in [11,12,1,2] %}
      {{ 40 if hour >= 14 else 20 }}
    {% else %}
      {{ 25 if hour >= 15 else 15 }}
    {% endif %}
soc_min_pct: |-
    {% if soc_min_pct_actueel_raw in
          ['unavailable', 'unknown', 'none'] %}
      {{ soc_min_pct_fallback | float(20) }}
    {% else %}
      {{ soc_min_pct_actueel_raw | float(20) }}
    {% endif %}
  pv_forecast_morgen_kwh: |-
    {{ states(
      'sensor.solcast_pv_forecast_voorspelling_morgen'
    ) | float(0) }}
  pv_forecast_resterend_vandaag_kwh: |-
    {{ states(
      'sensor.solcast_pv_forecast_resterende_voorspelling_vandaag'
    ) | float(0) }}
  pv_forecast_relevant_kwh: |-
    {{ pv_forecast_resterend_vandaag_kwh | float(0)
       if hour < 10
       else pv_forecast_morgen_kwh | float(0) }}
  forecast_pv_missing: |-
    {{ states(
         'sensor.solcast_pv_forecast_voorspelling_morgen'
       ) in ['unavailable','unknown','none']
       or
       states(
         'sensor.solcast_pv_forecast_resterende_voorspelling_vandaag'
       ) in ['unavailable','unknown','none'] }}
  verbruik_baseline_kwh: |-
    {{ states('input_number.solis_verbruik_baseline_kwh')
       | float(20) }}
  batt_capacity_kwh: 20
  soc_per_kwh: '{{ 100 / batt_capacity_kwh }}'
  pv_balans_kwh: |-
    {{ pv_forecast_relevant_kwh | float(0) -
       verbruik_baseline_kwh | float(20) }}
  soc_correctie_pct_netlaad_raw: |-
    {{ pv_balans_kwh | float(0) *
       soc_per_kwh | float(5) }}
  soc_correctie_pct: |-
    {% if forecast_pv_missing|string|lower == 'true' %}
      0
    {% else %}
      {% set raw =
        soc_correctie_pct_netlaad_raw | float(0) %}

      {% if raw > 20 %}
        20
      {% elif raw < -20 %}
        -20
      {% else %}
        {{ raw | round(1) }}
      {% endif %}
    {% endif %}
  ema_ochtend: |-
    {{ states('input_number.solis_import_ema_ochtend')
       | float(1.5) }}
  ema_middag: |-
    {{ states('input_number.solis_import_ema_middag')
       | float(-4.0) }}
  ema_avond: |-
    {{ states('input_number.solis_import_ema_avond')
       | float(3.5) }}
  ema_nacht: |-
    {{ states('input_number.solis_import_ema_nacht')
       | float(1.0) }}
  dagdeel_import_vooruit_kwh: |-
    {% if hour < 6 %}
      {{ [ema_nacht|float(0), 0]|max }}

    {% elif hour < 12 %}
      {{ [ema_ochtend|float(0), 0]|max +
         [ema_middag|float(0), 0]|max +
         [ema_avond|float(0), 0]|max +
         [ema_nacht|float(0), 0]|max }}

    {% elif hour < 18 %}
      {{ [ema_middag|float(0), 0]|max +
         [ema_avond|float(0), 0]|max +
         [ema_nacht|float(0), 0]|max }}

    {% else %}
      {{ [ema_avond|float(0), 0]|max +
         [ema_nacht|float(0), 0]|max }}
    {% endif %}
  soc_extra_dagdeel_pct: |-
    {% set raw =
      dagdeel_import_vooruit_kwh | float(0) *
      soc_per_kwh | float(5) %}

    {% if raw > 20 %}
      20
    {% elif raw < -20 %}
      -20
    {% else %}
      {{ raw | round(1) }}
    {% endif %}
  soc_net_charge_target: |-
    {% set raw_target =
      soc_min_pct | float(15) +
      15 -
      soc_correctie_pct | float(0) +
      soc_extra_dagdeel_pct | float(0) %}

    {% if raw_target > 80 %}
      80
    {% elif raw_target < soc_min_pct | float(15) %}
      {{ soc_min_pct | float(15) | int }}
    {% else %}
      {{ raw_target | int }}
    {% endif %}
  hoofdloop_discharge: '{{ states(''input_boolean.ems_battery_discharging'') == ''on'' }}'
  hoofdloop_actief: |-
    {{ (pv_surplus|string|lower == 'true')
       or
       (hoofdloop_discharge|string|lower == 'true') }}
  last_stop_timestamp: '{{ states(''input_datetime.ems_last_net_charge_stop'') }}'
  cooldown_active: |-
    {{ last_stop_timestamp not in ['unavailable', 'unknown']
       and
       (as_timestamp(now()) -
       as_timestamp(last_stop_timestamp, 0)) < 900 }}
  nu_net_charging: '{{ states(''input_boolean.ems_net_charging'') == ''on'' }}'
  net_charge_gewenst: |-
    {{ not (data_missing|string|lower == 'true')
       and
       not (hoofdloop_actief|string|lower == 'true')
       and
       (prijs_laadkans|string|lower == 'true')
       and
       soc | float(50) <
       soc_net_charge_target | float(20)
       and
       not (cooldown_active|string|lower == 'true')
       and
       (system_active|string|lower == 'true') }}
  net_charge_stop: |-
    {{ soc | float(50) >=
       soc_net_charge_target | float(20)
       or
       not (prijs_laadkans|string|lower == 'true')
       or
       (pv_surplus|string|lower == 'true')
       or
       (hoofdloop_discharge|string|lower == 'true')
       or
       not (system_active|string|lower == 'true') }}
  net_charge_power_w: |-
    {{ states('input_number.ems_net_charge_power')
       | float(1000) | int }}
  log_msg: >
    [NET-LAAD v42.8  SOC:{{ soc }}% | Tijd:{{ now().strftime('%H:%M:%S') }}|
    Target:{{ soc_net_charge_target }}% (boost:{{ soc_extra_dagdeel_pct }}pp) |
    PV:{{ net_power | int }}W (drempel:{{ pv_surplus_drempel_adaptief }}W,
    cloud-proxy:{{ cloud_proxy_pct }}%) | Tarief:{{ tarief_nu | round(4) }}€
    (groep:{{ tarief_groep_nu }}) | Goedkoop:{{ tarief_goedkoop }} | P75
    toekomst:{{ toekomstige_prijs_p75 | round(4) }}€ | Uplift:{{ prijs_uplift |
    round(4) }}€ | Min-uplift:{{ minimale_prijs_uplift | round(4) }}€ | Uplift
    significant:{{ prijs_uplift_significant }} | Laadkans:{{ prijs_laadkans }} |
    Piek24u:{{ max_prijs_relevant | round(4) }}€ | Volledig:{{ max_prijs_venster
    | round(4) }}€ | Horizon:{{ relevante_horizon_lengte }} | Schaal:{{
    drempel_schaal | round(2) }} | Surplus:{{ pv_surplus }} | Cooldown:{{
    cooldown_active }}

  • Micronikje
  • Registratie: Januari 2018
  • Laatst online: 18:24
Deel 7C.1 De leermodule.

Dit is tevens het laatste deel.
code:
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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
alias: Solis EMS v33 — Dagelijkse Learning
description: >
  Dagelijks om 23:59: berekent RTE over een voortschrijdend 7-dagen venster
  (SOC-drift-gecorrigeerd i.p.v. losse dagratio), past kp_adjustment bij.
  Berekent ook 7-daags EMA van dagverbruik en import per dagdeel.  kan het
  net-laad automatisch optimaliseren op basis van: 
  input_number.solis_kp_adjustment → PD-controller aanpassing
  input_number.solis_verbruik_baseline_kwh → PV-correction target
  input_number.solis_import_ema_* → Load shifting prioriteit
triggers:
  - trigger: time
    at: '23:59:00'
actions:
  - action: input_text.set_value
    target:
      entity_id: input_text.solis_rte_charge_hist
    data:
      value: '{{ charge_hist_str_new }}'
  - action: input_text.set_value
    target:
      entity_id: input_text.solis_rte_discharge_hist
    data:
      value: '{{ discharge_hist_str_new }}'
  - action: input_text.set_value
    target:
      entity_id: input_text.solis_rte_soc_hist
    data:
      value: '{{ soc_hist_str_new }}'
  - action: input_number.set_value
    target:
      entity_id: input_number.solis_rte_avg_7d
    data:
      value: '{{ rte_avg_7d_new | round(2) }}'
  - action: input_number.set_value
    target:
      entity_id: input_number.solis_kp_adjustment
    data:
      value: '{{ kp_adj_new }}'
  - action: input_number.set_value
    target:
      entity_id: input_number.solis_verbruik_baseline_kwh
    data:
      value: '{{ verbruik_baseline_nieuw | round(2) }}'
  - action: input_number.set_value
    target:
      entity_id: input_number.solis_import_ema_ochtend
    data:
      value: '{{ ema_ochtend_nieuw }}'
  - action: input_number.set_value
    target:
      entity_id: input_number.solis_import_ema_middag
    data:
      value: '{{ ema_middag_nieuw }}'
  - action: input_number.set_value
    target:
      entity_id: input_number.solis_import_ema_avond
    data:
      value: '{{ ema_avond_nieuw }}'
  - action: input_number.set_value
    target:
      entity_id: input_number.solis_import_ema_nacht
    data:
      value: '{{ ema_nacht_nieuw }}'
  - action: input_number.set_value
    target:
      entity_id: input_number.solis_verdienste_maand
    data:
      value: >
        {{ (states('input_number.solis_verdienste_maand') | float(0) +
        states('sensor.ems_totale_verdienste_vandaag') | float(0)) | round(2) }}
  - action: input_number.set_value
    target:
      entity_id: input_number.solis_verdienste_jaar
    data:
      value: >
        {{ (states('input_number.solis_verdienste_jaar') | float(0) +
        states('sensor.ems_totale_verdienste_vandaag') | float(0)) | round(2) }}
  - action: system_log.write
    data:
      message: >
        [EMS v33 LEARNING] Charge7d:{{ sum_charge_7d | round(2) }}kWh
        Discharge7d:{{ sum_discharge_7d | round(2) }}kWh SOCdrift:{{ delta_kwh |
        round(2) }}kWh RTE:{{ daily_rte }}% AvgRTE:{{ rte_avg_7d_new | round(2)
        }}% | kp_adj: {{ kp_adj_old }} -> {{ kp_adj_new }} | Baseline:{{
        verbruik_baseline_oud | round(2) }} -> {{ verbruik_baseline_nieuw |
        round(2) }}kWh | EMA O:{{ ema_ochtend_nieuw | round(2) }} M:{{
        ema_middag_nieuw | round(2) }} A:{{ ema_avond_nieuw | round(2) }} N:{{
        ema_nacht_nieuw | round(2) }}
      level: info
mode: single
max_exceeded: silent
variables:
  batt_charge_today: '{{ states(''sensor.solis_s6_eh1p_today_battery_charge_energy'') | float(0) }}'
  batt_discharge_today: >-
    {{ states('sensor.solis_s6_eh1p_today_battery_discharge_energy') | float(0)
    }}
  soc_now: '{{ states(''sensor.solis_s6_eh1p_battery_soc'') | float(50) }}'
  batt_kwh: 20
  _charge_state: '{{ states(''input_text.solis_rte_charge_hist'') }}'
  _discharge_state: '{{ states(''input_text.solis_rte_discharge_hist'') }}'
  _soc_state: '{{ states(''input_text.solis_rte_soc_hist'') }}'
  charge_hist_raw: >-
    {{ [] if (_charge_state in ['unknown','unavailable','none',''] or
    _charge_state is number) 
            else (((_charge_state|string).split(',') | map('float', 0) | list)) }}
  discharge_hist_raw: >-
    {{ [] if (_discharge_state in ['unknown','unavailable','none',''] or
    _discharge_state is number)
            else (((_discharge_state|string).split(',') | map('float', 0) | list)) }}
  soc_hist_raw: >-
    {{ [] if (_soc_state in ['unknown','unavailable','none',''] or _soc_state is
    number)
            else (((_soc_state|string).split(',') | map('float', 0) | list)) }}
  charge_list_new: '{{ (charge_hist_raw + [batt_charge_today])[-7:] }}'
  discharge_list_new: '{{ (discharge_hist_raw + [batt_discharge_today])[-7:] }}'
  soc_list_new: '{{ (soc_hist_raw + [soc_now])[-7:] }}'
  charge_hist_str_new: '{{ charge_list_new | join('','') }}'
  discharge_hist_str_new: '{{ discharge_list_new | join('','') }}'
  soc_hist_str_new: '{{ soc_list_new | join('','') }}'
  venster_dagen: '{{ charge_list_new | length }}'
  sum_charge_7d: '{{ charge_list_new | sum if charge_list_new | length > 0 else 0 }}'
  sum_discharge_7d: '{{ discharge_list_new | sum if discharge_list_new | length > 0 else 0 }}'
  soc_venster_start: '{{ soc_list_new[0] if soc_list_new | length > 0 else soc_now }}'
  delta_kwh: '{{ ((soc_now - soc_venster_start) / 100 * batt_kwh) | round(3) }}'
  daily_rte: |-
    {% if sum_charge_7d | float(0) < 0.5 %}
      {{ states('input_number.solis_rte_avg_7d') | float(92) }}
    {% else %}
      {% set raw_rte = ((sum_discharge_7d | float(0) + delta_kwh | float(0)) / sum_charge_7d | float(1) * 100) %}
      {% if delta_kwh | float(0) < 0 %}
        {% set raw_rte = (sum_discharge_7d | float(0) / (sum_charge_7d | float(1) - delta_kwh | float(0)) * 100) %}
      {% endif %}
      {{ [[raw_rte, 100] | min, 50] | max | round(1) }}
    {% endif %}
  rte_avg_7d_old: '{{ states(''input_number.solis_rte_avg_7d'') | float(92) }}'
  rte_avg_7d_new: '{{ daily_rte | float(rte_avg_7d_old) }}'
  rte_target: 85
  rte_error: '{{ rte_target - rte_avg_7d_new }}'
  kp_correction: '{{ (rte_error * 0.001) | round(4) }}'
  kp_adj_old: '{{ states(''input_number.solis_kp_adjustment'') | float(0) }}'
  kp_adj_new: '{{ [[kp_adj_old + kp_correction, 0.25] | min, -0.25] | max | round(4) }}'
  daily_verbruik: '{{ states(''input_number.solis_dagverbruik_gisteren'') | float(0) }}'
  verbruik_baseline_oud: '{{ states(''input_number.solis_verbruik_baseline_kwh'') | float(20) }}'
  verbruik_baseline_nieuw: |-
    {% if daily_verbruik | float(0) > 2 %}
      {{ ((1/7) * daily_verbruik) + ((6/7) * verbruik_baseline_oud) }}
    {% else %}
      {{ verbruik_baseline_oud }}
    {% endif %}
import_ochtend_dag: '{{ states(''sensor.energie_import_ochtend_dag_totaal'') | float(0) }}'
  export_ochtend_dag: >-
    {{ states('sensor.netto_energie_teruggeleverd_ochtend_vandaag_kwh') |
    float(0) }}
  netto_ochtend: '{{ import_ochtend_dag - export_ochtend_dag }}'
  import_middag_dag: '{{ states(''sensor.energie_import_middag_dag_totaal'') | float(0) }}'
  export_middag_dag: >-
    {{ states('sensor.netto_energie_teruggeleverd_middag_vandaag_kwh') |
    float(0) }}
  netto_middag: '{{ import_middag_dag - export_middag_dag }}'
  import_avond_dag: '{{ states(''sensor.energie_import_avond_dag_totaal'') | float(0) }}'
  export_avond_dag: >-
    {{ states('sensor.netto_energie_teruggeleverd_avond_vandaag_kwh') | float(0)
    }}
  netto_avond: '{{ import_avond_dag - export_avond_dag }}'
  import_nacht_dag: '{{ states(''sensor.energie_import_nacht_dag_totaal'') | float(0) }}'
  export_nacht_dag: >-
    {{ states('sensor.netto_energie_teruggeleverd_nacht_vandaag_kwh') | float(0)
    }}
  netto_nacht: '{{ import_nacht_dag - export_nacht_dag }}'
  ema_ochtend_oud: '{{ states(''input_number.solis_import_ema_ochtend'') | float(1.5) }}'
  ema_middag_oud: '{{ states(''input_number.solis_import_ema_middag'') | float(-4.0) }}'
  ema_avond_oud: '{{ states(''input_number.solis_import_ema_avond'') | float(3.5) }}'
  ema_nacht_oud: '{{ states(''input_number.solis_import_ema_nacht'') | float(1.0) }}'
  ema_ochtend_nieuw: '{{ (((1/7) * netto_ochtend) + ((6/7) * ema_ochtend_oud)) | round(3) }}'
  ema_middag_nieuw: '{{ (((1/7) * netto_middag) + ((6/7) * ema_middag_oud)) | round(3) }}'
  ema_avond_nieuw: '{{ (((1/7) * netto_avond) + ((6/7) * ema_avond_oud)) | round(3) }}'
  ema_nacht_nieuw: '{{ (((1/7) * netto_nacht) + ((6/7) * ema_nacht_oud)) | round(3) }}'
Pagina: 1