forked from etrel-development/hackaton-2021
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathocpp-charge-point-client.js
220 lines (188 loc) · 7.14 KB
/
ocpp-charge-point-client.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
const WebSocket = require("ws");
const moment = require("moment");
const UTILS = require("./utils.js");
// Connection settings
const OPENING_HANDSHAKE_TIMEOUT_MS = 120 * 1000; // wait time for protocol upgrade call
const AUTO_RECONNECT_INTERVAL_MS = 90 * 1000; // in case of connection loss, use this settings
const OCPP_HEARTBEAT_INTERVAL_OVERRIDE_MS = null; // override hb interval set by CS
// env variables
const CS_PROTOCOL = process.env.CS_PROTOCOL || "ws"; // use wss for SSL
const CS_HOST = process.env.CS_HOST || "localhost"; // central system host
const CS_PORT = process.env.CS_PORT || 8080; // port
const CONCURRENCY_LEVEL = process.env.CONCURRENCY_LEVEL || 1; // one client by default
const LOG_PAYLOAD = process.env.LOG_PAYLOAD === "true" || false; // data exchange verbose logging
const LOG_LIFECYCLE = process.env.LOG_LIFECYCLE === "true" || true; // lifecycle events (connect, reconnect, ping/pong)
// setup logging library
UTILS.Logging.EnablePayloadLogging = LOG_PAYLOAD;
UTILS.Logging.EnableLifecycleLogging = LOG_LIFECYCLE;
function WebSocketClient(cId, chargePointId) {
this.clientId = cId;
this.chargePointId = chargePointId;
this.pingTimeout = undefined;
this.autoReconnectInterval = AUTO_RECONNECT_INTERVAL_MS; // ms
this.ocppMessageCounter = 1; // ocpp communication contract; must increment on each message sent
this.ocppHeartBeatIntervalMs = 0; // comes from bootNotification response
this.ocppHeartBeatInterval = undefined; // interval object
}
WebSocketClient.prototype.open = function wscOpen(url) {
const that = this;
this.url = url;
this.instance = new WebSocket(this.url, ["ocpp1.6"], {
handshakeTimeout: OPENING_HANDSHAKE_TIMEOUT_MS,
// If the `rejectUnauthorized` option is not `false`, the server certificate
// is verified against a list of well-known CAs. An 'error' event is emitted
// if verification fails.
rejectUnauthorized: false,
});
const thisConnection = this.instance;
thisConnection.on("ping", () => {
UTILS.Fn.log(`Client ${that.clientId} received PING`);
clearTimeout(that.pingTimeout);
// Use `WebSocket#terminate()`, which immediately destroys the connection,
// instead of `WebSocket#close()`, which waits for the close timer.
// Delay should be equal to the interval at which your server
// sends out pings plus a conservative assumption of the latency.
that.pingTimeout = setTimeout(() => {
UTILS.Fn.log(`Client ${that.clientId} disconnected from server`);
thisConnection.terminate();
}, 30000 + 1000);
});
thisConnection.on("pong", () => {
// this is issued if client sends ping
UTILS.Fn.lifecyc("Event pong");
});
thisConnection.on("open", () => {
const bootNotificationRequest = {
chargeBoxIdentity: that.chargePointId,
chargeBoxSerialNumber: that.chargePointId,
chargePointModel: "ETREL INCH VIRTUAL Charger vOCPP16J",
chargePointSerialNumber: that.chargePointId,
chargePointVendor: "Etrel",
firmwareVersion: "1.0",
iccid: "",
imsi: "",
meterSerialNumber: "",
meterType: "",
};
const bootNotificationPayload = [
UTILS.OcppCallType.ClientToServer,
that.msgId(),
"BootNotification",
bootNotificationRequest,
];
that.send(bootNotificationPayload);
});
thisConnection.on("message", (data) => {
UTILS.Fn.data(`Client ${that.clientId} message received`, data);
let msgArr;
try {
msgArr = JSON.parse(data);
} catch (e) {
UTILS.Fn.err("Error parsing incoming json message");
return;
}
// in boot notification we receive interval for heartbeat
if (msgArr[0] === UTILS.OcppCallType.ServerToClient && msgArr[2].interval) {
// boot notification response
that.ocppHeartBeatIntervalMs =
OCPP_HEARTBEAT_INTERVAL_OVERRIDE_MS || msgArr[2].interval * 1000;
UTILS.Fn.lifecyc(
`Client ${that.clientId} Next interval will be at: ${moment().add(
that.ocppHeartBeatIntervalMs,
"ms"
)}`
);
that.ocppHeartBeatInterval = setInterval(() => {
that.send([
UTILS.OcppCallType.ClientToServer,
that.msgId(),
"Heartbeat",
{},
]); // ocpp heartbeat request
}, that.ocppHeartBeatIntervalMs);
} else {
UTILS.Fn.warn("Do not know what to do with following received message");
console.log(msgArr);
}
});
thisConnection.on("close", (code) => {
switch (
code // https://datatracker.ietf.org/doc/html/rfc6455#section-7.4.1
) {
case 1000: // 1000 indicates a normal closure, meaning that the purpose for which the connection was established has been fulfilled.
UTILS.Fn.err(
`Client ${that.clientId} - WebSocket: closed, code ${code}`
);
break;
case 1006: // Close Code 1006 is a special code that means the connection was closed abnormally (locally) by the browser implementation.
UTILS.Fn.err(
`Client ${that.clientId} - WebSocket: closed abnormally, code ${code}`
);
that.reconnect(code);
break;
default:
// Abnormal closure
UTILS.Fn.err(
`Client ${that.clientId} WebSocket: closed unknown, code ${code}`
);
that.reconnect(code);
break;
}
clearTimeout(that.pingTimeout);
});
thisConnection.on("error", (e) => {
switch (e.code) {
case "ECONNREFUSED":
UTILS.Fn.err(
`Client ${that.clientId} - Error ECONNREFUSED. Server is not accepting connections`
);
that.reconnect(e);
break;
default:
UTILS.Fn.err(`Client ${that.clientId} UNKNOWN ERROR`, e);
break;
}
});
};
WebSocketClient.prototype.msgId = function wscMsgId() {
// msg ID incremented
this.ocppMessageCounter += 1;
const inc = `${this.chargePointId}_${this.ocppMessageCounter}`;
return inc;
};
WebSocketClient.prototype.send = function wscSend(data, option) {
try {
const message = data[2];
const dataAsString = JSON.stringify(data);
UTILS.Fn.data(
`Client ${this.clientId} sending ${message} to CS: `,
dataAsString
);
this.instance.send(dataAsString, option);
} catch (e) {
this.instance.emit("error", e);
}
};
WebSocketClient.prototype.reconnect = function wscReconnect() {
UTILS.Fn.lifecyc(
`Client ${this.clientId} - WebSocketClient: retry in ${this.autoReconnectInterval}ms`
);
this.instance.removeAllListeners();
clearInterval(this.ocppHeartBeatInterval);
const that = this;
setTimeout(() => {
UTILS.Fn.lifecyc(
`Client ${that.clientId} - WebSocketClient retry reconnecting...`
);
that.open(that.url);
}, this.autoReconnectInterval);
};
// CONCURRENCY SETUP - running multiple clients
for (let clientIdx = 0; clientIdx < CONCURRENCY_LEVEL; clientIdx += 1) {
// build charge point identity - required by protocol
const chargePointId = `SI-${UTILS.Fn.uuidv4()}`; // required by protocol
const url = `${CS_PROTOCOL}://${CS_HOST}:${CS_PORT}/${chargePointId}?cid=${clientIdx}`; // central system url
UTILS.Fn.lifecyc(`Client is trying to connect to: ${url}`);
// create a client
new WebSocketClient(clientIdx, chargePointId).open(url);
}