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