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