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