npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

node-red-contrib-home-ems

v0.4.2

Published

Node-RED nodes for a home energy management system: ENTSO-E dynamic prices, PV production forecast, load forecast, and a battery dispatch planner.

Downloads

894

Readme

node-red-contrib-home-ems

Node-RED nodes for a home energy management system: ENTSO-E dynamic prices, PV production forecast, load forecast, and a battery dispatch planner driven by dynamic electricity prices and battery state of charge (SoC).

Six nodes so far:

  • entsoe-prices — fetches ENTSO-E day-ahead prices, applies a configurable cost model (consumption afname & injection injectie price) and emits the current price plus a forecast for the coming hours. Calls the API sparingly and caches to disk.
  • pv-forecast — forecasts PV production for the next 24–48h from your location and per-string orientation (tilt/azimuth/kWp), via Open-Meteo or Forecast.Solar. Emits current output and an hourly forecast. Caches to disk.
  • load-forecast — measures the intrinsic (uncontrollable) house load from your inverter's power streams, learns a baseload profile (per hour × week/weekend) and forecasts the coming 24–48h as a best / expected / worst band.
  • flex-controller — dispatches controllable loads (EV charger, heat-pump boost, dump loads) against the available surplus: enables them by priority in power steps (7A/15A, 50°/60°) with hysteresis, force-runs a critically-low EV, and reports its power + firm energy back into the loop.
  • battery-planner — decides whether your battery should run in self-use (cover home load) or passive mode (follow an external setpoint to discharge / inject into the grid). Uses prices + the net (production − load) forecast + firm flex demand to sell during expensive hours while reserving SoC when needed.
  • ems-status — a terminal display node: wire any component's output(s) into it and its latest values show under the node (no debug sidebar needed).

All forecast/state outputs plug straight into each other (see Composing).

Early release — the API may still change between 0.x versions.

Install

From Node-RED (Manage palette → Install) search for node-red-contrib-home-ems, or from the command line in your Node-RED user directory (~/.node-red):

npm install node-red-contrib-home-ems

Requires Node-RED ≥ 3.0 (tested with Node-RED 5.0 / Node.js 22).

The battery-planner node

Inputs — one input, routed by msg.topic

| msg.topic | msg.payload | Purpose | |-------------|-------------------------------------------------|---------| | prices | array of { start, value } for the next 24–48h | supply the dynamic price forecast | | soc | number (percent) | current battery state of charge | | tick | anything | recompute using stored prices + SoC (e.g. every minute) |

The price field names are configurable and tolerant: it works out of the box with Nordpool / ENTSO-E / Tibber-style payloads (start/startsAt/time for time, value/price/total for the price).

Output

The node sets the configured property (default msg.payload) and msg.topic = "battery-mode":

{
  "mode": "passive",
  "setpoint": -2500,
  "injecting": true,
  "soc": 72,
  "price": 0.34,
  "reason": "injecting @ price 0.34 (threshold:true rank:true)",
  "ts": "2026-07-26T18:00:00.000Z"
}
  • modeself_use or passive.
  • setpoint — watts; negative = discharge to grid (inject), 0 = self-use.

Map this downstream to your specific inverter/battery integration (Sungrow, Victron, Sessy, Home Assistant service call, …).

Decision logic

  1. SoC protection (hysteresis). Below Min SoC, force self_use and enter a recovery lock; stay in self-use until SoC climbs back to Resume SoC. This stops injecting before the battery runs empty.
  2. Injection strategy (when SoC is healthy) — enable either or both:
    • Price threshold — inject when the current price ≥ a fixed value.
    • Rank within forecast — inject during the top-N most expensive hours, or above a percentile, over a look-ahead window.
    • Combine the two with AND (both must hold) or OR (either holds).

The entsoe-prices node

Fetches ENTSO-E day-ahead prices and turns the raw market price (BELPEX/EPEX, €/MWh) into consumption and injection prices via a configurable cost model.

