TjappieW schreef op woensdag 16 september 2026 @ 17:50:
Ik laad en ontlaad via Battery Max Charge Current en Battery Max Discharge Current in zelfconsumptie modus. Het draait deze week als test en ik ben zeer tevreden over de resultaten.
Ik heb samen met Gemini een hele automatisering gemaakt met als doel NOM met o.a. een failsafe ter bescherming van de 3 fases (max current 23A), de accu beschermen tegen onnodige cycli, met PI (nee er is geen D), Deadband en EMS spread via schuifregelaars. Zelfs een digitale noodstop, zodra de switch uitgaat stopt de automasering automatisch.
Ik zal vanavond de automasering, templates en helpers hier delen.
Vandaag versie 4 met Gemini gemaakt, de afgelopen dagen heb ik allemaal kleine tweaks toegepast.
Even een korte samenvatting: woning uit 1962 met 3x25A en gasloos, 3900Wp zonnepanelen (180°), verwarming middels 7 kW pellet kachel, 15 kW Nexus thuisbatterij van Zonneplan, Quooker in de keuken, warm water badkamer via 120l elektrische boiler, koken op keramische kookplaat.
Met deze gegevens ben ik met Gemini aan de slag gegaan. De dongels van ZP zitten nog gewoon in de accu en hij staat in de modus zelfconsumptie. P1 meet ik via HomeWizard.
Sinds afgelopen zondagmiddag draait de automatisering en mijn verbruikskosten in de app van ZP zijn als volgt: zondag +€ 1.95, maandag -€ 0.85, dinsdag + € 0.41 en vandaag - € 0.52.
5 helpers:* input_boolean.nexus_batterij_failsafe
* input_number.nexus_pid_kp (min 0, max 2, step 0.05)
* input_number.nexus_pid_ki (min 0, max 0.2, step 0.005)
* input_number.nexus_em_spread (min 0.05, max 0.5, step 0.01)
* input_number.nexus_pid_deadband (min 0, 250, step 5, unit W)
templates.yaml:# =========================================================================
# 1. SLIMME KWARTIER PLANNER (Met Dynamische Rendementsdrempel / Spread)
# =========================================================================
- sensor:
- name: "Nexus Batterij Kwartier Strategie"
state: >
{% set soc = states('sensor.solis_s6_eh3p10k_h_zp_battery_soc_2') | float(0) %}
{% set current_hour = now().hour %}
{# Haal de zonne-prognose en huidige prijs op #}
{% set zon_prognose = states('sensor.solcast_pv_forecast_voorspelling_vandaag') | float(0) %}
{% set current_price = states('sensor.zonneplan_current_quarter_hourly_electricity_tariff') | float(0) %}
{# Harde SoC Beveiligingsgrenzen #}
{% if soc <= 7.0 or soc >= 95.0 %}
Dynamic
{% else %}
{# NACHT-FAILSAFE: Bij weinig zon 's nachts alvast bijladen tot 60% #}
{% if zon_prognose < 5.0 and current_hour >= 1 and current_hour <= 5 and soc < 60.0 %}
Laden
{# HARDE PRIORITEIT: Altijd volgas laden bij negatieve prijzen #}
{% elif current_price < 0.00 %}
Laden
{% else %}
{# Kwartierprijzen sorteren uit de forecast om de 'Spread' te berekenen #}
{% set raw_prices = state_attr('sensor.zonneplan_current_quarter_hourly_electricity_tariff', 'forecast') | map(attribute='price_tax_included.amount') | list %}
{% if raw_prices | length > 0 %}
{% set first_price = raw_prices[0] | float %}
{% set factor = 10000000.0 if first_price > 1000 else 1.0 %}
{% set prices = raw_prices | map('float') | map('multiply', 1/factor) | list %}
{% set sorted_prices = prices | sort %}
{# Bereken het maximale prijsverschil (Spread) van vandaag #}
{% set max_day_price = sorted_prices[-1] %}
{% set min_day_price = sorted_prices[0] %}
{% set actual_spread = max_day_price - min_day_price %}
{# Haal jouw ingestelde minimale spread op van het dashboard #}
{% set allowed_spread = states('input_number.nexus_em_spread') | float(0.10) %}
{# Als de spread groot genoeg is, mag er gehandeld worden #}
{% if actual_spread >= allowed_spread %}
{% set charge_threshold = sorted_prices[15] if sorted_prices | length >= 16 else sorted_prices[-1] %}
{% set discharge_threshold = sorted_prices[-16] if sorted_prices | length >= 16 else sorted_prices[0] %}
{% if current_price <= charge_threshold %}
Laden
{% elif current_price >= discharge_threshold %}
Ontladen
{% else %}
Dynamic
{% endif %}
{% else %}
Dynamic
{% endif %}
{% else %}
Dynamic
{% endif %}
{% endif %}
{% endif %}
# =========================================================================
# 2. DE REALTIME PI-REGELAAR (Nu met LOEPZUIVER Watt-filter & Deadband)
# =========================================================================
- trigger:
- platform: time_pattern
seconds: "/4"
sensor:
- name: "Nexus Batterij PID Output Amps"
unit_of_measurement: "A"
state: >
{% set soc_state = states('sensor.solis_s6_eh3p10k_h_zp_battery_soc_2') %}
{% set charge_entity = states('number.solis_s6_eh3p10k_h_zp_battery_max_charge_current_2') %}
{% if soc_state in ['unavailable', 'unknown'] or charge_entity in ['unavailable', 'unknown'] %}
0.0
{% else %}
{% set soc = soc_state | float(0) %}
{# Spanningen ophalen voor de 3-fase middeling #}
{% set v1 = states('sensor.p1_meter_spanning_fase_1') | float(230) %}
{% set v2 = states('sensor.p1_meter_spanning_fase_2') | float(230) %}
{% set v3 = states('sensor.p1_meter_spanning_fase_3') | float(230) %}
{% set voltage = (v1 + v2 + v3) / 3 %}
{# LOEPZUIVER INDUCTIEFILTER: We rekenen de vorige Ampère-output eerst om naar Watt #}
{% set raw_power = states('sensor.p1_meter_vermogen') | float(0) %}
{% set last_amps = states('sensor.nexus_batterij_pid_output_amps') | float(0) %}
{% set last_power = last_amps * voltage %}
{% set grid_power = (raw_power + last_power) / 2 %}
{# DYNAMISCHE DEADBAND (Live vanaf je dashboard slider, nu geoptimaliseerd voor 150W) #}
{% set deadband = states('input_number.nexus_pid_deadband') | float(150.0) %}
{% if grid_power | abs < deadband %}
{% set grid_power = 0.0 %}
{% endif %}
{# Live 3-Fase stroommeting ophalen voor hoofdzekeringbewaking #}
{% set l1 = states('sensor.p1_meter_stroom_fase_1') | float(0) %}
{% set l2 = states('sensor.p1_meter_stroom_fase_2') | float(0) %}
{% set l3 = states('sensor.p1_meter_stroom_fase_3') | float(0) %}
{% set max_fuse = 23.0 %}
{% set kp = states('input_number.nexus_pid_kp') | float(0.80) %}
{% set ki = states('input_number.nexus_pid_ki') | float(0.05) %}
{% set error_amps = grid_power / voltage %}
{% set last_integral = states('sensor.nexus_batterij_pid_output_integral_amps') | float(0) %}
{% set p_out = kp * error_amps %}
{% set current_integral = max(min(last_integral + (error_amps * 4), 50), -50) %}
{% set i_out = ki * current_integral %}
{% set pid_output = p_out + i_out %}
{% set final_amps = max(min(pid_output | round(1), 50.0), -50.0) %}
{% if final_amps < 0 and (l1 > max_fuse or l2 > max_fuse or l3 > max_fuse) %}
{% set final_amps = 0.0 %}
{% endif %}
{% if final_amps > 0 and (l1 < (max_fuse * -1) or l2 < (max_fuse * -1) or l3 < (max_fuse * -1)) %}
{% set final_amps = 0.0 %}
{% endif %}
{% if soc <= 7.0 and final_amps > 0 %}
0.0
{% elif soc >= 95.0 and final_amps < 0 %}
0.0
{% else %}
{{ final_amps }}
{% endif %}
{% endif %}
# =========================================================================
# 3. GEHEUGEN-TRACKER VOOR DE PI INTEGRAL COMPONENT
# =========================================================================
- trigger:
- platform: time_pattern
seconds: "/4"
sensor:
- name: "Nexus Batterij PID Output Integral Amps"
unit_of_measurement: "A"
state: >
{% set v1 = states('sensor.p1_meter_spanning_fase_1') | float(230) %}
{% set v2 = states('sensor.p1_meter_spanning_fase_2') | float(230) %}
{% set v3 = states('sensor.p1_meter_spanning_fase_3') | float(230) %}
{% set voltage = (v1 + v2 + v3) / 3 %}
{% set raw_power = states('sensor.p1_meter_vermogen') | float(0) %}
{% set last_amps = states('sensor.nexus_batterij_pid_output_amps') | float(0) %}
{% set last_power = last_amps * voltage %}
{% set grid_power = (raw_power + last_power) / 2 %}
{% set deadband = states('input_number.nexus_pid_deadband') | float(150.0) %}
{% if grid_power | abs < deadband %}
{% set grid_power = 0.0 %}
{% endif %}
{% set error_amps = grid_power / voltage %}
{% set last_integral = states('sensor.nexus_batterij_pid_output_integral_amps') | float(0) %}
{{ max(min(last_integral + (error_amps * 4), 300), -300) }} En de automatisering:alias: 'Batterij: Nexus Solis 15-Min & SoC PI Regeling'
description: >-
Stuurt de Solis aan op basis van Zonneplan kwartiertarieven, SoC
veiligheidsmarges, Dashboard Sliders én een Noodstop helper
triggers:
- entity_id: sensor.nexus_batterij_pid_output_amps
trigger: state
- entity_id: sensor.nexus_batterij_kwartier_strategie
trigger: state
- entity_id: input_boolean.nexus_batterij_failsafe
trigger: state
conditions: []
actions:
- choose:
- conditions:
- condition: state
entity_id: input_boolean.nexus_batterij_failsafe
state: 'off'
sequence:
- target:
entity_id: number.solis_s6_eh3p10k_h_zp_battery_max_charge_current_2
data:
value: 0
action: number.set_value
- target:
entity_id: number.solis_s6_eh3p10k_h_zp_battery_max_discharge_current_2
data:
value: 0
action: number.set_value
- conditions:
- condition: state
entity_id: input_boolean.nexus_batterij_failsafe
state: 'on'
- condition: state
entity_id: sensor.nexus_batterij_kwartier_strategie
state: Laden
- condition: numeric_state
entity_id: sensor.solis_s6_eh3p10k_h_zp_battery_soc_2
below: 95
sequence:
- target:
entity_id: number.solis_s6_eh3p10k_h_zp_battery_max_charge_current_2
data:
value: >-
{% set l1 = states('sensor.p1_meter_stroom_fase_1') | float(0)
%} {% set l2 = states('sensor.p1_meter_stroom_fase_2') |
float(0) %} {% set l3 = states('sensor.p1_meter_stroom_fase_3')
| float(0) %} {% set hoogste_belasting = max(l1, l2, l3) %} {%
set ruimte_per_fase = max(23.0 - hoogste_belasting, 0.0) %} {{
min(ruimte_per_fase * 6, 50.0) | round(1) }}
action: number.set_value
- target:
entity_id: number.solis_s6_eh3p10k_h_zp_battery_max_discharge_current_2
data:
value: 0
action: number.set_value
- conditions:
- condition: state
entity_id: input_boolean.nexus_batterij_failsafe
state: 'on'
- condition: state
entity_id: sensor.nexus_batterij_kwartier_strategie
state: Ontladen
- condition: numeric_state
entity_id: sensor.solis_s6_eh3p10k_h_zp_battery_soc_2
above: 7
sequence:
- target:
entity_id: number.solis_s6_eh3p10k_h_zp_battery_max_discharge_current_2
data:
value: >-
{% set l1 = states('sensor.p1_meter_stroom_fase_1') | float(0)
%} {% set l2 = states('sensor.p1_meter_stroom_fase_2') |
float(0) %} {% set l3 = states('sensor.p1_meter_stroom_fase_3')
| float(0) %} {% set hoogste_export = min(l1, l2, l3) %} {% set
ruimte_per_fase = max(23.0 + hoogste_export, 0.0) %} {{
min(ruimte_per_fase * 6, 50.0) | round(1) }}
action: number.set_value
- target:
entity_id: number.solis_s6_eh3p10k_h_zp_battery_max_charge_current_2
data:
value: 0
action: number.set_value
- conditions:
- condition: state
entity_id: input_boolean.nexus_batterij_failsafe
state: 'on'
- condition: template
value_template: >-
{{ states('sensor.nexus_batterij_kwartier_strategie') == 'Dynamic'
or
states('sensor.solis_s6_eh3p10k_h_zp_battery_soc_2') | float <= 7.0 or
states('sensor.solis_s6_eh3p10k_h_zp_battery_soc_2') | float >= 95.0 }}
sequence:
- choose:
- conditions:
- condition: numeric_state
entity_id: sensor.nexus_batterij_pid_output_amps
above: 0
- condition: numeric_state
entity_id: sensor.solis_s6_eh3p10k_h_zp_battery_soc_2
above: 7
sequence:
- target:
entity_id: >-
number.solis_s6_eh3p10k_h_zp_battery_max_discharge_current_2
data:
value: >-
{{ states('sensor.nexus_batterij_pid_output_amps') |
float }}
action: number.set_value
- target:
entity_id: >-
number.solis_s6_eh3p10k_h_zp_battery_max_charge_current_2
data:
value: 0
action: number.set_value
- conditions:
- condition: numeric_state
entity_id: sensor.nexus_batterij_pid_output_amps
below: 0
- condition: numeric_state
entity_id: sensor.solis_s6_eh3p10k_h_zp_battery_soc_2
below: 95
sequence:
- target:
entity_id: >-
number.solis_s6_eh3p10k_h_zp_battery_max_charge_current_2
data:
value: >-
{{ states('sensor.nexus_batterij_pid_output_amps') |
float | abs }}
action: number.set_value
- target:
entity_id: >-
number.solis_s6_eh3p10k_h_zp_battery_max_discharge_current_2
data:
value: 0
action: number.set_value
default:
- target:
entity_id: number.solis_s6_eh3p10k_h_zp_battery_max_charge_current_2
data:
value: 0
action: number.set_value
- target:
entity_id: number.solis_s6_eh3p10k_h_zp_battery_max_discharge_current_2
data:
value: 0
action: number.set_value
default:
- target:
entity_id: number.solis_s6_eh3p10k_h_zp_battery_max_charge_current_2
data:
value: 0
action: number.set_value
- target:
entity_id: number.solis_s6_eh3p10k_h_zp_battery_max_discharge_current_2
data:
value: 0
action: number.set_value
mode: restartGemini heeft er her en der wat toelichting ingezet i.v.m een aantal aanpassingen.
In verband met de Quooker en de kooplaat hebben we er een filter ingebouwd die er voor zorgt dat het niet zo'n zaagtand situatie wordt. Indien gewenst kan ik de grafieken van de P1, PID en SoC van de afgelopen dagen delen.
Heb je aanvullingen of verbeteringen? Ik hoor het graag! En zoals altijd, gebruik van de code is op eigen risico!