Language

ioBroker · price and CO2

Use electricity price and CO2 in ioBroker without building a custom adapter

If you already use the ioBroker JavaScript adapter, one script is enough. It polls the API every 30 minutes by default and writes simple states under 0_userdata.0.

Start free, plan further when needed

The ioBroker scripts work without an API key for up to 48 hours. For charging, storage or heat-pump logic across several days, you can try Private Pro with up to 120 hours free for 14 days.

Core logic explained early

The price logic is not “forecast only”. It deliberately works in two steps so that real market data wins whenever it exists.

Step 1As soon as official day-ahead exists for a slot, that value is preferred.
Step 2Forecast is used only for future slots outside the already available day-ahead coverage.
Why this mattersThe API therefore combines real day-ahead prices with forecast values only for the still-open part of the horizon.
The pragmatic path

For most setups, a small JavaScript script is more useful than a dedicated adapter. You see real states immediately, can visualize them, and can reuse the same data in your own automation logic later.

Requirement

You need the JavaScript adapter in ioBroker. The example uses the adapter's built-in axios plus standard functions such as createState and setState.

Recommended endpoint

ioBroker has its own summary path. It returns the compact fields that are usually enough for first automations, plus meta fields for limits and API key status.

https://api.energypriceforecast.eu/api/v1/iobroker/summary?country=de&hours=48&window_hours=4
Directly usablePrice, CO2 and window data arrive as simple JSON.
Own statesThe script writes values under 0_userdata.0.energypriceforecast.
Meta includedapi_key_state, allowed horizon and daily counters are written as states as well.

Alternative for custom price logic

If you do not want to work with the compact summary, there is also a second ioBroker path that returns only the price series. That path is meant for more advanced users who want to build their own window logic, charts, or pricing rules in JavaScript.

https://api.energypriceforecast.eu/api/v1/iobroker/prices?country=de&hours=48&resolution=15m
summaryBeginner path with current price, CO2 and already computed windows.
pricesRaw price series for your own scripts, charts and automation logic.
Important honestyDay-ahead stays native 15-minute. In 15m mode, forecast is not smoothed but simply repeated across four quarter-hour slots.

What the API actually returns

The endpoint returns a general automation summary. That is useful for ioBroker because you are not locked into one special adapter and can reuse the same data in scripts, dashboards or custom logic.

Part Content Why it matters
flatCurrent price, current CO2, boolean fields for active best windows and remaining minutes.Ideal for simple states.
priceCurrent price slot plus best_window and next_full_window as objects.More context for your own logic.
co2Current CO2 slot plus best_window and next_full_window.For CO2-aware automations.
sourceMetadata about day-ahead and forecast data.Useful for debugging and plausibility checks.
metaAccess and contract information such as allowed horizon, API key status and daily usage.Useful for tracking by user or integration.

If you need a real price series for your own calculations instead of the compact summary, use the separate /api/v1/iobroker/prices path. The summary intentionally stays on compact base-price data. The price-series path also returns base prices by default, but can now optionally return assumption-based retail totals.

Price series for your own scripts: iobroker/prices

The extra price-series path intentionally returns prices only, without CO2 fields and without precomputed window logic. That is exactly the point: you can decide inside ioBroker how you want to compute cheap windows, thresholds, or visualizations.

Parameter Meaning Typical use
mode=mixedDay-ahead first, forecast only for future hours not yet covered by official day-ahead data.Practical default.
mode=forecast_onlyForecast only, without day-ahead mixing.Comparison, debugging, or intentionally forecast-only logic.
price_mode=baseDefault mode with base or market price values.When the relative shape is enough for your automation.
price_mode=retailAssumption-based total retail price with markups, grid fees and VAT for supported markets.When you want to automate closer to an end-customer price.
plz=10115Required for Germany in retail mode so grid-fee assumptions can match the area.Mandatory for price_mode=retail in Germany.
resolution=15mContinuous quarter-hour series. Forecast hours are simply repeated across four quarter-hour slots.Charts and uniform slot logic.
resolution=nativeDay-ahead stays quarter-hourly, forecast stays hourly.When you want to preserve the original source resolution.
https://api.energypriceforecast.eu/api/v1/iobroker/prices?country=de&hours=48&mode=mixed&resolution=15m
https://api.energypriceforecast.eu/api/v1/iobroker/prices?country=de&hours=48&mode=mixed&resolution=15m&price_mode=retail&plz=10115

Important: in 15m mode, forecast values are not smoothed between two hours. The API does not invent intermediate prices. Instead, it clearly marks that these quarter-hour slots come from one hourly forecast block.

