Language

Node-RED · price and CO2

Use electricity price and CO2 in Node-RED without unnecessary clicking

For the first step, one importable summary flow is enough. If you want more later, there is also a dedicated price-series path for your own window logic, charts and special cases.

Core logic explained early

The price logic is not “forecast only”. It deliberately combines real market data with forecast values so a flow does not react unnecessarily to pure estimates.

Step 1As soon as official day-ahead exists for a slot, that value is preferred.
Step 2Forecast is used only for the part of the future not yet covered by official day-ahead data.
Why it mattersThe mixed path therefore combines real market data with forecast values only for the still-open part of the horizon.
Why this path makes sense

Node-RED can process JSON APIs directly. So you do not need a special node, only a small flow with Inject, Function, HTTP Request and Debug.

Important for beginners

In the example, the HTTP Request node is configured to return the response directly as a parsed object. There is also a catch path so errors do not disappear silently.

Recommended entry point: summary

Node-RED has its own summary path with current values, best windows and meta fields for limits and API key state. That is the most sensible starting point for first automations.

https://api.energypriceforecast.eu/api/v1/node-red/summary?country=de&hours=48&window_hours=4
Immediately visibleThe example flow writes the most important values directly into msg.*.
Easy to reuseLater you can feed the same fields into switch, dashboard or scheduler logic.
Meta includedmsg.api_key_state, msg.allowed_horizon_hours and more are included directly.

Alternative for your own price logic: node-red/prices

For more advanced flows there is also a second Node-RED path that returns only the price series. It is intended for your own window calculations, charts, thresholds, or combinations with PV, battery and house status.

https://api.energypriceforecast.eu/api/v1/node-red/prices?country=de&hours=48&mode=mixed&resolution=15m
summaryBeginner path with current price, CO2 and already computed windows.
pricesRaw price series for your own flows, own window logic and dashboards.
Important honestyIn 15m mode, forecast is not smoothed. Each forecast hour is simply repeated across four quarter-hour slots.

What the API actually returns

The summary endpoint returns a compact automation view. That is useful for Node-RED because you can use it immediately for simple flows while still keeping enough structure for later extensions.

Part Content Why it matters
flatCurrent price, current CO2, boolean fields for active best windows and remaining minutes.Ready to use in msg.*.
priceCurrent price slot plus best_window and next_full_window as objects.More context for your own flow logic.
co2Current CO2 slot plus best_window and next_full_window.For CO2-oriented decisions.
sourceMetadata about day-ahead and forecast data.Important for interpretation and debugging.
metaAccess and contract information such as API key state, allowed horizon and daily counters.Clean monitoring per user or flow.

If you need a real price series for your own calculations instead of the compact summary, use /api/v1/node-red/prices. The summary intentionally stays compact. The price series is intended for advanced custom flow logic.

Price series for your own flows: node-red/prices

The extra price-series path intentionally returns prices only, without CO2 fields and without precomputed window logic. That is exactly the point: in Node-RED you can decide for yourself how to evaluate or combine the slots with other signals.

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 or intentionally forecast-centered 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 regional grid-fee assumptions fit better.Mandatory for price_mode=retail in Germany.
resolution=15mContinuous quarter-hour series. Forecast hours are repeated across four quarter-hour slots.Charts and uniform slot logic.
resolution=nativeDay-ahead stays quarter-hourly, forecast stays hourly.When you want to keep the original source resolution.
https://api.energypriceforecast.eu/api/v1/node-red/prices?country=de&hours=48&mode=mixed&resolution=15m
https://api.energypriceforecast.eu/api/v1/node-red/prices?country=de&hours=48&mode=mixed&resolution=15m&price_mode=retail&plz=10115

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

Base price or total retail price?

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

summaryStays compact and deliberately 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.

Time horizon and resolution

The examples use hours=48 and, for the summary, window_hours=4. That is a sensible starting point, but not the full product limit.

Time horizonUse hours to define the requested horizon. 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. The authoritative field is always meta.allowed_horizon_hours.
ResolutionDay-ahead can arrive in quarter-hour slots, while forecast and CO2 are usually hourly.

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

Where to put the API key

In the example flow, the API key is set in the Function node. If the string stays empty, the request runs without a key and therefore with the free horizon. With a key you can, for example, test hours=120.

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

In the end, the authoritative value is not the number you ask for in the URL, but what the server actually allows in meta.allowed_horizon_hours.

Quick start: import the summary flow

  1. Click Import in the top right corner of Node-RED.
  2. Paste the complete JSON block below.
  3. Import it and then click Deploy.
  4. Open the debug sidebar and trigger the Inject node once manually.
Enough for the startThe flow intentionally shows the most important fields first instead of hiding them behind a complex control logic.
What you usually changeNormally only country=de in the URL and optionally the API key in the Function node.
What you will see afterwardsmsg.current_price, msg.current_co2_g_kwh, active best windows and the most important meta fields.

Import flow for the summary

Copy the full block and import it into Node-RED. The flow also catches HTTP and parsing errors through a catch path.

