ocpp-ws-io 2.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +397 -0
- package/dist/adapters/redis.d.mts +9 -0
- package/dist/adapters/redis.d.ts +9 -0
- package/dist/adapters/redis.js +423 -0
- package/dist/adapters/redis.js.map +1 -0
- package/dist/adapters/redis.mjs +396 -0
- package/dist/adapters/redis.mjs.map +1 -0
- package/dist/browser.d.mts +5036 -0
- package/dist/browser.d.ts +5036 -0
- package/dist/browser.js +1303 -0
- package/dist/browser.js.map +1 -0
- package/dist/browser.mjs +1284 -0
- package/dist/browser.mjs.map +1 -0
- package/dist/index-BixJj_yJ.d.mts +5325 -0
- package/dist/index-BixJj_yJ.d.ts +5325 -0
- package/dist/index.d.mts +449 -0
- package/dist/index.d.ts +449 -0
- package/dist/index.js +39469 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +39399 -0
- package/dist/index.mjs.map +1 -0
- package/dist/logger.d.mts +1 -0
- package/dist/logger.d.ts +1 -0
- package/dist/logger.js +25 -0
- package/dist/logger.js.map +1 -0
- package/dist/logger.mjs +3 -0
- package/dist/logger.mjs.map +1 -0
- package/package.json +111 -0
package/README.md
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
# ocpp-ws-io
|
|
2
|
+
|
|
3
|
+
> **Type-safe OCPP WebSocket RPC client & server for Node.js.**
|
|
4
|
+
>
|
|
5
|
+
> built with TypeScript — supports OCPP 1.6, 2.0.1, and 2.1 with full JSON schema validation, all security profiles, and clustering support.
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/ocpp-ws-io)
|
|
8
|
+
[](https://github.com/rohittiwari-dev/ocpp-ws-io/blob/main/LICENSE)
|
|
9
|
+
|
|
10
|
+
## 📚 Documentation
|
|
11
|
+
|
|
12
|
+
For full API reference, advanced usage, and guides, visit the **[Official Documentation](https://ocpp-ws-io.rohittiwari.me)**.
|
|
13
|
+
|
|
14
|
+
## ✨ Features
|
|
15
|
+
|
|
16
|
+
- ⚡ **Full OCPP-J RPC** — Compliant message framing
|
|
17
|
+
- 🔒 **Security Profiles 0–3** — Plain WS, Basic Auth, TLS, mTLS
|
|
18
|
+
- 🎯 **Type-Safe** — Auto-generated types for all OCPP messages
|
|
19
|
+
- 📐 **Strict Mode** — Optional JSON schema validation
|
|
20
|
+
- 📡 **Clustering** — Redis adapter support
|
|
21
|
+
- 🌐 **Browser Client** — Zero-dependency client for simulators
|
|
22
|
+
|
|
23
|
+
## 📦 Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install ocpp-ws-io
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## 🚀 Quick Start
|
|
30
|
+
|
|
31
|
+
### Client (Charging Station Simulator)
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { OCPPClient } from "ocpp-ws-io";
|
|
35
|
+
|
|
36
|
+
const client = new OCPPClient({
|
|
37
|
+
endpoint: "ws://localhost:3000",
|
|
38
|
+
identity: "CP001",
|
|
39
|
+
protocols: ["ocpp1.6"],
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
await client.connect();
|
|
43
|
+
|
|
44
|
+
// Fully typed call
|
|
45
|
+
const response = await client.call("ocpp1.6", "BootNotification", {
|
|
46
|
+
chargePointVendor: "VendorX",
|
|
47
|
+
chargePointModel: "ModelY",
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
console.log("Status:", response.status);
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Server (Central System)
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import { OCPPServer } from "ocpp-ws-io";
|
|
57
|
+
|
|
58
|
+
const server = new OCPPServer({
|
|
59
|
+
protocols: ["ocpp1.6", "ocpp2.0.1"],
|
|
60
|
+
logging: { prettify: true, exchangeLog: true, level: "info" },
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Optional: Add authentication ringfence
|
|
64
|
+
server.auth((ctx) => {
|
|
65
|
+
console.log(
|
|
66
|
+
`Connection from ${ctx.handshake.identity} at path ${ctx.handshake.pathname}`,
|
|
67
|
+
);
|
|
68
|
+
ctx.accept({ session: { authorized: true } });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
server.on("client", (client) => {
|
|
72
|
+
console.log(`${client.identity} connected`);
|
|
73
|
+
|
|
74
|
+
// Version-aware handler
|
|
75
|
+
client.handle("ocpp1.6", "BootNotification", ({ params }) => {
|
|
76
|
+
console.log("Boot from:", params.chargePointVendor);
|
|
77
|
+
return {
|
|
78
|
+
status: "Accepted",
|
|
79
|
+
currentTime: new Date().toISOString(),
|
|
80
|
+
interval: 300,
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
await server.listen(3000);
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## ⚙️ Configuration
|
|
89
|
+
|
|
90
|
+
### `OCPPClient` Options
|
|
91
|
+
|
|
92
|
+
| Option | Type | Default | Description |
|
|
93
|
+
| ------------------- | --------------------- | ---------- | --------------------------------------- |
|
|
94
|
+
| `identity` | `string` | _required_ | Charging station ID |
|
|
95
|
+
| `endpoint` | `string` | _required_ | WebSocket URL (`ws://` or `wss://`) |
|
|
96
|
+
| `protocols` | `OCPPProtocol[]` | `[]` | OCPP subprotocols to negotiate |
|
|
97
|
+
| `securityProfile` | `SecurityProfile` | `NONE` | Security profile (0–3) |
|
|
98
|
+
| `password` | `string \| Buffer` | — | Password for Basic Auth (Profile 1 & 2) |
|
|
99
|
+
| `tls` | `TLSOptions` | — | TLS/SSL options (Profile 2 & 3) |
|
|
100
|
+
| `reconnect` | `boolean` | `true` | Auto-reconnect on disconnect |
|
|
101
|
+
| `pingIntervalMs` | `number` | `30000` | Includes ±25% randomized jitter |
|
|
102
|
+
| `strictMode` | `boolean \| string[]` | `false` | Enable/restrict schema validation |
|
|
103
|
+
| `strictModeMethods` | `string[]` | — | Restrict validation to specific methods |
|
|
104
|
+
|
|
105
|
+
### `OCPPServer` Options
|
|
106
|
+
|
|
107
|
+
| Option | Type | Default | Description |
|
|
108
|
+
| -------------------- | ------------------ | --------- | ------------------------------------------ |
|
|
109
|
+
| `protocols` | `OCPPProtocol[]` | `[]` | Accepted OCPP subprotocols |
|
|
110
|
+
| `securityProfile` | `SecurityProfile` | `NONE` | Security profile for auto-created servers |
|
|
111
|
+
| `handshakeTimeoutMs` | `number` | `30000` | Timeout for WebSocket handshake (ms) |
|
|
112
|
+
| `tls` | `TLSOptions` | — | TLS options (Profile 2 & 3) |
|
|
113
|
+
| `logging` | `LoggingConfig` | `true` | Configure built-in logging |
|
|
114
|
+
| `sessionTtlMs` | `number` | `7200000` | Garbage collection inactivity timeout (ms) |
|
|
115
|
+
| `rateLimit` | `RateLimitOptions` | — | Token bucket socket & method rate-limiter |
|
|
116
|
+
|
|
117
|
+
## 🛠️ Advanced Server Configuration
|
|
118
|
+
|
|
119
|
+
### Handshake & Upgrades
|
|
120
|
+
|
|
121
|
+
You can fine-tune how the server handles the WebSocket upgrade process, including timeouts for custom auth logic.
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
const server = new OCPPServer({
|
|
125
|
+
// ...
|
|
126
|
+
handshakeTimeoutMs: 5000, // Timeout if auth callback takes too long (default 30s)
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
server.on("upgradeAborted", ({ identity, reason, socket }) => {
|
|
130
|
+
console.warn(`Handshake aborted for ${identity}: ${reason}`);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
server.on("upgradeError", ({ error, socket }) => {
|
|
134
|
+
console.error("Upgrade failed:", error);
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Server & Router Execution Flow
|
|
139
|
+
|
|
140
|
+
The `OCPPServer` and its internal `OCPPRouter` handle connections and messages in a strict, two-phase execution hierarchy:
|
|
141
|
+
|
|
142
|
+
#### 1. Connection Phase (HTTP Upgrade)
|
|
143
|
+
|
|
144
|
+
Executes before the WebSocket connection is officially accepted.
|
|
145
|
+
|
|
146
|
+
1. **Route Matching (`router.route`)**: The incoming URL is matched against defined patterns.
|
|
147
|
+
2. **Connection Middleware (`router.use`)**: Runs sequentially. Used to extract tokens, inspect headers, or implement early rate-limiting logic.
|
|
148
|
+
3. **Auth Callback (`router.auth`)**: Runs **last** in the HTTP upgrade chain. Used to validate credentials against a database and finally accept/reject the connection.
|
|
149
|
+
|
|
150
|
+
#### 2. Message Phase (WebSocket Open)
|
|
151
|
+
|
|
152
|
+
Executes after the connection is accepted and messages start flowing. 4. **Message Middleware (`client.use` / `server.use`)**: Intercepts every outgoing/incoming message for logging, schema validation, or metric tracking. 5. **Message Handlers (`client.handle` / `server.handle`)**: The **final piece of business logic** where the system reacts to a specific OCPP action (e.g., `BootNotification`).
|
|
153
|
+
|
|
154
|
+
## 📝 Logging
|
|
155
|
+
|
|
156
|
+
ocpp-ws-io comes with **built-in structured logging** via [voltlog-io](https://www.npmjs.com/package/voltlog-io).
|
|
157
|
+
|
|
158
|
+
### Default Behavior
|
|
159
|
+
|
|
160
|
+
By default (`logging: true`), logs are output as structured JSON to `stdout`.
|
|
161
|
+
|
|
162
|
+
```json
|
|
163
|
+
{
|
|
164
|
+
"level": 30,
|
|
165
|
+
"time": 1678900000000,
|
|
166
|
+
"msg": "Client connected",
|
|
167
|
+
"component": "OCPPServer",
|
|
168
|
+
"identity": "CP001"
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Pretty Printing & Exchange Logs
|
|
173
|
+
|
|
174
|
+
Enable `prettify` for development to see colored output with icons.
|
|
175
|
+
Enable `exchangeLog` to log all OCPP messages with direction (`IN`/`OUT`) and metadata.
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
const client = new OCPPClient({
|
|
179
|
+
// ...
|
|
180
|
+
logging: {
|
|
181
|
+
enabled: true,
|
|
182
|
+
prettify: true, // 🌈 Colors & icons
|
|
183
|
+
exchangeLog: true, // ⚡ Log all OCPP messages
|
|
184
|
+
level: "debug", // Default: 'info'
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
**Output:**
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
⚡ CP-101 → BootNotification [OUT]
|
|
193
|
+
✅ CP-101 ← BootNotification [IN] { latencyMs: 45 }
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Custom Logger
|
|
197
|
+
|
|
198
|
+
You can bring your own logger (Pino, Winston, etc.) by implementing `LoggerLike`:
|
|
199
|
+
|
|
200
|
+
````typescript
|
|
201
|
+
```typescript
|
|
202
|
+
import pino from "pino";
|
|
203
|
+
|
|
204
|
+
const client = new OCPPClient({
|
|
205
|
+
logging: {
|
|
206
|
+
handler: pino(), // Use existing logger instance
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
````
|
|
210
|
+
|
|
211
|
+
## 🛡️ Safety & Reliability
|
|
212
|
+
|
|
213
|
+
### Safe Calls (`safeCall`)
|
|
214
|
+
|
|
215
|
+
Perform RPC calls without `try/catch` blocks. Returns the response data on success, or `undefined` on failure while automatically logging the error. You can also pass per-call config options like timeouts.
|
|
216
|
+
|
|
217
|
+
````typescript
|
|
218
|
+
const result = await client.safeCall(
|
|
219
|
+
"ocpp1.6",
|
|
220
|
+
"Heartbeat",
|
|
221
|
+
{},
|
|
222
|
+
{
|
|
223
|
+
timeoutMs: 15000, // Finely control the timeout specifically for this request
|
|
224
|
+
},
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
if (result) {
|
|
228
|
+
// Checked for undefined
|
|
229
|
+
console.log("Heartbeat accepted:", result.currentTime);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
### Unicast Routing (`sendToClient` / `safeSendToClient`) [Server]
|
|
233
|
+
|
|
234
|
+
Send a message to a specific client ID, even if they are connected to a different node in the cluster.
|
|
235
|
+
|
|
236
|
+
You have two options depending on your error-handling preference:
|
|
237
|
+
|
|
238
|
+
#### 1. Standard approach (`sendToClient`)
|
|
239
|
+
Throws an error if the client responds with a `CALLERROR` or if the timeout is reached.
|
|
240
|
+
```typescript
|
|
241
|
+
try {
|
|
242
|
+
const result = await server.sendToClient(
|
|
243
|
+
"CP001",
|
|
244
|
+
"ocpp1.6",
|
|
245
|
+
"GetConfiguration",
|
|
246
|
+
{ key: ["ClockAlignedDataInterval"] },
|
|
247
|
+
{ timeoutMs: 10000 },
|
|
248
|
+
);
|
|
249
|
+
console.log("Configuration:", result);
|
|
250
|
+
} catch (error) {
|
|
251
|
+
console.error("Failed to get configuration:", error);
|
|
252
|
+
}
|
|
253
|
+
````
|
|
254
|
+
|
|
255
|
+
#### 2. Safe approach (`safeSendToClient`)
|
|
256
|
+
|
|
257
|
+
Returns the response on success, or `undefined` on error, automatically logging the failure internally.
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
const result = await server.safeSendToClient(
|
|
261
|
+
"CP001",
|
|
262
|
+
"ocpp1.6",
|
|
263
|
+
"GetConfiguration",
|
|
264
|
+
{ key: ["ClockAlignedDataInterval"] },
|
|
265
|
+
{ timeoutMs: 10000 },
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
if (result) {
|
|
269
|
+
console.log("Configuration:", result);
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### 2. Connection Middleware (Server)
|
|
274
|
+
|
|
275
|
+
For intercepting HTTP WebSocket Upgrade requests before they become an OCPP Client.
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
const rateLimiter = defineMiddleware(async (ctx) => {
|
|
279
|
+
const ip = ctx.handshake.remoteAddress;
|
|
280
|
+
if (isRateLimited(ip)) {
|
|
281
|
+
// Instantly aborts the WebSocket connection with an HTTP 429 status
|
|
282
|
+
ctx.reject(429, "Too Many Requests");
|
|
283
|
+
} else {
|
|
284
|
+
// Or proceed down the execution chain. You can optionally pass an object
|
|
285
|
+
// to next(), which will automatically be shallow-merged into `ctx.state`.
|
|
286
|
+
await ctx.next({ rateLimitRemaining: 99 });
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
server.use(rateLimiter);
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### 3. Advanced Rate Limiting (Token Bucket)
|
|
294
|
+
|
|
295
|
+
To protect your CSMS from Noisy-Neighbor problems or firmware loops (e.g., a charger rapidly spamming `MeterValues`), you can enable the built-in Token Bucket Rate Limiter.
|
|
296
|
+
|
|
297
|
+
The Rate Limiter safely throttles connections without dropping the connection immediately. You can define global limits, method-specific limits, and provide a custom action callback when the limit is breached.
|
|
298
|
+
|
|
299
|
+
```typescript
|
|
300
|
+
const server = new OCPPServer({
|
|
301
|
+
protocols: ["ocpp1.6"],
|
|
302
|
+
rateLimit: {
|
|
303
|
+
limit: 100, // Global limit
|
|
304
|
+
windowMs: 60000, // per 60 seconds
|
|
305
|
+
onLimitExceeded: "disconnect", // or "ignore", or a Custom Callback
|
|
306
|
+
methods: {
|
|
307
|
+
MeterValues: { limit: 10, windowMs: 60000 },
|
|
308
|
+
Heartbeat: { limit: 2, windowMs: 60000 },
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## 🧩 Middleware
|
|
315
|
+
|
|
316
|
+
Intercept and modify OCPP messages using the middleware stack.
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
// Add logging middleware (enabled by default)
|
|
320
|
+
client.use(async (ctx, next) => {
|
|
321
|
+
console.log(`Processing ${ctx.method}`);
|
|
322
|
+
await next();
|
|
323
|
+
});
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
## 📡 Clustering (Redis)
|
|
327
|
+
|
|
328
|
+
Scale your OCPP server across multiple nodes using Redis.
|
|
329
|
+
|
|
330
|
+
1. **Install Adapter**:
|
|
331
|
+
|
|
332
|
+
```bash
|
|
333
|
+
npm install ioredis
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
2. **Configure Server**:
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
import { OCPPServer } from "ocpp-ws-io";
|
|
340
|
+
import { RedisAdapter } from "ocpp-ws-io/adapters/redis";
|
|
341
|
+
import Redis from "ioredis";
|
|
342
|
+
|
|
343
|
+
const redis = new Redis(process.env.REDIS_URL);
|
|
344
|
+
const server = new OCPPServer({
|
|
345
|
+
protocols: ["ocpp1.6"],
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// Uses Redis Streams for clustering reliability
|
|
349
|
+
await server.setAdapter(new RedisAdapter(redis));
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
**Features:**
|
|
353
|
+
|
|
354
|
+
- **Unicast Routing**: Send messages to any client on any node.
|
|
355
|
+
- **Presence**: Track connected clients across the cluster.
|
|
356
|
+
- **Reliability**: Zero message loss during node restarts (via Redis Streams).
|
|
357
|
+
- **Batch Processing**: Use `server.broadcastBatch` to combine multi-node calls effortlessly.
|
|
358
|
+
|
|
359
|
+
### Custom Adapters (`EventAdapterInterface`)
|
|
360
|
+
|
|
361
|
+
You can build custom clustering adapters (e.g., RabbitMQ, Kafka, Postgres PUB/SUB) by implementing the exported `EventAdapterInterface`:
|
|
362
|
+
|
|
363
|
+
```typescript
|
|
364
|
+
import type { EventAdapterInterface } from "ocpp-ws-io";
|
|
365
|
+
|
|
366
|
+
export class CustomAdapter implements EventAdapterInterface {
|
|
367
|
+
async publish(channel: string, data: unknown): Promise<void> {
|
|
368
|
+
/* ... */
|
|
369
|
+
}
|
|
370
|
+
async subscribe(
|
|
371
|
+
channel: string,
|
|
372
|
+
handler: (data: unknown) => void,
|
|
373
|
+
): Promise<void> {
|
|
374
|
+
/* ... */
|
|
375
|
+
}
|
|
376
|
+
async unsubscribe(channel: string): Promise<void> {
|
|
377
|
+
/* ... */
|
|
378
|
+
}
|
|
379
|
+
async disconnect(): Promise<void> {
|
|
380
|
+
/* ... */
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Optional primitives for advanced routing:
|
|
384
|
+
async setPresence?(
|
|
385
|
+
identity: string,
|
|
386
|
+
nodeId: string,
|
|
387
|
+
ttl: number,
|
|
388
|
+
): Promise<void>;
|
|
389
|
+
async getPresence?(identity: string): Promise<string | null>;
|
|
390
|
+
async removePresence?(identity: string): Promise<void>;
|
|
391
|
+
async getPresenceBatch?(identities: string[]): Promise<(string | null)[]>;
|
|
392
|
+
async publishBatch?(
|
|
393
|
+
messages: { channel: string; data: unknown }[],
|
|
394
|
+
): Promise<void>;
|
|
395
|
+
async metrics?(): Promise<Record<string, unknown>>;
|
|
396
|
+
}
|
|
397
|
+
```
|