refactor: make the exponential backoff implementation for tunable
[e-mobility-charging-stations-simulator.git] / src / charging-station / ocpp / OCPPRequestService.ts
CommitLineData
d270cc87 1import Ajv, { type JSONSchemaType } from 'ajv';
b52c969d
JB
2import ajvFormats from 'ajv-formats';
3
2896e06d
JB
4import { type OCPPResponseService, OCPPServiceUtils } from './internal';
5import type { ChargingStation } from '../../charging-station';
268a74bb 6import { OCPPError } from '../../exception';
dada83ec 7import { PerformanceStatistics } from '../../performance';
e7aeea18 8import {
268a74bb 9 type EmptyObject,
27782dbc 10 type ErrorCallback,
268a74bb
JB
11 type ErrorResponse,
12 ErrorType,
13 type HandleErrorParams,
27782dbc 14 type IncomingRequestCommand,
268a74bb
JB
15 type JsonObject,
16 type JsonType,
17 MessageType,
18 type OCPPVersion,
27782dbc 19 type OutgoingRequest,
e7aeea18 20 RequestCommand,
27782dbc 21 type RequestParams,
268a74bb 22 type Response,
27782dbc 23 type ResponseCallback,
e0b0ee21 24 type ResponseType,
268a74bb 25} from '../../types';
60a74391 26import { Constants, Utils, logger } from '../../utils';
c0560973 27
e3018bc4
JB
28const moduleName = 'OCPPRequestService';
29
268a74bb 30export abstract class OCPPRequestService {
08f130a0 31 private static instance: OCPPRequestService | null = null;
d270cc87 32 private readonly version: OCPPVersion;
012ae1a9 33 private readonly ajv: Ajv;
9f2e3130 34 private readonly ocppResponseService: OCPPResponseService;
b3fc3ff5 35 protected abstract jsonSchemas: Map<RequestCommand, JSONSchemaType<JsonObject>>;
c0560973 36
d270cc87
JB
37 protected constructor(version: OCPPVersion, ocppResponseService: OCPPResponseService) {
38 this.version = version;
45988780 39 this.ajv = new Ajv({
98fc1389 40 keywords: ['javaType'],
45988780
JB
41 multipleOfPrecision: 2,
42 });
9952c548 43 ajvFormats(this.ajv);
d270cc87 44 this.ocppResponseService = ocppResponseService;
f7f98c68 45 this.requestHandler.bind(this);
511c1897 46 this.sendMessage.bind(this);
c75a6675 47 this.sendResponse.bind(this);
54ce9b7d 48 this.sendError.bind(this);
9952c548
JB
49 this.internalSendMessage.bind(this);
50 this.buildMessageToSend.bind(this);
51 this.validateRequestPayload.bind(this);
02887891 52 this.validateIncomingRequestResponsePayload.bind(this);
c0560973
JB
53 }
54
e7aeea18 55 public static getInstance<T extends OCPPRequestService>(
08f130a0 56 this: new (ocppResponseService: OCPPResponseService) => T,
e7aeea18
JB
57 ocppResponseService: OCPPResponseService
58 ): T {
1ca780f9 59 if (OCPPRequestService.instance === null) {
08f130a0 60 OCPPRequestService.instance = new this(ocppResponseService);
9f2e3130 61 }
08f130a0 62 return OCPPRequestService.instance as T;
9f2e3130
JB
63 }
64
c75a6675 65 public async sendResponse(
08f130a0 66 chargingStation: ChargingStation,
e7aeea18 67 messageId: string,
5cc4b63b 68 messagePayload: JsonType,
e7aeea18 69 commandName: IncomingRequestCommand
5eaabe90 70 ): Promise<ResponseType> {
5e0c67e8 71 try {
c75a6675 72 // Send response message
e7aeea18 73 return await this.internalSendMessage(
08f130a0 74 chargingStation,
e7aeea18
JB
75 messageId,
76 messagePayload,
77 MessageType.CALL_RESULT_MESSAGE,
78 commandName
79 );
5e0c67e8 80 } catch (error) {
dc922667 81 this.handleSendMessageError(chargingStation, commandName, error as Error, {
07561812 82 throwError: true,
dc922667 83 });
5e0c67e8
JB
84 }
85 }
86
e7aeea18 87 public async sendError(
08f130a0 88 chargingStation: ChargingStation,
e7aeea18
JB
89 messageId: string,
90 ocppError: OCPPError,
b3ec7bc1 91 commandName: RequestCommand | IncomingRequestCommand
e7aeea18 92 ): Promise<ResponseType> {
5e0c67e8
JB
93 try {
94 // Send error message
e7aeea18 95 return await this.internalSendMessage(
08f130a0 96 chargingStation,
e7aeea18
JB
97 messageId,
98 ocppError,
99 MessageType.CALL_ERROR_MESSAGE,
100 commandName
101 );
5e0c67e8 102 } catch (error) {
dc922667 103 this.handleSendMessageError(chargingStation, commandName, error as Error);
5e0c67e8
JB
104 }
105 }
106
e7aeea18 107 protected async sendMessage(
08f130a0 108 chargingStation: ChargingStation,
e7aeea18 109 messageId: string,
5cc4b63b 110 messagePayload: JsonType,
e7aeea18 111 commandName: RequestCommand,
be9b0d50 112 params: RequestParams = {
e7aeea18
JB
113 skipBufferingOnError: false,
114 triggerMessage: false,
8ec8e3d0 115 throwError: false,
e7aeea18
JB
116 }
117 ): Promise<ResponseType> {
5e0c67e8 118 try {
e7aeea18 119 return await this.internalSendMessage(
08f130a0 120 chargingStation,
e7aeea18
JB
121 messageId,
122 messagePayload,
123 MessageType.CALL_MESSAGE,
124 commandName,
125 params
126 );
5e0c67e8 127 } catch (error) {
8ec8e3d0
JB
128 this.handleSendMessageError(chargingStation, commandName, error as Error, {
129 throwError: params.throwError,
130 });
5e0c67e8
JB
131 }
132 }
133
02887891 134 private validateRequestPayload<T extends JsonObject>(
b52c969d 135 chargingStation: ChargingStation,
45988780 136 commandName: RequestCommand | IncomingRequestCommand,
b52c969d
JB
137 payload: T
138 ): boolean {
0638ddd2 139 if (chargingStation.getPayloadSchemaValidation() === false) {
b52c969d
JB
140 return true;
141 }
b3fc3ff5
JB
142 if (this.jsonSchemas.has(commandName as RequestCommand) === false) {
143 logger.warn(
144 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: No JSON schema found for command '${commandName}' PDU validation`
145 );
45988780
JB
146 return true;
147 }
b3fc3ff5
JB
148 const validate = this.ajv.compile(this.jsonSchemas.get(commandName as RequestCommand));
149 payload = Utils.cloneObject<T>(payload);
1799761a 150 OCPPServiceUtils.convertDateToISOString<T>(payload);
b52c969d
JB
151 if (validate(payload)) {
152 return true;
153 }
154 logger.error(
45988780 155 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: Command '${commandName}' request PDU is invalid: %j`,
b52c969d
JB
156 validate.errors
157 );
e909d2a7 158 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
b52c969d 159 throw new OCPPError(
01a4dcbb 160 OCPPServiceUtils.ajvErrorsToErrorType(validate.errors),
b52c969d
JB
161 'Request PDU is invalid',
162 commandName,
163 JSON.stringify(validate.errors, null, 2)
164 );
165 }
166
02887891 167 private validateIncomingRequestResponsePayload<T extends JsonObject>(
b3fc3ff5
JB
168 chargingStation: ChargingStation,
169 commandName: RequestCommand | IncomingRequestCommand,
170 payload: T
171 ): boolean {
172 if (chargingStation.getPayloadSchemaValidation() === false) {
173 return true;
174 }
175 if (
176 this.ocppResponseService.jsonIncomingRequestResponseSchemas.has(
177 commandName as IncomingRequestCommand
178 ) === false
179 ) {
180 logger.warn(
02887891 181 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: No JSON schema found for command '${commandName}' PDU validation`
b3fc3ff5
JB
182 );
183 return true;
184 }
185 const validate = this.ajv.compile(
186 this.ocppResponseService.jsonIncomingRequestResponseSchemas.get(
187 commandName as IncomingRequestCommand
188 )
189 );
190 payload = Utils.cloneObject<T>(payload);
191 OCPPServiceUtils.convertDateToISOString<T>(payload);
192 if (validate(payload)) {
193 return true;
194 }
195 logger.error(
02887891 196 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: Command '${commandName}' reponse PDU is invalid: %j`,
b3fc3ff5
JB
197 validate.errors
198 );
199 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
200 throw new OCPPError(
201 OCPPServiceUtils.ajvErrorsToErrorType(validate.errors),
202 'Response PDU is invalid',
203 commandName,
204 JSON.stringify(validate.errors, null, 2)
205 );
206 }
207
e7aeea18 208 private async internalSendMessage(
08f130a0 209 chargingStation: ChargingStation,
e7aeea18 210 messageId: string,
5cc4b63b 211 messagePayload: JsonType | OCPPError,
e7aeea18 212 messageType: MessageType,
72092cfc 213 commandName: RequestCommand | IncomingRequestCommand,
be9b0d50 214 params: RequestParams = {
e7aeea18
JB
215 skipBufferingOnError: false,
216 triggerMessage: false,
217 }
218 ): Promise<ResponseType> {
219 if (
3a13fc92
JB
220 (chargingStation.isInUnknownState() === true &&
221 commandName === RequestCommand.BOOT_NOTIFICATION) ||
222 (chargingStation.getOcppStrictCompliance() === false &&
223 chargingStation.isInUnknownState() === true) ||
224 chargingStation.isInAcceptedState() === true ||
225 (chargingStation.isInPendingState() === true &&
226 (params.triggerMessage === true || messageType === MessageType.CALL_RESULT_MESSAGE))
e7aeea18 227 ) {
caad9d6b
JB
228 // eslint-disable-next-line @typescript-eslint/no-this-alias
229 const self = this;
230 // Send a message through wsConnection
e7aeea18
JB
231 return Utils.promiseWithTimeout(
232 new Promise((resolve, reject) => {
e8a92d57
JB
233 /**
234 * Function that will receive the request's response
235 *
236 * @param payload -
237 * @param requestPayload -
238 */
239 const responseCallback = (payload: JsonType, requestPayload: JsonType): void => {
240 if (chargingStation.getEnableStatistics() === true) {
241 chargingStation.performanceStatistics?.addRequestStatistic(
242 commandName,
243 MessageType.CALL_RESULT_MESSAGE
244 );
245 }
246 // Handle the request's response
247 self.ocppResponseService
248 .responseHandler(
249 chargingStation,
250 commandName as RequestCommand,
251 payload,
252 requestPayload
253 )
254 .then(() => {
255 resolve(payload);
256 })
257 .catch((error) => {
258 reject(error);
259 })
260 .finally(() => {
261 chargingStation.requests.delete(messageId);
262 });
263 };
264
265 /**
266 * Function that will receive the request's error response
267 *
268 * @param error -
269 * @param requestStatistic -
270 */
271 const errorCallback = (error: OCPPError, requestStatistic = true): void => {
272 if (requestStatistic === true && chargingStation.getEnableStatistics() === true) {
273 chargingStation.performanceStatistics?.addRequestStatistic(
274 commandName,
275 MessageType.CALL_ERROR_MESSAGE
276 );
277 }
278 logger.error(
279 `${chargingStation.logPrefix()} Error occurred at ${OCPPServiceUtils.getMessageTypeString(
280 messageType
281 )} command ${commandName} with PDU %j:`,
282 messagePayload,
283 error
284 );
285 chargingStation.requests.delete(messageId);
286 reject(error);
287 };
288
764d2c91 289 if (chargingStation.getEnableStatistics() === true) {
551e477c 290 chargingStation.performanceStatistics?.addRequestStatistic(commandName, messageType);
764d2c91 291 }
e7aeea18 292 const messageToSend = this.buildMessageToSend(
08f130a0 293 chargingStation,
e7aeea18
JB
294 messageId,
295 messagePayload,
296 messageType,
297 commandName,
298 responseCallback,
a2d1c0f1 299 errorCallback
e7aeea18 300 );
1b821a64 301 let sendError = false;
e7aeea18 302 // Check if wsConnection opened
764d2c91
JB
303 const wsOpened = chargingStation.isWebSocketConnectionOpened() === true;
304 if (wsOpened) {
1431af78 305 const beginId = PerformanceStatistics.beginMeasure(commandName);
1b821a64 306 try {
72092cfc 307 chargingStation.wsConnection?.send(messageToSend);
18bf8274 308 logger.debug(
2cc5d5ec 309 `${chargingStation.logPrefix()} >> Command '${commandName}' sent ${OCPPServiceUtils.getMessageTypeString(
18bf8274
JB
310 messageType
311 )} payload: ${messageToSend}`
312 );
1b821a64 313 } catch (error) {
18bf8274 314 logger.error(
2cc5d5ec 315 `${chargingStation.logPrefix()} >> Command '${commandName}' failed to send ${OCPPServiceUtils.getMessageTypeString(
18bf8274
JB
316 messageType
317 )} payload: ${messageToSend}:`,
318 error
319 );
1b821a64
JB
320 sendError = true;
321 }
1431af78 322 PerformanceStatistics.endMeasure(commandName, beginId);
1b821a64 323 }
764d2c91 324 const wsClosedOrErrored = !wsOpened || sendError === true;
1b821a64
JB
325 if (wsClosedOrErrored && params.skipBufferingOnError === false) {
326 // Buffer
08f130a0 327 chargingStation.bufferMessage(messageToSend);
1b821a64
JB
328 // Reject and keep request in the cache
329 return reject(
330 new OCPPError(
331 ErrorType.GENERIC_ERROR,
332 `WebSocket closed or errored for buffered message id '${messageId}' with content '${messageToSend}'`,
333 commandName,
59b6ed8d 334 (messagePayload as JsonObject)?.details ?? Constants.EMPTY_FREEZED_OBJECT
1b821a64
JB
335 )
336 );
337 } else if (wsClosedOrErrored) {
e7aeea18
JB
338 const ocppError = new OCPPError(
339 ErrorType.GENERIC_ERROR,
1b821a64 340 `WebSocket closed or errored for non buffered message id '${messageId}' with content '${messageToSend}'`,
e7aeea18 341 commandName,
59b6ed8d 342 (messagePayload as JsonObject)?.details ?? Constants.EMPTY_FREEZED_OBJECT
e7aeea18 343 );
1b821a64
JB
344 // Reject response
345 if (messageType !== MessageType.CALL_MESSAGE) {
e7aeea18
JB
346 return reject(ocppError);
347 }
1b821a64 348 // Reject and remove request from the cache
a2d1c0f1 349 return errorCallback(ocppError, false);
caad9d6b 350 }
1b821a64 351 // Resolve response
e7aeea18 352 if (messageType !== MessageType.CALL_MESSAGE) {
e7aeea18
JB
353 return resolve(messagePayload);
354 }
e7aeea18
JB
355 }),
356 Constants.OCPP_WEBSOCKET_TIMEOUT,
357 new OCPPError(
358 ErrorType.GENERIC_ERROR,
359 `Timeout for message id '${messageId}'`,
360 commandName,
59b6ed8d 361 (messagePayload as JsonObject)?.details ?? Constants.EMPTY_FREEZED_OBJECT
e7aeea18
JB
362 ),
363 () => {
08f130a0 364 messageType === MessageType.CALL_MESSAGE && chargingStation.requests.delete(messageId);
caad9d6b 365 }
e7aeea18 366 );
caad9d6b 367 }
e7aeea18
JB
368 throw new OCPPError(
369 ErrorType.SECURITY_ERROR,
e3018bc4 370 `Cannot send command ${commandName} PDU when the charging station is in ${chargingStation.getRegistrationStatus()} state on the central server`,
e7aeea18
JB
371 commandName
372 );
c0560973
JB
373 }
374
e7aeea18 375 private buildMessageToSend(
08f130a0 376 chargingStation: ChargingStation,
e7aeea18 377 messageId: string,
5cc4b63b 378 messagePayload: JsonType | OCPPError,
e7aeea18 379 messageType: MessageType,
72092cfc
JB
380 commandName: RequestCommand | IncomingRequestCommand,
381 responseCallback: ResponseCallback,
382 errorCallback: ErrorCallback
e7aeea18 383 ): string {
e7accadb
JB
384 let messageToSend: string;
385 // Type of message
386 switch (messageType) {
387 // Request
388 case MessageType.CALL_MESSAGE:
389 // Build request
cda96260 390 this.validateRequestPayload(chargingStation, commandName, messagePayload as JsonObject);
08f130a0 391 chargingStation.requests.set(messageId, [
e7aeea18 392 responseCallback,
a2d1c0f1 393 errorCallback,
e7aeea18 394 commandName,
5cc4b63b 395 messagePayload as JsonType,
e7aeea18 396 ]);
b3ec7bc1
JB
397 messageToSend = JSON.stringify([
398 messageType,
399 messageId,
400 commandName,
401 messagePayload,
402 ] as OutgoingRequest);
e7accadb
JB
403 break;
404 // Response
405 case MessageType.CALL_RESULT_MESSAGE:
406 // Build response
02887891
JB
407 this.validateIncomingRequestResponsePayload(
408 chargingStation,
409 commandName,
410 messagePayload as JsonObject
411 );
b3ec7bc1 412 messageToSend = JSON.stringify([messageType, messageId, messagePayload] as Response);
e7accadb
JB
413 break;
414 // Error Message
415 case MessageType.CALL_ERROR_MESSAGE:
416 // Build Error Message
e7aeea18
JB
417 messageToSend = JSON.stringify([
418 messageType,
419 messageId,
b3ec7bc1
JB
420 (messagePayload as OCPPError)?.code ?? ErrorType.GENERIC_ERROR,
421 (messagePayload as OCPPError)?.message ?? '',
422 (messagePayload as OCPPError)?.details ?? { commandName },
423 ] as ErrorResponse);
e7accadb
JB
424 break;
425 }
426 return messageToSend;
427 }
428
dc922667 429 private handleSendMessageError(
08f130a0 430 chargingStation: ChargingStation,
e7aeea18
JB
431 commandName: RequestCommand | IncomingRequestCommand,
432 error: Error,
07561812 433 params: HandleErrorParams<EmptyObject> = { throwError: false }
e7aeea18 434 ): void {
60ddad53 435 logger.error(`${chargingStation.logPrefix()} Request command '${commandName}' error:`, error);
07561812 436 if (params?.throwError === true) {
e0a50bcd
JB
437 throw error;
438 }
5e0c67e8
JB
439 }
440
ef6fa3fb 441 // eslint-disable-next-line @typescript-eslint/no-unused-vars
e0b0ee21 442 public abstract requestHandler<ReqType extends JsonType, ResType extends JsonType>(
08f130a0 443 chargingStation: ChargingStation,
94a464f9 444 commandName: RequestCommand,
5cc4b63b 445 commandParams?: JsonType,
be9b0d50 446 params?: RequestParams
e0b0ee21 447 ): Promise<ResType>;
c0560973 448}