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