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