Language

Node-RED · price and CO2

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

Install the native integration directly from the Node-RED palette. Four nodes provide the summary, price series, ready-made true/false signals and stable cheapest-hour plans, while the existing import flows remain available as a transparent alternative.

Start free, plan further when needed

The Node-RED flow remains available without an API key for up to 48 hours. For planning 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 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.
Native nodes or a transparent import flow

For new installations, we recommend the native nodes. They handle configuration, errors and secure API-key storage. The flow built from standard nodes remains available below as an understandable alternative.

Important for beginners

The flow checks the HTTP status explicitly. If the API or network fails, the consumer release falls back to false, so an old ON value cannot remain active.

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

With the native node, enter the optional API key once in the shared Energy Price Forecast connection. Node-RED stores it as a credential and does not include it when exporting a flow. Without a key, requests use the free horizon; an entitled key can unlock values such as hours=120.

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

The code applies only to the alternative standard-node flow below. Do not share such an exported flow with a key filled in. The authoritative limit is not the requested URL value but meta.allowed_horizon_hours.

Quick start with the native package

  1. Open Menu → Manage palette → Install in Node-RED.
  2. Search for @backupbattery/node-red-energypriceforecast and install the package.
  3. Add and connect Inject, EPF summary and Debug nodes.
  4. Create an Energy Price Forecast connection in the summary node, select the market and horizon, then deploy.
npm install @backupbattery/node-red-energypriceforecast

Alternatively, run the command in the Node-RED user directory and restart Node-RED afterwards. The package requires Node.js 20 or newer.

EPF summaryWrites the complete response to msg.payload and common automation values to msg.energypriceforecast.
EPF pricesReturns the full price series in msg.payload.entries for charts and custom calculations.
EPF signalRoutes every summary through separate true and false outputs, optionally only when the state changes.
EPF plannerSelects N cheap individual hours in a fixed X-hour block or weekend window and locks the plan against later reshuffling.

Ready-made signals instead of Function code

Place EPF signal directly after the summary node. Output 1 means “condition matched”; output 2 means “condition did not match”. The original API response remains intact.

Inject → EPF summary → EPF signal → allow / block consumer
WindowsThe cheapest, greenest or combined continuous window is active now.
ThresholdsCurrent price or CO2 is above or below your limit, or the combined score reaches your minimum.
Operational safetyCheck data age, wire both states explicitly and route API failures through a Catch node to your chosen safe state.

For start/stop events, the signal node can emit only on state changes. By default its first input establishes the initial state without treating a deploy as a new event.

Cheapest individual hours for heat pumps and EVs

EPF planner processes the output from EPF prices. It can select, for example, the 12 cheapest individual hours in every 24-hour block or four charging hours during the weekend.

Inject → EPF prices → EPF planner → allow / block consumer
N of XSelect N individual cheap hours in a fixed X-hour block. Quarter-hour prices are correctly averaged into real clock hours before ranking.
WeekendPlan from Friday or Saturday until Monday 00:00 local time for flexible charging.
Stable planOnce the complete block is available, the plan is locked. Later forecast updates cannot move a running automation.

Block boundaries automatically use the electricity market's local time zone and remain correct across daylight-saving changes. Enable a persistent Node-RED context store if plans must survive restarts.

Alternative without the package: 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. In addition to debug data, the flow creates a safe Boolean signal under energypriceforecast/consumer_release for a wallbox, relay or further logic.

[
  {
    "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": "1800",
    "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 status = Number(msg.statusCode);\nif (!Number.isFinite(status) || status < 200 || status >= 300) {\n    msg.is_cheapest_window_now = false;\n    msg.automation_signal_fresh = false;\n    msg.error = 'HTTP ' + (Number.isFinite(status) ? status : 'unknown');\n    msg.payload = { error: msg.error };\n    return [null, msg];\n}\nconst 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.automation_signal_fresh = true;\nmsg.error = null;\nreturn [msg, null];",
    "outputs": 2,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 870,
    "y": 120,
    "wires": [["epf_summary_release", "epf_summary_debug"], ["epf_summary_release", "epf_summary_error_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_release",
    "type": "function",
    "z": "epf_summary_tab",
    "name": "Safe consumer release",
    "func": "msg.topic = 'energypriceforecast/consumer_release';\nmsg.payload = msg.automation_signal_fresh === true && msg.is_cheapest_window_now === true;\nreturn msg;",
    "outputs": 1,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 1120,
    "y": 180,
    "wires": [["epf_summary_release_debug"]]
  },
  {
    "id": "epf_summary_release_debug",
    "type": "debug",
    "z": "epf_summary_tab",
    "name": "Release consumer (Boolean)",
    "active": true,
    "tosidebar": true,
    "console": false,
    "tostatus": true,
    "complete": "payload",
    "targetType": "msg",
    "x": 1400,
    "y": 180,
    "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.is_cheapest_window_now = false;\nmsg.automation_signal_fresh = false;\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_release", "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": []
  }
]

For testing, the release is visible in the Debug sidebar. Then connect the Boolean node to your own device or MQTT logic. Test both true and false before connecting a real load.

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": "1800",
    "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 there a native Node-RED integration?

Yes. The official @backupbattery/node-red-energypriceforecast package provides dedicated nodes for the summary and price series. The standard-node flow remains available as an alternative for learning and customization.

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.

First public release

The native integration is published on npm and is being extended step by step with practical automation building blocks. It is intentionally still young, so feedback from real flows, markets and unusual setups will help shape the next releases.

Report bugs and technical suggestions in the GitHub issue tracker. For questions about your setup, contact StrompreisVorhersage@proton.me.

Related: ioBroker and Home Assistant.