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