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