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