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

ev-charging-simulator

v1.1.0

Published

Production-ready OCPP 1.6 EV charging station simulator. Works as a reusable TypeScript library or standalone server for testing CSMS integration.

Downloads

776

Readme

EV Charging Simulator

A production-ready OCPP 1.6 charging station simulator library and server for testing and development of EV charging infrastructure.

Works both as a reusable npm library and as a standalone virtual charger server.


Status & Badges

Tests npm version Node.js Version License codecov Security Status


Table of Contents


Features

Full OCPP 1.6 Support — Complete chargepoint-to-CSMS message protocol implementation

Async/Promise-Based — Clean async/await API with proper connection lifecycle management

Production-Ready — Comprehensive error handling, reconnection logic, and timeout management

Fleet Management — Simulate hundreds of chargers from simple config files

Flexible Configuration — JSON files, objects, or programmatic setup

ACE API Compatible — Optional HTTP compatibility layer for existing integrations

TypeScript — Full type safety with exported interfaces and enums

Tested — 97+ unit and integration tests with 100% passing coverage


Installation

npm install ev-charging-simulator

Or with yarn:

yarn add ev-charging-simulator

Quick Start

Minimal Example

import { Charger } from 'ev-charging-simulator';

const charger = new Charger({
  evseId: 'EVSE-ANON-1',
  connectors: 1,
  csmsUrl: 'ws://your-csms-server:9000/ocpp1.6',
});

// Connect and authenticate with CSMS
await charger.connect();

console.log(charger.isConnected()); // true

// Gracefully disconnect
await charger.disconnect();

With Full Configuration

import { Charger, ConnectorState } from 'ev-charging-simulator';

const charger = new Charger(
  {
    evseId: 'EVSE-DEMO-1',
    connectors: 2,
    csmsUrl: 'ws://localhost:9000/ocpp1.6',
    power: { amps: 32, volts: 230 }, // 7.36 kW
  },
  {
    bootOverrides: {
      chargePointVendor: 'GenericVendor',
      chargePointModel: 'GenericModel',
      firmwareVersion: '1.0.0',
    },
  }
);

// Connect with timeout handling
const connectPromise = charger.connect();
await Promise.race([
  connectPromise,
  new Promise((_, reject) =>
    setTimeout(() => reject(new Error('Connection timeout')), 5000)
  ),
]);

// Simulate connector status changes
await charger.setStatus(ConnectorState.Available, 1);
console.log(charger.getState()); // ConnectorState.Available

Core Concepts

Charger Lifecycle

const charger = new Charger({ evseId: 'EVSE-1', connectors: 1 });

// 1. Initialize (constructor only, non-blocking)
// 2. Connect to CSMS
await charger.connect(); // Async: waits for boot notification

// 3. Manage connector states
await charger.setStatus(ConnectorState.Available);
await charger.localStart(1, 'RFID_TAG');
await charger.stopConnector(1);

// 4. Disconnect gracefully
await charger.disconnect();

// 5. Complete shutdown
await charger.shutdown();

Connection States

  • Disconnected — Not connected to CSMS
  • Connecting — WebSocket open, boot notification pending
  • Connected — Boot notification received, ready for transactions
  • Reconnecting — Auto-reconnect with exponential backoff

Connector States

import { ConnectorState } from 'ev-charging-simulator';

type ConnectorState =
  | 'Available'   // Ready to charge
  | 'Preparing'   // Preparing for transaction
  | 'Charging'    // Active transaction
  | 'Finishing'   // Transaction ending
  | 'Unavailable' // Offline/maintenance
  | 'Faulted';    // Error condition

API Reference

Charger Constructor

interface ChargerOptions {
  csmsUrl?: string;                    // CSMS WebSocket URL (env: CSMS_URL)
  connectors?: number;                 // Number of connectors (default: 1)
  bootOverrides?: Partial<BootOptions>; // Override boot notification
  brandProfile?: BrandProfile | null;  // Brand-specific behavior
  chargerPassword?: string;            // ACE API password
}

new Charger(properties: ChargerProperties, options?: ChargerOptions)

Core Methods

async connect(): Promise<void>

Establishes WebSocket connection and waits for boot notification acceptance.

try {
  await charger.connect();
} catch (err) {
  console.error('Failed to connect:', err.message);
}

Behavior:

  • Throws if CSMS is unreachable
  • Waits for BootNotification response
  • Automatically negotiates heartbeat interval
  • Sets up message/ping handlers

async disconnect(): Promise<void>

