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