fix: various fixes to files handling and their content caching
[e-mobility-charging-stations-simulator.git] / src / charging-station / ocpp / OCPPRequestService.ts
CommitLineData
d270cc87 1import Ajv, { type JSONSchemaType } from 'ajv';
b52c969d
JB
2import ajvFormats from 'ajv-formats';
3
4c3c0d59
JB
4import { OCPPConstants } from './OCPPConstants';
5import type { OCPPResponseService } from './OCPPResponseService';
6import { OCPPServiceUtils } from './OCPPServiceUtils';
2896e06d 7import type { ChargingStation } from '../../charging-station';
268a74bb 8import { OCPPError } from '../../exception';
b84bca85 9import { PerformanceStatistics } from '../../performance';
e7aeea18 10import {
27782dbc 11 type ErrorCallback,
268a74bb
JB
12 type ErrorResponse,
13 ErrorType,
27782dbc 14 type IncomingRequestCommand,
268a74bb
JB
15 type JsonObject,
16 type JsonType,
17 MessageType,
18 type OCPPVersion,
27782dbc 19 type OutgoingRequest,
e7aeea18 20 RequestCommand,
27782dbc 21 type RequestParams,
268a74bb 22 type Response,
27782dbc 23 type ResponseCallback,
e0b0ee21 24 type ResponseType,
268a74bb 25} from '../../types';
7671fa0b 26import { Constants, ErrorUtils, Utils, logger } from '../../utils';
c0560973 27
e3018bc4
JB
28const moduleName = 'OCPPRequestService';
29
268a74bb 30export abstract class OCPPRequestService {
08f130a0 31 private static instance: OCPPRequestService | null = null;
d270cc87 32 private readonly version: OCPPVersion;
012ae1a9 33 private readonly ajv: Ajv;
9f2e3130 34 private readonly ocppResponseService: OCPPResponseService;
b3fc3ff5 35 protected abstract jsonSchemas: Map<RequestCommand, JSONSchemaType<JsonObject>>;
c0560973 36
d270cc87
JB
37 protected constructor(version: OCPPVersion, ocppResponseService: OCPPResponseService) {
38 this.version = version;
45988780 39 this.ajv = new Ajv({
98fc1389 40 keywords: ['javaType'],
45988780
JB
41 multipleOfPrecision: 2,
42 });
9952c548 43 ajvFormats(this.ajv);
d270cc87 44 this.ocppResponseService = ocppResponseService;
31f59c6d
JB
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;
c0560973
JB
102 }
103
e7aeea18 104 public static getInstance<T extends OCPPRequestService>(
08f130a0 105 this: new (ocppResponseService: OCPPResponseService) => T,
e7aeea18
JB
106 ocppResponseService: OCPPResponseService
107 ): T {
1ca780f9 108 if (OCPPRequestService.instance === null) {
08f130a0 109 OCPPRequestService.instance = new this(ocppResponseService);
9f2e3130 110 }
08f130a0 111 return OCPPRequestService.instance as T;
9f2e3130
JB
112 }
113
c75a6675 114 public async sendResponse(
08f130a0 115 chargingStation: ChargingStation,
e7aeea18 116 messageId: string,
5cc4b63b 117 messagePayload: JsonType,
e7aeea18 118 commandName: IncomingRequestCommand
5eaabe90 119 ): Promise<ResponseType> {
5e0c67e8 120 try {
c75a6675 121 // Send response message
e7aeea18 122 return await this.internalSendMessage(
08f130a0 123 chargingStation,
e7aeea18
JB
124 messageId,
125 messagePayload,
126 MessageType.CALL_RESULT_MESSAGE,
127 commandName
128 );
5e0c67e8 129 } catch (error) {
7671fa0b 130 ErrorUtils.handleSendMessageError(chargingStation, commandName, error as Error, {
07561812 131 throwError: true,
dc922667 132 });
5e0c67e8
JB
133 }
134 }
135
e7aeea18 136 public async sendError(
08f130a0 137 chargingStation: ChargingStation,
e7aeea18
JB
138 messageId: string,
139 ocppError: OCPPError,
b3ec7bc1 140 commandName: RequestCommand | IncomingRequestCommand
e7aeea18 141 ): Promise<ResponseType> {
5e0c67e8
JB
142 try {
143 // Send error message
e7aeea18 144 return await this.internalSendMessage(
08f130a0 145 chargingStation,
e7aeea18
JB
146 messageId,
147 ocppError,
148 MessageType.CALL_ERROR_MESSAGE,
149 commandName
150 );
5e0c67e8 151 } catch (error) {
7671fa0b 152 ErrorUtils.handleSendMessageError(chargingStation, commandName, error as Error);
5e0c67e8
JB
153 }
154 }
155
e7aeea18 156 protected async sendMessage(
08f130a0 157 chargingStation: ChargingStation,
e7aeea18 158 messageId: string,
5cc4b63b 159 messagePayload: JsonType,
e7aeea18 160 commandName: RequestCommand,
be9b0d50 161 params: RequestParams = {
e7aeea18
JB
162 skipBufferingOnError: false,
163 triggerMessage: false,
8ec8e3d0 164 throwError: false,
e7aeea18
JB
165 }
166 ): Promise<ResponseType> {
7b5dbe91
JB
167 params = {
168 ...{ skipBufferingOnError: false, triggerMessage: false, throwError: false },
169 ...params,
170 };
5e0c67e8 171 try {
e7aeea18 172 return await this.internalSendMessage(
08f130a0 173 chargingStation,
e7aeea18
JB
174 messageId,
175 messagePayload,
176 MessageType.CALL_MESSAGE,
177 commandName,
178 params
179 );
5e0c67e8 180 } catch (error) {
7671fa0b 181 ErrorUtils.handleSendMessageError(chargingStation, commandName, error as Error, {
8ec8e3d0
JB
182 throwError: params.throwError,
183 });
5e0c67e8
JB
184 }
185 }
186
02887891 187 private validateRequestPayload<T extends JsonObject>(
b52c969d 188 chargingStation: ChargingStation,
45988780 189 commandName: RequestCommand | IncomingRequestCommand,
b52c969d
JB
190 payload: T
191 ): boolean {
0638ddd2 192 if (chargingStation.getPayloadSchemaValidation() === false) {
b52c969d
JB
193 return true;
194 }
b3fc3ff5
JB
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 );
45988780
JB
199 return true;
200 }
b3fc3ff5
JB
201 const validate = this.ajv.compile(this.jsonSchemas.get(commandName as RequestCommand));
202 payload = Utils.cloneObject<T>(payload);
1799761a 203 OCPPServiceUtils.convertDateToISOString<T>(payload);
b52c969d
JB
204 if (validate(payload)) {
205 return true;
206 }
207 logger.error(
45988780 208 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: Command '${commandName}' request PDU is invalid: %j`,
b52c969d
JB
209 validate.errors
210 );
e909d2a7 211 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
b52c969d 212 throw new OCPPError(
01a4dcbb 213 OCPPServiceUtils.ajvErrorsToErrorType(validate.errors),
b52c969d
JB
214 'Request PDU is invalid',
215 commandName,
216 JSON.stringify(validate.errors, null, 2)
217 );
218 }
219
02887891 220 private validateIncomingRequestResponsePayload<T extends JsonObject>(
b3fc3ff5
JB
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(
02887891 234 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: No JSON schema found for command '${commandName}' PDU validation`
b3fc3ff5
JB
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(
02887891 249 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: Command '${commandName}' reponse PDU is invalid: %j`,
b3fc3ff5
JB
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
e7aeea18 261 private async internalSendMessage(
08f130a0 262 chargingStation: ChargingStation,
e7aeea18 263 messageId: string,
5cc4b63b 264 messagePayload: JsonType | OCPPError,
e7aeea18 265 messageType: MessageType,
72092cfc 266 commandName: RequestCommand | IncomingRequestCommand,
be9b0d50 267 params: RequestParams = {
e7aeea18
JB
268 skipBufferingOnError: false,
269 triggerMessage: false,
7b5dbe91 270 throwError: false,
e7aeea18
JB
271 }
272 ): Promise<ResponseType> {
7b5dbe91
JB
273 params = {
274 ...{ skipBufferingOnError: false, triggerMessage: false, throwError: false },
275 ...params,
276 };
e7aeea18 277 if (
f7c2994d 278 (chargingStation.inUnknownState() === true &&
3a13fc92
JB
279 commandName === RequestCommand.BOOT_NOTIFICATION) ||
280 (chargingStation.getOcppStrictCompliance() === false &&
f7c2994d
JB
281 chargingStation.inUnknownState() === true) ||
282 chargingStation.inAcceptedState() === true ||
283 (chargingStation.inPendingState() === true &&
3a13fc92 284 (params.triggerMessage === true || messageType === MessageType.CALL_RESULT_MESSAGE))
e7aeea18 285 ) {
caad9d6b
JB
286 // eslint-disable-next-line @typescript-eslint/no-this-alias
287 const self = this;
288 // Send a message through wsConnection
e7aeea18
JB
289 return Utils.promiseWithTimeout(
290 new Promise((resolve, reject) => {
e8a92d57
JB
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
764d2c91 347 if (chargingStation.getEnableStatistics() === true) {
551e477c 348 chargingStation.performanceStatistics?.addRequestStatistic(commandName, messageType);
764d2c91 349 }
e7aeea18 350 const messageToSend = this.buildMessageToSend(
08f130a0 351 chargingStation,
e7aeea18
JB
352 messageId,
353 messagePayload,
354 messageType,
355 commandName,
356 responseCallback,
a2d1c0f1 357 errorCallback
e7aeea18 358 );
1b821a64 359 let sendError = false;
e7aeea18 360 // Check if wsConnection opened
764d2c91
JB
361 const wsOpened = chargingStation.isWebSocketConnectionOpened() === true;
362 if (wsOpened) {
1431af78 363 const beginId = PerformanceStatistics.beginMeasure(commandName);
1b821a64 364 try {
72092cfc 365 chargingStation.wsConnection?.send(messageToSend);
18bf8274 366 logger.debug(
2cc5d5ec 367 `${chargingStation.logPrefix()} >> Command '${commandName}' sent ${OCPPServiceUtils.getMessageTypeString(
18bf8274
JB
368 messageType
369 )} payload: ${messageToSend}`
370 );
1b821a64 371 } catch (error) {
18bf8274 372 logger.error(
2cc5d5ec 373 `${chargingStation.logPrefix()} >> Command '${commandName}' failed to send ${OCPPServiceUtils.getMessageTypeString(
18bf8274
JB
374 messageType
375 )} payload: ${messageToSend}:`,
376 error
377 );
1b821a64
JB
378 sendError = true;
379 }
1431af78 380 PerformanceStatistics.endMeasure(commandName, beginId);
1b821a64 381 }
764d2c91 382 const wsClosedOrErrored = !wsOpened || sendError === true;
1b821a64
JB
383 if (wsClosedOrErrored && params.skipBufferingOnError === false) {
384 // Buffer
08f130a0 385 chargingStation.bufferMessage(messageToSend);
1b821a64
JB
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,
59b6ed8d 392 (messagePayload as JsonObject)?.details ?? Constants.EMPTY_FREEZED_OBJECT
1b821a64
JB
393 )
394 );
395 } else if (wsClosedOrErrored) {
e7aeea18
JB
396 const ocppError = new OCPPError(
397 ErrorType.GENERIC_ERROR,
1b821a64 398 `WebSocket closed or errored for non buffered message id '${messageId}' with content '${messageToSend}'`,
e7aeea18 399 commandName,
59b6ed8d 400 (messagePayload as JsonObject)?.details ?? Constants.EMPTY_FREEZED_OBJECT
e7aeea18 401 );
1b821a64
JB
402 // Reject response
403 if (messageType !== MessageType.CALL_MESSAGE) {
e7aeea18
JB
404 return reject(ocppError);
405 }
1b821a64 406 // Reject and remove request from the cache
a2d1c0f1 407 return errorCallback(ocppError, false);
caad9d6b 408 }
1b821a64 409 // Resolve response
e7aeea18 410 if (messageType !== MessageType.CALL_MESSAGE) {
e7aeea18
JB
411 return resolve(messagePayload);
412 }
e7aeea18 413 }),
d8b1fab1 414 OCPPConstants.OCPP_WEBSOCKET_TIMEOUT,
e7aeea18
JB
415 new OCPPError(
416 ErrorType.GENERIC_ERROR,
417 `Timeout for message id '${messageId}'`,
418 commandName,
59b6ed8d 419 (messagePayload as JsonObject)?.details ?? Constants.EMPTY_FREEZED_OBJECT
e7aeea18
JB
420 ),
421 () => {
08f130a0 422 messageType === MessageType.CALL_MESSAGE && chargingStation.requests.delete(messageId);
caad9d6b 423 }
e7aeea18 424 );
caad9d6b 425 }
e7aeea18
JB
426 throw new OCPPError(
427 ErrorType.SECURITY_ERROR,
e3018bc4 428 `Cannot send command ${commandName} PDU when the charging station is in ${chargingStation.getRegistrationStatus()} state on the central server`,
e7aeea18
JB
429 commandName
430 );
c0560973
JB
431 }
432
e7aeea18 433 private buildMessageToSend(
08f130a0 434 chargingStation: ChargingStation,
e7aeea18 435 messageId: string,
5cc4b63b 436 messagePayload: JsonType | OCPPError,
e7aeea18 437 messageType: MessageType,
72092cfc
JB
438 commandName: RequestCommand | IncomingRequestCommand,
439 responseCallback: ResponseCallback,
440 errorCallback: ErrorCallback
e7aeea18 441 ): string {
e7accadb
JB
442 let messageToSend: string;
443 // Type of message
444 switch (messageType) {
445 // Request
446 case MessageType.CALL_MESSAGE:
447 // Build request
cda96260 448 this.validateRequestPayload(chargingStation, commandName, messagePayload as JsonObject);
08f130a0 449 chargingStation.requests.set(messageId, [
e7aeea18 450 responseCallback,
a2d1c0f1 451 errorCallback,
e7aeea18 452 commandName,
5cc4b63b 453 messagePayload as JsonType,
e7aeea18 454 ]);
b3ec7bc1
JB
455 messageToSend = JSON.stringify([
456 messageType,
457 messageId,
458 commandName,
459 messagePayload,
460 ] as OutgoingRequest);
e7accadb
JB
461 break;
462 // Response
463 case MessageType.CALL_RESULT_MESSAGE:
464 // Build response
02887891
JB
465 this.validateIncomingRequestResponsePayload(
466 chargingStation,
467 commandName,
468 messagePayload as JsonObject
469 );
b3ec7bc1 470 messageToSend = JSON.stringify([messageType, messageId, messagePayload] as Response);
e7accadb
JB
471 break;
472 // Error Message
473 case MessageType.CALL_ERROR_MESSAGE:
474 // Build Error Message
e7aeea18
JB
475 messageToSend = JSON.stringify([
476 messageType,
477 messageId,
b3ec7bc1
JB
478 (messagePayload as OCPPError)?.code ?? ErrorType.GENERIC_ERROR,
479 (messagePayload as OCPPError)?.message ?? '',
480 (messagePayload as OCPPError)?.details ?? { commandName },
481 ] as ErrorResponse);
e7accadb
JB
482 break;
483 }
484 return messageToSend;
485 }
486
ef6fa3fb 487 // eslint-disable-next-line @typescript-eslint/no-unused-vars
e0b0ee21 488 public abstract requestHandler<ReqType extends JsonType, ResType extends JsonType>(
08f130a0 489 chargingStation: ChargingStation,
94a464f9 490 commandName: RequestCommand,
5cc4b63b 491 commandParams?: JsonType,
be9b0d50 492 params?: RequestParams
e0b0ee21 493 ): Promise<ResType>;
c0560973 494}