OCPP WS IOocpp-ws-io

Browser Client

Use BrowserOCPPClient in browser environments — React, Vue, Next.js, and more.

BrowserOCPPClient is a lightweight OCPP WebSocket RPC client designed for browser environments. It enables building charge point simulators, testing dashboards, and debugging tools directly in the browser — using the same typed API as OCPPClient.

import { BrowserOCPPClient } from 'ocpp-ws-io/browser';

[!IMPORTANT] The browser client is designed for testing and simulating charge points — it does not support all OCPP features. Missing: Security Profiles (0–3), Strict Mode (schema validation), TLS/mTLS configuration, WebSocket Ping/Pong, and custom HTTP headers. For production charge point communication, use OCPPClient in a Node.js environment.

Limitations vs OCPPClient

OCPPClient depends on Node.js modules (ws, node:crypto, node:events, node:net) — it can't run in the browser. BrowserOCPPClient replaces these with browser-native APIs, but this comes with trade-offs:

FeatureOCPPClientBrowserOCPPClient
WebSocketws (Node.js)Native browser WebSocket
Eventsnode:eventsCustom lightweight EventEmitter
ID Generation@paralleldrive/cuid2@paralleldrive/cuid2
Security Profiles0–3 (TLS, mTLS, certs)N/A (handled by browser/TLS)
Custom Headers✅ via ws❌ browser limitation
Ping/Pong✅❌ browser limitation
Type Safety✅ Full✅ Full (same generated types)
Reconnection✅✅
Strict Mode✅❌

Quick Start

import { BrowserOCPPClient } from 'ocpp-ws-io/browser';

const client = new BrowserOCPPClient({
	endpoint: 'wss://csms.example.com/ocpp',
	identity: 'CP001',
	protocols: ['ocpp1.6'],
});

// Register a handler — params are auto-typed for OCPP 1.6 Reset
client.handle('Reset', ({ params }) => {
	console.log('Reset type:', params.type);
	return { status: 'Accepted' };
});

// Connect and send a BootNotification
await client.connect();

const response = await client.call('ocpp1.6', 'BootNotification', {
	chargePointVendor: 'VendorX',
	chargePointModel: 'ModelY',
});

console.log('Status:', response.status); // typed: "Accepted" | "Pending" | "Rejected"

Configuration

const client = new BrowserOCPPClient(options: BrowserClientOptions);

BrowserClientOptions

