1 import Ajv
, { type JSONSchemaType
, type ValidateFunction
} from
'ajv';
2 import ajvFormats from
'ajv-formats';
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';
14 type IncomingRequestCommand
,
22 type ResponseCallback
,
27 formatDurationMilliSeconds
,
28 handleSendMessageError
,
33 const moduleName
= 'OCPPRequestService';
35 const defaultRequestParams
: RequestParams
= {
36 skipBufferingOnError
: false,
37 triggerMessage
: false,
41 export abstract class OCPPRequestService
{
42 private static instance
: OCPPRequestService
| null = null;
43 private readonly version
: OCPPVersion
;
44 private readonly ajv
: Ajv
;
45 private readonly ocppResponseService
: OCPPResponseService
;
46 private readonly jsonValidateFunctions
: Map
<RequestCommand
, ValidateFunction
<JsonType
>>;
47 protected abstract jsonSchemas
: Map
<RequestCommand
, JSONSchemaType
<JsonType
>>;
49 protected constructor(version
: OCPPVersion
, ocppResponseService
: OCPPResponseService
) {
50 this.version
= version
;
52 keywords
: ['javaType'],
53 multipleOfPrecision
: 2,
56 this.jsonValidateFunctions
= new Map
<RequestCommand
, ValidateFunction
<JsonType
>>();
57 this.ocppResponseService
= ocppResponseService
;
58 this.requestHandler
= this.requestHandler
.bind(this) as <
59 // eslint-disable-next-line @typescript-eslint/no-unused-vars
60 ReqType
extends JsonType
,
61 ResType
extends JsonType
,
63 chargingStation
: ChargingStation
,
64 commandName
: RequestCommand
,
65 commandParams
?: JsonType
,
66 params
?: RequestParams
,
67 ) => Promise
<ResType
>;
68 this.sendMessage
= this.sendMessage
.bind(this) as (
69 chargingStation
: ChargingStation
,
71 messagePayload
: JsonType
,
72 commandName
: RequestCommand
,
73 params
?: RequestParams
,
74 ) => Promise
<ResponseType
>;
75 this.sendResponse
= this.sendResponse
.bind(this) as (
76 chargingStation
: ChargingStation
,
78 messagePayload
: JsonType
,
79 commandName
: IncomingRequestCommand
,
80 ) => Promise
<ResponseType
>;
81 this.sendError
= this.sendError
.bind(this) as (
82 chargingStation
: ChargingStation
,
85 commandName
: RequestCommand
| IncomingRequestCommand
,
86 ) => Promise
<ResponseType
>;
87 this.internalSendMessage
= this.internalSendMessage
.bind(this) as (
88 chargingStation
: ChargingStation
,
90 messagePayload
: JsonType
| OCPPError
,
91 messageType
: MessageType
,
92 commandName
: RequestCommand
| IncomingRequestCommand
,
93 params
?: RequestParams
,
94 ) => Promise
<ResponseType
>;
95 this.buildMessageToSend
= this.buildMessageToSend
.bind(this) as (
96 chargingStation
: ChargingStation
,
98 messagePayload
: JsonType
| OCPPError
,
99 messageType
: MessageType
,
100 commandName
: RequestCommand
| IncomingRequestCommand
,
102 this.validateRequestPayload
= this.validateRequestPayload
.bind(this) as <T
extends JsonType
>(
103 chargingStation
: ChargingStation
,
104 commandName
: RequestCommand
| IncomingRequestCommand
,
107 this.validateIncomingRequestResponsePayload
= this.validateIncomingRequestResponsePayload
.bind(
109 ) as <T
extends JsonType
>(
110 chargingStation
: ChargingStation
,
111 commandName
: RequestCommand
| IncomingRequestCommand
,
116 public static getInstance
<T
extends OCPPRequestService
>(
117 this: new (ocppResponseService
: OCPPResponseService
) => T
,
118 ocppResponseService
: OCPPResponseService
,
120 if (OCPPRequestService
.instance
=== null) {
121 OCPPRequestService
.instance
= new this(ocppResponseService
);
123 return OCPPRequestService
.instance
as T
;
126 public async sendResponse(
127 chargingStation
: ChargingStation
,
129 messagePayload
: JsonType
,
130 commandName
: IncomingRequestCommand
,
131 ): Promise
<ResponseType
> {
133 // Send response message
134 return await this.internalSendMessage(
138 MessageType
.CALL_RESULT_MESSAGE
,
142 handleSendMessageError(chargingStation
, commandName
, error
as Error, {
149 public async sendError(
150 chargingStation
: ChargingStation
,
152 ocppError
: OCPPError
,
153 commandName
: RequestCommand
| IncomingRequestCommand
,
154 ): Promise
<ResponseType
> {
156 // Send error message
157 return await this.internalSendMessage(
161 MessageType
.CALL_ERROR_MESSAGE
,
165 handleSendMessageError(chargingStation
, commandName
, error
as Error);
170 protected async sendMessage(
171 chargingStation
: ChargingStation
,
173 messagePayload
: JsonType
,
174 commandName
: RequestCommand
,
175 params
?: RequestParams
,
176 ): Promise
<ResponseType
> {
178 ...defaultRequestParams
,
182 return await this.internalSendMessage(
186 MessageType
.CALL_MESSAGE
,
191 handleSendMessageError(chargingStation
, commandName
, error
as Error, {
192 throwError
: params
.throwError
,
198 private validateRequestPayload
<T
extends JsonType
>(
199 chargingStation
: ChargingStation
,
200 commandName
: RequestCommand
| IncomingRequestCommand
,
203 if (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false) {
206 if (this.jsonSchemas
.has(commandName
as RequestCommand
) === false) {
208 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: No JSON schema found for command '${commandName}' PDU validation`,
212 const validate
= this.getJsonRequestValidateFunction
<T
>(commandName
as RequestCommand
);
213 payload
= cloneObject
<T
>(payload
);
214 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
);
215 if (validate(payload
)) {
219 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: Command '${commandName}' request PDU is invalid: %j`,
222 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
224 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
),
225 'Request PDU is invalid',
227 JSON
.stringify(validate
.errors
, undefined, 2),
231 private getJsonRequestValidateFunction
<T
extends JsonType
>(commandName
: RequestCommand
) {
232 if (this.jsonValidateFunctions
.has(commandName
) === false) {
233 this.jsonValidateFunctions
.set(
235 this.ajv
.compile
<T
>(this.jsonSchemas
.get(commandName
)!).bind(this),
238 return this.jsonValidateFunctions
.get(commandName
)!;
241 private validateIncomingRequestResponsePayload
<T
extends JsonType
>(
242 chargingStation
: ChargingStation
,
243 commandName
: RequestCommand
| IncomingRequestCommand
,
246 if (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false) {
250 this.ocppResponseService
.jsonIncomingRequestResponseSchemas
.has(
251 commandName
as IncomingRequestCommand
,
255 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: No JSON schema found for command '${commandName}' PDU validation`,
259 const validate
= this.getJsonRequestResponseValidateFunction
<T
>(
260 commandName
as IncomingRequestCommand
,
262 payload
= cloneObject
<T
>(payload
);
263 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
);
264 if (validate(payload
)) {
268 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: Command '${commandName}' reponse PDU is invalid: %j`,
271 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
273 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
),
274 'Response PDU is invalid',
276 JSON
.stringify(validate
.errors
, undefined, 2),
280 private getJsonRequestResponseValidateFunction
<T
extends JsonType
>(
281 commandName
: IncomingRequestCommand
,
284 this.ocppResponseService
.jsonIncomingRequestResponseValidateFunctions
.has(commandName
) ===
287 this.ocppResponseService
.jsonIncomingRequestResponseValidateFunctions
.set(
290 .compile
<T
>(this.ocppResponseService
.jsonIncomingRequestResponseSchemas
.get(commandName
)!)
294 return this.ocppResponseService
.jsonIncomingRequestResponseValidateFunctions
.get(commandName
)!;
297 private async internalSendMessage(
298 chargingStation
: ChargingStation
,
300 messagePayload
: JsonType
| OCPPError
,
301 messageType
: MessageType
,
302 commandName
: RequestCommand
| IncomingRequestCommand
,
303 params
?: RequestParams
,
304 ): Promise
<ResponseType
> {
306 ...defaultRequestParams
,
310 (chargingStation
.inUnknownState() === true &&
311 commandName
=== RequestCommand
.BOOT_NOTIFICATION
) ||
312 (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false &&
313 chargingStation
.inUnknownState() === true) ||
314 chargingStation
.inAcceptedState() === true ||
315 (chargingStation
.inPendingState() === true &&
316 (params
.triggerMessage
=== true || messageType
=== MessageType
.CALL_RESULT_MESSAGE
))
318 // eslint-disable-next-line @typescript-eslint/no-this-alias
320 // Send a message through wsConnection
321 return new Promise
<ResponseType
>((resolve
, reject
) => {
323 * Function that will receive the request's response
326 * @param requestPayload -
328 const responseCallback
= (payload
: JsonType
, requestPayload
: JsonType
): void => {
329 if (chargingStation
.stationInfo
?.enableStatistics
=== true) {
330 chargingStation
.performanceStatistics
?.addRequestStatistic(
332 MessageType
.CALL_RESULT_MESSAGE
,
335 // Handle the request's response
336 self.ocppResponseService
339 commandName
as RequestCommand
,
348 chargingStation
.requests
.delete(messageId
);
353 * Function that will receive the request's error response
356 * @param requestStatistic -
358 const errorCallback
= (ocppError
: OCPPError
, requestStatistic
= true): void => {
359 if (requestStatistic
=== true && chargingStation
.stationInfo
?.enableStatistics
=== true) {
360 chargingStation
.performanceStatistics
?.addRequestStatistic(
362 MessageType
.CALL_ERROR_MESSAGE
,
366 `${chargingStation.logPrefix()} Error occurred at ${OCPPServiceUtils.getMessageTypeString(
368 )} command ${commandName} with PDU %j:`,
372 chargingStation
.requests
.delete(messageId
);
376 const handleSendError
= (ocppError
: OCPPError
): void => {
377 if (params
?.skipBufferingOnError
=== false) {
379 chargingStation
.bufferMessage(messageToSend
);
380 if (messageType
=== MessageType
.CALL_MESSAGE
) {
381 this.cacheRequestPromise(
384 messagePayload
as JsonType
,
390 } else if (messageType
=== MessageType
.CALL_MESSAGE
) {
391 // Remove request from the cache
392 chargingStation
.requests
.delete(messageId
);
394 return reject(ocppError
);
397 if (chargingStation
.stationInfo
?.enableStatistics
=== true) {
398 chargingStation
.performanceStatistics
?.addRequestStatistic(commandName
, messageType
);
400 const messageToSend
= this.buildMessageToSend(
407 // Check if wsConnection opened
408 if (chargingStation
.isWebSocketConnectionOpened() === true) {
409 const beginId
= PerformanceStatistics
.beginMeasure(commandName
);
410 const sendTimeout
= setTimeout(() => {
411 return handleSendError(
413 ErrorType
.GENERIC_ERROR
,
414 `Timeout ${formatDurationMilliSeconds(
415 OCPPConstants.OCPP_WEBSOCKET_TIMEOUT,
417 params?.skipBufferingOnError === false ? '' : 'non '
418 }buffered message id '${messageId}' with content '${messageToSend}`,
420 (messagePayload
as OCPPError
).details
,
423 }, OCPPConstants
.OCPP_WEBSOCKET_TIMEOUT
);
424 chargingStation
.wsConnection
?.send(messageToSend
, (error
?: Error) => {
425 PerformanceStatistics
.endMeasure(commandName
, beginId
);
426 clearTimeout(sendTimeout
);
427 if (isNullOrUndefined(error
)) {
429 `${chargingStation.logPrefix()} >> Command '${commandName}' sent ${OCPPServiceUtils.getMessageTypeString(
431 )} payload: ${messageToSend}`,
433 if (messageType
=== MessageType
.CALL_MESSAGE
) {
434 this.cacheRequestPromise(
437 messagePayload
as JsonType
,
444 return resolve(messagePayload
);
447 return handleSendError(
449 ErrorType
.GENERIC_ERROR
,
450 `WebSocket errored for ${
451 params?.skipBufferingOnError === false ? '' : 'non '
452 }buffered message id '${messageId}' with content '${messageToSend}'`,
454 { name
: error
.name
, message
: error
.message
, stack
: error
.stack
},
460 return handleSendError(
462 ErrorType
.GENERIC_ERROR
,
463 `WebSocket closed for ${
464 params?.skipBufferingOnError === false ? '' : 'non '
465 }buffered message id '${messageId}' with content '${messageToSend}'`,
467 (messagePayload
as OCPPError
).details
,
474 ErrorType
.SECURITY_ERROR
,
475 `Cannot send command ${commandName} PDU when the charging station is in ${chargingStation.getRegistrationStatus()} state on the central server`,
480 private buildMessageToSend(
481 chargingStation
: ChargingStation
,
483 messagePayload
: JsonType
| OCPPError
,
484 messageType
: MessageType
,
485 commandName
: RequestCommand
| IncomingRequestCommand
,
487 let messageToSend
: string;
489 switch (messageType
) {
491 case MessageType
.CALL_MESSAGE
:
493 this.validateRequestPayload(chargingStation
, commandName
, messagePayload
as JsonType
);
494 messageToSend
= JSON
.stringify([
499 ] as OutgoingRequest
);
502 case MessageType
.CALL_RESULT_MESSAGE
:
504 this.validateIncomingRequestResponsePayload(
507 messagePayload
as JsonType
,
509 messageToSend
= JSON
.stringify([messageType
, messageId
, messagePayload
] as Response
);
512 case MessageType
.CALL_ERROR_MESSAGE
:
513 // Build Error Message
514 messageToSend
= JSON
.stringify([
517 (messagePayload
as OCPPError
).code
,
518 (messagePayload
as OCPPError
).message
,
519 (messagePayload
as OCPPError
).details
?? {
520 command
: (messagePayload
as OCPPError
).command
?? commandName
,
525 return messageToSend
;
528 private cacheRequestPromise(
529 chargingStation
: ChargingStation
,
531 messagePayload
: JsonType
,
532 commandName
: RequestCommand
| IncomingRequestCommand
,
533 responseCallback
: ResponseCallback
,
534 errorCallback
: ErrorCallback
,
536 chargingStation
.requests
.set(messageId
, [
544 // eslint-disable-next-line @typescript-eslint/no-unused-vars
545 public abstract requestHandler
<ReqType
extends JsonType
, ResType
extends JsonType
>(
546 chargingStation
: ChargingStation
,
547 commandName
: RequestCommand
,
548 // FIXME: should be ReqType
549 commandParams
?: JsonType
,
550 params
?: RequestParams
,