OCPP WS IOocpp-ws-io
Smart Charge Engine

Smart Charge Engine

Library-agnostic OCPP smart charging constraint solver for EV charge point operators — distribute power fairly and safely across chargers.

Smart Charge Engine

ocpp-smart-charge-engine solves one problem: how to distribute your site's grid power fairly and safely among EV chargers using OCPP's SetChargingProfile command.

This package is completely library-agnostic. It does not care whether you use ocpp-ws-io, raw WebSockets, or any other OCPP implementation. You supply a dispatcher callback — what you do inside it is entirely up to you.


How It Works

The engine:

  1. Accepts sessions when cars connect (addSession())
  2. Runs an allocation algorithm (Equal Share, Priority, or Time-of-Use)
  3. Calls your dispatcher with the computed power limit for each session
  4. Your dispatcher sends SetChargingProfile via whatever OCPP library you use

Features

⚡ Library Agnostic

Works with ocpp-ws-io, raw WebSockets, MQTT, or any OCPP implementation. You provide the dispatcher callback.

🔌 3 Built-in Strategies

Equal Share (fairness), Priority (fleet preference), Time-of-Use (cost optimization). Hot-swap at runtime.

🔒 Safety Margins

Configurable safety buffer prevents tripping breakers. Per-session minChargeRateKw prevents EV faults.

🏢 Multi-Panel Sites

One engine per sub-panel. Hierarchical grid with master engine updating sub-panel budgets.

📊 Auto-Dispatch

Periodic rebalancing on a timer — ideal for TOU tariffs that change power limits throughout the day.

🔧 OCPP Profile Builders

Version-specific helpers for 1.6, 2.0.1, and 2.1 — including V2G discharge limits.


Install

bash npm install ocpp-smart-charge-engine
bash pnpm add ocpp-smart-charge-engine
bash yarn add ocpp-smart-charge-engine
bash bun add ocpp-smart-charge-engine

Charger Compatibility: SetChargingProfile is part of the OCPP 1.6 Smart Charging optional profile and is mandatory in OCPP 2.0.1+. If a charger rejects the command, the engine emits a dispatchError event and continues dispatching to all other sessions.


Quick Start

Create the Engine

engine.ts
import { SmartChargingEngine, Strategies } from "ocpp-smart-charge-engine";
import { buildOcpp16Profile } from "ocpp-smart-charge-engine/builders";

const engine = new SmartChargingEngine({
  siteId: "SITE-HQ-001",
  maxGridPowerKw: 100,     // 100kW grid connection
  safetyMarginPct: 5,      // Use max 95kW, leave 5% buffer
  algorithm: Strategies.EQUAL_SHARE,

  // The ONLY integration point — use whatever OCPP library you have
  dispatcher: async ({ clientId, connectorId, sessionProfile }) => {
    await yourOcppServer.sendToClient(clientId, "SetChargingProfile", {
      connectorId,
      csChargingProfiles: buildOcpp16Profile(sessionProfile),
    });
  },
});

Register Sessions When Cars Connect

handlers.ts
// In your OCPP StartTransaction handler:
engine.addSession({
  transactionId: payload.transactionId,
  clientId: client.identity,
  connectorId: payload.connectorId,
  maxHardwarePowerKw: 22,     // Charger hardware rating
  minChargeRateKw: 1.4,       // IEC 61851 minimum (6A × 230V)
});

// Recalculate and dispatch profiles to all active chargers
await engine.dispatch();

Remove Sessions When Cars Leave

handlers.ts
// In your OCPP StopTransaction handler:
engine.removeSession(payload.transactionId);
await engine.dispatch(); // Redistribute power to remaining sessions

Enable Auto-Dispatch (Optional)

engine.ts
// Recalculate every 60 seconds — useful for TOU tariffs
engine.startAutoDispatch(60_000);

Complete Example with ocpp-ws-io

main.ts
import { OCPPServer } from "ocpp-ws-io";
import { SmartChargingEngine, Strategies } from "ocpp-smart-charge-engine";
import { buildOcpp16Profile } from "ocpp-smart-charge-engine/builders";

