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