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
,
25 import { cloneObject
, handleSendMessageError
, isNullOrUndefined
, logger
} from
'../../utils';
27 const moduleName
= 'OCPPRequestService';
29 const defaultRequestParams
: RequestParams
= {
30 skipBufferingOnError
: false,
31 triggerMessage
: false,
35 export abstract class OCPPRequestService
{
36 private static instance
: OCPPRequestService
| null = null;
37 private readonly version
: OCPPVersion
;
38 private readonly ajv
: Ajv
;
39 private readonly ocppResponseService
: OCPPResponseService
;
40 private readonly jsonValidateFunctions
: Map
<RequestCommand
, ValidateFunction
<JsonType
>>;
41 protected abstract jsonSchemas
: Map
<RequestCommand
, JSONSchemaType
<JsonType
>>;
43 protected constructor(version
: OCPPVersion
, ocppResponseService
: OCPPResponseService
) {
44 this.version
= version
;
46 keywords
: ['javaType'],
47 multipleOfPrecision
: 2,
50 this.jsonValidateFunctions
= new Map
<RequestCommand
, ValidateFunction
<JsonType
>>();
51 this.ocppResponseService
= ocppResponseService
;
52 this.requestHandler
= this.requestHandler
.bind(this) as <
53 // eslint-disable-next-line @typescript-eslint/no-unused-vars
54 ReqType
extends JsonType
,
55 ResType
extends JsonType
,
57 chargingStation
: ChargingStation
,
58 commandName
: RequestCommand
,
59 commandParams
?: JsonType
,
60 params
?: RequestParams
,
61 ) => Promise
<ResType
>;
62 this.sendMessage
= this.sendMessage
.bind(this) as (
63 chargingStation
: ChargingStation
,
65 messagePayload
: JsonType
,
66 commandName
: RequestCommand
,
67 params
?: RequestParams
,
68 ) => Promise
<ResponseType
>;
69 this.sendResponse
= this.sendResponse
.bind(this) as (
70 chargingStation
: ChargingStation
,
72 messagePayload
: JsonType
,
73 commandName
: IncomingRequestCommand
,
74 ) => Promise
<ResponseType
>;
75 this.sendError
= this.sendError
.bind(this) as (
76 chargingStation
: ChargingStation
,
79 commandName
: RequestCommand
| IncomingRequestCommand
,
80 ) => Promise
<ResponseType
>;
81 this.internalSendMessage
= this.internalSendMessage
.bind(this) as (
82 chargingStation
: ChargingStation
,
84 messagePayload
: JsonType
| OCPPError
,
85 messageType
: MessageType
,
86 commandName
: RequestCommand
| IncomingRequestCommand
,
87 params
?: RequestParams
,
88 ) => Promise
<ResponseType
>;
89 this.buildMessageToSend
= this.buildMessageToSend
.bind(this) as (
90 chargingStation
: ChargingStation
,
92 messagePayload
: JsonType
| OCPPError
,
93 messageType
: MessageType
,
94 commandName
: RequestCommand
| IncomingRequestCommand
,
96 this.validateRequestPayload
= this.validateRequestPayload
.bind(this) as <T
extends JsonType
>(
97 chargingStation
: ChargingStation
,
98 commandName
: RequestCommand
| IncomingRequestCommand
,
101 this.validateIncomingRequestResponsePayload
= this.validateIncomingRequestResponsePayload
.bind(
103 ) as <T
extends JsonType
>(
104 chargingStation
: ChargingStation
,
105 commandName
: RequestCommand
| IncomingRequestCommand
,
110 public static getInstance
<T
extends OCPPRequestService
>(
111 this: new (ocppResponseService
: OCPPResponseService
) => T
,
112 ocppResponseService
: OCPPResponseService
,
114 if (OCPPRequestService
.instance
=== null) {
115 OCPPRequestService
.instance
= new this(ocppResponseService
);
117 return OCPPRequestService
.instance
as T
;
120 public async sendResponse(
121 chargingStation
: ChargingStation
,
123 messagePayload
: JsonType
,
124 commandName
: IncomingRequestCommand
,
125 ): Promise
<ResponseType
> {
127 // Send response message
128 return await this.internalSendMessage(
132 MessageType
.CALL_RESULT_MESSAGE
,
136 handleSendMessageError(chargingStation
, commandName
, error
as Error, {
143 public async sendError(
144 chargingStation
: ChargingStation
,
146 ocppError
: OCPPError
,
147 commandName
: RequestCommand
| IncomingRequestCommand
,
148 ): Promise
<ResponseType
> {
150 // Send error message
151 return await this.internalSendMessage(
155 MessageType
.CALL_ERROR_MESSAGE
,
159 handleSendMessageError(chargingStation
, commandName
, error
as Error);
164 protected async sendMessage(
165 chargingStation
: ChargingStation
,
167 messagePayload
: JsonType
,
168 commandName
: RequestCommand
,
169 params
?: RequestParams
,
170 ): Promise
<ResponseType
> {
172 ...defaultRequestParams
,
176 return await this.internalSendMessage(
180 MessageType
.CALL_MESSAGE
,
185 handleSendMessageError(chargingStation
, commandName
, error
as Error, {
186 throwError
: params
.throwError
,
192 private validateRequestPayload
<T
extends JsonType
>(
193 chargingStation
: ChargingStation
,
194 commandName
: RequestCommand
| IncomingRequestCommand
,
197 if (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false) {
200 if (this.jsonSchemas
.has(commandName
as RequestCommand
) === false) {
202 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: No JSON schema found for command '${commandName}' PDU validation`,
206 const validate
= this.getJsonRequestValidateFunction
<T
>(commandName
as RequestCommand
);
207 payload
= cloneObject
<T
>(payload
);
208 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
);
209 if (validate(payload
)) {
213 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: Command '${commandName}' request PDU is invalid: %j`,
216 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
218 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
),
219 'Request PDU is invalid',
221 JSON
.stringify(validate
.errors
, undefined, 2),
225 private getJsonRequestValidateFunction
<T
extends JsonType
>(commandName
: RequestCommand
) {
226 if (this.jsonValidateFunctions
.has(commandName
) === false) {
227 this.jsonValidateFunctions
.set(
229 this.ajv
.compile
<T
>(this.jsonSchemas
.get(commandName
)!).bind(this),
232 return this.jsonValidateFunctions
.get(commandName
)!;
235 private validateIncomingRequestResponsePayload
<T
extends JsonType
>(
236 chargingStation
: ChargingStation
,
237 commandName
: RequestCommand
| IncomingRequestCommand
,
240 if (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false) {
244 this.ocppResponseService
.jsonIncomingRequestResponseSchemas
.has(
245 commandName
as IncomingRequestCommand
,
249 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: No JSON schema found for command '${commandName}' PDU validation`,
253 const validate
= this.getJsonRequestResponseValidateFunction
<T
>(
254 commandName
as IncomingRequestCommand
,
256 payload
= cloneObject
<T
>(payload
);
257 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
);
258 if (validate(payload
)) {
262 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: Command '${commandName}' reponse PDU is invalid: %j`,
265 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
267 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
),
268 'Response PDU is invalid',
270 JSON
.stringify(validate
.errors
, undefined, 2),
274 private getJsonRequestResponseValidateFunction
<T
extends JsonType
>(
275 commandName
: IncomingRequestCommand
,
278 this.ocppResponseService
.jsonIncomingRequestResponseValidateFunctions
.has(commandName
) ===
281 this.ocppResponseService
.jsonIncomingRequestResponseValidateFunctions
.set(
284 .compile
<T
>(this.ocppResponseService
.jsonIncomingRequestResponseSchemas
.get(commandName
)!)
288 return this.ocppResponseService
.jsonIncomingRequestResponseValidateFunctions
.get(commandName
)!;
291 private async internalSendMessage(
292 chargingStation
: ChargingStation
,
294 messagePayload
: JsonType
| OCPPError
,
295 messageType
: MessageType
,
296 commandName
: RequestCommand
| IncomingRequestCommand
,
297 params
?: RequestParams
,
298 ): Promise
<ResponseType
> {
300 ...defaultRequestParams
,
304 (chargingStation
.inUnknownState() === true &&
305 commandName
=== RequestCommand
.BOOT_NOTIFICATION
) ||
306 (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false &&
307 chargingStation
.inUnknownState() === true) ||
308 chargingStation
.inAcceptedState() === true ||
309 (chargingStation
.inPendingState() === true &&
310 (params
.triggerMessage
=== true || messageType
=== MessageType
.CALL_RESULT_MESSAGE
))
312 // eslint-disable-next-line @typescript-eslint/no-this-alias
314 // Send a message through wsConnection
315 return new Promise
<ResponseType
>((resolve
, reject
) => {
317 * Function that will receive the request's response
320 * @param requestPayload -
322 const responseCallback
= (payload
: JsonType
, requestPayload
: JsonType
): void => {
323 if (chargingStation
.stationInfo
?.enableStatistics
=== true) {
324 chargingStation
.performanceStatistics
?.addRequestStatistic(
326 MessageType
.CALL_RESULT_MESSAGE
,
329 // Handle the request's response
330 self.ocppResponseService
333 commandName
as RequestCommand
,
342 chargingStation
.requests
.delete(messageId
);
347 * Function that will receive the request's error response
350 * @param requestStatistic -
352 const errorCallback
= (ocppError
: OCPPError
, requestStatistic
= true): void => {
353 if (requestStatistic
=== true && chargingStation
.stationInfo
?.enableStatistics
=== true) {
354 chargingStation
.performanceStatistics
?.addRequestStatistic(
356 MessageType
.CALL_ERROR_MESSAGE
,
360 `${chargingStation.logPrefix()} Error occurred at ${OCPPServiceUtils.getMessageTypeString(
362 )} command ${commandName} with PDU %j:`,
366 chargingStation
.requests
.delete(messageId
);
370 const rejectAndCleanRequestsCache
= (ocppError
: OCPPError
): void => {
371 // Remove request from the cache
372 if (messageType
=== MessageType
.CALL_MESSAGE
) {
373 chargingStation
.requests
.delete(messageId
);
375 return reject(ocppError
);
378 const handleSendError
= (ocppError
: OCPPError
): void => {
379 if (params
?.skipBufferingOnError
=== false) {
381 chargingStation
.bufferMessage(messageToSend
);
382 if (messageType
=== MessageType
.CALL_MESSAGE
) {
383 this.cacheRequestPromise(
386 messagePayload
as JsonType
,
392 // Reject and keep request in the cache
393 return reject(ocppError
);
395 return rejectAndCleanRequestsCache(ocppError
);
398 if (chargingStation
.stationInfo
?.enableStatistics
=== true) {
399 chargingStation
.performanceStatistics
?.addRequestStatistic(commandName
, messageType
);
401 const messageToSend
= this.buildMessageToSend(
408 // Check if wsConnection opened
409 if (chargingStation
.isWebSocketConnectionOpened() === true) {
410 const beginId
= PerformanceStatistics
.beginMeasure(commandName
);
411 const sendTimeout
= setTimeout(() => {
412 return rejectAndCleanRequestsCache(
414 ErrorType
.GENERIC_ERROR
,
415 `Timeout for message id '${messageId}'`,
417 (messagePayload
as OCPPError
).details
,
420 }, OCPPConstants
.OCPP_WEBSOCKET_TIMEOUT
);
421 chargingStation
.wsConnection
?.send(messageToSend
, (error
?: Error) => {
422 PerformanceStatistics
.endMeasure(commandName
, beginId
);
423 clearTimeout(sendTimeout
);
424 if (isNullOrUndefined(error
)) {
426 `${chargingStation.logPrefix()} >> Command '${commandName}' sent ${OCPPServiceUtils.getMessageTypeString(
428 )} payload: ${messageToSend}`,
430 if (messageType
=== MessageType
.CALL_MESSAGE
) {
431 this.cacheRequestPromise(
434 messagePayload
as JsonType
,
441 return resolve(messagePayload
);
444 return handleSendError(
446 ErrorType
.GENERIC_ERROR
,
447 `WebSocket errored for ${
448 params?.skipBufferingOnError === false ? '' : 'non '
449 }buffered message id '${messageId}' with content '${messageToSend}'`,
451 { name
: error
.name
, message
: error
.message
, stack
: error
.stack
},
457 return handleSendError(
459 ErrorType
.GENERIC_ERROR
,
460 `WebSocket closed for ${
461 params?.skipBufferingOnError === false ? '' : 'non '
462 }buffered message id '${messageId}' with content '${messageToSend}'`,
464 (messagePayload
as OCPPError
).details
,
471 ErrorType
.SECURITY_ERROR
,
472 `Cannot send command ${commandName} PDU when the charging station is in ${chargingStation.getRegistrationStatus()} state on the central server`,
477 private buildMessageToSend(
478 chargingStation
: ChargingStation
,
480 messagePayload
: JsonType
| OCPPError
,
481 messageType
: MessageType
,
482 commandName
: RequestCommand
| IncomingRequestCommand
,
484 let messageToSend
: string;
486 switch (messageType
) {
488 case MessageType
.CALL_MESSAGE
:
490 this.validateRequestPayload(chargingStation
, commandName
, messagePayload
as JsonType
);
491 messageToSend
= JSON
.stringify([
496 ] as OutgoingRequest
);
499 case MessageType
.CALL_RESULT_MESSAGE
:
501 this.validateIncomingRequestResponsePayload(
504 messagePayload
as JsonType
,
506 messageToSend
= JSON
.stringify([messageType
, messageId
, messagePayload
] as Response
);
509 case MessageType
.CALL_ERROR_MESSAGE
:
510 // Build Error Message
511 messageToSend
= JSON
.stringify([
514 (messagePayload
as OCPPError
).code
,
515 (messagePayload
as OCPPError
).message
,
516 (messagePayload
as OCPPError
).details
?? {
517 command
: (messagePayload
as OCPPError
).command
?? commandName
,
522 return messageToSend
;
525 private cacheRequestPromise(
526 chargingStation
: ChargingStation
,
528 messagePayload
: JsonType
,
529 commandName
: RequestCommand
| IncomingRequestCommand
,
530 responseCallback
: ResponseCallback
,
531 errorCallback
: ErrorCallback
,
533 chargingStation
.requests
.set(messageId
, [
541 // eslint-disable-next-line @typescript-eslint/no-unused-vars
542 public abstract requestHandler
<ReqType
extends JsonType
, ResType
extends JsonType
>(
543 chargingStation
: ChargingStation
,
544 commandName
: RequestCommand
,
545 // FIXME: should be ReqType
546 commandParams
?: JsonType
,
547 params
?: RequestParams
,