OptionTypeDefaultDescription
identitystringrequiredCharging station ID
endpointstringrequiredWebSocket URL (ws:// or wss://)
protocolsstring[][]OCPP subprotocols to negotiate
queryRecord<string, string> | string—URL query parameters, as an object or a query string ("a=1&b=2", leading ? optional); added after any query in endpoint
reconnectbooleantrueAuto-reconnect on disconnect
maxReconnectsnumberInfinityMax reconnection attempts
backoffMinnumber1000First reconnect delay (ms), doubled after each failed attempt; every wait adds up to 25% at random
backoffMaxnumber30000Where the doubling stops (ms); the random 25% still applies on top
callTimeoutMsnumber30000Default RPC call timeout (ms)
handlerTimeoutMsnumber0 (off)How long a handler may take to answer an incoming call. Past it, the call is answered InternalError, the handler's signal aborts with a TimeoutError, and the error is logged
callConcurrencynumber1Outbound CALLs in flight at once. 1 follows OCPP-J §4.1.1 (Synchronicity): a new CALL waits until the previous one is answered or has timed out. Above 1 only for a CSMS known to cope
maxBadMessagesnumber50Close on the Nth bad message in a row; a valid message resets the count and empty frames are ignored. 0 = zero tolerance, Infinity = no limit.
badMessageWindowMsnumber—Also reset a run of bad messages once it is older than this many ms.
respondWithDetailedErrorsbooleanfalseSend a handler error's properties (not its stack) in CALLERROR details. Logs a warning when on; keep it off in production
idGenerator() => string—Creates the message ID of each outgoing CALL and SEND. A per-call idempotencyKey wins; without either, a random UUID
idValidator(id: string) => boolean—Checks each incoming CALL / SEND message ID; without it any string is accepted

WebSocket compression is not an option here: the browser negotiates it itself, and all major browsers offer it. It is used only when the server accepts it (compression on OCPPServer, off by default).


Middleware

The BrowserOCPPClient supports the exact same MiddlewareStack execution pattern as the Node.js client. This allows you to attach custom interceptors via client.use() directly in the browser:

import {
	BrowserOCPPClient,
	defineRpcMiddleware,
	createLoggingMiddleware,
} from 'ocpp-ws-io/browser';

const client = new BrowserOCPPClient({
	identity: 'CS001',
	endpoint: 'wss://csms.example.com/ocpp',
	protocols: ['ocpp1.6'],
});

// 1. Attach the built-in structured logger (outputs to browser console)
client.use(createLoggingMiddleware(console, client.identity));

// 2. Attach your own middleware: here, how long each message takes.
// Leave the payload as it is: a key its schema does not define fails strict mode.
const browserInterceptor = defineRpcMiddleware(async (ctx, next) => {
	const started = performance.now();
	await next();
	console.log(
		`[Browser] ${ctx.method}: ${Math.round(performance.now() - started)} ms`
	);
});

client.use(browserInterceptor);

Connection Lifecycle

Connection States

BrowserOCPPClient.CONNECTING; // 0
BrowserOCPPClient.OPEN; // 1
BrowserOCPPClient.CLOSING; // 2
BrowserOCPPClient.CLOSED; // 3

// Check current state
if (client.state === BrowserOCPPClient.OPEN) {
	await client.call('Heartbeat', {});
}

Properties

PropertyTypeDescription
client.identitystringCharging station identity
client.protocolstring | undefinedNegotiated OCPP subprotocol
client.stateConnectionStateCurrent connection state

Making Calls

// Version-aware call — fully typed params and response
const result = await client.call('ocpp1.6', 'BootNotification', {
	chargePointVendor: 'VendorX',
	chargePointModel: 'ModelY',
});
result.status; // typed: "Accepted" | "Pending" | "Rejected"

// OCPP 2.0.1 — different shape, still fully typed
const result201 = await client.call('ocpp2.0.1', 'BootNotification', {
	chargingStation: { model: 'ModelX', vendorName: 'VendorY' },
	reason: 'PowerUp',
});

// Short form: typed for the client's `protocols`
const res = await client.call('Heartbeat', {});

// A client configured for several versions: narrow to the negotiated one
const v201 = client.forProtocol('ocpp2.0.1'); // BrowserOCPPClient<"ocpp2.0.1"> | undefined

// An action the types do not know: unchecked(), with the response type you expect
import { unchecked } from 'ocpp-ws-io/browser';

const custom = await client.call<{ result: string }>(
	unchecked('VendorAction'),
	{
		data: 'hello',
	}
);

Call Options

// Timeout
const res = await client.call(
	'Reset',
	{ type: 'Soft' },
	{
		timeoutMs: 5000,
	}
);

// AbortSignal
const controller = new AbortController();
const res2 = await client.call(
	'Heartbeat',
	{},
	{
		signal: controller.signal,
	}
);

// Cancel the call
controller.abort();

Handling Incoming Calls

Register handlers for incoming server-to-client calls. The API is identical to OCPPClient.handle().

// Version-specific handler — only triggers for ocpp1.6
client.handle('ocpp1.6', 'Reset', ({ params }) => {
	params.type; // typed: "Hard" | "Soft" (OCPP 1.6 shape)
	return { status: 'Accepted' };
});

// Generic handler — triggers for any negotiated protocol
client.handle('Reset', ({ params, protocol }) => {
	console.log(`Reset via ${protocol}:`, params.type);
	return { status: 'Accepted' };
});

// Wildcard handler — catches all unhandled methods
client.handle(({ params, method }) => {
	console.log(`Unhandled method: ${method}`);
	return {};
});

Handler Priority

When a call arrives, handlers are checked in this order:

  1. Version-specific handler (e.g., "ocpp1.6:Reset")
  2. Generic handler (e.g., "Reset")
  3. Wildcard handler

NOREPLY

Return NOREPLY from a handler to suppress the automatic response:

import { NOREPLY } from 'ocpp-ws-io/browser';

client.handle('StatusNotification', ({ params }) => {
	// Process but don't send a response
	return NOREPLY;
});

An async handler can return it too, after its work.

Removing Handlers

client.removeHandler('Reset'); // remove generic
client.removeHandler('ocpp1.6', 'Reset'); // remove version-specific
client.removeHandler(); // remove wildcard
client.removeAllHandlers(); // remove all

Events

client.on('open', event => {
	/* WebSocket connected */
});

client.on('close', ({ code, reason }) => {
	/* WebSocket disconnected */
});

client.on('closing', () => {
	/* close() started: no reconnect follows; close fires once the socket has closed */
});

client.on('error', error => {
	/* Connection or WebSocket error */
});

client.on('connecting', ({ url }) => {
	/* Attempting connection */
});

client.on('reconnect', ({ attempt, delay }) => {
	/* Scheduled reconnection */
});

client.on('message', ({ message, direction, ctx }) => {
	/* OCPP message */
	/* message direction */
	/* ctx context form client */
});

client.on('call', call => {
	/* Incoming OCPP Call */
});

client.on('callResult', result => {
	/* Received CallResult */
});

client.on('callError', error => {
	/* Received CallError */
});

client.on('badMessage', ({ message, error }) => {
	/* Malformed message received */
});

Reconnection

BrowserOCPPClient supports automatic reconnection with exponential backoff, identical to OCPPClient. The first wait is backoffMin, it doubles after every failed attempt up to backoffMax, and every wait adds a fresh random amount of up to 25%. A wait is never shorter than backoffMin, and clients that reached backoffMax still spread out after a CSMS restart (OCPP-J 2.0.1 / 2.1 §5.3):

const client = new BrowserOCPPClient({
	identity: 'CP001',
	endpoint: 'wss://csms.example.com/ocpp',
	protocols: ['ocpp1.6'],
	reconnect: true, // default: true
	maxReconnects: 10, // default: Infinity
	backoffMin: 1000, // default: 1000ms
	backoffMax: 30000, // default: 30000ms
});

client.on('reconnect', ({ attempt, delay }) => {
	console.log(`Reconnect attempt ${attempt} in ${delay}ms`);
});

client.on('close', ({ code, reason }) => {
	console.log(`Disconnected: ${code} ${reason}`);
});

Reconfigurable Options

Unlike the constructor which creates a new connection, reconfigure() allows you to change the behavior of the existing client instance. Some options take effect immediately, while others only apply on the next connection attempt.

OptionEffect on Reconfigure
identityApplies on next connect() or reconnect
endpointApplies on next connect() or reconnect
protocolsApplies on next connect() or reconnect
queryApplies on next connect() or reconnect
reconnectImmediate — Cancels pending reconnects if false
maxReconnectsImmediate — Evaluated on next disconnect
backoffMinImmediate — Used for next reconnect calculation
backoffMaxImmediate — Used for next reconnect calculation
callTimeoutMsImmediate — Applies to all future .call()s
handlerTimeoutMsImmediate — Applies to handlers started after it
callConcurrencyImmediate — Dynamically resizes the processing queue
maxBadMessagesImmediate — Applies to future malformed messages
badMessageWindowMsImmediate — Applies to future bad-message windows
respondWithDetailedErrorsImmediate — Applies to future thrown RPC errors

Closing the Connection

// Graceful close (waits for pending calls)
await client.close();

// Custom close code and reason
await client.close({ code: 1000, reason: 'User navigated away' });

// Force close (immediately terminates)
await client.close({ force: true });

// Wait for in-flight calls to complete first
await client.close({ awaitPending: true });

Framework Integration

React

import { useEffect, useRef, useState } from "react";
import { BrowserOCPPClient } from "ocpp-ws-io/browser";

function useOCPP(identity: string, endpoint: string) {
	const clientRef = useRef<BrowserOCPPClient | null>(null);
	const [connected, setConnected] = useState(false);

	useEffect(() => {
		const client = new BrowserOCPPClient({
			identity,
			endpoint,
			protocols: ["ocpp1.6"],
		});

		client.on("open", () => setConnected(true));
		client.on("close", () => setConnected(false));

		client.connect();
		clientRef.current = client;

		return () => {
			client.close();
		};
	}, [identity, endpoint]);

	return { client: clientRef.current, connected };
}

// Usage
function ChargingStation() {
	const { client, connected } = useOCPP(
		"CP001",
		"wss://csms.example.com/ocpp",
	);

	const sendBoot = async () => {
		if (!client) return;
		const res = await client.call("BootNotification", {
			chargePointVendor: "VendorX",
			chargePointModel: "ModelY",
		});
		console.log("Boot:", res.status);
	};

	return (
		<div>
			<p>Status: {connected ? "Connected" : "Disconnected"}</p>
			<button onClick={sendBoot} disabled={!connected}>
				Send Boot
			</button>
		</div>
	);
}

Next.js (Client Component)

'use client';
import { BrowserOCPPClient } from 'ocpp-ws-io/browser';

// ✅ Works in client components — no Node.js dependencies
const client = new BrowserOCPPClient({
	identity: 'CP001',
	endpoint: 'wss://csms.example.com/ocpp',
	protocols: ['ocpp1.6'],
});

Note: OCPPClient from "ocpp-ws-io" cannot be imported in client components — it requires Node.js modules. Use BrowserOCPPClient from "ocpp-ws-io/browser" instead.

Vue

import { ref, onMounted, onUnmounted } from 'vue';
import { BrowserOCPPClient } from 'ocpp-ws-io/browser';

export function useOCPP(identity: string, endpoint: string) {
	const client = ref<BrowserOCPPClient | null>(null);
	const connected = ref(false);

	onMounted(() => {
		const c = new BrowserOCPPClient({
			identity,
			endpoint,
			protocols: ['ocpp1.6'],
		});

		c.on('open', () => (connected.value = true));
		c.on('close', () => (connected.value = false));

		c.connect();
		client.value = c;
	});

	onUnmounted(() => {
		client.value?.close();
	});

	return { client, connected };
}

Browser Exports

All exports available from the ocpp-ws-io/browser subpath:

import {
	// Client
	BrowserOCPPClient,

	// Error classes
	RPCGenericError,
	RPCNotImplementedError,
	RPCNotSupportedError,
	RPCInternalError,
	RPCProtocolError,
	RPCSecurityError,
	RPCFormatViolationError,
	RPCFormationViolationError,
	RPCPropertyConstraintViolationError,
	RPCOccurrenceConstraintViolationError,
	RPCOccurenceConstraintViolationError, // 1.6 spelling, one `r`
	RPCTypeConstraintViolationError,
	RPCMessageTypeNotSupportedError,
	RPCFrameworkError,
	TimeoutError,

	// Utilities
	createRPCError,
	getErrorPlainObject,

	// Middleware Helpers
	defineRpcMiddleware,
	createLoggingMiddleware,

	// Constants
	ConnectionState,
	MessageType,
	NOREPLY,
} from 'ocpp-ws-io/browser';

All types from the main package (OCPPProtocol, AllMethodNames, OCPPRequestType, OCPPResponseType, etc.) are re-exported for full type safety.

On this page