ocpp-protocol-proxy 0.1.1
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 +196 -0
- package/dist/index.d.ts +251 -0
- package/dist/index.js +871 -0
- package/package.json +76 -0
package/README.md
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/rohittiwari-dev/ocpp-ws-io/main/assets/ocpp-protocol-proxy.png" alt="ocpp-protocol-proxy" width="420" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
**Transport-agnostic OCPP version translation proxy** — translate any OCPP version to any other, with pluggable middleware, stateful session management, and spec-compliant presets.
|
|
7
|
+
|
|
8
|
+
[](../../LICENSE)
|
|
9
|
+
[](https://www.typescriptlang.org)
|
|
10
|
+
|
|
11
|
+
Part of the [ocpp-ws-io](https://ocpp-ws-io.rohittiwari.me) ecosystem.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Why?
|
|
16
|
+
|
|
17
|
+
Legacy OCPP 1.6 charge points can't speak to modern OCPP 2.1 central systems. Instead of rewriting firmware or maintaining dual-protocol backends, drop a translation proxy in between.
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
┌─────────┐ OCPP 1.6 ┌───────────┐ OCPP 2.1 ┌──────┐
|
|
21
|
+
│ EVSE │ ───────────────── │ PROXY │ ───────────────── │ CSMS │
|
|
22
|
+
│ (1.6) │ │ (translate)│ │(2.1) │
|
|
23
|
+
└─────────┘ └───────────┘ └──────┘
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Features
|
|
27
|
+
|
|
28
|
+
- ⚡ **Any-to-any translation** — 1.6 ↔ 2.0.1 ↔ 2.1
|
|
29
|
+
- 🔌 **Transport agnostic** — Core logic doesn't depend on WebSockets or Node.js
|
|
30
|
+
- 🧩 **Modular presets** — Import only the OCPP profiles you need
|
|
31
|
+
- 🔗 **Middleware pipeline** — Pre/post translation hooks for logging, validation, telemetry
|
|
32
|
+
- 💾 **Stateful sessions** — UUID ↔ integer transaction ID mapping across messages
|
|
33
|
+
- 📊 **Built-in telemetry** — Latency tracking middleware included
|
|
34
|
+
- ✅ **All 28 OCPP 1.6 messages** — Complete Core + optional profiles mapped to 2.1
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install ocpp-protocol-proxy
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quick Start
|
|
43
|
+
|
|
44
|
+
### Basic Usage with Presets
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import { OCPPProtocolProxy, presets, OcppWsIoAdapter } from "ocpp-protocol-proxy";
|
|
48
|
+
|
|
49
|
+
const proxy = new OCPPProtocolProxy({
|
|
50
|
+
upstreamEndpoint: "ws://your-csms:9000",
|
|
51
|
+
upstreamProtocol: "ocpp2.1",
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Load all OCPP 1.6 → 2.1 translation presets
|
|
55
|
+
proxy.translate(presets.ocpp16_to_ocpp21);
|
|
56
|
+
|
|
57
|
+
const adapter = new OcppWsIoAdapter({
|
|
58
|
+
port: 9001,
|
|
59
|
+
protocols: ["ocpp1.6"],
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
await proxy.listenOnAdapter(adapter);
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Custom Overrides
|
|
66
|
+
|
|
67
|
+
Use presets as a base and override specific actions:
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
proxy.translate({
|
|
71
|
+
upstream: {
|
|
72
|
+
...presets.ocpp16_to_ocpp21.upstream,
|
|
73
|
+
|
|
74
|
+
// Override with custom business logic
|
|
75
|
+
"ocpp1.6:StartTransaction": async (params, ctx) => {
|
|
76
|
+
console.log(`Custom StartTx for ${ctx.identity}`, params);
|
|
77
|
+
return {
|
|
78
|
+
action: "TransactionEvent",
|
|
79
|
+
payload: { /* your custom 2.1 mapping */ },
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
downstream: { ...presets.ocpp16_to_ocpp21.downstream },
|
|
84
|
+
responses: { ...presets.ocpp16_to_ocpp21.responses },
|
|
85
|
+
errors: { ...presets.ocpp16_to_ocpp21.errors },
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Selective Presets
|
|
90
|
+
|
|
91
|
+
Import only the OCPP profiles you need for tree-shaking:
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import {
|
|
95
|
+
corePreset,
|
|
96
|
+
smartChargingPreset,
|
|
97
|
+
firmwarePreset,
|
|
98
|
+
reservationPreset,
|
|
99
|
+
localAuthPreset,
|
|
100
|
+
} from "ocpp-protocol-proxy";
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
| Preset | Profile | Messages |
|
|
104
|
+
|:---|:---|:---|
|
|
105
|
+
| `corePreset` | Core (mandatory) | 16: Boot, Auth, Start/Stop Tx, MeterValues, StatusNotification, Reset, Unlock, TriggerMessage, and more |
|
|
106
|
+
| `smartChargingPreset` | Smart Charging | SetChargingProfile, ClearChargingProfile, GetCompositeSchedule |
|
|
107
|
+
| `firmwarePreset` | Firmware Mgmt | UpdateFirmware, FirmwareStatusNotification, GetLog→GetDiagnostics |
|
|
108
|
+
| `reservationPreset` | Reservation | ReserveNow, CancelReservation |
|
|
109
|
+
| `localAuthPreset` | Local Auth List | GetLocalListVersion, SendLocalList |
|
|
110
|
+
|
|
111
|
+
## Middleware
|
|
112
|
+
|
|
113
|
+
Intercept messages before or after translation:
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
import type { ProxyMiddleware } from "ocpp-protocol-proxy";
|
|
117
|
+
|
|
118
|
+
const logger: ProxyMiddleware = async (message, context, direction, phase) => {
|
|
119
|
+
console.log(`[${phase}] ${direction} — ${context.identity}`, message);
|
|
120
|
+
return undefined; // pass through unchanged
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const proxy = new OCPPProtocolProxy({
|
|
124
|
+
upstreamEndpoint: "ws://csms:9000",
|
|
125
|
+
upstreamProtocol: "ocpp2.1",
|
|
126
|
+
middlewares: [logger],
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Middleware runs at 4 lifecycle points:
|
|
131
|
+
| Phase | Direction | When |
|
|
132
|
+
|:---|:---|:---|
|
|
133
|
+
| `pre` | `upstream` | Before translating EVSE→CSMS calls |
|
|
134
|
+
| `post` | `upstream` | After translating, before forwarding to CSMS |
|
|
135
|
+
| `pre` | `response` | Before translating CSMS response back |
|
|
136
|
+
| `post` | `response` | After translating, before returning to EVSE |
|
|
137
|
+
|
|
138
|
+
## Custom Session Store
|
|
139
|
+
|
|
140
|
+
The default `InMemorySessionStore` works for single-instance deployments. For clustered setups, implement `ISessionStore`:
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
import type { ISessionStore } from "ocpp-protocol-proxy";
|
|
144
|
+
|
|
145
|
+
class RedisSessionStore implements ISessionStore {
|
|
146
|
+
async set(identity: string, key: string, value: any) { /* ... */ }
|
|
147
|
+
async get<T>(identity: string, key: string): Promise<T | undefined> { /* ... */ }
|
|
148
|
+
async delete(identity: string, key: string) { /* ... */ }
|
|
149
|
+
async clear(identity: string) { /* ... */ }
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Events
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
proxy.on("connection", (identity, protocol) => { /* EVSE connected */ });
|
|
157
|
+
proxy.on("disconnect", (identity) => { /* EVSE disconnected */ });
|
|
158
|
+
proxy.on("translationError", (err, msg, ctx) => { /* translation failed */ });
|
|
159
|
+
proxy.on("middlewareError", (err, msg, ctx) => { /* middleware threw */ });
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Architecture
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
src/
|
|
166
|
+
├── core/
|
|
167
|
+
│ ├── types.ts # OCPPMessage, TranslationMap, ITransportAdapter
|
|
168
|
+
│ ├── translator.ts # Pure translation engine
|
|
169
|
+
│ └── session.ts # ISessionStore + InMemorySessionStore
|
|
170
|
+
├── presets/
|
|
171
|
+
│ ├── index.ts # Merged preset + mergePresets utility
|
|
172
|
+
│ ├── core.ts # Core profile (16 messages)
|
|
173
|
+
│ ├── smart-charging.ts # Smart Charging (3 messages)
|
|
174
|
+
│ ├── firmware.ts # Firmware Management (4 messages)
|
|
175
|
+
│ ├── reservation.ts # Reservation (2 messages)
|
|
176
|
+
│ ├── local-auth.ts # Local Auth List (2 messages)
|
|
177
|
+
│ └── status-enums.ts # StatusNotification enum mapping tables
|
|
178
|
+
├── adapters/
|
|
179
|
+
│ └── ocpp-ws-io.adapter.ts # WebSocket adapter using ocpp-ws-io
|
|
180
|
+
├── middlewares/
|
|
181
|
+
│ └── telemetry.ts # Latency tracking middleware
|
|
182
|
+
├── proxy.ts # OCPPProtocolProxy orchestrator
|
|
183
|
+
└── index.ts # Public API
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Related Packages
|
|
187
|
+
|
|
188
|
+
| Package | Description |
|
|
189
|
+
|:---|:---|
|
|
190
|
+
| [ocpp-ws-io](https://npmjs.com/ocpp-ws-io) | Core OCPP WebSocket RPC client & server |
|
|
191
|
+
| [ocpp-ws-cli](https://npmjs.com/ocpp-ws-cli) | CLI for simulation & testing |
|
|
192
|
+
| [voltlog-io](https://npmjs.com/voltlog-io) | Structured logger |
|
|
193
|
+
|
|
194
|
+
## License
|
|
195
|
+
|
|
196
|
+
[MIT](../../LICENSE) © 2026 Rohit Tiwari
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { OCPPClient } from 'ocpp-ws-io';
|
|
2
|
+
import { EventEmitter } from 'node:events';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A generic interface for storing session state per connection/identity
|
|
6
|
+
* across asynchronous OCPP messages.
|
|
7
|
+
* E.g., mapping a numeric 1.6 transactionId to a 2.1 UUID string.
|
|
8
|
+
*/
|
|
9
|
+
interface ISessionStore {
|
|
10
|
+
/**
|
|
11
|
+
* Set a key-value pair tied to a specific identity's session.
|
|
12
|
+
*/
|
|
13
|
+
set(identity: string, key: string, value: any): Promise<void>;
|
|
14
|
+
/**
|
|
15
|
+
* Retrieve a value tied to a specific identity's session.
|
|
16
|
+
*/
|
|
17
|
+
get<T = any>(identity: string, key: string): Promise<T | undefined>;
|
|
18
|
+
/**
|
|
19
|
+
* Delete a key tied to a specific identity's session.
|
|
20
|
+
*/
|
|
21
|
+
delete(identity: string, key: string): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Clear all session data for a specific identity (e.g. on disconnect).
|
|
24
|
+
*/
|
|
25
|
+
clear(identity: string): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
declare class InMemorySessionStore implements ISessionStore {
|
|
28
|
+
private store;
|
|
29
|
+
set(identity: string, key: string, value: any): Promise<void>;
|
|
30
|
+
get<T = any>(identity: string, key: string): Promise<T | undefined>;
|
|
31
|
+
delete(identity: string, key: string): Promise<void>;
|
|
32
|
+
clear(identity: string): Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
declare enum MessageType {
|
|
36
|
+
CALL = 2,
|
|
37
|
+
CALLRESULT = 3,
|
|
38
|
+
CALLERROR = 4
|
|
39
|
+
}
|
|
40
|
+
type OCPPMessage = {
|
|
41
|
+
type: MessageType.CALL;
|
|
42
|
+
messageId: string;
|
|
43
|
+
action: string;
|
|
44
|
+
payload: any;
|
|
45
|
+
} | {
|
|
46
|
+
type: MessageType.CALLRESULT;
|
|
47
|
+
messageId: string;
|
|
48
|
+
payload: any;
|
|
49
|
+
} | {
|
|
50
|
+
type: MessageType.CALLERROR;
|
|
51
|
+
messageId: string;
|
|
52
|
+
errorCode: string;
|
|
53
|
+
errorDescription: string;
|
|
54
|
+
errorDetails: any;
|
|
55
|
+
};
|
|
56
|
+
interface TranslationContext {
|
|
57
|
+
identity: string;
|
|
58
|
+
sourceProtocol: string;
|
|
59
|
+
targetProtocol: string;
|
|
60
|
+
session: ISessionStore;
|
|
61
|
+
}
|
|
62
|
+
type TranslationResult = {
|
|
63
|
+
action?: string;
|
|
64
|
+
payload: any;
|
|
65
|
+
};
|
|
66
|
+
type MiddlewarePhase = "pre" | "post";
|
|
67
|
+
type MiddlewareDirection = "upstream" | "downstream" | "response" | "error";
|
|
68
|
+
/**
|
|
69
|
+
* Middleware function signature.
|
|
70
|
+
* Return the (possibly mutated) message to pass it along,
|
|
71
|
+
* or return undefined to pass the original message unchanged.
|
|
72
|
+
*/
|
|
73
|
+
type ProxyMiddleware = (message: OCPPMessage, context: TranslationContext, direction: MiddlewareDirection, phase: MiddlewarePhase) => Promise<OCPPMessage | undefined>;
|
|
74
|
+
type TranslationMap = {
|
|
75
|
+
/** EVSE -> CSMS call mappers, keyed by `sourceProtocol:Action` */
|
|
76
|
+
upstream: Record<string, (params: any, context: TranslationContext) => TranslationResult | Promise<TranslationResult>>;
|
|
77
|
+
/** CSMS -> EVSE call mappers, keyed by `targetProtocol:Action` */
|
|
78
|
+
downstream: Record<string, (params: any, context: TranslationContext) => TranslationResult | Promise<TranslationResult>>;
|
|
79
|
+
/** Response payload mappers, keyed by `targetProtocol:ActionResponse` */
|
|
80
|
+
responses?: Record<string, (params: any, context: TranslationContext) => any | Promise<any>>;
|
|
81
|
+
/** Error mappers, keyed by `sourceProtocol:Error` */
|
|
82
|
+
errors?: Record<string, (errorCode: string, errorDescription: string, errorDetails: any, context: TranslationContext) => {
|
|
83
|
+
errorCode: string;
|
|
84
|
+
errorDescription: string;
|
|
85
|
+
errorDetails: any;
|
|
86
|
+
} | Promise<{
|
|
87
|
+
errorCode: string;
|
|
88
|
+
errorDescription: string;
|
|
89
|
+
errorDetails: any;
|
|
90
|
+
}>>;
|
|
91
|
+
};
|
|
92
|
+
interface IConnection {
|
|
93
|
+
identity: string;
|
|
94
|
+
protocol: string;
|
|
95
|
+
send(message: OCPPMessage): Promise<OCPPMessage | undefined>;
|
|
96
|
+
onMessage(handler: (message: OCPPMessage) => Promise<OCPPMessage | undefined>): void;
|
|
97
|
+
onClose(handler: () => void): void;
|
|
98
|
+
}
|
|
99
|
+
interface ITransportAdapter {
|
|
100
|
+
listen(onConnection: (connection: IConnection) => void): Promise<void>;
|
|
101
|
+
close(): Promise<void>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
declare class OcppWsIoConnection implements IConnection {
|
|
105
|
+
private client;
|
|
106
|
+
identity: string;
|
|
107
|
+
protocol: string;
|
|
108
|
+
private messageHandler?;
|
|
109
|
+
constructor(client: OCPPClient);
|
|
110
|
+
private setupCatchAll;
|
|
111
|
+
send(message: OCPPMessage): Promise<OCPPMessage | undefined>;
|
|
112
|
+
onMessage(handler: (message: OCPPMessage) => Promise<OCPPMessage | undefined>): void;
|
|
113
|
+
onClose(handler: () => void): void;
|
|
114
|
+
}
|
|
115
|
+
interface WsAdapterOptions {
|
|
116
|
+
port: number;
|
|
117
|
+
protocols: string[];
|
|
118
|
+
}
|
|
119
|
+
declare class OcppWsIoAdapter implements ITransportAdapter {
|
|
120
|
+
private server;
|
|
121
|
+
private port;
|
|
122
|
+
private protocols;
|
|
123
|
+
httpServer?: any;
|
|
124
|
+
constructor(options: WsAdapterOptions);
|
|
125
|
+
listen(onConnection: (connection: IConnection) => void): Promise<void>;
|
|
126
|
+
close(): Promise<void>;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
declare class OCPPTranslator {
|
|
130
|
+
private translationMap;
|
|
131
|
+
constructor(translationMap: TranslationMap);
|
|
132
|
+
updateMap(map: Partial<TranslationMap>): void;
|
|
133
|
+
translateUpstreamCall(message: Extract<OCPPMessage, {
|
|
134
|
+
type: MessageType.CALL;
|
|
135
|
+
}>, context: TranslationContext): Promise<Extract<OCPPMessage, {
|
|
136
|
+
type: MessageType.CALL;
|
|
137
|
+
}>>;
|
|
138
|
+
translateDownstreamCall(message: Extract<OCPPMessage, {
|
|
139
|
+
type: MessageType.CALL;
|
|
140
|
+
}>, context: TranslationContext): Promise<Extract<OCPPMessage, {
|
|
141
|
+
type: MessageType.CALL;
|
|
142
|
+
}>>;
|
|
143
|
+
translateCallResult(message: Extract<OCPPMessage, {
|
|
144
|
+
type: MessageType.CALLRESULT;
|
|
145
|
+
}>, originalAction: string, context: TranslationContext): Promise<Extract<OCPPMessage, {
|
|
146
|
+
type: MessageType.CALLRESULT;
|
|
147
|
+
}>>;
|
|
148
|
+
translateCallError(message: Extract<OCPPMessage, {
|
|
149
|
+
type: MessageType.CALLERROR;
|
|
150
|
+
}>, context: TranslationContext): Promise<Extract<OCPPMessage, {
|
|
151
|
+
type: MessageType.CALLERROR;
|
|
152
|
+
}>>;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Example built-in middleware that tracks translation latency.
|
|
157
|
+
* In production, push metrics to Datadog, Prometheus, or OpenTelemetry.
|
|
158
|
+
*/
|
|
159
|
+
declare const TelemetryMiddleware: ProxyMiddleware;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Core profile preset — the only mandatory OCPP 1.6 profile.
|
|
163
|
+
* Covers all 16 Core messages.
|
|
164
|
+
*/
|
|
165
|
+
declare const corePreset: Partial<TranslationMap>;
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Firmware Management profile preset.
|
|
169
|
+
* Note: OCPP 2.1 renames GetDiagnostics→GetLog and
|
|
170
|
+
* DiagnosticsStatusNotification→LogStatusNotification.
|
|
171
|
+
*/
|
|
172
|
+
declare const firmwarePreset: Partial<TranslationMap>;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Local Auth List Management profile preset.
|
|
176
|
+
*/
|
|
177
|
+
declare const localAuthPreset: Partial<TranslationMap>;
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Reservation profile preset.
|
|
181
|
+
*/
|
|
182
|
+
declare const reservationPreset: Partial<TranslationMap>;
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Smart Charging profile preset.
|
|
186
|
+
* Covers: SetChargingProfile, ClearChargingProfile, GetCompositeSchedule.
|
|
187
|
+
*/
|
|
188
|
+
declare const smartChargingPreset: Partial<TranslationMap>;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* OCPP 1.6 status → OCPP 2.1 connectorStatus enum mapping.
|
|
192
|
+
* Per OCPP 2.0.1 Part 2, Section K.10 — ConnectorStatusEnumType.
|
|
193
|
+
*/
|
|
194
|
+
declare const statusMap16to21: Record<string, string>;
|
|
195
|
+
/**
|
|
196
|
+
* OCPP 2.1 connectorStatus → OCPP 1.6 status mapping (best-effort reverse).
|
|
197
|
+
* 2.1 has fewer granular statuses, so some information is lost.
|
|
198
|
+
*/
|
|
199
|
+
declare const statusMap21to16: Record<string, string>;
|
|
200
|
+
/**
|
|
201
|
+
* OCPP 2.1 operationalStatus → OCPP 1.6 availability type mapping.
|
|
202
|
+
*/
|
|
203
|
+
declare const availabilityMap21to16: Record<string, string>;
|
|
204
|
+
/**
|
|
205
|
+
* OCPP 1.6 availability type → OCPP 2.1 operationalStatus mapping.
|
|
206
|
+
*/
|
|
207
|
+
declare const availabilityMap16to21: Record<string, string>;
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Combined preset dictionary.
|
|
211
|
+
* `presets.ocpp16_to_ocpp21` includes ALL profiles merged.
|
|
212
|
+
*
|
|
213
|
+
* For selective use, import individual presets:
|
|
214
|
+
* ```ts
|
|
215
|
+
* import { corePreset, smartChargingPreset } from "ocpp-protocol-proxy";
|
|
216
|
+
* proxy.translate(corePreset);
|
|
217
|
+
* proxy.translate(smartChargingPreset);
|
|
218
|
+
* ```
|
|
219
|
+
*/
|
|
220
|
+
declare const presets: {
|
|
221
|
+
ocpp16_to_ocpp21: TranslationMap;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
interface OCPPProtocolProxyOptions {
|
|
225
|
+
upstreamEndpoint: string;
|
|
226
|
+
upstreamProtocol: string;
|
|
227
|
+
sessionStore?: ISessionStore;
|
|
228
|
+
middlewares?: ProxyMiddleware[];
|
|
229
|
+
}
|
|
230
|
+
declare class OCPPProtocolProxy extends EventEmitter {
|
|
231
|
+
private options;
|
|
232
|
+
private translator;
|
|
233
|
+
private clients;
|
|
234
|
+
private sessionStore;
|
|
235
|
+
private adapters;
|
|
236
|
+
constructor(options: OCPPProtocolProxyOptions);
|
|
237
|
+
/** Register translation maps (can be called multiple times to layer presets). */
|
|
238
|
+
translate(map: Partial<TranslationMap>): void;
|
|
239
|
+
/** Start listening on a transport adapter (WS, HTTP, etc.). */
|
|
240
|
+
listenOnAdapter(adapter: ITransportAdapter): Promise<void>;
|
|
241
|
+
/**
|
|
242
|
+
* Execute all registered middlewares sequentially.
|
|
243
|
+
* If a middleware returns a message, it replaces the current message.
|
|
244
|
+
*/
|
|
245
|
+
private executeMiddlewares;
|
|
246
|
+
private handleNewConnection;
|
|
247
|
+
/** Gracefully close all upstream connections and adapters. */
|
|
248
|
+
close(): Promise<void>;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export { type IConnection, type ISessionStore, type ITransportAdapter, InMemorySessionStore, MessageType, type MiddlewareDirection, type MiddlewarePhase, type OCPPMessage, OCPPProtocolProxy, type OCPPProtocolProxyOptions, OCPPTranslator, OcppWsIoAdapter, OcppWsIoConnection, type ProxyMiddleware, TelemetryMiddleware, type TranslationContext, type TranslationMap, type TranslationResult, type WsAdapterOptions, availabilityMap16to21, availabilityMap21to16, corePreset, firmwarePreset, localAuthPreset, presets, reservationPreset, smartChargingPreset, statusMap16to21, statusMap21to16 };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,871 @@
|
|
|
1
|
+
// src/core/types.ts
|
|
2
|
+
var MessageType = /* @__PURE__ */ ((MessageType2) => {
|
|
3
|
+
MessageType2[MessageType2["CALL"] = 2] = "CALL";
|
|
4
|
+
MessageType2[MessageType2["CALLRESULT"] = 3] = "CALLRESULT";
|
|
5
|
+
MessageType2[MessageType2["CALLERROR"] = 4] = "CALLERROR";
|
|
6
|
+
return MessageType2;
|
|
7
|
+
})(MessageType || {});
|
|
8
|
+
|
|
9
|
+
// src/adapters/ocpp-ws-io.adapter.ts
|
|
10
|
+
var OcppWsIoConnection = class {
|
|
11
|
+
constructor(client) {
|
|
12
|
+
this.client = client;
|
|
13
|
+
this.identity = client.identity;
|
|
14
|
+
this.protocol = client.protocol || "ocpp1.6";
|
|
15
|
+
this.setupCatchAll();
|
|
16
|
+
}
|
|
17
|
+
identity;
|
|
18
|
+
protocol;
|
|
19
|
+
messageHandler;
|
|
20
|
+
setupCatchAll() {
|
|
21
|
+
this.client.handle(async (action, ctx) => {
|
|
22
|
+
const incomingMessage = {
|
|
23
|
+
type: 2 /* CALL */,
|
|
24
|
+
messageId: ctx.messageId || `msg-${Date.now()}`,
|
|
25
|
+
action,
|
|
26
|
+
payload: ctx.params
|
|
27
|
+
};
|
|
28
|
+
if (!this.messageHandler) {
|
|
29
|
+
throw new Error("No message handler attached to connection");
|
|
30
|
+
}
|
|
31
|
+
const result = await this.messageHandler(incomingMessage);
|
|
32
|
+
if (result) {
|
|
33
|
+
if (result.type === 3 /* CALLRESULT */) {
|
|
34
|
+
return result.payload;
|
|
35
|
+
}
|
|
36
|
+
if (result.type === 4 /* CALLERROR */) {
|
|
37
|
+
const err = new Error(
|
|
38
|
+
result.errorDescription || "Unknown error occurred during proxy processing"
|
|
39
|
+
);
|
|
40
|
+
err.code = result.errorCode;
|
|
41
|
+
throw err;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return {};
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
async send(message) {
|
|
48
|
+
if (message.type === 2 /* CALL */) {
|
|
49
|
+
const rawResult = await this.client.call(message.action, message.payload);
|
|
50
|
+
return {
|
|
51
|
+
type: 3 /* CALLRESULT */,
|
|
52
|
+
messageId: message.messageId,
|
|
53
|
+
payload: rawResult
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return void 0;
|
|
57
|
+
}
|
|
58
|
+
onMessage(handler) {
|
|
59
|
+
this.messageHandler = handler;
|
|
60
|
+
}
|
|
61
|
+
onClose(handler) {
|
|
62
|
+
this.client.on("close", handler);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
var OcppWsIoAdapter = class {
|
|
66
|
+
server;
|
|
67
|
+
port;
|
|
68
|
+
protocols;
|
|
69
|
+
httpServer;
|
|
70
|
+
constructor(options) {
|
|
71
|
+
this.port = options.port;
|
|
72
|
+
this.protocols = options.protocols;
|
|
73
|
+
}
|
|
74
|
+
async listen(onConnection) {
|
|
75
|
+
const { OCPPServer } = await import("ocpp-ws-io");
|
|
76
|
+
this.server = new OCPPServer({ protocols: this.protocols });
|
|
77
|
+
this.server.on("client", (client) => {
|
|
78
|
+
const conn = new OcppWsIoConnection(client);
|
|
79
|
+
onConnection(conn);
|
|
80
|
+
});
|
|
81
|
+
this.httpServer = await this.server.listen(this.port);
|
|
82
|
+
}
|
|
83
|
+
async close() {
|
|
84
|
+
if (this.server) {
|
|
85
|
+
await this.server.close();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// src/core/session.ts
|
|
91
|
+
var InMemorySessionStore = class {
|
|
92
|
+
// Map<identity, Map<key, value>>
|
|
93
|
+
store = /* @__PURE__ */ new Map();
|
|
94
|
+
async set(identity, key, value) {
|
|
95
|
+
if (!this.store.has(identity)) {
|
|
96
|
+
this.store.set(identity, /* @__PURE__ */ new Map());
|
|
97
|
+
}
|
|
98
|
+
this.store.get(identity).set(key, value);
|
|
99
|
+
}
|
|
100
|
+
async get(identity, key) {
|
|
101
|
+
const session = this.store.get(identity);
|
|
102
|
+
return session ? session.get(key) : void 0;
|
|
103
|
+
}
|
|
104
|
+
async delete(identity, key) {
|
|
105
|
+
const session = this.store.get(identity);
|
|
106
|
+
if (session) {
|
|
107
|
+
session.delete(key);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async clear(identity) {
|
|
111
|
+
this.store.delete(identity);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// src/core/translator.ts
|
|
116
|
+
var OCPPTranslator = class {
|
|
117
|
+
constructor(translationMap) {
|
|
118
|
+
this.translationMap = translationMap;
|
|
119
|
+
}
|
|
120
|
+
updateMap(map) {
|
|
121
|
+
this.translationMap.upstream = {
|
|
122
|
+
...this.translationMap.upstream,
|
|
123
|
+
...map.upstream
|
|
124
|
+
};
|
|
125
|
+
this.translationMap.downstream = {
|
|
126
|
+
...this.translationMap.downstream,
|
|
127
|
+
...map.downstream
|
|
128
|
+
};
|
|
129
|
+
if (map.responses) {
|
|
130
|
+
this.translationMap.responses = {
|
|
131
|
+
...this.translationMap.responses,
|
|
132
|
+
...map.responses
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (map.errors) {
|
|
136
|
+
this.translationMap.errors = {
|
|
137
|
+
...this.translationMap.errors,
|
|
138
|
+
...map.errors
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async translateUpstreamCall(message, context) {
|
|
143
|
+
const key = `${context.sourceProtocol}:${message.action}`;
|
|
144
|
+
const mapper = this.translationMap.upstream[key];
|
|
145
|
+
if (!mapper) {
|
|
146
|
+
return message;
|
|
147
|
+
}
|
|
148
|
+
const translated = await mapper(message.payload, context);
|
|
149
|
+
return {
|
|
150
|
+
type: 2 /* CALL */,
|
|
151
|
+
messageId: message.messageId,
|
|
152
|
+
action: translated.action || message.action,
|
|
153
|
+
payload: translated.payload
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
async translateDownstreamCall(message, context) {
|
|
157
|
+
const key = `${context.targetProtocol}:${message.action}`;
|
|
158
|
+
const mapper = this.translationMap.downstream[key];
|
|
159
|
+
if (!mapper) {
|
|
160
|
+
return message;
|
|
161
|
+
}
|
|
162
|
+
const translated = await mapper(message.payload, context);
|
|
163
|
+
return {
|
|
164
|
+
type: 2 /* CALL */,
|
|
165
|
+
messageId: message.messageId,
|
|
166
|
+
action: translated.action || message.action,
|
|
167
|
+
payload: translated.payload
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
async translateCallResult(message, originalAction, context) {
|
|
171
|
+
const responseKey = `${context.targetProtocol}:${originalAction}Response`;
|
|
172
|
+
const responseMapper = this.translationMap.responses?.[responseKey];
|
|
173
|
+
if (responseMapper) {
|
|
174
|
+
const translatedPayload = await responseMapper(message.payload, context);
|
|
175
|
+
return {
|
|
176
|
+
type: 3 /* CALLRESULT */,
|
|
177
|
+
messageId: message.messageId,
|
|
178
|
+
payload: translatedPayload
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
return message;
|
|
182
|
+
}
|
|
183
|
+
async translateCallError(message, context) {
|
|
184
|
+
const errorKey = `${context.sourceProtocol}:Error`;
|
|
185
|
+
const errorMapper = this.translationMap.errors?.[errorKey];
|
|
186
|
+
if (errorMapper) {
|
|
187
|
+
const translated = await errorMapper(
|
|
188
|
+
message.errorCode,
|
|
189
|
+
message.errorDescription,
|
|
190
|
+
message.errorDetails,
|
|
191
|
+
context
|
|
192
|
+
);
|
|
193
|
+
return {
|
|
194
|
+
type: 4 /* CALLERROR */,
|
|
195
|
+
messageId: message.messageId,
|
|
196
|
+
errorCode: translated.errorCode,
|
|
197
|
+
errorDescription: translated.errorDescription,
|
|
198
|
+
errorDetails: translated.errorDetails
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
return message;
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// src/middlewares/telemetry.ts
|
|
206
|
+
var TelemetryMiddleware = async (message, context, _direction, phase) => {
|
|
207
|
+
if (phase === "pre") {
|
|
208
|
+
await context.session.set(
|
|
209
|
+
context.identity,
|
|
210
|
+
`telemetryStart_${message.messageId}`,
|
|
211
|
+
Date.now()
|
|
212
|
+
);
|
|
213
|
+
} else if (phase === "post") {
|
|
214
|
+
const startTime = await context.session.get(
|
|
215
|
+
context.identity,
|
|
216
|
+
`telemetryStart_${message.messageId}`
|
|
217
|
+
);
|
|
218
|
+
if (startTime) {
|
|
219
|
+
const _latency = Date.now() - startTime;
|
|
220
|
+
await context.session.delete(
|
|
221
|
+
context.identity,
|
|
222
|
+
`telemetryStart_${message.messageId}`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return void 0;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// src/presets/core.ts
|
|
230
|
+
import { randomUUID } from "crypto";
|
|
231
|
+
|
|
232
|
+
// src/presets/status-enums.ts
|
|
233
|
+
var statusMap16to21 = {
|
|
234
|
+
Available: "Available",
|
|
235
|
+
Preparing: "Occupied",
|
|
236
|
+
Charging: "Occupied",
|
|
237
|
+
SuspendedEVSE: "Occupied",
|
|
238
|
+
SuspendedEV: "Occupied",
|
|
239
|
+
Finishing: "Occupied",
|
|
240
|
+
Reserved: "Reserved",
|
|
241
|
+
Unavailable: "Unavailable",
|
|
242
|
+
Faulted: "Faulted"
|
|
243
|
+
};
|
|
244
|
+
var statusMap21to16 = {
|
|
245
|
+
Available: "Available",
|
|
246
|
+
Occupied: "Charging",
|
|
247
|
+
// Best guess — could be Preparing/SuspendedEV/etc.
|
|
248
|
+
Reserved: "Reserved",
|
|
249
|
+
Unavailable: "Unavailable",
|
|
250
|
+
Faulted: "Faulted"
|
|
251
|
+
};
|
|
252
|
+
var availabilityMap21to16 = {
|
|
253
|
+
Operative: "Available",
|
|
254
|
+
Inoperative: "Unavailable"
|
|
255
|
+
};
|
|
256
|
+
var availabilityMap16to21 = {
|
|
257
|
+
Available: "Operative",
|
|
258
|
+
Unavailable: "Inoperative"
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
// src/presets/core.ts
|
|
262
|
+
var txIdCounter = 1;
|
|
263
|
+
var corePreset = {
|
|
264
|
+
upstream: {
|
|
265
|
+
"ocpp1.6:BootNotification": (params) => ({
|
|
266
|
+
action: "BootNotification",
|
|
267
|
+
payload: {
|
|
268
|
+
reason: "PowerUp",
|
|
269
|
+
chargingStation: {
|
|
270
|
+
model: params.chargePointModel,
|
|
271
|
+
vendorName: params.chargePointVendor,
|
|
272
|
+
firmwareVersion: params.firmwareVersion,
|
|
273
|
+
serialNumber: params.chargePointSerialNumber
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}),
|
|
277
|
+
"ocpp1.6:Heartbeat": () => ({
|
|
278
|
+
action: "Heartbeat",
|
|
279
|
+
payload: {}
|
|
280
|
+
}),
|
|
281
|
+
"ocpp1.6:StatusNotification": (params) => ({
|
|
282
|
+
action: "StatusNotification",
|
|
283
|
+
payload: {
|
|
284
|
+
timestamp: params.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
|
|
285
|
+
connectorStatus: statusMap16to21[params.status] || params.status,
|
|
286
|
+
evseId: params.connectorId,
|
|
287
|
+
connectorId: params.connectorId
|
|
288
|
+
}
|
|
289
|
+
}),
|
|
290
|
+
"ocpp1.6:Authorize": (params) => ({
|
|
291
|
+
action: "Authorize",
|
|
292
|
+
payload: {
|
|
293
|
+
idToken: {
|
|
294
|
+
idToken: params.idTag,
|
|
295
|
+
type: "ISO14443"
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}),
|
|
299
|
+
"ocpp1.6:StartTransaction": async (params, ctx) => {
|
|
300
|
+
const proxyGeneratedTxId = randomUUID();
|
|
301
|
+
await ctx.session.set(ctx.identity, "pendingStartTx", {
|
|
302
|
+
uuid: proxyGeneratedTxId,
|
|
303
|
+
connectorId: params.connectorId
|
|
304
|
+
});
|
|
305
|
+
return {
|
|
306
|
+
action: "TransactionEvent",
|
|
307
|
+
payload: {
|
|
308
|
+
eventType: "Started",
|
|
309
|
+
timestamp: params.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
|
|
310
|
+
triggerReason: "Authorized",
|
|
311
|
+
seqNo: 0,
|
|
312
|
+
transactionInfo: {
|
|
313
|
+
transactionId: proxyGeneratedTxId
|
|
314
|
+
},
|
|
315
|
+
idToken: {
|
|
316
|
+
idToken: params.idTag,
|
|
317
|
+
type: "ISO14443"
|
|
318
|
+
},
|
|
319
|
+
evse: {
|
|
320
|
+
id: params.connectorId,
|
|
321
|
+
connectorId: params.connectorId
|
|
322
|
+
},
|
|
323
|
+
meterValue: [
|
|
324
|
+
{
|
|
325
|
+
timestamp: params.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
|
|
326
|
+
sampledValue: [
|
|
327
|
+
{
|
|
328
|
+
value: params.meterStart,
|
|
329
|
+
measurand: "Energy.Active.Import.Register"
|
|
330
|
+
}
|
|
331
|
+
]
|
|
332
|
+
}
|
|
333
|
+
]
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
},
|
|
337
|
+
"ocpp1.6:StopTransaction": async (params, ctx) => {
|
|
338
|
+
const numericId = params.transactionId;
|
|
339
|
+
const uuid = await ctx.session.get(
|
|
340
|
+
ctx.identity,
|
|
341
|
+
`txId_int2uuid_${numericId}`
|
|
342
|
+
);
|
|
343
|
+
return {
|
|
344
|
+
action: "TransactionEvent",
|
|
345
|
+
payload: {
|
|
346
|
+
eventType: "Ended",
|
|
347
|
+
timestamp: params.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
|
|
348
|
+
triggerReason: params.reason || "Local",
|
|
349
|
+
seqNo: 1,
|
|
350
|
+
transactionInfo: {
|
|
351
|
+
transactionId: uuid || randomUUID(),
|
|
352
|
+
stoppedReason: params.reason || "Local"
|
|
353
|
+
},
|
|
354
|
+
idToken: params.idTag ? {
|
|
355
|
+
idToken: params.idTag,
|
|
356
|
+
type: "ISO14443"
|
|
357
|
+
} : void 0,
|
|
358
|
+
meterValue: [
|
|
359
|
+
{
|
|
360
|
+
timestamp: params.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
|
|
361
|
+
sampledValue: [
|
|
362
|
+
{
|
|
363
|
+
value: params.meterStop,
|
|
364
|
+
measurand: "Energy.Active.Import.Register"
|
|
365
|
+
}
|
|
366
|
+
]
|
|
367
|
+
}
|
|
368
|
+
]
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
},
|
|
372
|
+
"ocpp1.6:MeterValues": async (params, ctx) => {
|
|
373
|
+
const numericId = params.transactionId;
|
|
374
|
+
const uuid = numericId ? await ctx.session.get(
|
|
375
|
+
ctx.identity,
|
|
376
|
+
`txId_int2uuid_${numericId}`
|
|
377
|
+
) : void 0;
|
|
378
|
+
return {
|
|
379
|
+
action: "TransactionEvent",
|
|
380
|
+
payload: {
|
|
381
|
+
eventType: "Updated",
|
|
382
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
383
|
+
triggerReason: "MeterValuePeriodic",
|
|
384
|
+
seqNo: 0,
|
|
385
|
+
transactionInfo: uuid ? { transactionId: uuid } : void 0,
|
|
386
|
+
evse: {
|
|
387
|
+
id: params.connectorId,
|
|
388
|
+
connectorId: params.connectorId
|
|
389
|
+
},
|
|
390
|
+
meterValue: (params.meterValue || []).map((mv) => ({
|
|
391
|
+
timestamp: mv.timestamp,
|
|
392
|
+
sampledValue: (mv.sampledValue || []).map((sv) => ({
|
|
393
|
+
value: sv.value,
|
|
394
|
+
measurand: sv.measurand || "Energy.Active.Import.Register",
|
|
395
|
+
unit: sv.unit,
|
|
396
|
+
context: sv.context,
|
|
397
|
+
location: sv.location
|
|
398
|
+
}))
|
|
399
|
+
}))
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
downstream: {
|
|
405
|
+
"ocpp2.1:RemoteStartTransaction": (params) => ({
|
|
406
|
+
action: "RemoteStartTransaction",
|
|
407
|
+
payload: {
|
|
408
|
+
connectorId: params.evseId || 1,
|
|
409
|
+
idTag: params.idToken?.idToken
|
|
410
|
+
}
|
|
411
|
+
}),
|
|
412
|
+
"ocpp2.1:RemoteStopTransaction": async (params, ctx) => {
|
|
413
|
+
const uuid = params.transactionId;
|
|
414
|
+
const numericId = uuid ? await ctx.session.get(ctx.identity, `txId_uuid2int_${uuid}`) : void 0;
|
|
415
|
+
return {
|
|
416
|
+
action: "RemoteStopTransaction",
|
|
417
|
+
payload: {
|
|
418
|
+
transactionId: numericId ?? 0
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
},
|
|
422
|
+
"ocpp2.1:ChangeAvailability": (params) => ({
|
|
423
|
+
action: "ChangeAvailability",
|
|
424
|
+
payload: {
|
|
425
|
+
connectorId: params.evse?.connectorId || params.evse?.id || 0,
|
|
426
|
+
type: params.operationalStatus === "Operative" ? "Operative" : "Inoperative"
|
|
427
|
+
}
|
|
428
|
+
}),
|
|
429
|
+
"ocpp2.1:Reset": (params) => ({
|
|
430
|
+
action: "Reset",
|
|
431
|
+
payload: {
|
|
432
|
+
type: params.type === "OnIdle" ? "Soft" : params.type
|
|
433
|
+
}
|
|
434
|
+
}),
|
|
435
|
+
"ocpp2.1:UnlockConnector": (params) => ({
|
|
436
|
+
action: "UnlockConnector",
|
|
437
|
+
payload: {
|
|
438
|
+
connectorId: params.connectorId || params.evseId || 1
|
|
439
|
+
}
|
|
440
|
+
}),
|
|
441
|
+
"ocpp2.1:TriggerMessage": (params) => ({
|
|
442
|
+
action: "TriggerMessage",
|
|
443
|
+
payload: {
|
|
444
|
+
requestedMessage: params.requestedMessage,
|
|
445
|
+
connectorId: params.evse?.connectorId || params.evse?.id
|
|
446
|
+
}
|
|
447
|
+
})
|
|
448
|
+
},
|
|
449
|
+
responses: {
|
|
450
|
+
"ocpp2.1:BootNotificationResponse": (params) => ({
|
|
451
|
+
currentTime: params.currentTime,
|
|
452
|
+
interval: params.interval,
|
|
453
|
+
status: params.status
|
|
454
|
+
}),
|
|
455
|
+
"ocpp2.1:HeartbeatResponse": (params) => ({
|
|
456
|
+
currentTime: params.currentTime
|
|
457
|
+
}),
|
|
458
|
+
"ocpp2.1:StatusNotificationResponse": () => ({}),
|
|
459
|
+
"ocpp2.1:AuthorizeResponse": (params) => ({
|
|
460
|
+
idTagInfo: {
|
|
461
|
+
status: params.idTokenInfo?.status || "Accepted"
|
|
462
|
+
}
|
|
463
|
+
}),
|
|
464
|
+
"ocpp2.1:TransactionEventResponse": async (params, ctx) => {
|
|
465
|
+
const pending = await ctx.session.get(ctx.identity, "pendingStartTx");
|
|
466
|
+
const numericTxId = txIdCounter++;
|
|
467
|
+
if (pending) {
|
|
468
|
+
await ctx.session.set(
|
|
469
|
+
ctx.identity,
|
|
470
|
+
`txId_int2uuid_${numericTxId}`,
|
|
471
|
+
pending.uuid
|
|
472
|
+
);
|
|
473
|
+
await ctx.session.set(
|
|
474
|
+
ctx.identity,
|
|
475
|
+
`txId_uuid2int_${pending.uuid}`,
|
|
476
|
+
numericTxId
|
|
477
|
+
);
|
|
478
|
+
await ctx.session.delete(ctx.identity, "pendingStartTx");
|
|
479
|
+
}
|
|
480
|
+
return {
|
|
481
|
+
idTagInfo: {
|
|
482
|
+
status: params.idTokenInfo?.status || "Accepted"
|
|
483
|
+
},
|
|
484
|
+
transactionId: numericTxId
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
errors: {
|
|
489
|
+
"ocpp2.1:Error": (errorCode, errorDescription, errorDetails) => {
|
|
490
|
+
const errorMap = {
|
|
491
|
+
SecurityError: "InternalError",
|
|
492
|
+
FormatViolation: "FormationViolation",
|
|
493
|
+
MessageTypeNotSupported: "NotSupported",
|
|
494
|
+
PropertyConstraintViolation: "PropertyConstraintViolation",
|
|
495
|
+
OccurrenceConstraintViolation: "OccurenceConstraintViolation",
|
|
496
|
+
TypeConstraintViolation: "TypeConstraintViolation",
|
|
497
|
+
GenericError: "GenericError",
|
|
498
|
+
NotImplemented: "NotImplemented",
|
|
499
|
+
NotSupported: "NotSupported",
|
|
500
|
+
ProtocolError: "ProtocolError",
|
|
501
|
+
RpcFrameworkError: "InternalError"
|
|
502
|
+
};
|
|
503
|
+
return {
|
|
504
|
+
errorCode: errorMap[errorCode] || "InternalError",
|
|
505
|
+
errorDescription,
|
|
506
|
+
errorDetails
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
// src/presets/firmware.ts
|
|
513
|
+
var firmwarePreset = {
|
|
514
|
+
upstream: {
|
|
515
|
+
"ocpp1.6:FirmwareStatusNotification": (params) => ({
|
|
516
|
+
action: "FirmwareStatusNotification",
|
|
517
|
+
payload: {
|
|
518
|
+
status: params.status
|
|
519
|
+
}
|
|
520
|
+
}),
|
|
521
|
+
"ocpp1.6:DiagnosticsStatusNotification": (params) => ({
|
|
522
|
+
action: "LogStatusNotification",
|
|
523
|
+
payload: {
|
|
524
|
+
status: params.status === "Uploaded" ? "Uploaded" : params.status,
|
|
525
|
+
requestId: 0
|
|
526
|
+
}
|
|
527
|
+
})
|
|
528
|
+
},
|
|
529
|
+
downstream: {
|
|
530
|
+
"ocpp2.1:UpdateFirmware": (params) => ({
|
|
531
|
+
action: "UpdateFirmware",
|
|
532
|
+
payload: {
|
|
533
|
+
location: params.firmware?.location || params.location,
|
|
534
|
+
retrieveDate: params.firmware?.retrieveDateTime || params.retrieveDate,
|
|
535
|
+
retries: params.retries,
|
|
536
|
+
retryInterval: params.retryInterval
|
|
537
|
+
}
|
|
538
|
+
}),
|
|
539
|
+
"ocpp2.1:GetLog": (params) => ({
|
|
540
|
+
action: "GetDiagnostics",
|
|
541
|
+
payload: {
|
|
542
|
+
location: params.log?.remoteLocation || "",
|
|
543
|
+
startTime: params.log?.oldestTimestamp,
|
|
544
|
+
stopTime: params.log?.latestTimestamp,
|
|
545
|
+
retries: params.retries,
|
|
546
|
+
retryInterval: params.retryInterval
|
|
547
|
+
}
|
|
548
|
+
})
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
// src/presets/local-auth.ts
|
|
553
|
+
var localAuthPreset = {
|
|
554
|
+
downstream: {
|
|
555
|
+
"ocpp2.1:GetLocalListVersion": () => ({
|
|
556
|
+
action: "GetLocalListVersion",
|
|
557
|
+
payload: {}
|
|
558
|
+
}),
|
|
559
|
+
"ocpp2.1:SendLocalList": (params) => ({
|
|
560
|
+
action: "SendLocalList",
|
|
561
|
+
payload: {
|
|
562
|
+
listVersion: params.versionNumber,
|
|
563
|
+
updateType: params.updateType,
|
|
564
|
+
localAuthorizationList: (params.localAuthorizationList || []).map(
|
|
565
|
+
(entry) => ({
|
|
566
|
+
idTag: entry.idToken?.idToken,
|
|
567
|
+
idTagInfo: entry.idTokenInfo ? {
|
|
568
|
+
status: entry.idTokenInfo.status,
|
|
569
|
+
expiryDate: entry.idTokenInfo.cacheExpiryDateTime
|
|
570
|
+
} : void 0
|
|
571
|
+
})
|
|
572
|
+
)
|
|
573
|
+
}
|
|
574
|
+
})
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
// src/presets/reservation.ts
|
|
579
|
+
var reservationPreset = {
|
|
580
|
+
downstream: {
|
|
581
|
+
"ocpp2.1:ReserveNow": (params) => ({
|
|
582
|
+
action: "ReserveNow",
|
|
583
|
+
payload: {
|
|
584
|
+
connectorId: params.evseId || 0,
|
|
585
|
+
expiryDate: params.expiryDateTime,
|
|
586
|
+
idTag: params.idToken?.idToken,
|
|
587
|
+
reservationId: params.id
|
|
588
|
+
}
|
|
589
|
+
}),
|
|
590
|
+
"ocpp2.1:CancelReservation": (params) => ({
|
|
591
|
+
action: "CancelReservation",
|
|
592
|
+
payload: {
|
|
593
|
+
reservationId: params.reservationId
|
|
594
|
+
}
|
|
595
|
+
})
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
// src/presets/smart-charging.ts
|
|
600
|
+
var smartChargingPreset = {
|
|
601
|
+
downstream: {
|
|
602
|
+
"ocpp2.1:SetChargingProfile": (params) => ({
|
|
603
|
+
action: "SetChargingProfile",
|
|
604
|
+
payload: {
|
|
605
|
+
connectorId: params.evseId,
|
|
606
|
+
csChargingProfiles: params.chargingProfile
|
|
607
|
+
}
|
|
608
|
+
}),
|
|
609
|
+
"ocpp2.1:ClearChargingProfile": (params) => ({
|
|
610
|
+
action: "ClearChargingProfile",
|
|
611
|
+
payload: {
|
|
612
|
+
id: params.chargingProfileId,
|
|
613
|
+
connectorId: params.chargingProfileCriteria?.evseId,
|
|
614
|
+
chargingProfilePurpose: params.chargingProfileCriteria?.chargingProfilePurpose,
|
|
615
|
+
stackLevel: params.chargingProfileCriteria?.stackLevel
|
|
616
|
+
}
|
|
617
|
+
}),
|
|
618
|
+
"ocpp2.1:GetCompositeSchedule": (params) => ({
|
|
619
|
+
action: "GetCompositeSchedule",
|
|
620
|
+
payload: {
|
|
621
|
+
connectorId: params.evseId,
|
|
622
|
+
duration: params.duration,
|
|
623
|
+
chargingRateUnit: params.chargingRateUnit
|
|
624
|
+
}
|
|
625
|
+
})
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
// src/presets/index.ts
|
|
630
|
+
function mergePresets(...maps) {
|
|
631
|
+
const merged = {
|
|
632
|
+
upstream: {},
|
|
633
|
+
downstream: {},
|
|
634
|
+
responses: {},
|
|
635
|
+
errors: {}
|
|
636
|
+
};
|
|
637
|
+
for (const map of maps) {
|
|
638
|
+
if (map.upstream) Object.assign(merged.upstream, map.upstream);
|
|
639
|
+
if (map.downstream) Object.assign(merged.downstream, map.downstream);
|
|
640
|
+
if (map.responses) Object.assign(merged.responses, map.responses);
|
|
641
|
+
if (map.errors) Object.assign(merged.errors, map.errors);
|
|
642
|
+
}
|
|
643
|
+
return merged;
|
|
644
|
+
}
|
|
645
|
+
var presets = {
|
|
646
|
+
ocpp16_to_ocpp21: mergePresets(
|
|
647
|
+
corePreset,
|
|
648
|
+
smartChargingPreset,
|
|
649
|
+
firmwarePreset,
|
|
650
|
+
reservationPreset,
|
|
651
|
+
localAuthPreset
|
|
652
|
+
)
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
// src/proxy.ts
|
|
656
|
+
import { EventEmitter } from "events";
|
|
657
|
+
var OCPPProtocolProxy = class extends EventEmitter {
|
|
658
|
+
constructor(options) {
|
|
659
|
+
super();
|
|
660
|
+
this.options = options;
|
|
661
|
+
this.translator = new OCPPTranslator({ upstream: {}, downstream: {} });
|
|
662
|
+
this.sessionStore = options.sessionStore || new InMemorySessionStore();
|
|
663
|
+
}
|
|
664
|
+
translator;
|
|
665
|
+
clients = /* @__PURE__ */ new Map();
|
|
666
|
+
sessionStore;
|
|
667
|
+
adapters = [];
|
|
668
|
+
/** Register translation maps (can be called multiple times to layer presets). */
|
|
669
|
+
translate(map) {
|
|
670
|
+
this.translator.updateMap(map);
|
|
671
|
+
}
|
|
672
|
+
/** Start listening on a transport adapter (WS, HTTP, etc.). */
|
|
673
|
+
async listenOnAdapter(adapter) {
|
|
674
|
+
this.adapters.push(adapter);
|
|
675
|
+
await adapter.listen((connection) => this.handleNewConnection(connection));
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Execute all registered middlewares sequentially.
|
|
679
|
+
* If a middleware returns a message, it replaces the current message.
|
|
680
|
+
*/
|
|
681
|
+
async executeMiddlewares(message, context, direction, phase) {
|
|
682
|
+
let currentMsg = message;
|
|
683
|
+
for (const mw of this.options.middlewares || []) {
|
|
684
|
+
try {
|
|
685
|
+
const result = await mw(currentMsg, context, direction, phase);
|
|
686
|
+
if (result) {
|
|
687
|
+
currentMsg = result;
|
|
688
|
+
}
|
|
689
|
+
} catch (err) {
|
|
690
|
+
this.emit("middlewareError", err, currentMsg, context);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
return currentMsg;
|
|
694
|
+
}
|
|
695
|
+
handleNewConnection(evseConnection) {
|
|
696
|
+
const identity = evseConnection.identity;
|
|
697
|
+
const sourceProtocol = evseConnection.protocol;
|
|
698
|
+
const targetProtocol = this.options.upstreamProtocol;
|
|
699
|
+
const context = {
|
|
700
|
+
identity,
|
|
701
|
+
sourceProtocol,
|
|
702
|
+
targetProtocol,
|
|
703
|
+
session: this.sessionStore
|
|
704
|
+
};
|
|
705
|
+
this.emit("connection", identity, sourceProtocol);
|
|
706
|
+
import("ocpp-ws-io").then(({ OCPPClient }) => {
|
|
707
|
+
const upstreamClient = new OCPPClient({
|
|
708
|
+
endpoint: this.options.upstreamEndpoint,
|
|
709
|
+
protocols: [targetProtocol],
|
|
710
|
+
identity,
|
|
711
|
+
strictMode: false
|
|
712
|
+
});
|
|
713
|
+
this.clients.set(identity, upstreamClient);
|
|
714
|
+
const connectionPromise = upstreamClient.connect();
|
|
715
|
+
evseConnection.onMessage(async (msg) => {
|
|
716
|
+
await connectionPromise;
|
|
717
|
+
if (msg.type === 2 /* CALL */) {
|
|
718
|
+
try {
|
|
719
|
+
const preMsg = await this.executeMiddlewares(
|
|
720
|
+
msg,
|
|
721
|
+
context,
|
|
722
|
+
"upstream",
|
|
723
|
+
"pre"
|
|
724
|
+
);
|
|
725
|
+
const translatedCall = await this.translator.translateUpstreamCall(
|
|
726
|
+
preMsg,
|
|
727
|
+
context
|
|
728
|
+
);
|
|
729
|
+
const postMsg = await this.executeMiddlewares(
|
|
730
|
+
translatedCall,
|
|
731
|
+
context,
|
|
732
|
+
"upstream",
|
|
733
|
+
"post"
|
|
734
|
+
);
|
|
735
|
+
const rawResponse = await upstreamClient.call(
|
|
736
|
+
postMsg.action,
|
|
737
|
+
postMsg.payload
|
|
738
|
+
);
|
|
739
|
+
const responseMsg = {
|
|
740
|
+
type: 3 /* CALLRESULT */,
|
|
741
|
+
messageId: msg.messageId,
|
|
742
|
+
payload: rawResponse
|
|
743
|
+
};
|
|
744
|
+
const preResMsg = await this.executeMiddlewares(
|
|
745
|
+
responseMsg,
|
|
746
|
+
context,
|
|
747
|
+
"response",
|
|
748
|
+
"pre"
|
|
749
|
+
);
|
|
750
|
+
const translatedResponse = await this.translator.translateCallResult(
|
|
751
|
+
preResMsg,
|
|
752
|
+
translatedCall.action,
|
|
753
|
+
context
|
|
754
|
+
);
|
|
755
|
+
return await this.executeMiddlewares(
|
|
756
|
+
translatedResponse,
|
|
757
|
+
context,
|
|
758
|
+
"response",
|
|
759
|
+
"post"
|
|
760
|
+
);
|
|
761
|
+
} catch (err) {
|
|
762
|
+
const errMessage = {
|
|
763
|
+
type: 4 /* CALLERROR */,
|
|
764
|
+
messageId: msg.messageId,
|
|
765
|
+
errorCode: err.code || "InternalError",
|
|
766
|
+
errorDescription: err.message,
|
|
767
|
+
errorDetails: {}
|
|
768
|
+
};
|
|
769
|
+
this.emit("translationError", err, msg, context);
|
|
770
|
+
return await this.executeMiddlewares(
|
|
771
|
+
errMessage,
|
|
772
|
+
context,
|
|
773
|
+
"error",
|
|
774
|
+
"post"
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
return void 0;
|
|
779
|
+
});
|
|
780
|
+
upstreamClient.handle(async (action, ctx) => {
|
|
781
|
+
const downstreamCall = {
|
|
782
|
+
type: 2 /* CALL */,
|
|
783
|
+
messageId: ctx.messageId || `csms-${Date.now()}`,
|
|
784
|
+
action,
|
|
785
|
+
payload: ctx.params
|
|
786
|
+
};
|
|
787
|
+
try {
|
|
788
|
+
const preMsg = await this.executeMiddlewares(
|
|
789
|
+
downstreamCall,
|
|
790
|
+
context,
|
|
791
|
+
"downstream",
|
|
792
|
+
"pre"
|
|
793
|
+
);
|
|
794
|
+
const translated = await this.translator.translateDownstreamCall(
|
|
795
|
+
preMsg,
|
|
796
|
+
context
|
|
797
|
+
);
|
|
798
|
+
const postMsg = await this.executeMiddlewares(
|
|
799
|
+
translated,
|
|
800
|
+
context,
|
|
801
|
+
"downstream",
|
|
802
|
+
"post"
|
|
803
|
+
);
|
|
804
|
+
const evseResponse = await evseConnection.send(postMsg);
|
|
805
|
+
if (evseResponse && evseResponse.type === 3 /* CALLRESULT */) {
|
|
806
|
+
const preResMsg = await this.executeMiddlewares(
|
|
807
|
+
evseResponse,
|
|
808
|
+
context,
|
|
809
|
+
"response",
|
|
810
|
+
"pre"
|
|
811
|
+
);
|
|
812
|
+
const mappedResponse = await this.translator.translateCallResult(
|
|
813
|
+
preResMsg,
|
|
814
|
+
downstreamCall.action,
|
|
815
|
+
context
|
|
816
|
+
);
|
|
817
|
+
const postResMsg = await this.executeMiddlewares(
|
|
818
|
+
mappedResponse,
|
|
819
|
+
context,
|
|
820
|
+
"response",
|
|
821
|
+
"post"
|
|
822
|
+
);
|
|
823
|
+
return postResMsg.payload;
|
|
824
|
+
}
|
|
825
|
+
} catch (err) {
|
|
826
|
+
this.emit("translationError", err, downstreamCall, context);
|
|
827
|
+
throw err;
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
evseConnection.onClose(() => {
|
|
831
|
+
upstreamClient.close();
|
|
832
|
+
this.sessionStore.clear(identity);
|
|
833
|
+
this.clients.delete(identity);
|
|
834
|
+
this.emit("disconnect", identity);
|
|
835
|
+
});
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
/** Gracefully close all upstream connections and adapters. */
|
|
839
|
+
async close() {
|
|
840
|
+
for (const client of this.clients.values()) {
|
|
841
|
+
try {
|
|
842
|
+
await client.close();
|
|
843
|
+
} catch {
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
this.clients.clear();
|
|
847
|
+
for (const adapter of this.adapters) {
|
|
848
|
+
await adapter.close();
|
|
849
|
+
}
|
|
850
|
+
this.adapters = [];
|
|
851
|
+
}
|
|
852
|
+
};
|
|
853
|
+
export {
|
|
854
|
+
InMemorySessionStore,
|
|
855
|
+
MessageType,
|
|
856
|
+
OCPPProtocolProxy,
|
|
857
|
+
OCPPTranslator,
|
|
858
|
+
OcppWsIoAdapter,
|
|
859
|
+
OcppWsIoConnection,
|
|
860
|
+
TelemetryMiddleware,
|
|
861
|
+
availabilityMap16to21,
|
|
862
|
+
availabilityMap21to16,
|
|
863
|
+
corePreset,
|
|
864
|
+
firmwarePreset,
|
|
865
|
+
localAuthPreset,
|
|
866
|
+
presets,
|
|
867
|
+
reservationPreset,
|
|
868
|
+
smartChargingPreset,
|
|
869
|
+
statusMap16to21,
|
|
870
|
+
statusMap21to16
|
|
871
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ocpp-protocol-proxy",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Transport-agnostic OCPP version translation proxy — translate any OCPP version to any other with pluggable middleware, stateful sessions, and spec-compliant presets.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.mjs",
|
|
13
|
+
"require": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./presets": {
|
|
16
|
+
"types": "./dist/presets/index.d.ts",
|
|
17
|
+
"import": "./dist/presets/index.mjs",
|
|
18
|
+
"require": "./dist/presets/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./adapters": {
|
|
21
|
+
"types": "./dist/adapters/ocpp-ws-io.adapter.d.ts",
|
|
22
|
+
"import": "./dist/adapters/ocpp-ws-io.adapter.mjs",
|
|
23
|
+
"require": "./dist/adapters/ocpp-ws-io.adapter.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsup",
|
|
28
|
+
"dev": "tsup --watch",
|
|
29
|
+
"test": "vitest run"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18.0.0"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"ocpp",
|
|
36
|
+
"ocpp1.6",
|
|
37
|
+
"ocpp2.0.1",
|
|
38
|
+
"ocpp2.1",
|
|
39
|
+
"protocol-proxy",
|
|
40
|
+
"protocol-translation",
|
|
41
|
+
"ev-charging",
|
|
42
|
+
"csms",
|
|
43
|
+
"evse",
|
|
44
|
+
"charge-point",
|
|
45
|
+
"middleware",
|
|
46
|
+
"version-translation",
|
|
47
|
+
"electric-vehicle",
|
|
48
|
+
"e-mobility",
|
|
49
|
+
"typescript"
|
|
50
|
+
],
|
|
51
|
+
"author": "Rohit Tiwari <rohit@rohittiwari.me>",
|
|
52
|
+
"license": "MIT",
|
|
53
|
+
"repository": {
|
|
54
|
+
"type": "git",
|
|
55
|
+
"url": "https://github.com/rohittiwari-dev/ocpp-ws-io",
|
|
56
|
+
"directory": "packages/ocpp-protocol-proxy"
|
|
57
|
+
},
|
|
58
|
+
"bugs": {
|
|
59
|
+
"url": "https://github.com/rohittiwari-dev/ocpp-ws-io/issues"
|
|
60
|
+
},
|
|
61
|
+
"homepage": "https://ocpp-ws-io.rohittiwari.me/docs/protocol-proxy",
|
|
62
|
+
"dependencies": {
|
|
63
|
+
"ocpp-ws-io": "*"
|
|
64
|
+
},
|
|
65
|
+
"files": [
|
|
66
|
+
"dist",
|
|
67
|
+
"README.md",
|
|
68
|
+
"LICENSE"
|
|
69
|
+
],
|
|
70
|
+
"devDependencies": {
|
|
71
|
+
"@types/node": "^20.19.35",
|
|
72
|
+
"tsup": "^8.5.1",
|
|
73
|
+
"typescript": "^5.9.3",
|
|
74
|
+
"vitest": "^3.2.4"
|
|
75
|
+
}
|
|
76
|
+
}
|