node-red-contrib-nordpool-chargecheap 1.0.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.
๐ง 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 |
๐ 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).
๐ก Future Enhancements (Ideas)
- Dual-threshold exposure (min + max reference simultaneously).
- Selection lock to avoid reshuffle when tomorrow data appears.
- Multiple HA entities (charge vs discharge).
- Alternative reference strategies (median, weighted, percentile).
- FX conversion for EUR โ SEK with live rates.
๐ 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) |
๐ 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.