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