Gracefully closes WebSocket connection without state cleanup.

await charger.disconnect();

async shutdown(): Promise<void>

Performs full cleanup: stops timers, rejects pending requests, closes connection.

await charger.shutdown();

async setStatus(status: ConnectorState, connectorId: number = 1): Promise<void>

Updates connector state and sends StatusNotification to CSMS.

await charger.setStatus(ConnectorState.Charging, 1);

async localStart(connectorId: number = 1, idTag: string = 'LOCALTAG'): Promise<void>

Initiates a local/simulated charge transaction.

await charger.localStart(1, 'RFID_TAG_123');

async stopConnector(connectorId: number = 1): Promise<boolean>

Stops active transaction on connector.

const stopped = await charger.stopConnector(1);

isConnected(): boolean

Check connection status (non-blocking).

if (charger.isConnected()) {
  console.log('Connected to CSMS');
}

getState(): ConnectorState

Get current state of default connector (1).

const state = charger.getState();

snapshot(): ChargerSnapshot

Get full snapshot of charger state.

const {
  evseId,
  connected,
  state,
  power,
  firmwareVersion,
} = charger.snapshot();

Usage Examples

Example 1: Simulate Multiple Chargers from Config

import { loadChargersFromConfig } from 'ev-charging-simulator';

// Load from JSON file
const chargers = await loadChargersFromConfig('./chargers.json', {
  csmsUrl: 'ws://localhost:9000/ocpp1.6',
  connectors: 2,
  bootTemplate: {
    chargePointVendor: 'MyVendor',
    chargePointModel: 'MyModel',
  },
});

// Connect all
await Promise.all(chargers.map(c => c.connect()));

console.log(
  `Connected ${chargers.filter(c => c.isConnected()).length}/${chargers.length}`
);

// Example: trigger charging on first charger
await chargers[0].setStatus('Charging', 1);

chargers.json:

{
  "evses": [
    {
      "evseId": "CHARGER_001",
      "connectors": 2,
      "power": { "amps": 32, "volts": 230 }
    },
    {
      "evseId": "CHARGER_002",
      "connectors": 2,
      "power": { "amps": 16, "volts": 230 }
    }
  ]
}

Example 2: Simulated Charging Flow

import { Charger, ConnectorState } from 'ev-charging-simulator';

async function simulateChargingSession(charger: Charger) {
  // Step 1: Available
  await charger.setStatus(ConnectorState.Available, 1);

  // Step 2: RFID card presented → Start transaction
  await charger.localStart(1, 'USER_RFID_123');

  // Step 3: Charging
  await charger.setStatus(ConnectorState.Charging, 1);
  console.log('Charging... [simulating for 5 seconds]');
  await new Promise(r => setTimeout(r, 5000));

  // Step 4: Stop charging
  await charger.stopConnector(1);

  // Step 5: Back to Available
  await charger.setStatus(ConnectorState.Available, 1);
  console.log('Session complete');
}

const charger = new Charger({ evseId: 'DEMO-1', connectors: 1 });
await charger.connect();
await simulateChargingSession(charger);
await charger.shutdown();

Example 3: Error Handling and Reconnection

import { Charger } from 'ev-charging-simulator';

const charger = new Charger({
  evseId: 'RESILIENT-1',
  csmsUrl: 'ws://csms.example.com:9000/ocpp1.6',
});

// Connect with timeout
try {
  const timeoutPromise = new Promise<void>((_, reject) =>
    setTimeout(() => reject(new Error('Connection timeout')), 10000)
  );
  await Promise.race([charger.connect(), timeoutPromise]);
  console.log('Connected!');
} catch (err) {
  console.error('Connection failed:', err.message);
  process.exit(1);
}

// Monitor connectivity
const checkConnection = setInterval(() => {
  const status = charger.isConnected() ? 'OK' : 'DISCONNECTED';
  console.log(`[${new Date().toISOString()}] Status: ${status}`);
}, 30000);

// Graceful shutdown
process.on('SIGINT', async () => {
  clearInterval(checkConnection);
  await charger.shutdown();
  process.exit(0);
});

Configuration

Via Environment Variables

export CSMS_URL=ws://csms.example.com:9000/ocpp1.6
export ACE_LOGIN_PASSWORD=super_secret_123

Via Configuration File

evse-config.json:

