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
,
23 type ResponseCallback
,
29 handleSendMessageError
,
34 const moduleName
= 'OCPPRequestService';
36 const defaultRequestParams
: RequestParams
= {
37 skipBufferingOnError
: false,
38 triggerMessage
: false,
42 export abstract class OCPPRequestService
{
43 private static instance
: OCPPRequestService
| null = null;
44 private readonly version
: OCPPVersion
;
45 private readonly ajv
: Ajv
;
46 private readonly ocppResponseService
: OCPPResponseService
;
47 private readonly jsonValidateFunctions
: Map
<RequestCommand
, ValidateFunction
<JsonType
>>;
48 protected abstract jsonSchemas
: Map
<RequestCommand
, JSONSchemaType
<JsonType
>>;
50 protected constructor(version
: OCPPVersion
, ocppResponseService
: OCPPResponseService
) {
51 this.version
= version
;
53 keywords
: ['javaType'],
54 multipleOfPrecision
: 2,
57 this.jsonValidateFunctions
= new Map
<RequestCommand
, ValidateFunction
<JsonType
>>();
58 this.ocppResponseService
= ocppResponseService
;
59 this.requestHandler
= this.requestHandler
.bind(this) as <
60 // eslint-disable-next-line @typescript-eslint/no-unused-vars
61 ReqType
extends JsonType
,
62 ResType
extends JsonType
,
64 chargingStation
: ChargingStation
,
65 commandName
: RequestCommand
,
66 commandParams
?: JsonType
,
67 params
?: RequestParams
,
68 ) => Promise
<ResType
>;
69 this.sendMessage
= this.sendMessage
.bind(this) as (
70 chargingStation
: ChargingStation
,
72 messagePayload
: JsonType
,
73 commandName
: RequestCommand
,
74 params
?: RequestParams
,
75 ) => Promise
<ResponseType
>;
76 this.sendResponse
= this.sendResponse
.bind(this) as (
77 chargingStation
: ChargingStation
,
79 messagePayload
: JsonType
,
80 commandName
: IncomingRequestCommand
,
81 ) => Promise
<ResponseType
>;
82 this.sendError
= this.sendError
.bind(this) as (
83 chargingStation
: ChargingStation
,
86 commandName
: RequestCommand
| IncomingRequestCommand
,
87 ) => Promise
<ResponseType
>;
88 this.internalSendMessage
= this.internalSendMessage
.bind(this) as (
89 chargingStation
: ChargingStation
,
91 messagePayload
: JsonType
| OCPPError
,
92 messageType
: MessageType
,
93 commandName
: RequestCommand
| IncomingRequestCommand
,
94 params
?: RequestParams
,
95 ) => Promise
<ResponseType
>;
96 this.buildMessageToSend
= this.buildMessageToSend
.bind(this) as (
97 chargingStation
: ChargingStation
,
99 messagePayload
: JsonType
| OCPPError
,
100 messageType
: MessageType
,
101 commandName
: RequestCommand
| IncomingRequestCommand
,
103 this.validateRequestPayload
= this.validateRequestPayload
.bind(this) as <T
extends JsonType
>(
104 chargingStation
: ChargingStation
,
105 commandName
: RequestCommand
| IncomingRequestCommand
,
108 this.validateIncomingRequestResponsePayload
= this.validateIncomingRequestResponsePayload
.bind(
110 ) as <T
extends JsonType
>(
111 chargingStation
: ChargingStation
,
112 commandName
: RequestCommand
| IncomingRequestCommand
,
117 public static getInstance
<T
extends OCPPRequestService
>(
118 this: new (ocppResponseService
: OCPPResponseService
) => T
,
119 ocppResponseService
: OCPPResponseService
,
121 if (OCPPRequestService
.instance
=== null) {
122 OCPPRequestService
.instance
= new this(ocppResponseService
);
124 return OCPPRequestService
.instance
as T
;
127 public async sendResponse(
128 chargingStation
: ChargingStation
,
130 messagePayload
: JsonType
,
131 commandName
: IncomingRequestCommand
,
132 ): Promise
<ResponseType
> {
134 // Send response message
135 return await this.internalSendMessage(
139 MessageType
.CALL_RESULT_MESSAGE
,
143 handleSendMessageError(chargingStation
, commandName
, error
as Error, {
150 public async sendError(
151 chargingStation
: ChargingStation
,
153 ocppError
: OCPPError
,
154 commandName
: RequestCommand
| IncomingRequestCommand
,
155 ): Promise
<ResponseType
> {
157 // Send error message
158 return await this.internalSendMessage(
162 MessageType
.CALL_ERROR_MESSAGE
,
166 handleSendMessageError(chargingStation
, commandName
, error
as Error);
171 protected async sendMessage(
172 chargingStation
: ChargingStation
,
174 messagePayload
: JsonType
,
175 commandName
: RequestCommand
,
176 params
?: RequestParams
,
177 ): Promise
<ResponseType
> {
179 ...defaultRequestParams
,
183 return await this.internalSendMessage(
187 MessageType
.CALL_MESSAGE
,
192 handleSendMessageError(chargingStation
, commandName
, error
as Error, {
193 throwError
: params
.throwError
,
199 private validateRequestPayload
<T
extends JsonType
>(
200 chargingStation
: ChargingStation
,
201 commandName
: RequestCommand
| IncomingRequestCommand
,
204 if (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false) {
207 if (this.jsonSchemas
.has(commandName
as RequestCommand
) === false) {
209 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: No JSON schema found for command '${commandName}' PDU validation`,
213 const validate
= this.getJsonRequestValidateFunction
<T
>(commandName
as RequestCommand
);
214 payload
= cloneObject
<T
>(payload
);
215 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
);
216 if (validate(payload
)) {
220 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: Command '${commandName}' request PDU is invalid: %j`,
223 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
225 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
),
226 'Request PDU is invalid',
228 JSON
.stringify(validate
.errors
, undefined, 2),
232 private getJsonRequestValidateFunction
<T
extends JsonType
>(commandName
: RequestCommand
) {
233 if (this.jsonValidateFunctions
.has(commandName
) === false) {
234 this.jsonValidateFunctions
.set(
236 this.ajv
.compile
<T
>(this.jsonSchemas
.get(commandName
)!).bind(this),
239 return this.jsonValidateFunctions
.get(commandName
)!;
242 private validateIncomingRequestResponsePayload
<T
extends JsonType
>(
243 chargingStation
: ChargingStation
,
244 commandName
: RequestCommand
| IncomingRequestCommand
,
247 if (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false) {
251 this.ocppResponseService
.jsonIncomingRequestResponseSchemas
.has(
252 commandName
as IncomingRequestCommand
,
256 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: No JSON schema found for command '${commandName}' PDU validation`,
260 const validate
= this.getJsonRequestResponseValidateFunction
<T
>(
261 commandName
as IncomingRequestCommand
,
263 payload
= cloneObject
<T
>(payload
);
264 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
);
265 if (validate(payload
)) {
269 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: Command '${commandName}' reponse PDU is invalid: %j`,
272 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
274 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
),
275 'Response PDU is invalid',
277 JSON
.stringify(validate
.errors
, undefined, 2),
281 private getJsonRequestResponseValidateFunction
<T
extends JsonType
>(
282 commandName
: IncomingRequestCommand
,
285 this.ocppResponseService
.jsonIncomingRequestResponseValidateFunctions
.has(commandName
) ===
288 this.ocppResponseService
.jsonIncomingRequestResponseValidateFunctions
.set(
291 .compile
<T
>(this.ocppResponseService
.jsonIncomingRequestResponseSchemas
.get(commandName
)!)
295 return this.ocppResponseService
.jsonIncomingRequestResponseValidateFunctions
.get(commandName
)!;
298 private async internalSendMessage(
299 chargingStation
: ChargingStation
,
301 messagePayload
: JsonType
| OCPPError
,
302 messageType
: MessageType
,
303 commandName
: RequestCommand
| IncomingRequestCommand
,
304 params
?: RequestParams
,
305 ): Promise
<ResponseType
> {
307 ...defaultRequestParams
,
311 (chargingStation
.inUnknownState() === true &&
312 commandName
=== RequestCommand
.BOOT_NOTIFICATION
) ||
313 (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false &&
314 chargingStation
.inUnknownState() === true) ||
315 chargingStation
.inAcceptedState() === true ||
316 (chargingStation
.inPendingState() === true &&
317 (params
.triggerMessage
=== true || messageType
=== MessageType
.CALL_RESULT_MESSAGE
))
319 // eslint-disable-next-line @typescript-eslint/no-this-alias
321 // Send a message through wsConnection
322 return new Promise
<ResponseType
>((resolve
, reject
) => {
324 * Function that will receive the request's response
327 * @param requestPayload -
329 const responseCallback
= (payload
: JsonType
, requestPayload
: JsonType
): void => {
330 if (chargingStation
.stationInfo
?.enableStatistics
=== true) {
331 chargingStation
.performanceStatistics
?.addRequestStatistic(
333 MessageType
.CALL_RESULT_MESSAGE
,
336 // Handle the request's response
337 self.ocppResponseService
340 commandName
as RequestCommand
,
351 chargingStation
.requests
.delete(messageId
);
356 * Function that will receive the request's error response
359 * @param requestStatistic -
361 const errorCallback
= (ocppError
: OCPPError
, requestStatistic
= true): void => {
362 if (requestStatistic
=== true && chargingStation
.stationInfo
?.enableStatistics
=== true) {
363 chargingStation
.performanceStatistics
?.addRequestStatistic(
365 MessageType
.CALL_ERROR_MESSAGE
,
369 `${chargingStation.logPrefix()} Error occurred at ${OCPPServiceUtils.getMessageTypeString(
371 )} command ${commandName} with PDU %j:`,
375 chargingStation
.requests
.delete(messageId
);
379 const rejectWithOcppError
= (ocppError
: OCPPError
): void => {
381 if (messageType
!== MessageType
.CALL_MESSAGE
) {
382 return reject(ocppError
);
384 // Reject and remove request from the cache
385 return errorCallback(ocppError
, false);
388 const bufferAndRejectWithOcppError
= (ocppError
: OCPPError
): void => {
390 chargingStation
.bufferMessage(messageToSend
);
391 if (messageType
=== MessageType
.CALL_MESSAGE
) {
392 this.cacheRequestPromise(
395 messagePayload
as JsonType
,
401 // Reject and keep request in the cache
402 return reject(ocppError
);
405 if (chargingStation
.stationInfo
?.enableStatistics
=== true) {
406 chargingStation
.performanceStatistics
?.addRequestStatistic(commandName
, messageType
);
408 const messageToSend
= this.buildMessageToSend(
415 // Check if wsConnection opened
416 if (chargingStation
.isWebSocketConnectionOpened() === true) {
417 const beginId
= PerformanceStatistics
.beginMeasure(commandName
);
418 const sendTimeout
= setTimeout(() => {
419 return rejectWithOcppError(
421 ErrorType
.GENERIC_ERROR
,
422 `Timeout for message id '${messageId}'`,
424 (messagePayload
as JsonObject
)?.details
?? Constants
.EMPTY_FROZEN_OBJECT
,
427 }, OCPPConstants
.OCPP_WEBSOCKET_TIMEOUT
);
428 chargingStation
.wsConnection
?.send(messageToSend
, (error
?: Error) => {
429 PerformanceStatistics
.endMeasure(commandName
, beginId
);
430 clearTimeout(sendTimeout
);
431 if (isNullOrUndefined(error
)) {
433 `${chargingStation.logPrefix()} >> Command '${commandName}' sent ${OCPPServiceUtils.getMessageTypeString(
435 )} payload: ${messageToSend}`,
437 if (messageType
=== MessageType
.CALL_MESSAGE
) {
438 this.cacheRequestPromise(
441 messagePayload
as JsonType
,
448 if (messageType
!== MessageType
.CALL_MESSAGE
) {
449 return resolve(messagePayload
);
452 const ocppError
= new OCPPError(
453 ErrorType
.GENERIC_ERROR
,
454 `WebSocket errored for ${
455 params?.skipBufferingOnError === false ? '' : 'non '
456 }buffered message id '${messageId}' with content '${messageToSend}'`,
458 { name
: error
.name
, message
: error
.message
, stack
: error
.stack
},
460 if (params
?.skipBufferingOnError
=== false) {
461 return bufferAndRejectWithOcppError(ocppError
);
463 return rejectWithOcppError(ocppError
);
467 const ocppError
= new OCPPError(
468 ErrorType
.GENERIC_ERROR
,
469 `WebSocket closed for ${
470 params?.skipBufferingOnError === false ? '' : 'non '
471 }buffered message id '${messageId}' with content '${messageToSend}'`,
473 (messagePayload
as JsonObject
)?.details
?? Constants
.EMPTY_FROZEN_OBJECT
,
475 if (params
?.skipBufferingOnError
=== false) {
476 return bufferAndRejectWithOcppError(ocppError
);
478 return rejectWithOcppError(ocppError
);
483 ErrorType
.SECURITY_ERROR
,
484 `Cannot send command ${commandName} PDU when the charging station is in ${chargingStation.getRegistrationStatus()} state on the central server`,
489 private buildMessageToSend(
490 chargingStation
: ChargingStation
,
492 messagePayload
: JsonType
| OCPPError
,
493 messageType
: MessageType
,
494 commandName
: RequestCommand
| IncomingRequestCommand
,
496 let messageToSend
: string;
498 switch (messageType
) {
500 case MessageType
.CALL_MESSAGE
:
502 this.validateRequestPayload(chargingStation
, commandName
, messagePayload
as JsonType
);
503 messageToSend
= JSON
.stringify([
508 ] as OutgoingRequest
);
511 case MessageType
.CALL_RESULT_MESSAGE
:
513 this.validateIncomingRequestResponsePayload(
516 messagePayload
as JsonType
,
518 messageToSend
= JSON
.stringify([messageType
, messageId
, messagePayload
] as Response
);
521 case MessageType
.CALL_ERROR_MESSAGE
:
522 // Build Error Message
523 messageToSend
= JSON
.stringify([
526 (messagePayload
as OCPPError
)?.code
?? ErrorType
.GENERIC_ERROR
,
527 (messagePayload
as OCPPError
)?.message
?? '',
528 (messagePayload
as OCPPError
)?.details
?? { commandName
},
532 return messageToSend
;
535 private cacheRequestPromise(
536 chargingStation
: ChargingStation
,
538 messagePayload
: JsonType
,
539 commandName
: RequestCommand
| IncomingRequestCommand
,
540 responseCallback
: ResponseCallback
,
541 errorCallback
: ErrorCallback
,
543 chargingStation
.requests
.set(messageId
, [
551 // eslint-disable-next-line @typescript-eslint/no-unused-vars
552 public abstract requestHandler
<ReqType
extends JsonType
, ResType
extends JsonType
>(
553 chargingStation
: ChargingStation
,
554 commandName
: RequestCommand
,
555 // FIXME: should be ReqType
556 commandParams
?: JsonType
,
557 params
?: RequestParams
,