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