Base price or total retail price?

The ioBroker summary path still deliberately returns base or market prices. The prices path also returns base prices by default, but can now optionally output assumption-based retail totals.

summaryStays compact and remains on base prices.
prices with price_mode=retailCan return an optional 15-minute or native series as an assumption-based total retail price.
Important honestyRetail is not your exact bill amount. It is a modeled end price based on assumptions. In Germany, retail mode requires a postcode. Supported markets are currently DE, NL, DK1, DK2, AT and NO1 to NO5.

Time horizon and resolution

The example uses hours=48 and window_hours=4. That is a sensible starting point, but not the only possible setup.

Time horizonUse hours to define how far ahead the request should look. Publicly, we currently communicate up to 120 hours for the price forecast.
Actually allowedWithout an API key, 48 hours are currently free. A key may allow more, but more than 120 hours should not be advertised as a reliable public promise right now. The authoritative field is always meta.allowed_horizon_hours.
ResolutionDay-ahead can arrive in quarter-hour slots, while forecast and CO2 are usually hourly.

Important: source.price.day_ahead_entries and source.price.forecast_entries count slots, not hours. That is why values such as 97 day-ahead and 0 forecast can be perfectly valid.

Quick start for copy and paste

  1. Open the JavaScript adapter in ioBroker Admin.
  2. Create a new JavaScript script.
  3. Paste the complete block below and save it.
  4. Run it once manually or wait briefly. New states should appear under 0_userdata.0.energypriceforecast.
Built for beginnersThe script creates the required states by itself. You do not need to prepare data points manually first.
What you usually changeNormally only COUNTRY and optionally API_KEY.
What you should see afterwardsCurrent price, current CO2, active best windows, next full future windows and the most important API meta fields.

Ready-to-use script for the JavaScript adapter

The script fetches data immediately at startup and then every 30 minutes by default. That is a sensible default for many setups: day-ahead data does not change constantly, but the current slot, active windows and forecast context still move forward. If you do not have an API key, just leave API_KEY empty.

const axios = require("axios");

const COUNTRY = "de";
const API_KEY = "";
const API_URL = "https://api.energypriceforecast.eu/api/v1/iobroker/summary?country=" + COUNTRY + "&hours=48&window_hours=4";
const ROOT = "0_userdata.0.energypriceforecast";
const POLL_MS = 30 * 60 * 1000;

const STATE_DEFINITIONS = [
  [".current_price", 0, { name: "Current price", type: "number", role: "value", unit: "EUR/kWh", read: true, write: false }],
  [".current_co2_g_kwh", 0, { name: "Current CO2 intensity", type: "number", role: "value", unit: "gCO2/kWh", read: true, write: false }],
  [".cheapest_window_start", "", { name: "Cheapest window start", type: "string", role: "text", read: true, write: false }],
  [".greenest_window_start", "", { name: "Greenest window start", type: "string", role: "text", read: true, write: false }],
  [".is_cheapest_window_now", false, { name: "Best price window active now", type: "boolean", role: "indicator", read: true, write: false }],
  [".cheapest_window_remaining_minutes", 0, { name: "Best price window remaining minutes", type: "number", role: "value.interval", unit: "min", read: true, write: false }],
  [".next_full_cheapest_window_start", "", { name: "Next full price window", type: "string", role: "text", read: true, write: false }],
  [".is_greenest_window_now", false, { name: "Best CO2 window active now", type: "boolean", role: "indicator", read: true, write: false }],
  [".api_key_state", "missing", { name: "API key state", type: "string", role: "text", read: true, write: false }],
  [".allowed_horizon_hours", 0, { name: "Allowed horizon", type: "number", role: "value", unit: "h", read: true, write: false }],
  [".used_horizon_hours", 0, { name: "Used horizon", type: "number", role: "value", unit: "h", read: true, write: false }],
  [".rate_limit_daily", 0, { name: "Daily limit", type: "number", role: "value", read: true, write: false }],
  [".used_calls_today", 0, { name: "Used calls today", type: "number", role: "value", read: true, write: false }],
  [".last_update", "", { name: "Last update", type: "string", role: "text", read: true, write: false }],
  [".last_error", "", { name: "Last error", type: "string", role: "text", read: true, write: false }]
];

STATE_DEFINITIONS.forEach(([suffix, initialValue, definition]) => {
  createState(ROOT + suffix, initialValue, definition);
});

function setTextState(suffix, value) {
  setState(ROOT + suffix, value == null ? "" : String(value), true);
}

