node-red-contrib-nordpool-chargecheap 1.0.2-rc.1
Nordpool price analyzer with smart night/day, rolling 24h and HA override integration for Node-RED
node-red-contrib-nordpool-chargecheap
A Node-RED node for analyzing Nordpool electricity prices and automatically selecting the cheapest (or most expensive) time periods for charging, discharging, load shifting or other automation purposes.
โ Key Features
- Selects the cheapest or most expensive price intervals within a configurable time window.
- Two selection strategies:
- Discrete selection: pick the N best (cheap) or worst (expensive) intervals.
- Contiguous block mode: find one continuous block of length N with lowest (cheap) or highest (expensive) average price.
- Supports overnight windows (start > stop) and rolling 24h mode (start == stop).
- Rolling 24h mode provides a dynamic 24h window anchored to the most recent occurrence of the chosen start hour.
- Home Assistant integration via service-style payload (e.g.
input_number.set_value). - Dynamic runtime override via incoming
msg.start,msg.stop,msg.count. - Automatic unit normalization (รre / SEK / EUR โ รre).
- HA override (
msg.ha_enable="off") keeps calculating selection but sends a fixed fallback (force_value) to HA. - Full context reset using
msg.reset. - Detects interval length (15 / 30 / 60 minutes or other).
- Rich diagnostic attributes (block averages, reference thresholds, data completeness).
- Inverted mode for high-price alerting or battery discharge strategy.
- Additional semantic attributes clarifying override mode, selection purpose and next effective reference.
- Slot alignment logic: If you select e.g.
start=23,stop=0with 15-minute slots, you will get the slots starting at 23:00, 23:15, 23:30, and 23:45 (not slots starting at 00:00 or later). This ensures interval selection is intuitive and matches human expectations.
๐ง Selection Logic Summary
| Mode | What is selected | reference_price meaning |
reference_price_role |
|---|---|---|---|
| Cheap (default) | Lowest priced intervals | Highest price among selected cheap intervals (upper cheap boundary) | upper_bound_for_charging |
| Expensive (invert) | Highest priced intervals | Lowest price among selected expensive intervals (lower expensive boundary) | lower_bound_for_discharging |
Why this reference logic?
- In cheap mode, the selected cheap set forms a โprice corridorโ โค
reference_price. The reference becomes a ceiling you still accept for charging. - In expensive mode, the selected expensive set forms a corridor โฅ
reference_price. The reference becomes a floor beyond which discharging / shedding may occur.
You also get reference_price_numeric (pure number) and reference_price_effective (null during HA override).
In contiguous block mode the node finds the single block (length = count) with:
- Lowest average (cheap mode)
- Highest average (expensive mode)
๐ Rolling 24h vs Normal Periods
- Normal period (start != stop):
- If
start < stop: same-day window (e.g. 07โ15). - If
start > stop: overnight window crossing midnight (e.g. 22โ06).
- If
- Rolling 24h (start == stop):
- A 24h dynamic span beginning at the most recent occurrence of that hour.
- If current time is earlier than the start hour, the window anchors to yesterdayโs occurrence.
- Example: At 21:30 with start=16 (rolling) โ window = today 16:00 โ tomorrow 15:59:59.
- Attribute:
rolling_24h = on.
โ๏ธ Node Configuration (Editor Fields)
| Field | Description |
|---|---|
| Name | Display name for the node |
| Start hour | Start of selection window (0โ23) |
| Stop hour | End of selection window (0โ23); if same as start โ rolling 24h |
| Count | Number of intervals to select (N price slots) |
| Invert selection | Choose expensive instead of cheap intervals |
| Contiguous block mode | Select one continuous block instead of distinct intervals |
| Payload ON | Output 1 payload when current time is inside a selected slot |
| Payload OFF | Output 2 payload when outside/idle |
| Force value outside period | Value pushed to HA when not active or under override |
| Home Assistant entity | HA entity ID (e.g. input_number.elpris) |
| Debug | Enables verbose node.debug logs |
๐ Outputs (4)
| Output | Purpose | Example Payload |
|---|---|---|
| 1 | Active slot indicator (ON) | "on" (configurable) |
| 2 | Inactive indicator (OFF) | "off" (configurable) |
| 3 | State + attributes object | { "state": 153.22, "attributes": { ... } } |
| 4 | Home Assistant service message (optional) | { "action": "input_number.set_value", "data": { "entity_id": "input_number.elpris", "value": 153.22 } } |
If HA override (msg.ha_enable="off") is active, output 4 sends the configured force_value instead of reference_price.
๐ฌ Runtime Inputs
| Property | Type | Description |
|---|---|---|
msg.data |
object | Nordpool data wrapper (see format below) |
msg.start |
number/string | Override start hour (0โ23) |
msg.stop |
number/string | Override stop hour (0โ23) |
msg.count |
number/string | Override count (number of intervals) |
msg.ha_enable |
string | "on" (normal) or "off" (HA override) |
msg.reset |
any | Full reset of internal context |
Example injection:
{
"start": 22,
"stop": 6,
"count": 8,
"ha_enable": "on",
"data": {
"attributes": {
"raw_today": [
{ "start": "2025-10-28T00:00:00+01:00", "value": 94.35 },
{ "start": "2025-10-28T00:15:00+01:00", "value": 92.12 }
],
"raw_tomorrow": [
{ "start": "2025-10-29T00:00:00+01:00", "value": 101.44 }
],
"unit_of_measurement": "SEK/kWh"
}
}
}
Reset:
{ "reset": true }
Disable HA dynamic updates:
{ "ha_enable": "off" }
Re-enable:
{ "ha_enable": "on" }
๐ฆ Expected Nordpool Data Structure
Minimal attributes:
raw_today: Array of objects withstartandvalue(orprice).- Optional
raw_tomorrow: Same shape. unit_of_measurement: e.g.รre/kWh,SEK/kWh,EUR/kWh.- Optional
price_in_cents: true(already รre). - Deduplication performed on timestamp.
๐งฎ Interval Detection
The node infers interval_minutes from the smallest positive gap between consecutive timestamps.
Attributes: interval_minutes, expected_points, actual_points, missing_points, and partial_period (true if incomplete data).
If interval โฅ 55 minutes, count is capped at 23.
๐ Reference Price Semantics (Detailed)
Attributes:
reference_price: Formatted รre string (e.g.153.22รre).reference_price_numeric: Pure number (e.g.153.22).reference_price_mode:cheap_selection_maxorexpensive_selection_min.reference_price_role:upper_bound_for_chargingorlower_bound_for_discharging.reference_price_effective: Equalsreference_priceunless HA override is active (then null).next_reference_when_enabled: Shows future effective reference during override (HA disabled).
Use cases:
- Charging logic: Activate when current spot price โค
reference_price_numeric(cheap mode). - Discharging logic: Activate when current spot price โฅ
reference_price_numeric(expensive mode).
๐ Attribute Overview (Output 3 payload.attributes)
| Attribute | Meaning |
|---|---|
time_01, time_02, ... |
Selected intervals (localized time + price) |
count |
Number of selected intervals |
selection_mode |
cheap or expensive |
selection_strategy |
discrete_slots or contiguous_block |
reference_price |
Threshold string |
reference_price_numeric |
Numeric threshold |
reference_price_mode |
Semantics of selection boundary |
reference_price_role |
Domain-oriented purpose |
reference_price_effective |
Null when override active |
next_reference_when_enabled |
Future reference if override off later |
max_time, min_time |
Extremes within selected set |
search_period |
Localized start โ end label |
data_source |
Merged sets used (e.g. today + tomorrow) |
interval_minutes |
Detected slot length |
contiguous_mode |
on / off |
rolling_24h |
on if rolling 24h logic used |
block_mode_start / block_mode_stop |
Bounds of contiguous block |
block_mode_average |
Average price of block |
total_hours_span |
Duration of evaluated window |
expected_points / actual_points |
Diagnostics |
missing_points |
Count of missing slots |
partial_period |
True if incomplete data |
single_selection |
True if only one slot selected |
ha_override |
on if HA override active |
control_mode |
override or normal |
ha_sent_value |
Value pushed to HA entity this cycle |
calculated_at |
ISO timestamp of calculation |
slot_alignment |
First/last selected slot time |
๐ Home Assistant Integration
If ha_entity is set (e.g. input_number.elpris), output 4 sends service-style payloads:
Active slot:
{
"action": "input_number.set_value",
"data": { "entity_id": "input_number.elpris", "value": 153.42 }
}
Outside slot OR override:
{
"action": "input_number.set_value",
"data": { "entity_id": "input_number.elpris", "value": -600 }
}
Disable dynamic updates (override battery logic but still preview future reference):
{ "ha_enable": "off" }
Re-enable:
{ "ha_enable": "on" }
During override:
ha_override = onreference_price_effective = nullnext_reference_when_enabledshows what would be used if re-enabled now.
๐ Reset Behavior
Sending any truthy msg.reset:
- Clears stored data (
today_data,yesterday_data,tomorrow_data) - Clears selection parameters (
start_time,stop_time,count_hour) - Clears
ha_enabled - Emits status โFull context resetโ
Example:
{ "reset": true }
๐ Example Battery Use Case
Cheap mode:
- Select e.g. 12 cheapest 15-min slots (3h total) in evening for charging.
- Use Output 1 to turn charger relay ON only when active slot.
- Use
reference_price_numericin HA automation to decide dynamic pre-charging threshold.
Expensive mode:
- Invert selection to mark high-price windows.
- Use Output 1 to trigger battery discharge or load shedding when inside expensive window.
Override:
- Temporarily force HA entity to a known fallback (e.g. -600) while still previewing future thresholds.
๐งช Example Flow Outline
- Nordpool upstream node fetches raw_today/raw_tomorrow.
- Function/Change nodes send dynamic overrides (
msg.count,msg.start,msg.stop). - This node calculates selection and outputs:
- Output 1 โ charger control
- Output 2 โ fallback/off
- Output 3 โ attributes for dashboards / DB
- Output 4 โ HA reference value injection
- Optional UI to toggle
ha_enable.
๐ Installation
From Node-RED editor:
- Menu โ Manage palette โ Install
- Search:
node-red-contrib-nordpool-chargecheap
Or via npm in Node-RED user directory:
npm install node-red-contrib-nordpool-chargecheap
Restart Node-RED if needed.
โ ๏ธ Notes & Edge Cases
- Missing tomorrow data โ
partial_period: true. - Rolling 24h mode may show partial data until future hours arrive.
countauto-clamped if more than available intervals.- Large gaps or malformed timestamps are ignored after dedupe.
- Interval detection outside 15/30/60 still supported (custom sources).
- Slot alignment: If your selection window does not align with slot boundaries, the node will include all slots starting at or after the start time and strictly before the stop time. For example, with 15-minute slots and start=23, stop=0, the slots chosen will be 23:00, 23:15, 23:30, and 23:45.
๐ Troubleshooting
| Symptom | Possible Cause | Fix |
|---|---|---|
Waiting for Nordpool data |
raw_today empty |
Check upstream feed |
partial_period true |
Incomplete tomorrow data | Wait for publication |
Unexpected reference_price_effective = null |
HA override active | Send {"ha_enable":"on"} |
| Charger not turning on | Not in active selected slot | Inspect time_XX + system clock |
| Price mismatch | Unit conversion discrepancy | Verify unit_of_measurement |
| Selection seems to shift during day | New tomorrow data appended | Consider lock logic (future enhancement) |
| Unexpected slot times | Selection window does not align with slot boundaries | Adjust your start/stop times to match slot intervals (e.g., use start=23:00 if slots start at 23:00) |
๐ License & Contributions
Open to:
- Performance improvements
- New selection heuristics
- HA-specific enhancements
(Insert license statement here, e.g. MIT.)
๐ผ Screenshots
๐ Quick Reference Cheat Sheet
| Task | Payload |
|---|---|
| Override window | { "start": 7, "stop": 23 } |
| Change count | { "count": 12 } |
| Enable HA | { "ha_enable": "on" } |
| Disable HA | { "ha_enable": "off" } |
| Full reset | { "reset": true } |
| Switch to expensive mode | Toggle invert selection in node config |
| Rolling 24h mode | Set start == stop (e.g. both 16) |
๐ Suggested HA Automations (Example)
Cheap mode charge trigger:
alias: Charge when cheap slot active
trigger:
- platform: state
entity_id: sensor.nordpool_chargecheap_active # If you map output 1 โ on/off helper
condition:
- condition: template
value_template: "{{ state_attr('sensor.nordpool_chargecheap','selection_mode') == 'cheap' }}"
action:
- service: switch.turn_on
target: { entity_id: switch.charger }
Expensive mode discharge trigger:
alias: Discharge when expensive slot active
trigger:
- platform: state
entity_id: sensor.nordpool_chargecheap_active
condition:
- condition: template
value_template: "{{ state_attr('sensor.nordpool_chargecheap','selection_mode') == 'expensive' }}"
action:
- service: switch.turn_on
target: { entity_id: switch.discharge_relay }
Override awareness:
alias: Notify HA override
trigger:
- platform: state
entity_id: sensor.nordpool_chargecheap
condition:
- condition: template
value_template: "{{ state_attr('sensor.nordpool_chargecheap','ha_override') == 'on' }}"
action:
- service: persistent_notification.create
data:
title: "Nordpool Override Active"
message: >
Dynamic pricing paused. Future reference would be:
{{ state_attr('sensor.nordpool_chargecheap','next_reference_when_enabled') }} รre.
โน๏ธ Versioning Notes
If you upgrade from an earlier version:
- New attributes (
reference_price_role,reference_price_numeric, override diagnostics) are additive. - No breaking changes in output ordering or fundamental logic.
Enjoy smarter price-based automation! Contributions and suggestions are welcome.