1 import Ajv
, { type JSONSchemaType
} 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 protected abstract jsonSchemas
: Map
<RequestCommand
, JSONSchemaType
<JsonObject
>>;
49 protected constructor(version
: OCPPVersion
, ocppResponseService
: OCPPResponseService
) {
50 this.version
= version
;
52 keywords
: ['javaType'],
53 multipleOfPrecision
: 2,
56 this.ocppResponseService
= ocppResponseService
;
57 this.requestHandler
= this.requestHandler
.bind(this) as <
58 ReqType
extends JsonType
,
59 ResType
extends JsonType
61 chargingStation
: ChargingStation
,
62 commandName
: RequestCommand
,
63 commandParams
?: JsonType
,
64 params
?: RequestParams
65 ) => Promise
<ResType
>;
66 this.sendMessage
= this.sendMessage
.bind(this) as (
67 chargingStation
: ChargingStation
,
69 messagePayload
: JsonType
,
70 commandName
: RequestCommand
,
71 params
?: RequestParams
72 ) => Promise
<ResponseType
>;
73 this.sendResponse
= this.sendResponse
.bind(this) as (
74 chargingStation
: ChargingStation
,
76 messagePayload
: JsonType
,
77 commandName
: IncomingRequestCommand
78 ) => Promise
<ResponseType
>;
79 this.sendError
= this.sendError
.bind(this) as (
80 chargingStation
: ChargingStation
,
83 commandName
: RequestCommand
| IncomingRequestCommand
84 ) => Promise
<ResponseType
>;
85 this.internalSendMessage
= this.internalSendMessage
.bind(this) as (
86 chargingStation
: ChargingStation
,
88 messagePayload
: JsonType
| OCPPError
,
89 messageType
: MessageType
,
90 commandName
: RequestCommand
| IncomingRequestCommand
,
91 params
?: RequestParams
92 ) => Promise
<ResponseType
>;
93 this.buildMessageToSend
= this.buildMessageToSend
.bind(this) as (
94 chargingStation
: ChargingStation
,
96 messagePayload
: JsonType
| OCPPError
,
97 messageType
: MessageType
,
98 commandName
: RequestCommand
| IncomingRequestCommand
,
99 responseCallback
: ResponseCallback
,
100 errorCallback
: ErrorCallback
102 this.validateRequestPayload
= this.validateRequestPayload
.bind(this) as <T
extends JsonObject
>(
103 chargingStation
: ChargingStation
,
104 commandName
: RequestCommand
| IncomingRequestCommand
,
107 this.validateIncomingRequestResponsePayload
= this.validateIncomingRequestResponsePayload
.bind(
109 ) as <T
extends JsonObject
>(
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, {
148 public async sendError(
149 chargingStation
: ChargingStation
,
151 ocppError
: OCPPError
,
152 commandName
: RequestCommand
| IncomingRequestCommand
153 ): Promise
<ResponseType
> {
155 // Send error message
156 return await this.internalSendMessage(
160 MessageType
.CALL_ERROR_MESSAGE
,
164 handleSendMessageError(chargingStation
, commandName
, error
as Error);
168 protected async sendMessage(
169 chargingStation
: ChargingStation
,
171 messagePayload
: JsonType
,
172 commandName
: RequestCommand
,
173 params
: RequestParams
= defaultRequestParams
174 ): Promise
<ResponseType
> {
176 ...defaultRequestParams
,
180 return await this.internalSendMessage(
184 MessageType
.CALL_MESSAGE
,
189 handleSendMessageError(chargingStation
, commandName
, error
as Error, {
190 throwError
: params
.throwError
,
195 private validateRequestPayload
<T
extends JsonObject
>(
196 chargingStation
: ChargingStation
,
197 commandName
: RequestCommand
| IncomingRequestCommand
,
200 if (chargingStation
.getOcppStrictCompliance() === false) {
203 if (this.jsonSchemas
.has(commandName
as RequestCommand
) === false) {
205 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: No JSON schema found for command '${commandName}' PDU validation`
209 const validate
= this.ajv
.compile(this.jsonSchemas
.get(commandName
as RequestCommand
));
210 payload
= cloneObject
<T
>(payload
);
211 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
);
212 if (validate(payload
)) {
216 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: Command '${commandName}' request PDU is invalid: %j`,
219 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
221 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
),
222 'Request PDU is invalid',
224 JSON
.stringify(validate
.errors
, null, 2)
228 private validateIncomingRequestResponsePayload
<T
extends JsonObject
>(
229 chargingStation
: ChargingStation
,
230 commandName
: RequestCommand
| IncomingRequestCommand
,
233 if (chargingStation
.getOcppStrictCompliance() === false) {
237 this.ocppResponseService
.jsonIncomingRequestResponseSchemas
.has(
238 commandName
as IncomingRequestCommand
242 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: No JSON schema found for command '${commandName}' PDU validation`
246 const validate
= this.ajv
.compile(
247 this.ocppResponseService
.jsonIncomingRequestResponseSchemas
.get(
248 commandName
as IncomingRequestCommand
251 payload
= cloneObject
<T
>(payload
);
252 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
);
253 if (validate(payload
)) {
257 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: Command '${commandName}' reponse PDU is invalid: %j`,
260 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
262 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
),
263 'Response PDU is invalid',
265 JSON
.stringify(validate
.errors
, null, 2)
269 private async internalSendMessage(
270 chargingStation
: ChargingStation
,
272 messagePayload
: JsonType
| OCPPError
,
273 messageType
: MessageType
,
274 commandName
: RequestCommand
| IncomingRequestCommand
,
275 params
: RequestParams
= defaultRequestParams
276 ): Promise
<ResponseType
> {
278 ...defaultRequestParams
,
282 (chargingStation
.inUnknownState() === true &&
283 commandName
=== RequestCommand
.BOOT_NOTIFICATION
) ||
284 (chargingStation
.getOcppStrictCompliance() === false &&
285 chargingStation
.inUnknownState() === true) ||
286 chargingStation
.inAcceptedState() === true ||
287 (chargingStation
.inPendingState() === true &&
288 (params
.triggerMessage
=== true || messageType
=== MessageType
.CALL_RESULT_MESSAGE
))
290 // eslint-disable-next-line @typescript-eslint/no-this-alias
292 // Send a message through wsConnection
293 return promiseWithTimeout(
294 new Promise((resolve
, reject
) => {
296 * Function that will receive the request's response
299 * @param requestPayload -
301 const responseCallback
= (payload
: JsonType
, requestPayload
: JsonType
): void => {
302 if (chargingStation
.getEnableStatistics() === true) {
303 chargingStation
.performanceStatistics
?.addRequestStatistic(
305 MessageType
.CALL_RESULT_MESSAGE
308 // Handle the request's response
309 self.ocppResponseService
312 commandName
as RequestCommand
,
323 chargingStation
.requests
.delete(messageId
);
328 * Function that will receive the request's error response
331 * @param requestStatistic -
333 const errorCallback
= (error
: OCPPError
, requestStatistic
= true): void => {
334 if (requestStatistic
=== true && chargingStation
.getEnableStatistics() === true) {
335 chargingStation
.performanceStatistics
?.addRequestStatistic(
337 MessageType
.CALL_ERROR_MESSAGE
341 `${chargingStation.logPrefix()} Error occurred at ${OCPPServiceUtils.getMessageTypeString(
343 )} command ${commandName} with PDU %j:`,
347 chargingStation
.requests
.delete(messageId
);
351 if (chargingStation
.getEnableStatistics() === true) {
352 chargingStation
.performanceStatistics
?.addRequestStatistic(commandName
, messageType
);
354 const messageToSend
= this.buildMessageToSend(
363 let sendError
= false;
364 // Check if wsConnection opened
365 const wsOpened
= chargingStation
.isWebSocketConnectionOpened() === true;
367 const beginId
= PerformanceStatistics
.beginMeasure(commandName
);
369 chargingStation
.wsConnection
?.send(messageToSend
);
371 `${chargingStation.logPrefix()} >> Command '${commandName}' sent ${OCPPServiceUtils.getMessageTypeString(
373 )} payload: ${messageToSend}`
377 `${chargingStation.logPrefix()} >> Command '${commandName}' failed to send ${OCPPServiceUtils.getMessageTypeString(
379 )} payload: ${messageToSend}:`,
384 PerformanceStatistics
.endMeasure(commandName
, beginId
);
386 const wsClosedOrErrored
= !wsOpened
|| sendError
=== true;
387 if (wsClosedOrErrored
&& params
.skipBufferingOnError
=== false) {
389 chargingStation
.bufferMessage(messageToSend
);
390 // Reject and keep request in the cache
393 ErrorType
.GENERIC_ERROR
,
394 `WebSocket closed or errored for buffered message id '${messageId}' with content '${messageToSend}'`,
396 (messagePayload
as JsonObject
)?.details
?? Constants
.EMPTY_FREEZED_OBJECT
399 } else if (wsClosedOrErrored
) {
400 const ocppError
= new OCPPError(
401 ErrorType
.GENERIC_ERROR
,
402 `WebSocket closed or errored for non buffered message id '${messageId}' with content '${messageToSend}'`,
404 (messagePayload
as JsonObject
)?.details
?? Constants
.EMPTY_FREEZED_OBJECT
407 if (messageType
!== MessageType
.CALL_MESSAGE
) {
408 return reject(ocppError
);
410 // Reject and remove request from the cache
411 return errorCallback(ocppError
, false);
414 if (messageType
!== MessageType
.CALL_MESSAGE
) {
415 return resolve(messagePayload
);
418 OCPPConstants
.OCPP_WEBSOCKET_TIMEOUT
,
420 ErrorType
.GENERIC_ERROR
,
421 `Timeout for message id '${messageId}'`,
423 (messagePayload
as JsonObject
)?.details
?? Constants
.EMPTY_FREEZED_OBJECT
426 messageType
=== MessageType
.CALL_MESSAGE
&& chargingStation
.requests
.delete(messageId
);
431 ErrorType
.SECURITY_ERROR
,
432 `Cannot send command ${commandName} PDU when the charging station is in ${chargingStation.getRegistrationStatus()} state on the central server`,
437 private buildMessageToSend(
438 chargingStation
: ChargingStation
,
440 messagePayload
: JsonType
| OCPPError
,
441 messageType
: MessageType
,
442 commandName
: RequestCommand
| IncomingRequestCommand
,
443 responseCallback
: ResponseCallback
,
444 errorCallback
: ErrorCallback
446 let messageToSend
: string;
448 switch (messageType
) {
450 case MessageType
.CALL_MESSAGE
:
452 this.validateRequestPayload(chargingStation
, commandName
, messagePayload
as JsonObject
);
453 chargingStation
.requests
.set(messageId
, [
457 messagePayload
as JsonType
,
459 messageToSend
= JSON
.stringify([
464 ] as OutgoingRequest
);
467 case MessageType
.CALL_RESULT_MESSAGE
:
469 this.validateIncomingRequestResponsePayload(
472 messagePayload
as JsonObject
474 messageToSend
= JSON
.stringify([messageType
, messageId
, messagePayload
] as Response
);
477 case MessageType
.CALL_ERROR_MESSAGE
:
478 // Build Error Message
479 messageToSend
= JSON
.stringify([
482 (messagePayload
as OCPPError
)?.code
?? ErrorType
.GENERIC_ERROR
,
483 (messagePayload
as OCPPError
)?.message
?? '',
484 (messagePayload
as OCPPError
)?.details
?? { commandName
},
488 return messageToSend
;
491 // eslint-disable-next-line @typescript-eslint/no-unused-vars
492 public abstract requestHandler
<ReqType
extends JsonType
, ResType
extends JsonType
>(
493 chargingStation
: ChargingStation
,
494 commandName
: RequestCommand
,
495 commandParams
?: JsonType
,
496 params
?: RequestParams