Skip to content

Zonneplan

Jeroen Simonetti edited this page May 31, 2026 · 3 revisions

Zonneplan — Price Fetcher

The zonneplan_prices helper fetches hourly dynamic electricity prices from the Zonneplan API and publishes them in mimirheim's expected JSON format. It is a drop-in replacement for the Nordpool helper for users whose energy supplier is Zonneplan.

Zonneplan is a Dutch energy supplier that publishes dynamic (hourly) all-in consumer prices. No API key is required — authentication uses an email one-time-password (OTP) flow that the daemon handles automatically.

Full auto-generated field reference: Config-Zonneplan

You can configure and enable this helper from the browser UI instead of editing YAML directly — see Helpers/Config-Editor for setup instructions.


What it does

  1. Subscribes to trigger_topic.
  2. On trigger: checks whether a valid access token is available on disk.
    • If no token exists, or the token has expired and cannot be refreshed, the daemon sends a login email to the configured address and begins polling for activation. The user must click the link in the email once.
  3. Fetches the price_per_hour list from the Zonneplan API for the current connection.
  4. Applies the import_formula and export_formula to compute all-in prices per step.
  5. Publishes a JSON array of timestamped steps to output_topic (retained).
  6. Optionally publishes to mimir_trigger_topic to start a mimirheim solve immediately.

The payload format is:

[
  {
    "ts": "2026-05-28T10:00:00+00:00",
    "import_eur_per_kwh": 0.154619,
    "export_eur_per_kwh": 0.0,
    "confidence": 1.0
  }
]

All timestamps are UTC. The confidence value is always 1.0 (Zonneplan publishes confirmed hourly prices, not forecasts).


First-time authentication

Zonneplan uses an email OTP flow. The daemon handles the entire flow automatically:

  1. First run with no token: the daemon logs a WARNING and sends a login email to the configured address. Check your inbox and click the activation link.
  2. Polling: while waiting for the click, each trigger cycle logs a WARNING (Still waiting for Zonneplan activation — click the link...). No manual restart is needed.
  3. After activation: the daemon saves the token to token_file and immediately fetches prices. The activation link is only needed once.
  4. Subsequent runs: the access token is refreshed automatically. A new login email is only sent again if the refresh token itself expires (typically after weeks or months).

The pending-auth state is persisted to a _pending.json file alongside the token file. If the container restarts mid-authentication, the daemon resumes polling the same activation UUID rather than sending a new email.

Token file persistence

The token_file path must be on a persistent volume. If it lives inside the container filesystem, it is lost on every container restart and the email activation must be repeated each time.

When running as a Home Assistant add-on, /config is already a persistent volume — set token_file: /config/zonneplan_token.json.

For plain Docker, mount a host directory and set token_file to a path within it:

# docker-compose.yml (excerpt)
volumes:
  - ./data:/data
# config.yaml
zonneplan:
  token_file: /data/zonneplan_token.json

When to run it

Zonneplan publishes the next day's prices at some point in the afternoon. Running the trigger at 15:00 and again at 17:00 UTC covers the typical publication window reliably:

schedules:
  - "0 15 * * *": mimir/input/tools/prices/trigger
  - "0 17 * * *": mimir/input/tools/prices/trigger

Pricing formulas

The Zonneplan API returns two price values per hour:

  • price — the all-in consumer import price including VAT (EUR/kWh)
  • price_excl_tax — the import price excluding VAT (EUR/kWh)

The import_formula and export_formula are Python expressions evaluated at runtime for each hourly step. Three variables are available:

Variable Type Description
price float All-in import price incl. VAT, EUR/kWh
price_excl_tax float Import price excl. VAT, EUR/kWh
ts datetime Step start time (UTC-aware)

The formula is evaluated directly as Python. The config file is treated as operator-controlled code (like a shell script); do not load it from untrusted sources.

Formula examples

Pass through the all-in price unchanged (default):

zonneplan:
  import_formula: "price"
  export_formula: "price_excl_tax"

Add a fixed per-kWh supplier markup on top of the excl-tax price:

zonneplan:
  import_formula: "price_excl_tax * 1.21 + 0.05"

Time-of-use feed-in credit (lower overnight):

zonneplan:
  export_formula: "price_excl_tax * (0.8 if 7 <= ts.hour <= 22 else 0.5)"

Configuration example

mqtt:
  host: localhost
  port: 1883
  client_id: mimir-zonneplan-prices

zonneplan:
  email: [email protected]
  token_file: /config/zonneplan_token.json
  import_formula: "price"
  export_formula: "price_excl_tax"

mimir_topic_prefix: mimir

signal_mimir: true
# mimir_trigger_topic defaults to: mimir/input/trigger

ha_discovery:
  enabled: true
  device_name: "MIMIRHEIM Zonneplan"

stats_topic: mimirheim-helpers/stats/zonneplan-prices

Derived topics

With mimir_topic_prefix: mimir:

Config field Derived topic
output_topic mimir/input/prices
mimir_trigger_topic mimir/input/trigger

Home Assistant forecast sensor

Set ha_discovery.forecast_sensor: true to publish a sensor entity that shows the current next-step import price and exposes the full forecast array as JSON attributes.

  • State: import_eur_per_kwh of the first (nearest-future) step, rounded to 4 decimal places
  • Unit: EUR/kWh — no HA device_class (non-standard unit)
  • Attributes: forecast key containing the full array. Each step: import (EUR/kWh, 4 dp), export (EUR/kWh, 4 dp), c (confidence, 2 dp), ts.
  • Enabled by default: true
ha_discovery:
  enabled: true
  forecast_sensor: true

Example apexcharts-card transform to plot import price over time:

- entity: sensor.zonneplan_prices_forecast
  attribute: forecast
  transform: "return x.map(s => [new Date(s.ts).getTime(), s.import])"

Clone this wiki locally