refactor: rename constant to a more sensible name
[e-mobility-charging-stations-simulator.git] / src / charging-station / ocpp / OCPPRequestService.ts
CommitLineData
ec0eebcc 1import Ajv, { type JSONSchemaType, type ValidateFunction } from 'ajv';
b52c969d
JB
2import ajvFormats from 'ajv-formats';
3
4c3c0d59
JB
4import { OCPPConstants } from './OCPPConstants';
5import type { OCPPResponseService } from './OCPPResponseService';
6import { OCPPServiceUtils } from './OCPPServiceUtils';
2896e06d 7import type { ChargingStation } from '../../charging-station';
268a74bb 8import { OCPPError } from '../../exception';
b84bca85 9import { PerformanceStatistics } from '../../performance';
e7aeea18 10import {
27782dbc 11 type ErrorCallback,
268a74bb
JB
12 type ErrorResponse,
13 ErrorType,
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';
1b2acf4e 26import { Constants, cloneObject, handleSendMessageError, logger } from '../../utils';
c0560973 27
e3018bc4
JB
28const moduleName = 'OCPPRequestService';
29
b9da1bc2
JB
30const defaultRequestParams: RequestParams = {
31 skipBufferingOnError: false,
32 triggerMessage: false,
33 throwError: false,
34};
35
268a74bb 36export abstract class OCPPRequestService {
08f130a0 37 private static instance: OCPPRequestService | null = null;
d270cc87 38 private readonly version: OCPPVersion;
012ae1a9 39 private readonly ajv: Ajv;
9f2e3130 40 private readonly ocppResponseService: OCPPResponseService;
291b5ec8
JB
41 private readonly jsonValidateFunctions: Map<RequestCommand, ValidateFunction<JsonType>>;
42 protected abstract jsonSchemas: Map<RequestCommand, JSONSchemaType<JsonType>>;
c0560973 43
d270cc87
JB
44 protected constructor(version: OCPPVersion, ocppResponseService: OCPPResponseService) {
45 this.version = version;
45988780 46 this.ajv = new Ajv({
98fc1389 47 keywords: ['javaType'],
45988780
JB
48 multipleOfPrecision: 2,
49 });
9952c548 50 ajvFormats(this.ajv);
291b5ec8 51 this.jsonValidateFunctions = new Map<RequestCommand, ValidateFunction<JsonType>>();
d270cc87 52 this.ocppResponseService = ocppResponseService;
31f59c6d 53 this.requestHandler = this.requestHandler.bind(this) as <
e1d9a0f4 54 // eslint-disable-next-line @typescript-eslint/no-unused-vars
31f59c6d 55 ReqType extends JsonType,
5edd8ba0 56 ResType extends JsonType,
31f59c6d
JB
57 >(
58 chargingStation: ChargingStation,
59 commandName: RequestCommand,
60 commandParams?: JsonType,
5edd8ba0 61 params?: RequestParams,
31f59c6d
JB
62 ) => Promise<ResType>;
63 this.sendMessage = this.sendMessage.bind(this) as (
64 chargingStation: ChargingStation,
65 messageId: string,
66 messagePayload: JsonType,
67 commandName: RequestCommand,
5edd8ba0 68 params?: RequestParams,
31f59c6d
JB
69 ) => Promise<ResponseType>;
70 this.sendResponse = this.sendResponse.bind(this) as (
71 chargingStation: ChargingStation,
72 messageId: string,
73 messagePayload: JsonType,
5edd8ba0 74 commandName: IncomingRequestCommand,
31f59c6d
JB
75 ) => Promise<ResponseType>;
76 this.sendError = this.sendError.bind(this) as (
77 chargingStation: ChargingStation,
78 messageId: string,
79 ocppError: OCPPError,
5edd8ba0 80 commandName: RequestCommand | IncomingRequestCommand,
31f59c6d
JB
81 ) => Promise<ResponseType>;
82 this.internalSendMessage = this.internalSendMessage.bind(this) as (
83 chargingStation: ChargingStation,
84 messageId: string,
85 messagePayload: JsonType | OCPPError,
86 messageType: MessageType,
87 commandName: RequestCommand | IncomingRequestCommand,
5edd8ba0 88 params?: RequestParams,
31f59c6d
JB
89 ) => Promise<ResponseType>;
90 this.buildMessageToSend = this.buildMessageToSend.bind(this) as (
91 chargingStation: ChargingStation,
92 messageId: string,
93 messagePayload: JsonType | OCPPError,
94 messageType: MessageType,
95 commandName: RequestCommand | IncomingRequestCommand,
31f59c6d 96 ) => string;
291b5ec8 97 this.validateRequestPayload = this.validateRequestPayload.bind(this) as <T extends JsonType>(
31f59c6d
JB
98 chargingStation: ChargingStation,
99 commandName: RequestCommand | IncomingRequestCommand,
5edd8ba0 100 payload: T,
31f59c6d
JB
101 ) => boolean;
102 this.validateIncomingRequestResponsePayload = this.validateIncomingRequestResponsePayload.bind(
5edd8ba0 103 this,
291b5ec8 104 ) as <T extends JsonType>(
31f59c6d
JB
105 chargingStation: ChargingStation,
106 commandName: RequestCommand | IncomingRequestCommand,
5edd8ba0 107 payload: T,
31f59c6d 108 ) => boolean;
c0560973
JB
109 }
110
e7aeea18 111 public static getInstance<T extends OCPPRequestService>(
08f130a0 112 this: new (ocppResponseService: OCPPResponseService) => T,
5edd8ba0 113 ocppResponseService: OCPPResponseService,
e7aeea18 114 ): T {
1ca780f9 115 if (OCPPRequestService.instance === null) {
08f130a0 116 OCPPRequestService.instance = new this(ocppResponseService);
9f2e3130 117 }
08f130a0 118 return OCPPRequestService.instance as T;
9f2e3130
JB
119 }
120
c75a6675 121 public async sendResponse(
08f130a0 122 chargingStation: ChargingStation,
e7aeea18 123 messageId: string,
5cc4b63b 124 messagePayload: JsonType,
5edd8ba0 125 commandName: IncomingRequestCommand,
5eaabe90 126 ): Promise<ResponseType> {
5e0c67e8 127 try {
c75a6675 128 // Send response message
e7aeea18 129 return await this.internalSendMessage(
08f130a0 130 chargingStation,
e7aeea18
JB
131 messageId,
132 messagePayload,
133 MessageType.CALL_RESULT_MESSAGE,
5edd8ba0 134 commandName,
e7aeea18 135 );
5e0c67e8 136 } catch (error) {
fa5995d6 137 handleSendMessageError(chargingStation, commandName, error as Error, {
07561812 138 throwError: true,
dc922667 139 });
e1d9a0f4 140 return null;
5e0c67e8
JB
141 }
142 }
143
e7aeea18 144 public async sendError(
08f130a0 145 chargingStation: ChargingStation,
e7aeea18
JB
146 messageId: string,
147 ocppError: OCPPError,
5edd8ba0 148 commandName: RequestCommand | IncomingRequestCommand,
e7aeea18 149 ): Promise<ResponseType> {
5e0c67e8
JB
150 try {
151 // Send error message
e7aeea18 152 return await this.internalSendMessage(
08f130a0 153 chargingStation,
e7aeea18
JB
154 messageId,
155 ocppError,
156 MessageType.CALL_ERROR_MESSAGE,
5edd8ba0 157 commandName,
e7aeea18 158 );
5e0c67e8 159 } catch (error) {
fa5995d6 160 handleSendMessageError(chargingStation, commandName, error as Error);
e1d9a0f4 161 return null;
5e0c67e8
JB
162 }
163 }
164
e7aeea18 165 protected async sendMessage(
08f130a0 166 chargingStation: ChargingStation,
e7aeea18 167 messageId: string,
5cc4b63b 168 messagePayload: JsonType,
e7aeea18 169 commandName: RequestCommand,
7f3decca 170 params?: RequestParams,
e7aeea18 171 ): Promise<ResponseType> {
7b5dbe91 172 params = {
b9da1bc2 173 ...defaultRequestParams,
7b5dbe91
JB
174 ...params,
175 };
5e0c67e8 176 try {
e7aeea18 177 return await this.internalSendMessage(
08f130a0 178 chargingStation,
e7aeea18
JB
179 messageId,
180 messagePayload,
181 MessageType.CALL_MESSAGE,
182 commandName,
5edd8ba0 183 params,
e7aeea18 184 );
5e0c67e8 185 } catch (error) {
fa5995d6 186 handleSendMessageError(chargingStation, commandName, error as Error, {
8ec8e3d0
JB
187 throwError: params.throwError,
188 });
e1d9a0f4 189 return null;
5e0c67e8
JB
190 }
191 }
192
291b5ec8 193 private validateRequestPayload<T extends JsonType>(
b52c969d 194 chargingStation: ChargingStation,
45988780 195 commandName: RequestCommand | IncomingRequestCommand,
5edd8ba0 196 payload: T,
b52c969d 197 ): boolean {
5398cecf 198 if (chargingStation.stationInfo?.ocppStrictCompliance === false) {
b52c969d
JB
199 return true;
200 }
b3fc3ff5
JB
201 if (this.jsonSchemas.has(commandName as RequestCommand) === false) {
202 logger.warn(
5edd8ba0 203 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: No JSON schema found for command '${commandName}' PDU validation`,
b3fc3ff5 204 );
45988780
JB
205 return true;
206 }
0b0ca54f 207 const validate = this.getJsonRequestValidateFunction<T>(commandName as RequestCommand);
9bf0ef23 208 payload = cloneObject<T>(payload);
1799761a 209 OCPPServiceUtils.convertDateToISOString<T>(payload);
b52c969d
JB
210 if (validate(payload)) {
211 return true;
212 }
213 logger.error(
45988780 214 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: Command '${commandName}' request PDU is invalid: %j`,
5edd8ba0 215 validate.errors,
b52c969d 216 );
e909d2a7 217 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
b52c969d 218 throw new OCPPError(
9ff486f4 219 OCPPServiceUtils.ajvErrorsToErrorType(validate.errors),
b52c969d
JB
220 'Request PDU is invalid',
221 commandName,
4ed03b6e 222 JSON.stringify(validate.errors, undefined, 2),
b52c969d
JB
223 );
224 }
225
0b0ca54f
JB
226 private getJsonRequestValidateFunction<T extends JsonType>(commandName: RequestCommand) {
227 if (this.jsonValidateFunctions.has(commandName) === false) {
228 this.jsonValidateFunctions.set(
229 commandName,
230 this.ajv.compile<T>(this.jsonSchemas.get(commandName)!).bind(this),
231 );
232 }
233 return this.jsonValidateFunctions.get(commandName)!;
234 }
235
291b5ec8 236 private validateIncomingRequestResponsePayload<T extends JsonType>(
b3fc3ff5
JB
237 chargingStation: ChargingStation,
238 commandName: RequestCommand | IncomingRequestCommand,
5edd8ba0 239 payload: T,
b3fc3ff5 240 ): boolean {
5398cecf 241 if (chargingStation.stationInfo?.ocppStrictCompliance === false) {
b3fc3ff5
JB
242 return true;
243 }
244 if (
245 this.ocppResponseService.jsonIncomingRequestResponseSchemas.has(
5edd8ba0 246 commandName as IncomingRequestCommand,
b3fc3ff5
JB
247 ) === false
248 ) {
249 logger.warn(
5edd8ba0 250 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: No JSON schema found for command '${commandName}' PDU validation`,
b3fc3ff5
JB
251 );
252 return true;
253 }
0b0ca54f 254 const validate = this.getJsonRequestResponseValidateFunction<T>(
ec0eebcc 255 commandName as IncomingRequestCommand,
0b0ca54f 256 );
9bf0ef23 257 payload = cloneObject<T>(payload);
b3fc3ff5
JB
258 OCPPServiceUtils.convertDateToISOString<T>(payload);
259 if (validate(payload)) {
260 return true;
261 }
262 logger.error(
02887891 263 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: Command '${commandName}' reponse PDU is invalid: %j`,
5edd8ba0 264 validate.errors,
b3fc3ff5
JB
265 );
266 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
267 throw new OCPPError(
9ff486f4 268 OCPPServiceUtils.ajvErrorsToErrorType(validate.errors),
b3fc3ff5
JB
269 'Response PDU is invalid',
270 commandName,
4ed03b6e 271 JSON.stringify(validate.errors, undefined, 2),
b3fc3ff5
JB
272 );
273 }
274
0b0ca54f
JB
275 private getJsonRequestResponseValidateFunction<T extends JsonType>(
276 commandName: IncomingRequestCommand,
277 ) {
278 if (
279 this.ocppResponseService.jsonIncomingRequestResponseValidateFunctions.has(commandName) ===
280 false
281 ) {
282 this.ocppResponseService.jsonIncomingRequestResponseValidateFunctions.set(
283 commandName,
284 this.ajv
285 .compile<T>(this.ocppResponseService.jsonIncomingRequestResponseSchemas.get(commandName)!)
286 .bind(this),
287 );
288 }
289 return this.ocppResponseService.jsonIncomingRequestResponseValidateFunctions.get(commandName)!;
290 }
291
e7aeea18 292 private async internalSendMessage(
08f130a0 293 chargingStation: ChargingStation,
e7aeea18 294 messageId: string,
5cc4b63b 295 messagePayload: JsonType | OCPPError,
e7aeea18 296 messageType: MessageType,
72092cfc 297 commandName: RequestCommand | IncomingRequestCommand,
7f3decca 298 params?: RequestParams,
e7aeea18 299 ): Promise<ResponseType> {
7b5dbe91 300 params = {
b9da1bc2 301 ...defaultRequestParams,
7b5dbe91
JB
302 ...params,
303 };
e7aeea18 304 if (
f7c2994d 305 (chargingStation.inUnknownState() === true &&
3a13fc92 306 commandName === RequestCommand.BOOT_NOTIFICATION) ||
5398cecf 307 (chargingStation.stationInfo?.ocppStrictCompliance === false &&
f7c2994d
JB
308 chargingStation.inUnknownState() === true) ||
309 chargingStation.inAcceptedState() === true ||
310 (chargingStation.inPendingState() === true &&
3a13fc92 311 (params.triggerMessage === true || messageType === MessageType.CALL_RESULT_MESSAGE))
e7aeea18 312 ) {
caad9d6b
JB
313 // eslint-disable-next-line @typescript-eslint/no-this-alias
314 const self = this;
315 // Send a message through wsConnection
5b2721db 316 return new Promise<ResponseType>((resolve, reject) => {
1b2acf4e
JB
317 /**
318 * Function that will receive the request's response
319 *
320 * @param payload -
321 * @param requestPayload -
322 */
323 const responseCallback = (payload: JsonType, requestPayload: JsonType): void => {
324 if (chargingStation.stationInfo?.enableStatistics === true) {
325 chargingStation.performanceStatistics?.addRequestStatistic(
326 commandName,
327 MessageType.CALL_RESULT_MESSAGE,
e8a92d57 328 );
1b2acf4e
JB
329 }
330 // Handle the request's response
331 self.ocppResponseService
332 .responseHandler(
333 chargingStation,
334 commandName as RequestCommand,
335 payload,
336 requestPayload,
337 )
338 .then(() => {
339 resolve(payload);
340 })
341 .catch((error) => {
342 reject(error);
343 })
344 .finally(() => {
345 chargingStation.requests.delete(messageId);
346 });
347 };
e8a92d57 348
1b2acf4e
JB
349 /**
350 * Function that will receive the request's error response
351 *
352 * @param error -
353 * @param requestStatistic -
354 */
355 const errorCallback = (error: OCPPError, requestStatistic = true): void => {
356 if (requestStatistic === true && chargingStation.stationInfo?.enableStatistics === true) {
357 chargingStation.performanceStatistics?.addRequestStatistic(
358 commandName,
359 MessageType.CALL_ERROR_MESSAGE,
360 );
764d2c91 361 }
1b2acf4e
JB
362 logger.error(
363 `${chargingStation.logPrefix()} Error occurred at ${OCPPServiceUtils.getMessageTypeString(
364 messageType,
365 )} command ${commandName} with PDU %j:`,
e7aeea18 366 messagePayload,
1b2acf4e 367 error,
e7aeea18 368 );
1b2acf4e
JB
369 chargingStation.requests.delete(messageId);
370 reject(error);
371 };
372
373 if (chargingStation.stationInfo?.enableStatistics === true) {
374 chargingStation.performanceStatistics?.addRequestStatistic(commandName, messageType);
375 }
376 const messageToSend = this.buildMessageToSend(
377 chargingStation,
378 messageId,
379 messagePayload,
380 messageType,
381 commandName,
1b2acf4e 382 );
1b2acf4e 383 // Check if wsConnection opened
1a32c36b 384 if (chargingStation.isWebSocketConnectionOpened() === true) {
1b2acf4e 385 const beginId = PerformanceStatistics.beginMeasure(commandName);
1a32c36b
JB
386 const sendTimeout = setTimeout(() => {
387 return errorCallback(
388 new OCPPError(
389 ErrorType.GENERIC_ERROR,
390 `Timeout for message id '${messageId}'`,
391 commandName,
392 (messagePayload as JsonObject)?.details ?? Constants.EMPTY_FROZEN_OBJECT,
393 ),
394 false,
395 );
396 }, OCPPConstants.OCPP_WEBSOCKET_TIMEOUT);
397 chargingStation.wsConnection?.send(messageToSend, (error?: Error) => {
82fa1110
JB
398 clearTimeout(sendTimeout);
399 if (error) {
1a32c36b
JB
400 const ocppError = new OCPPError(
401 ErrorType.GENERIC_ERROR,
82fa1110
JB
402 `WebSocket errored for ${
403 params?.skipBufferingOnError === false ? '' : 'non '
404 }buffered message id '${messageId}' with content '${messageToSend}'`,
1a32c36b 405 commandName,
ca483a45 406 { name: error.name, message: error.message, stack: error.stack },
1a32c36b 407 );
82fa1110
JB
408 if (params?.skipBufferingOnError === false) {
409 // Buffer
410 chargingStation.bufferMessage(messageToSend);
411 // Reject and keep request in the cache
412 return reject(ocppError);
413 }
1a32c36b
JB
414 // Reject response
415 if (messageType !== MessageType.CALL_MESSAGE) {
416 return reject(ocppError);
417 }
418 // Reject and remove request from the cache
419 return errorCallback(ocppError, false);
420 }
1a32c36b 421 });
82fa1110
JB
422 if (messageType === MessageType.CALL_MESSAGE) {
423 this.cacheRequestPromise(
424 chargingStation,
82fa1110
JB
425 messageId,
426 messagePayload as JsonType,
427 commandName,
54a8fbc7
JB
428 responseCallback,
429 errorCallback,
82fa1110
JB
430 );
431 }
1a32c36b
JB
432 logger.debug(
433 `${chargingStation.logPrefix()} >> Command '${commandName}' sent ${OCPPServiceUtils.getMessageTypeString(
434 messageType,
435 )} payload: ${messageToSend}`,
436 );
1b2acf4e 437 PerformanceStatistics.endMeasure(commandName, beginId);
82fa1110
JB
438 } else {
439 if (params?.skipBufferingOnError === false) {
440 // Buffer
441 chargingStation.bufferMessage(messageToSend);
442 if (messageType === MessageType.CALL_MESSAGE) {
443 this.cacheRequestPromise(
444 chargingStation,
82fa1110
JB
445 messageId,
446 messagePayload as JsonType,
447 commandName,
54a8fbc7
JB
448 responseCallback,
449 errorCallback,
82fa1110
JB
450 );
451 }
452 }
1b2acf4e
JB
453 // Reject and keep request in the cache
454 return reject(
455 new OCPPError(
e7aeea18 456 ErrorType.GENERIC_ERROR,
82fa1110
JB
457 `WebSocket closed for ${
458 params?.skipBufferingOnError === false ? '' : 'non '
459 }buffered message id '${messageId}' with content '${messageToSend}'`,
e7aeea18 460 commandName,
2035255d 461 (messagePayload as JsonObject)?.details ?? Constants.EMPTY_FROZEN_OBJECT,
1b2acf4e
JB
462 ),
463 );
1b2acf4e
JB
464 }
465 // Resolve response
466 if (messageType !== MessageType.CALL_MESSAGE) {
467 return resolve(messagePayload);
468 }
469 });
caad9d6b 470 }
e7aeea18
JB
471 throw new OCPPError(
472 ErrorType.SECURITY_ERROR,
e3018bc4 473 `Cannot send command ${commandName} PDU when the charging station is in ${chargingStation.getRegistrationStatus()} state on the central server`,
5edd8ba0 474 commandName,
e7aeea18 475 );
c0560973
JB
476 }
477
e7aeea18 478 private buildMessageToSend(
08f130a0 479 chargingStation: ChargingStation,
e7aeea18 480 messageId: string,
5cc4b63b 481 messagePayload: JsonType | OCPPError,
e7aeea18 482 messageType: MessageType,
72092cfc 483 commandName: RequestCommand | IncomingRequestCommand,
e7aeea18 484 ): string {
e7accadb
JB
485 let messageToSend: string;
486 // Type of message
487 switch (messageType) {
488 // Request
489 case MessageType.CALL_MESSAGE:
490 // Build request
291b5ec8 491 this.validateRequestPayload(chargingStation, commandName, messagePayload as JsonType);
b3ec7bc1
JB
492 messageToSend = JSON.stringify([
493 messageType,
494 messageId,
495 commandName,
496 messagePayload,
497 ] as OutgoingRequest);
e7accadb
JB
498 break;
499 // Response
500 case MessageType.CALL_RESULT_MESSAGE:
501 // Build response
02887891
JB
502 this.validateIncomingRequestResponsePayload(
503 chargingStation,
504 commandName,
291b5ec8 505 messagePayload as JsonType,
02887891 506 );
b3ec7bc1 507 messageToSend = JSON.stringify([messageType, messageId, messagePayload] as Response);
e7accadb
JB
508 break;
509 // Error Message
510 case MessageType.CALL_ERROR_MESSAGE:
511 // Build Error Message
e7aeea18
JB
512 messageToSend = JSON.stringify([
513 messageType,
514 messageId,
b3ec7bc1
JB
515 (messagePayload as OCPPError)?.code ?? ErrorType.GENERIC_ERROR,
516 (messagePayload as OCPPError)?.message ?? '',
517 (messagePayload as OCPPError)?.details ?? { commandName },
518 ] as ErrorResponse);
e7accadb
JB
519 break;
520 }
521 return messageToSend;
522 }
523
82fa1110
JB
524 private cacheRequestPromise(
525 chargingStation: ChargingStation,
82fa1110
JB
526 messageId: string,
527 messagePayload: JsonType,
528 commandName: RequestCommand | IncomingRequestCommand,
54a8fbc7
JB
529 responseCallback: ResponseCallback,
530 errorCallback: ErrorCallback,
82fa1110
JB
531 ): void {
532 chargingStation.requests.set(messageId, [
533 responseCallback,
534 errorCallback,
535 commandName,
536 messagePayload,
537 ]);
538 }
539
ef6fa3fb 540 // eslint-disable-next-line @typescript-eslint/no-unused-vars
e0b0ee21 541 public abstract requestHandler<ReqType extends JsonType, ResType extends JsonType>(
08f130a0 542 chargingStation: ChargingStation,
94a464f9 543 commandName: RequestCommand,
e1d9a0f4 544 // FIXME: should be ReqType
5cc4b63b 545 commandParams?: JsonType,
5edd8ba0 546 params?: RequestParams,
e0b0ee21 547 ): Promise<ResType>;
c0560973 548}