function setNumberState(suffix, value, digits) {
  if (!Number.isFinite(value)) {
    return;
  }

  const normalized = typeof digits === "number" ? Number(value.toFixed(digits)) : Number(value);
  setState(ROOT + suffix, normalized, true);
}

function setBooleanState(suffix, value) {
  if (typeof value !== "boolean") {
    return;
  }

  setState(ROOT + suffix, value, true);
}

async function updateEnergyPriceForecast() {
  const headers = {};

  if (API_KEY.trim()) {
    headers.Authorization = "Bearer " + API_KEY.trim();
  }

  try {
    const response = await axios.get(API_URL, {
      timeout: 10000,
      headers
    });

    const data = response.data || {};
    const flat = data.flat || {};
    const meta = data.meta || {};

    setNumberState(".current_price", flat.current_price, 4);
    setNumberState(".current_co2_g_kwh", flat.current_co2_g_kwh, 1);
    setTextState(".cheapest_window_start", flat.cheapest_window_start);
    setTextState(".greenest_window_start", flat.greenest_window_start);
    setBooleanState(".is_cheapest_window_now", flat.is_cheapest_window_now);
    setNumberState(".cheapest_window_remaining_minutes", flat.cheapest_window_remaining_minutes);
    setTextState(".next_full_cheapest_window_start", data.price && data.price.next_full_window ? data.price.next_full_window.start : null);
    setBooleanState(".is_greenest_window_now", flat.is_greenest_window_now);
    setTextState(".api_key_state", meta.api_key_state || (API_KEY.trim() ? "unknown" : "missing"));
    setNumberState(".allowed_horizon_hours", meta.allowed_horizon_hours);
    setNumberState(".used_horizon_hours", meta.used_horizon_hours);
    setNumberState(".rate_limit_daily", meta.rate_limit_daily);
    setNumberState(".used_calls_today", meta.used_calls_today);
    setTextState(".last_update", data.generated_at);
    setTextState(".last_error", "");
  } catch (error) {
    const statusCode = error && error.response ? error.response.status : null;
    const errorData = error && error.response ? error.response.data || {} : {};
    const errorMeta = errorData.meta || {};

    setTextState(".api_key_state", errorMeta.api_key_state || (API_KEY.trim() ? "error" : "missing"));

    if (statusCode === 401) {
      setTextState(".last_error", "401: API key missing, invalid, or disabled.");
    } else if (statusCode === 403) {
      setTextState(".last_error", "403: Access is not allowed for this route, market, or contract state.");
    } else if (statusCode === 429) {
      setTextState(".last_error", "429: Daily limit reached.");
    } else {
      setTextState(".last_error", "Request failed.");
    }

    console.error("EnergyPriceForecast error:", statusCode, error && error.message ? error.message : error);
  }
}

updateEnergyPriceForecast();
setInterval(updateEnergyPriceForecast, POLL_MS);

If you want another market, normally only COUNTRY changes. For Denmark, always use dk1 or dk2. If you intentionally want more frequent polling, change POLL_MS yourself. The default is deliberately conservative.

Where to put the API key

The key goes directly into the API_KEY variable. If you have received a test key, you can also raise the requested horizon in the URL, for example to 120 hours.

const API_KEY = "YOUR_TEST_KEY";
const API_URL = "https://api.energypriceforecast.eu/api/v1/iobroker/summary?country=de&hours=120&window_hours=4";

Important: the final authority is not only the URL, but meta.allowed_horizon_hours. That field tells you what your current access is actually allowed to use server-side.

For automations: separate "run now" from "plan later"

A common mistake is to look only at cheapest_window_start. That is enough for display, but often too rough for real control. With is_cheapest_window_now, you can directly detect whether the best price window is already active. With next_full_cheapest_window_start, you can plan the next full future window.

on({ id: ROOT + ".is_cheapest_window_now", change: "any" }, obj => {
  if (obj.state.val === true) {
    log("Best price window is active now.");
    // enable the wallbox or start EV charging
  }
});

Pragmatically speaking: for EV charging, heat pumps, or batteries, a boolean for "now" is more robust than comparing one future timestamp.

Which states the script creates

