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