{
  "evses": [
    {
      "evseId": "CHARGER-A",
      "connectors": 2,
      "csmsUrl": "ws://localhost:9000/ocpp1.6",
      "power": { "amps": 32, "volts": 230 },
      "boot": {
        "chargePointVendor": "GenericVendor",
        "chargePointModel": "GenericModel",
        "firmwareVersion": "1.0.0"
      },
      "location": {
        "id": "LOC_001",
        "name": "Downtown Station"
      }
    }
  ]
}

Programmatically

const chargers = [
  {
    evseId: 'CHARGER-1',
    connectors: 1,
    csmsUrl: 'ws://localhost:9000/ocpp1.6',
    power: { amps: 16, volts: 230 },
  },
];

const instances = createChargers(chargers);

Error Handling

Connection Errors

try {
  await charger.connect();
} catch (err) {
  if (err.message.includes('timeout')) {
    console.error('CSMS took too long to respond');
  } else if (err.message.includes('refused')) {
    console.error('CSMS is not accepting connections');
  } else {
    console.error('Unexpected error:', err.message);
  }
}

Transaction Errors

const started = await charger.localStart(1, 'TAG');
// If authorization fails or connector is busy, start is ignored safely
// Check status to verify:
const state = charger.getState();
if (state !== ConnectorState.Charging) {
  console.log('Transaction did not start');
}

Retry Pattern

async function connectWithRetry(
  charger: Charger,
  maxAttempts = 3,
  delayMs = 1000
) {
  for (let i = 1; i <= maxAttempts; i++) {
    try {
      await charger.connect();
      return;
    } catch (err) {
      if (i === maxAttempts) throw err;
      console.log(`Attempt ${i} failed, retrying in ${delayMs}ms...`);
      await new Promise(r => setTimeout(r, delayMs * i)); // Exponential backoff
    }
  }
}

await connectWithRetry(charger);

Troubleshooting

Connection Stuck / Timeout

Symptom: await charger.connect() hangs indefinitely

Solutions:

  1. Verify CSMS URL is correct and reachable:

    telnet csms.example.com 9000
  2. Check firewall rules allow WebSocket (port 9000 or your CSMS port)

  3. Add explicit timeout:

    const timeoutMs = 10000;
    await Promise.race([
      charger.connect(),
      new Promise((_, r) =>
        setTimeout(() => r(new Error('Timeout')), timeoutMs)
      ),
    ]);

Charger Disconnects Immediately

Symptom: Connection succeeds but charger only stays connected briefly

Solutions:

  1. Review CSMS logs for boot notification rejection

  2. Verify boot options (vendor, model, firmware) are acceptable to CSMS

  3. Check if heartbeat is being negotiated:

    charger.getHeartbeatPeriodMs(); // Should be non-zero after connect

Charger State Not Updating

Symptom: setStatus() doesn't reflect changes

Solutions:

  1. Always await async methods:

    await charger.setStatus(ConnectorState.Charging, 1); // ✓ Correct
    charger.setStatus(ConnectorState.Charging, 1);      // ✗ Fire-and-forget
  2. Verify no pending transactions or state conflicts

  3. Check CSMS logs for ChangeAvailability or StatusNotification rejections

Memory Leaks

All timers and WebSocket listeners are properly cleaned up with:

await charger.shutdown(); // Cleans everything

Ensure shutdown() is called on exit for proper resource cleanup.


Testing

Running Tests Locally

# Run all tests once
npm test

# Watch mode (re-run on file changes)
npm run test:watch

# Generate coverage report
npm run test:coverage

Test Coverage

This project maintains 100% code coverage with 97+ tests across:

  • Unit Tests — Individual component behavior
  • Integration Tests — OCPP protocol message flows
  • Error Handling — Edge cases and failure scenarios
  • Firmware Manager — Update lifecycle management
  • Configuration Loading — JSON parsing and validation

Coverage breaks down as:

| Area | Coverage | |------|----------| | Charger API | 100% | | OCPP Protocol | 100% | | WebSocket Connection | 100% | | Error Handling | 100% | | Route Handlers | 100% |

Continuous Integration

Every push and pull request automatically triggers:

  1. Build validation — TypeScript compilation
  2. Unit tests — Full test suite on Node 18, 20, 22
  3. Security scan — Dependency audit + Trivy scanning
  4. Coverage reporting — Metrics uploaded to Codecov

View CI status and coverage:

Coverage Details:

The coverage badge shows the percentage of code lines executed by tests. Click the codecov badge to see:

  • Line-by-line coverage map
  • Commit history of coverage changes
  • Comparison with previous versions
  • Detailed coverage by file

License

MIT — See LICENSE file for details


Questions? Issues? Open an issue on GitHub or check the test suite for comprehensive usage examples.