State Meaning Typical use
0_userdata.0.energypriceforecast.current_priceCurrent price in EUR/kWh.Threshold logic or visualization.
0_userdata.0.energypriceforecast.current_co2_g_kwhCurrent CO2 intensity.CO2-aware automations.
0_userdata.0.energypriceforecast.cheapest_window_startStart time of the cheapest future window.Later charging or heating triggers.
0_userdata.0.energypriceforecast.greenest_window_startStart time of the lowest-CO2 future window.CO2-oriented control.
0_userdata.0.energypriceforecast.is_cheapest_window_nowtrue if the best price window is already active.Direct run/allow logic without your own time comparisons.
0_userdata.0.energypriceforecast.cheapest_window_remaining_minutesRemaining minutes of the currently best price window.Keep devices running only while the window is still active.
0_userdata.0.energypriceforecast.next_full_cheapest_window_startStart time of the next full future price window.Planning instead of immediate decisions.
0_userdata.0.energypriceforecast.api_key_stateStatus of the API key in use.Troubleshooting and checking the key state.
0_userdata.0.energypriceforecast.allowed_horizon_hoursServer-side maximum horizon currently allowed.Check whether a key unlocks more.
0_userdata.0.energypriceforecast.used_calls_todayRequests already used today.Keep an eye on limits or monitoring.
0_userdata.0.energypriceforecast.last_errorLast request-related error message.Troubleshooting without jumping directly into adapter logs.

Errors and limits

What does 401 mean?

The API key is missing, wrong, or disabled. The script writes both api_key_state and a readable error message for exactly that reason.

What does 403 mean?

The request is technically valid, but not allowed for this access mode. That is usually about route, market, or contract state rather than a simple typo.

What does 429 mean?

The daily limit has been reached. That is exactly why rate_limit_daily and used_calls_today exist.

How often should I poll?

We recommend 30 minutes by default here. For many setups that is enough because day-ahead data does not change all the time. At the same time, the current slot and window status still stay fresh enough. If you intentionally want to react closer to quarter-hour slots, you can switch polling to 15 minutes yourself.

Is there already a warning for extreme hours?

Yes. The product already has a warning for possible extreme hours, for example in the app or web app via the Tail-Beta or extreme-path signal. In the ioBroker endpoint itself, however, that signal is not yet exposed as its own stable field.

Adapt it to your ioBroker setup with AI

This prompt is for custom JavaScript logic. It makes the AI establish whether the compact summary is enough or whether you really need the complete price series for custom windows, charts or thresholds.

Protect your API key

Never give an AI your real API key. Keep YOUR_API_KEY in the script and insert the key locally in the ioBroker JavaScript adapter.

Show the copyable prompt
Help me build a robust ioBroker JavaScript-adapter script using the Energy Price Forecast EU API.

Read the API contract first:
https://energypriceforecast.eu/openapi/integration-api.json

Do not invent API fields. In mixed mode, official day-ahead prices are used first and forecast values only extend the remaining open horizon.

Before writing code, ask me no more than these questions, one at a time:
1. Country or bidding zone?
2. Which load and which ioBroker object ID should be controlled?
3. Do I need only current values and ready-made windows, or a complete price series for custom logic?
4. Power, required runtime and latest completion time?
5. Optimise for price, CO2 or both?
6. Free without a key and up to 48 hours, or Private Pro with up to 120 hours?
7. Base/market price or assumption-based retail price? Ask for the postal code for German retail pricing.

API selection:
- Simple automation: /api/v1/iobroker/summary
- Custom price logic or charts: /api/v1/iobroker/prices?mode=mixed&resolution=15m
- Free: hours=48 and no Authorization header.
- Private Pro: hours=120 and header Authorization: Bearer YOUR_API_KEY.
- Never ask me to paste the real key into the chat.

Script rules:
- Use axios in the JavaScript adapter, a timeout and a 30-minute fetch interval.
- Create readable states below 0_userdata.0.energypriceforecast and update them deterministically.
- Handle HTTP errors, empty responses and timestamps; check meta.api_key_state and meta.allowed_horizon_hours.
- With resolution=15m, native day-ahead quarter-hours stay native; hourly forecast values are repeated four times without invented interpolation.
- Explain every field used and every assumption.
- Start with a test mode that only updates states or sends a message. Put real load switching in a separate, clearly marked step.
- An API error must never switch the load on unexpectedly.
- Finish with a checklist for HTTP 200, plausible states, allowed horizon and test triggering.

Ask when information is missing. Do not silently assume object IDs, tariffs or electrical limits.

Machine-readable foundation: OpenAPI contract for the integration endpoints.

Small test phase

A small test phase is currently running for the new ioBroker path. The most useful feedback comes from real setups, not theory alone. The key questions are: Is the setup understandable, do the values look plausible, and are the error states clear enough?

If you want to test this in a real ioBroker setup, send a short note with your setup, market and rough goal to StrompreisVorhersage@proton.me. The first integrations are still intentionally handled manually.

Related: Home Assistant and evcc.