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.
EPF summary,EPF prices,EPF signalandEPF planner- API keys are stored securely in Node-RED's credential store
- up to 48 hours without a key and up to 120 hours with an entitled key
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.
mixed path therefore combines real market data with forecast values only for the still-open part of the horizon.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.
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=4msg.*.msg.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=15msummaryBeginner path with current price, CO2 and already computed windows.pricesRaw price series for your own flows, own window logic and dashboards.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 |
|---|---|---|
flat | Current price, current CO2, boolean fields for active best windows and remaining minutes. | Ready to use in msg.*. |
price | Current price slot plus best_window and next_full_window as objects. | More context for your own flow logic. |
co2 | Current CO2 slot plus best_window and next_full_window. | For CO2-oriented decisions. |
source | Metadata about day-ahead and forecast data. | Important for interpretation and debugging. |
meta | Access 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=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 or intentionally forecast-centered 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 regional grid-fee assumptions fit better. | Mandatory for price_mode=retail in Germany. |
resolution=15m | Continuous quarter-hour series. Forecast hours are 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 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.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.
hours to define the requested horizon. Publicly, we currently communicate up to 120 hours for the price forecast.meta.allowed_horizon_hours.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
- Open
Menu → Manage palette → Installin Node-RED. - Search for
@backupbattery/node-red-energypriceforecastand install the package. - Add and connect
Inject,EPF summaryandDebugnodes. - Create an Energy Price Forecast connection in the summary node, select the market and horizon, then deploy.
npm install @backupbattery/node-red-energypriceforecastAlternatively, 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 consumerCatch 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 consumerBlock 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
- Click
Importin the top right corner of Node-RED. - Paste the complete JSON block below.
- Import it and then click
Deploy. - Open the debug sidebar and trigger the Inject node once manually.
country=de in the URL and optionally the API key in the Function node.msg.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_price | Current price in EUR/kWh. | Simple price logic or debug output. |
msg.current_co2_g_kwh | Current CO2 intensity. | CO2-oriented control decisions. |
msg.is_cheapest_window_now | true when the best price window is already active. | Direct run decision for consumers. |
msg.cheapest_window_remaining_minutes | Remaining runtime of the currently best price window. | Run a device only as long as the window is still active. |
msg.next_full_cheapest_window_start | Start time of the next full price window. | Planning instead of immediate triggering. |
msg.price_entries | Full price series from the price-series path. | Your own window logic, charts and special cases. |
msg.series_assumptions | Retail assumptions such as postcode, grid-fee source and VAT. | Debugging and plausibility checks in total-price mode. |
msg.api_key_state | Status of the used API key. | Clean monitoring per user or flow. |
msg.allowed_horizon_hours | Server-side allowed maximum horizon. | Check whether a key unlocks more. |
msg.used_calls_today | Requests 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.