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