API usage is minimised

  • Results are cached per day and persisted to a file in the Node-RED user directory, so a restart does not re-fetch.
  • The API is called only when today is missing, or — after a configurable cutoff hour (default 13:00, when tomorrow's prices are published) — when tomorrow is still missing, with a retry back-off.
  • Emitting is decoupled from fetching: an internal Tick (default 60s) or an input message emits from cache without hitting the API.

Input (single, topic-routed)

| msg.topic | Effect | |------------------------|--------| | tick / poll / '' | maybe fetch (only if needed), then emit | | refresh / fetch | force an API call, then emit | | emit | emit from cache only — never calls the API |

Outputs (two)

  1. currentmsg.topic = "price/current", payload = the bucket for the current hour (afname/injectie/belpex + breakdown).
  2. forecastmsg.topic = "prices", payload = array of upcoming hourly buckets, each with a value field (per forecast value field). Wire this straight into battery-planner.

Each bucket:

{
  "start": "2026-07-26T18:00:00.000Z",
  "belpex_eur_mwh": 65, "belpex_cent_kwh": 6.5,
  "energy_cent_kwh": 8.09, "net_cent_kwh": 6.8, "taxes_cent_kwh": 6.76,
  "afname_cent_kwh": 21.64, "injectie_cent_kwh": 5.31,
  "night": false, "value": 5.31
}

Cost model (all editable)

  • afname = energy component (factor × belpex + adder) × multiplier
    • net tariff (day/night) + fixed taxes/levies.
  • injectie = factor × belpex + adder.
  • Prices are in c€/kWh (1 €/MWh = 0.1 c€/kWh).
  • Neutral defaults = the raw market price (0.1 × belpex_eur_mwh) with no markups. Plug in your own supplier/grid tariffs. Belgian (Fluvius Limburg + Eneco dynamic) example: energyFactor 0.102, energyAdder 1, energyMultiplier 1.06, netNormal 6.80, netNight 5.63, taxes 6.7571, injectionAdder −1.188.

Token

Set your ENTSO-E securityToken on the node, or via the ENTSOE_TOKEN environment variable.

The pv-forecast node

Forecasts PV production for the coming hours from your location and per-string orientation. Add one row per string/plane (tilt, azimuth, kWp); an east-west array is simply two rows. Azimuth convention: south = 0, east = −90, west = +90 (same for both providers).

Providers (selectable)

  • Open-Meteo — free, no key, generous limits, up to 16 days. Fetches tilted irradiance (GTI) per string and converts with a simple model: watts = kWp × GTI × systemEfficiency, capped at the inverter AC max.
  • Forecast.Solar — PV-specific, returns AC watts directly (the model coefficients are then ignored). Public tier needs no key; add one (node or FORECAST_SOLAR_KEY env) for higher limits/horizon.

One request is made per string and the results are summed. The forecast is re-fetched at most every Refresh hours (default 3) and cached to disk; emitting (internal Tick, or an input message) is decoupled from fetching.

Input / outputs

Same topic routing as entsoe-prices (tick/refresh/emit). Two outputs:

  1. currenttopic "pv/current", payload for the current hour (watts, wh, wh_rest_today, wh_forecast_total).
  2. forecasttopic "production", array of upcoming hourly buckets, each with watts, wh and a value field (watts).

See examples/pv-forecast.json.

The load-forecast node

Learns and forecasts the intrinsic (uncontrollable) house consumption as a band.

  • Measure — send topic: measure with instantaneous power (W) { pv, grid_import, grid_export, battery_charge, battery_discharge }; the load is derived by energy balance (load = pv + grid_import + battery_discharge − grid_export − battery_charge). You can also send { load } directly.
  • Learn — "certain" scheduled components (pool, hot water, heating) and the current controllable-load power are subtracted from the measured load first, so the learned residual is the clean intrinsic baseload — never polluted by control-activated consumption (see below).
  • Forecast — hourly best / expected / worst (W). Certain components are added to all cases; "opportunistic" loads only widen the worst case.
  • Steer — turn any load on/off by name with topic: enable / topic: disable (payload = the component name, e.g. "zwembad"). Or topic: component { name, enabled?, power?, ... } for a full update, or topic: components to replace the whole list.

Controllable loads stay out

Controllable loads (EV, heat-pump boost, dump loads) are not demand — they are flexibility, owned by the flex-controller. Feed their current power back via topic: control (a number or { name: power }), or include it in the measure payload ({ …, controllable: 3450 } / { …, loads: { ev: 3450 } }), and it is excluded from learning. This keeps the forecast from reasoning circularly (forecasting surplus-driven charging as if it were firm demand).

Outputs: current (topic "load/current": load, controllable_power, intrinsic_load) and forecast (topic "load", buckets with best/expected/worst and a value = expected). See examples/load-forecast.json.

The flex-controller node

A balance-regulated controller for the controllable loads. Instead of computing a surplus and allocating it once, it regulates on the measured power balance and nudges the loads incrementally, by priority.

Error signal:

headroom = battery + grid − batteryReserve(soc)

battery = + charging / − discharging, grid = + export / − import. Each tick: headroom above the deadband → raise the highest-priority load a step; below it → shed the lowest-priority active load first; otherwise hold.

  • Assets — an EV uses modulate (continuous current between minAmps/ maxAmps, with voltage/phases) + optional socDriven (socWish/socMust/capacityWh/targetSoc). A relay uses steps (e.g. [{"label":"boost","power":1500}]). priority: low = added first, shed last. A critically-low connected EV (soc < socMust) is force-run (firm).
  • Gradual, paced ramp — a modulating load is raised by rampAmps (the rampAmpsFast step above fastStepHeadroom) at a time, and only after a fresh battery reading, so it waits for the previous change to land before the next. It also won't get ahead of the charger's reported actual current (commanded 8A but 7A measured → hold). A higher-priority load is filled to its max before the next starts (EV to max, then the heat pump); shedding is immediate. A SoC-driven asset is treated as not connected until a connected: true arrives — an EV that hasn't reported is never charged.
  • SoC policy — below socMinSteer the battery reserves its full charge power (loads only run on surplus beyond it); between that and socHigh it reserves the chargeBand first; at/above socHigh the reservation drops.
  • Inputstopic: battery (W, ±), topic: grid (W, ±), topic: soc (home-battery %), topic: carsoc { name, soc }, topic: connected { name, connected }, topic: actual { name, amps|power }, topic: enable/disable.
  • Outputscommands (topic "flex/commands": per-asset { name, on, amps, step, power, actual, reason } → wire to your relays/charger) and state (topic "flex/state": headroom, reserve_charge, controllable_power, firm_wh, tier).

Why battery + grid (not grid export alone): battery + grid = PV − intrinsic − controllable, so the signal excludes the planner's deliberate battery discharge — selling at a good price never shows up as surplus. It also needs no per-device metering: an unmetered heat-pump relay that draws nothing (water already hot) simply doesn't lower the headroom, so the surplus flows to the next load. Example: PV 3000W, house 1000W, planner selling 2500W → grid export is 4500W, but battery + grid = −2500 + 4500 = 2000W, the real PV surplus.

See examples/flex-controller.json.

The ems-status node

A terminal display node for readable, at-a-glance monitoring — much handier than debug nodes for following a running flow. Drop one after a component and wire that component's output(s) into it; its latest values appear under the node. It auto-detects the channel from msg.topic:

| Component | Status line | |---|---| | pv-forecast | now W · rest today · 48h · peak | | entsoe-prices | afname / injectie · 48h min–max | | load-forecast | now · intrinsic · flex · forecast peak | | flex-controller | headroom · active assets · firm energy | | battery-planner | mode · setpoint · SoC · injection floor |

Wire both outputs of a two-output node into the same status node to see current and forecast together. See examples/full-ems.json.

For entsoe-prices you can set the price field (auto / afname / injectie / belpex) — use two status nodes on the forecast output to show afname and injectie side by side, each with its own 48h min–max.

For load-forecast you can set the load view:

  • summary — now / 24h total / peak;
  • blocks — expected energy per clock-aligned 6h block (e.g. per 6u 18u:4.4 0u:2.1 6u:5.3 12u:6.0), chronological — handy to see and tune the overnight consumption that drives the planner's reserve floor;
  • sparkline — a 24h bar-chart of the expected load shape.

The battery-planner line also shows the reserve energy behind the floor (floor 79% (reserve 5.9kWh)).

Composing the nodes

The nodes form a closed loop — outputs carry the topics/fields each next node reads, so wiring is direct (no mapping):

entsoe-prices ─(prices)───────────────┐
pv-forecast   ─(production)──┬─────────┤
                             │         ├─►  battery-planner ─► mode
load-forecast ─(load)────────┼─────────┤          ▲
   ▲ control (controllable_power)      │          │ reserve (firm_wh)
   └───────────────── flex-controller ─┴──────────┘
                          ▲  surplus / carsoc / enable
                          └─ commands ─► your relays / EV charger
  • The planner reserves SoC for a coming deficit (production − load) and for firm flex demand (firm_wh from a critically-low EV), and injects freely when a surplus is expected.
  • flex-controller feeds its controllable_power back to load-forecast so learning stays clean, and its firm_wh to the planner's reserve input.

See examples/full-ems.json, or the simpler examples/entsoe-to-planner.json.

Unit note: the price forecast value is in c€/kWh by default. If you enable the planner's fixed price threshold, set it in the same unit (e.g. 8, not 0.30). The rank strategies (top-N / percentile) are unit-agnostic.

Example wiring

See examples/basic-flow.json — import it via Menu → Import. It wires an inject node (SoC), a prices source, and a 1-minute tick into the planner, then a debug node on the output.

Typical sources in a Home Assistant + Node-RED setup:

  • SoC — a current state / events: state node on your battery sensor → set topic to soc.
  • Prices — your dynamic-tariff integration (Nordpool, ENTSO-E, Tibber) → set topic to prices.
  • tick — an inject node repeating every minute.

Development

npm install
npm test        # runs the pure-logic unit tests (node --test)

The decision logic lives in lib/planner.js with no Node-RED dependency, so it is unit-tested in isolation and reusable by upcoming nodes.

License

MIT © io-things