[
  {
    "id": "epf_summary_tab",
    "type": "tab",
    "label": "EPF Summary",
    "disabled": false,
    "info": ""
  },
  {
    "id": "epf_summary_inject",
    "type": "inject",
    "z": "epf_summary_tab",
    "name": "Load summary",
    "props": [],
    "repeat": "900",
    "once": true,
    "onceDelay": "1",
    "topic": "",
    "x": 130,
    "y": 120,
    "wires": [["epf_summary_prepare"]]
  },
  {
    "id": "epf_summary_prepare",
    "type": "function",
    "z": "epf_summary_tab",
    "name": "Set API key and URL",
    "func": "const API_KEY = \"\";\\nmsg.url = \"https://api.energypriceforecast.eu/api/v1/node-red/summary?country=de&hours=48&window_hours=4\";\\nmsg.headers = {};\\nif (API_KEY.trim()) {\\n    msg.headers.Authorization = \"Bearer \" + API_KEY.trim();\\n}\\nreturn msg;",
    "outputs": 1,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 390,
    "y": 120,
    "wires": [["epf_summary_http"]]
  },
  {
    "id": "epf_summary_http",
    "type": "http request",
    "z": "epf_summary_tab",
    "name": "Fetch summary",
    "method": "GET",
    "ret": "obj",
    "paytoqs": "ignore",
    "url": "",
    "persist": false,
    "x": 630,
    "y": 120,
    "wires": [["epf_summary_extract"]]
  },
  {
    "id": "epf_summary_extract",
    "type": "function",
    "z": "epf_summary_tab",
    "name": "Expose key fields",
    "func": "const data = msg.payload || {};\\nconst flat = data.flat || {};\\nconst meta = data.meta || {};\\nconst price = data.price || {};\\nconst co2 = data.co2 || {};\\nmsg.current_price = flat.current_price;\\nmsg.current_co2_g_kwh = flat.current_co2_g_kwh;\\nmsg.cheapest_window_start = flat.cheapest_window_start || null;\\nmsg.greenest_window_start = flat.greenest_window_start || null;\\nmsg.is_cheapest_window_now = flat.is_cheapest_window_now === true;\\nmsg.cheapest_window_remaining_minutes = flat.cheapest_window_remaining_minutes ?? null;\\nmsg.next_full_cheapest_window_start = price.next_full_window ? price.next_full_window.start : null;\\nmsg.next_full_greenest_window_start = co2.next_full_window ? co2.next_full_window.start : null;\\nmsg.api_key_state = meta.api_key_state || 'missing';\\nmsg.allowed_horizon_hours = meta.allowed_horizon_hours;\\nmsg.used_horizon_hours = meta.used_horizon_hours;\\nmsg.rate_limit_daily = meta.rate_limit_daily;\\nmsg.used_calls_today = meta.used_calls_today;\\nmsg.error = null;\\nreturn msg;",
    "outputs": 1,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 870,
    "y": 120,
    "wires": [["epf_summary_debug"]]
  },
  {
    "id": "epf_summary_debug",
    "type": "debug",
    "z": "epf_summary_tab",
    "name": "Show summary",
    "active": true,
    "tosidebar": true,
    "console": false,
    "tostatus": false,
    "complete": "true",
    "targetType": "full",
    "x": 1110,
    "y": 120,
    "wires": []
  },
  {
    "id": "epf_summary_catch",
    "type": "catch",
    "z": "epf_summary_tab",
    "name": "Catch errors",
    "scope": null,
    "uncaught": false,
    "x": 140,
    "y": 220,
    "wires": [["epf_summary_error"]]
  },
  {
    "id": "epf_summary_error",
    "type": "function",
    "z": "epf_summary_tab",
    "name": "Make error readable",
    "func": "const message = msg.error && msg.error.message ? msg.error.message : 'Unknown error';\\nmsg.payload = { error: message, source: msg.error && msg.error.source ? msg.error.source : null };\\nreturn msg;",
    "outputs": 1,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 400,
    "y": 220,
    "wires": [["epf_summary_error_debug"]]
  },
  {
    "id": "epf_summary_error_debug",
    "type": "debug",
    "z": "epf_summary_tab",
    "name": "Summary error",
    "active": true,
    "tosidebar": true,
    "console": false,
    "tostatus": true,
    "complete": "payload",
    "targetType": "msg",
    "x": 660,
    "y": 220,
    "wires": []
  }
]

After that, you can replace the Debug node with your own dashboard, notification or scheduler logic.

Advanced: import the price-series flow

If you prefer to calculate things yourself, the second flow is intended for the raw price series. It stores the full slot list in msg.price_entries.