const server = new OCPPServer({ protocols: ["ocpp1.6"] });
const clients = new Map();

const engine = new SmartChargingEngine({
  siteId: "MY-SITE",
  maxGridPowerKw: 100,
  safetyMarginPct: 5,
  algorithm: Strategies.EQUAL_SHARE,
  autoClearOnRemove: true,

  dispatcher: async ({ clientId, connectorId, sessionProfile }) => {
    const client = clients.get(clientId);
    if (!client) return;
    await client.call("SetChargingProfile", {
      connectorId,
      csChargingProfiles: buildOcpp16Profile(sessionProfile),
    });
  },

  clearDispatcher: async ({ clientId, connectorId }) => {
    const client = clients.get(clientId);
    if (!client) return;
    await client.call("ClearChargingProfile", {
      connectorId,
      chargingProfilePurpose: "TxProfile",
      stackLevel: 0,
    });
  },
});

server.on("client", (client) => {
  clients.set(client.identity, client);
  client.once("close", () => clients.delete(client.identity));

  client.handle("ocpp1.6", "BootNotification", () => ({
    currentTime: new Date().toISOString(),
    interval: 30,
    status: "Accepted",
  }));

  client.handle("ocpp1.6", "StartTransaction", async (ctx) => {
    engine.addSession({
      transactionId: ctx.payload.transactionId,
      clientId: client.identity,
      connectorId: ctx.payload.connectorId,
      maxHardwarePowerKw: 22,
      minChargeRateKw: 1.4,
    });
    await engine.dispatch();
    return {
      idTagInfo: { status: "Accepted" },
      transactionId: ctx.payload.transactionId,
    };
  });

  client.handle("ocpp1.6", "StopTransaction", async (ctx) => {
    engine.safeRemoveSession(ctx.payload.transactionId);
    await engine.dispatch();
    return { idTagInfo: { status: "Accepted" } };
  });
});

engine.startAutoDispatch(60_000);
await server.listen(3000);

Dispatcher Examples

The dispatcher is library-agnostic. Here's how to use it with different setups:

With ocpp-ws-io — OCPP 2.0.1

import { buildOcpp201Profile } from "ocpp-smart-charge-engine/builders";

dispatcher: async ({ clientId, connectorId, sessionProfile }) => {
  await client.call("SetChargingProfile", {
    evseId: connectorId, // NOTE: connectorId → evseId in 2.0.1
    chargingProfile: buildOcpp201Profile(sessionProfile),
  });
};

Mixed Fleet (1.6 + 2.0.1)

import { buildOcpp16Profile, buildOcpp201Profile } from "ocpp-smart-charge-engine/builders";

dispatcher: async ({ clientId, connectorId, sessionProfile }) => {
  const client = clients.get(clientId);
  const protocol = client.protocol;

  if (protocol === "ocpp1.6") {
    await client.call("SetChargingProfile", {
      connectorId,
      csChargingProfiles: buildOcpp16Profile(sessionProfile),
    });
  } else {
    await client.call("SetChargingProfile", {
      evseId: connectorId,
      chargingProfile: buildOcpp201Profile(sessionProfile),
    });
  }
};

Raw WebSocket

import { buildOcpp16Profile } from "ocpp-smart-charge-engine/builders";

dispatcher: async ({ clientId, connectorId, sessionProfile }) => {
  const ws = wsMap.get(clientId);
  ws?.send(JSON.stringify([
    2, crypto.randomUUID(), "SetChargingProfile",
    { connectorId, csChargingProfiles: buildOcpp16Profile(sessionProfile) },
  ]));
};

ClearChargingProfile

When a car leaves, send ClearChargingProfile to remove throttling:

const engine = new SmartChargingEngine({
  // ...
  clearDispatcher: async ({ clientId, connectorId }) => {
    await client.call("ClearChargingProfile", {
      connectorId,
      chargingProfilePurpose: "TxProfile",
      stackLevel: 0,
    });
  },
  autoClearOnRemove: true, // auto-fires clearDispatcher on removeSession()
});

