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