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.
- dedicated ioBroker endpoint with a compact structure for scripts
- one script writes price, CO2, active best windows and meta fields into states
- optional API key support, for example 120h in the current test phase
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.
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.
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=40_userdata.0.energypriceforecast.api_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=15msummaryBeginner path with current price, CO2 and already computed windows.pricesRaw price series for your own scripts, charts and automation logic.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 |
|---|---|---|
flat | Current price, current CO2, boolean fields for active best windows and remaining minutes. | Ideal for simple states. |
price | Current price slot plus best_window and next_full_window as objects. | More context for your own logic. |
co2 | Current CO2 slot plus best_window and next_full_window. | For CO2-aware automations. |
source | Metadata about day-ahead and forecast data. | Useful for debugging and plausibility checks. |
meta | Access 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=mixed | Day-ahead first, forecast only for future hours not yet covered by official day-ahead data. | Practical default. |
mode=forecast_only | Forecast only, without day-ahead mixing. | Comparison, debugging, or intentionally forecast-only logic. |
price_mode=base | Default mode with base or market price values. | When the relative shape is enough for your automation. |
price_mode=retail | Assumption-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=10115 | Required for Germany in retail mode so grid-fee assumptions can match the area. | Mandatory for price_mode=retail in Germany. |
resolution=15m | Continuous quarter-hour series. Forecast hours are simply repeated across four quarter-hour slots. | Charts and uniform slot logic. |
resolution=native | Day-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.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.
hours to define how far ahead the request should look. Publicly, we currently communicate up to 120 hours for the price forecast.meta.allowed_horizon_hours.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
- Open the JavaScript adapter in ioBroker Admin.
- Create a new JavaScript script.
- Paste the complete block below and save it.
- Run it once manually or wait briefly. New states should appear under
0_userdata.0.energypriceforecast.
COUNTRY and optionally API_KEY.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_price | Current price in EUR/kWh. | Threshold logic or visualization. |
0_userdata.0.energypriceforecast.current_co2_g_kwh | Current CO2 intensity. | CO2-aware automations. |
0_userdata.0.energypriceforecast.cheapest_window_start | Start time of the cheapest future window. | Later charging or heating triggers. |
0_userdata.0.energypriceforecast.greenest_window_start | Start time of the lowest-CO2 future window. | CO2-oriented control. |
0_userdata.0.energypriceforecast.is_cheapest_window_now | true if the best price window is already active. | Direct run/allow logic without your own time comparisons. |
0_userdata.0.energypriceforecast.cheapest_window_remaining_minutes | Remaining 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_start | Start time of the next full future price window. | Planning instead of immediate decisions. |
0_userdata.0.energypriceforecast.api_key_state | Status of the API key in use. | Troubleshooting and checking the key state. |
0_userdata.0.energypriceforecast.allowed_horizon_hours | Server-side maximum horizon currently allowed. | Check whether a key unlocks more. |
0_userdata.0.energypriceforecast.used_calls_today | Requests already used today. | Keep an eye on limits or monitoring. |
0_userdata.0.energypriceforecast.last_error | Last 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.
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.