// Or manually:
await engine.clearDispatch();      // clear ALL sessions
await engine.clearDispatch(42);    // clear only transactionId 42

Profile Builders

Version-specific helpers convert raw SessionProfile numbers into the correct OCPP shape:

HelperOCPP VersionPayload FieldSchedule Shape
buildOcpp16Profile()1.6csChargingProfilesSingle object
buildOcpp201Profile()2.0.1chargingProfileArray
buildOcpp21Profile()2.1chargingProfileArray + V2G fields

Builder Options

buildOcpp16Profile(sessionProfile, {
  stackLevel: 0,
  purpose: "TxProfile",    // "TxProfile" | "TxDefaultProfile" | "ChargePointMaxProfile"
  rateUnit: "W",            // "W" | "A"
  numberPhases: 3,
  periods: [                // Multi-period schedule
    { startPeriod: 0, limit: 22000, numberPhases: 3 },
    { startPeriod: 7200, limit: 7000, numberPhases: 3 },
  ],
});

OCPP 2.1 V2G Discharge

import { buildOcpp21Profile } from "ocpp-smart-charge-engine/builders";

buildOcpp21Profile(sessionProfile, {
  dischargeLimitW: 7400, // Allow 7.4kW V2G discharge (ISO 15118-20)
});

API Reference

Constructor Options

OptionTypeDefaultDescription
siteIdstringrequiredHuman-readable site identifier
maxGridPowerKwnumberrequiredMaximum site grid power in kW
dispatcherChargingProfileDispatcherrequiredYour OCPP send function
clearDispatcherClearProfileDispatcherSends ClearChargingProfile
autoClearOnRemovebooleanfalseAuto-clear profile on removeSession()
algorithmStrategyEQUAL_SHAREAllocation strategy
safetyMarginPctnumber5Power held in reserve (%)
phases1 | 33AC phase count
voltageVnumber230Grid voltage for amps calculation
timeOfUseWindowsTimeOfUseWindow[][]Peak windows (TIME_OF_USE only)
debugbooleanfalseVerbose logging

addSession() Options

OptionTypeDefaultDescription
transactionIdnumber | stringrequiredOCPP transaction ID
clientIdstringrequiredCharging station identity
connectorIdnumber1Connector / EVSE ID
maxHardwarePowerKwnumberCharger hardware limit
maxEvAcceptancePowerKwnumberEV acceptance limit
minChargeRateKwnumber0Minimum power floor
prioritynumber1Priority (PRIORITY strategy)
phases1 | 3site defaultPhase count for connector
metadataobjectArbitrary data (stored, not used)

Methods

MethodDescription
addSession(session)Register a session. Throws DuplicateSessionError if exists
removeSession(txId)Remove a session. Throws SessionNotFoundError if not found
safeRemoveSession(txId)Remove without throwing
optimize()Calculate profiles without dispatching
dispatch()Calculate + dispatch to all active sessions
clearDispatch(txId?)Send ClearChargingProfile to one or all
startAutoDispatch(ms)Start periodic dispatch (min 1000ms)
stopAutoDispatch()Stop auto-dispatch
setGridLimit(kw)Update grid limit at runtime
setAlgorithm(strategy)Hot-swap algorithm at runtime
setSafetyMargin(pct)Update safety margin at runtime
getSessions()Read-only array of active sessions
isEmpty()true when no sessions registered

Events

EventPayloadWhen
sessionAddedActiveSessionSession registered
sessionRemovedActiveSessionSession removed
optimizedSessionProfile[]After optimize()
dispatchedSessionProfile[]After all dispatchers settle
dispatchErrorDispatchErrorEventDispatcher throws; engine continues
clearedClearDispatchPayloadAfter clearDispatcher succeeds
clearErrorClearDispatchPayload & { error }clearDispatcher throws
autoDispatchStartednumber (intervalMs)After startAutoDispatch()
autoDispatchStoppedAfter stopAutoDispatch()
errorErrorStrategy function throws

What's Next?

On this page