Last year I had 11 solar panels installed on my roof, and this year I expanded it to a total of 28 solar panels.
When expanding the solar panels, I also bought a Sigenergy Sigenstor (3x9kWh), a 15kW TP inverter, and a gateway.
Since I like automating devices myself, I have disabled Sigenergy's AI system and let the HAEO handle the battery.
HAEO takes all the inputs from the house and creates a forecast like below.

Everyone would have a different setup, but here is the list of main components I have.
- Solar forecast via Open-Meteo Solar Forecast and a custom template that will take into account curtailment of ZonnePlan Powerplay.
- Automation to execute the current desired status of HAEO
- Template to fetch forecasted grid price information
- HA Sigenergy Integration
Setting HAEO
Following the documentation in this HAEO page would be enough to set up almost everything HAEO requires.
But Grid component requires buy and sell prices, so please check Price Information section. After making sure HAEO is ready and working add HAEO Forecast to a dashboard to visualize actions planned. If you are okay with what is being forecasted, add an automation in Home Assistant to apply HAEO's desired state.
Note: I don't have any special Policy, but I have a Charge cost of 0.01 in the battery component to prevent cycling the battery for little gains.
Automation
This automation YAML is auto-generated from Rust code with unit tests, so it's quite verbose. My Sigen device has a garage_ prefix in sensor names; if you don't have it, just remove the prefix by using the input and copy the code.
Price Information
HAEO supports Nord Pool pricing natively, but I have chosen to go with epexpredictor.batzill.com. The predictor uses models to predict the market, but also uses real market data when it is available, so it's the best of both worlds. I have created buy and sell price sensors to parse prediction data and apply Zonneplan's pricing.
sensor:
- platform: rest
resource: "https://epexpredictor.batzill.com/prices_short?hours=120&surcharge=0&taxPercent=0&country=NL&evaluation=false&unit=EUR_PER_KWH&hourly=true&timezone=Europe%2FAmsterdam"
method: GET
unique_id: epex_predict
name: "EPEX Predict"
unit_of_measurement: €/kWh
scan_interval: 3600
value_template: "{{ value_json.t[0] }}"
json_attributes:
- s
- t
template:
- sensor:
- name: "EPEX Sell Price"
unique_id: epex_sell_price
unit_of_measurement: "EUR/kWh"
device_class: monetary
state_class: total
state: >
{% set network = 0.02 %}
{% set peak_mult = 1.1 %}
{% set vat = 1.21 %}
{# Sunrise/sunset as minutes since midnight #}
{% set sr = as_timestamp(state_attr('sun.sun', 'next_rising')) | timestamp_custom('%H', true) | int * 60
+ as_timestamp(state_attr('sun.sun', 'next_rising')) | timestamp_custom('%M', true) | int %}
{% set ss = as_timestamp(state_attr('sun.sun', 'next_setting')) | timestamp_custom('%H', true) | int * 60
+ as_timestamp(state_attr('sun.sun', 'next_setting')) | timestamp_custom('%M', true) | int %}
{% set now_h = now().strftime('%Y-%m-%dT%H') %}
{% set s = state_attr('sensor.epex_predict', 's') or [] %}
{% set t = state_attr('sensor.epex_predict', 't') or [] %}
{% set ns = namespace(v=none) %}
{% for i in range(s | count) %}
{% if (s[i] | timestamp_custom('%Y-%m-%dT%H', true)) == now_h %}
{% set epex = t[i] %}
{% set hour = (s[i] | timestamp_custom('%H', true)) | int %}
{% set minute = (s[i] | timestamp_custom('%M', true)) | int %}
{% set hour_start = hour * 60 + minute %}
{% set hour_end = hour_start + 60 %}
{% set overlap = [0, [hour_end, ss] | min - [hour_start, sr] | max] | max %}
{% set day_frac = overlap / 60 %}
{% if (epex + network) > 0 %}
{% set ns.v = (epex * (day_frac * peak_mult + (1 - day_frac)) * vat + network) | round(4) %}
{% else %}
{% set ns.v = (epex * vat + network) | round(4) %}
{% endif %}
{% endif %}
{% endfor %}
{{ ns.v if ns.v is not none else 'unavailable' }}
attributes:
schedule: >
{% set network = 0.02 %}
{% set peak_mult = 1.1 %}
{% set vat = 1.21 %}
{% set sr = as_timestamp(state_attr('sun.sun', 'next_rising')) | timestamp_custom('%H', true) | int * 60
+ as_timestamp(state_attr('sun.sun', 'next_rising')) | timestamp_custom('%M', true) | int %}
{% set ss = as_timestamp(state_attr('sun.sun', 'next_setting')) | timestamp_custom('%H', true) | int * 60
+ as_timestamp(state_attr('sun.sun', 'next_setting')) | timestamp_custom('%M', true) | int %}
{% set s = state_attr('sensor.epex_predict', 's') or [] %}
{% set t = state_attr('sensor.epex_predict', 't') or [] %}
{% set ns = namespace(d={}) %}
{% for i in range(s | count) %}
{% set epex = t[i] %}
{% set hour = (s[i] | timestamp_custom('%H', true)) | int %}
{% set minute = (s[i] | timestamp_custom('%M', true)) | int %}
{% set hour_start = hour * 60 + minute %}
{% set hour_end = hour_start + 60 %}
{% set overlap = [0, [hour_end, ss] | min - [hour_start, sr] | max] | max %}
{% set day_frac = overlap / 60 %}
{% if (epex + network) > 0 %}
{% set sell = (epex * (day_frac * peak_mult + (1 - day_frac)) * vat + network) | round(4) %}
{% else %}
{% set sell = (epex * vat + network) | round(4) %}
{% endif %}
{% set ns.d = dict(ns.d, **{s[i] | timestamp_local: sell}) %}
{% endfor %}
{{ ns.d }}
forecasts: >
{% set network = 0.02 %}
{% set peak_mult = 1.1 %}
{% set vat = 1.21 %}
{% set sr = as_timestamp(state_attr('sun.sun', 'next_rising')) | timestamp_custom('%H', true) | int * 60
+ as_timestamp(state_attr('sun.sun', 'next_rising')) | timestamp_custom('%M', true) | int %}
{% set ss = as_timestamp(state_attr('sun.sun', 'next_setting')) | timestamp_custom('%H', true) | int * 60
+ as_timestamp(state_attr('sun.sun', 'next_setting')) | timestamp_custom('%M', true) | int %}
{% set s = state_attr('sensor.epex_predict', 's') or [] %}
{% set t = state_attr('sensor.epex_predict', 't') or [] %}
{% set ns = namespace(forecasts=[]) %}
{% for i in range([s|count, t|count]|min) %}
{% set start_ts = s[i] %}
{% if start_ts is number %}
{% set start_iso = (start_ts | int) | timestamp_local('%Y-%m-%dT%H:%M:%S%z') %}
{% else %}
{% set start_iso = start_ts %}
{% endif %}
{% if i + 1 < (s|count) %}
{% set next_ts = s[i+1] %}
{% if next_ts is number %}
{% set end_iso = (next_ts | int) | timestamp_local('%Y-%m-%dT%H:%M:%S%z') %}
{% else %}
{% set end_iso = next_ts %}
{% endif %}
{% else %}
{% if start_ts is number %}
{% set end_iso = ((start_ts | int) + 3600) | timestamp_local('%Y-%m-%dT%H:%M:%S%z') %}
{% else %}
{% set end_iso = start_iso %}
{% endif %}
{% endif %}
{% set hour = (start_ts | timestamp_custom('%H', true)) | int %}
{% set minute = (start_ts | timestamp_custom('%M', true)) | int %}
{% set hour_start = hour * 60 + minute %}
{% set hour_end = hour_start + 60 %}
{% set overlap = [0, [hour_end, ss] | min - [hour_start, sr] | max] | max %}
{% set day_frac = overlap / 60 %}
{% set epex = t[i] %}
{% if (epex + network) > 0 %}
{% set per_kwh = (epex * (day_frac * peak_mult + (1 - day_frac)) * vat + network) | round(4) %}
{% else %}
{% set per_kwh = (epex * vat + network) | round(4) %}
{% endif %}
{% set ns.forecasts = ns.forecasts + [{
"start_time": start_iso,
"end_time": end_iso,
"per_kwh": per_kwh
}] %}
{% endfor %}
{{ ns.forecasts }}
- name: "EPEX Buy Price"
unique_id: epex_buy_price
unit_of_measurement: "EUR/kWh"
device_class: monetary
state_class: total
# State = current hour's buy price
state: >
{% set surcharge = 0.09161 %}
{% set network = 0.02 %}
{% set vat = 1.21 %}
{% set now_h = now().strftime('%Y-%m-%dT%H') %}
{% set s = state_attr('sensor.epex_predict', 's') or [] %}
{% set t = state_attr('sensor.epex_predict', 't') or [] %}
{% set ns = namespace(v=none) %}
{% for i in range(s | count) %}
{% if (s[i] | timestamp_custom('%Y-%m-%dT%H', true)) == now_h %}
{% set ns.v = ((t[i] + surcharge) * vat + network) | round(4) %}
{% endif %}
{% endfor %}
{{ ns.v if ns.v is not none else 'unavailable' }}
attributes:
# Full schedule: { "2026-04-11T14:00:00+02:00": 0.2341, ... }
schedule: >
{% set surcharge = 0.09161 %}
{% set network = 0.02 %}
{% set vat = 1.21 %}
{% set s = state_attr('sensor.epex_predict', 's') or [] %}
{% set t = state_attr('sensor.epex_predict', 't') or [] %}
{% set ns = namespace(d={}) %}
{% for i in range(s | count) %}
{% set buy = ((t[i] + surcharge) * vat + network) | round(4) %}
{% set ns.d = dict(ns.d, **{s[i] | timestamp_local: buy}) %}
{% endfor %}
{{ ns.d }}
forecasts: >
{% set surcharge = 0.09161 %}
{% set network = 0.02 %}
{% set vat = 1.21 %}
{% set s = state_attr('sensor.epex_predict', 's') or [] %}
{% set t = state_attr('sensor.epex_predict', 't') or [] %}
{% set ns = namespace(forecasts=[]) %}
{% for i in range([s|count, t|count]|min) %}
{% set start_ts = s[i] %}
{# convert numeric UNIX timestamp to ISO 8601 with local offset; if already a string, treat as-is #}
{% if start_ts is number %}
{% set start_iso = (start_ts | int) | timestamp_local('%Y-%m-%dT%H:%M:%S%z') %}
{% else %}
{% set start_iso = start_ts %}
{% endif %}
{# determine end_time: use next start timestamp if available, otherwise add one hour #}
{% if i + 1 < (s|count) %}
{% set next_ts = s[i+1] %}
{% if next_ts is number %}
{% set end_iso = (next_ts | int) | timestamp_local('%Y-%m-%dT%H:%M:%S%z') %}
{% else %}
{% set end_iso = next_ts %}
{% endif %}
{% else %}
{% if start_ts is number %}
{% set end_iso = ((start_ts | int) + 3600) | timestamp_local('%Y-%m-%dT%H:%M:%S%z') %}
{% else %}
{% set end_iso = start_iso %}
{% endif %}
{% endif %}
{% set per_kwh = ((t[i] + surcharge) * vat + network) | round(4) %}
{% set ns.forecasts = ns.forecasts + [{
"start_time": start_iso,
"end_time": end_iso,
"per_kwh": per_kwh
}] %}
{% endfor %}
{{ ns.forecasts }}