[
  {
    "id": "epf_prices_tab",
    "type": "tab",
    "label": "EPF Prices",
    "disabled": false,
    "info": ""
  },
  {
    "id": "epf_prices_inject",
    "type": "inject",
    "z": "epf_prices_tab",
    "name": "Load price series",
    "props": [],
    "repeat": "900",
    "once": false,
    "onceDelay": "1",
    "topic": "",
    "x": 130,
    "y": 120,
    "wires": [["epf_prices_prepare"]]
  },
  {
    "id": "epf_prices_prepare",
    "type": "function",
    "z": "epf_prices_tab",
    "name": "Set parameters",
    "func": "const API_KEY = \"\";\\nconst BASE_URL = \"https://api.energypriceforecast.eu/api/v1/node-red/prices?country=de&hours=48&mode=mixed&resolution=15m\";\\nconst USE_RETAIL = false;\\nconst PLZ = \"10115\";\\nmsg.url = USE_RETAIL ? BASE_URL + \"&price_mode=retail&plz=\" + encodeURIComponent(PLZ) : BASE_URL;\\nmsg.headers = {};\\nif (API_KEY.trim()) {\\n    msg.headers.Authorization = \"Bearer \" + API_KEY.trim();\\n}\\nreturn msg;",
    "outputs": 1,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 360,
    "y": 120,
    "wires": [["epf_prices_http"]]
  },
  {
    "id": "epf_prices_http",
    "type": "http request",
    "z": "epf_prices_tab",
    "name": "Fetch price series",
    "method": "GET",
    "ret": "obj",
    "paytoqs": "ignore",
    "url": "",
    "persist": false,
    "x": 610,
    "y": 120,
    "wires": [["epf_prices_extract"]]
  },
  {
    "id": "epf_prices_extract",
    "type": "function",
    "z": "epf_prices_tab",
    "name": "Forward series",
    "func": "const data = msg.payload || {};\\nmsg.price_entries = Array.isArray(data.entries) ? data.entries : [];\\nmsg.first_price_slot = msg.price_entries.length ? msg.price_entries[0] : null;\\nmsg.price_series_source = data.source || {};\\nmsg.api_key_state = data.meta && data.meta.api_key_state ? data.meta.api_key_state : 'missing';\\nmsg.allowed_horizon_hours = data.meta ? data.meta.allowed_horizon_hours : null;\\nmsg.series_request = data.request || {};\\nmsg.series_assumptions = data.assumptions || null;\\nmsg.error = null;\\nreturn msg;",
    "outputs": 1,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 860,
    "y": 120,
    "wires": [["epf_prices_debug"]]
  },
  {
    "id": "epf_prices_debug",
    "type": "debug",
    "z": "epf_prices_tab",
    "name": "Show price series",
    "active": true,
    "tosidebar": true,
    "console": false,
    "tostatus": false,
    "complete": "true",
    "targetType": "full",
    "x": 1110,
    "y": 120,
    "wires": []
  }
]

For retail mode in Germany, set USE_RETAIL = true and enter a suitable PLZ postcode in the Function node.

For automations: separate display and execution logic

For dashboards, msg.cheapest_window_start may already be enough. For real control decisions, however, it is often more important to know whether the best window is already active. That is why the summary flow also sets msg.is_cheapest_window_now and msg.cheapest_window_remaining_minutes.

if (msg.is_cheapest_window_now) {
  // enable the consumer now
} else if (msg.next_full_cheapest_window_start) {
  // use the next full window for planning
}

return msg;

Pragmatically, a boolean for “now” is usually more valuable for automations than only a timestamp in the future.

Which message fields you will have afterwards

Field Meaning Typical use
msg.current_priceCurrent price in EUR/kWh.Simple price logic or debug output.
msg.current_co2_g_kwhCurrent CO2 intensity.CO2-oriented control decisions.
msg.is_cheapest_window_nowtrue when the best price window is already active.Direct run decision for consumers.
msg.cheapest_window_remaining_minutesRemaining runtime of the currently best price window.Run a device only as long as the window is still active.
msg.next_full_cheapest_window_startStart time of the next full price window.Planning instead of immediate triggering.
msg.price_entriesFull price series from the price-series path.Your own window logic, charts and special cases.
msg.series_assumptionsRetail assumptions such as postcode, grid-fee source and VAT.Debugging and plausibility checks in total-price mode.
msg.api_key_stateStatus of the used API key.Clean monitoring per user or flow.
msg.allowed_horizon_hoursServer-side allowed maximum horizon.Check whether a key unlocks more.
msg.used_calls_todayRequests already consumed today.Keep an eye on limits.

Important notes

Is this an official Node-RED node?

No. It is intentionally a normal flow built from standard nodes. That makes it easier to understand at the beginning and easier to adapt to your own automations.

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 Node-RED endpoint itself, however, that signal is not yet exposed as its own stable field.

What if I get HTML instead of JSON in the debug pane?

Then the URL most likely points to the wrong path or to a broken intermediate address. For Node-RED you should use the API URLs from this page directly.

Small test phase

There is currently a small test phase for the new Node-RED integration. We are mainly looking for real flows where it becomes visible quickly whether the import examples are understandable and whether the data can be processed cleanly in real automations.

If you actively use Node-RED and want to test this, send a short note with your setup and market to StrompreisVorhersage@proton.me. The first test integrations are still handled manually on purpose.

Related: ioBroker and Home Assistant.