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
,
26 import { Constants
, Utils
, handleSendMessageError
, logger
} from
'../../utils';
28 const moduleName
= 'OCPPRequestService';
30 const defaultRequestParams
: RequestParams
= {
31 skipBufferingOnError
: false,
32 triggerMessage
: false,
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
>>;
43 protected constructor(version
: OCPPVersion
, ocppResponseService
: OCPPResponseService
) {
44 this.version
= version
;
46 keywords
: ['javaType'],
47 multipleOfPrecision
: 2,
50 this.ocppResponseService
= ocppResponseService
;
51 this.requestHandler
= this.requestHandler
.bind(this) as <
52 ReqType
extends JsonType
,
53 ResType
extends JsonType
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
,
63 messagePayload
: JsonType
,
64 commandName
: RequestCommand
,
65 params
?: RequestParams
66 ) => Promise
<ResponseType
>;
67 this.sendResponse
= this.sendResponse
.bind(this) as (
68 chargingStation
: ChargingStation
,
70 messagePayload
: JsonType
,
71 commandName
: IncomingRequestCommand
72 ) => Promise
<ResponseType
>;
73 this.sendError
= this.sendError
.bind(this) as (
74 chargingStation
: ChargingStation
,
77 commandName
: RequestCommand
| IncomingRequestCommand
78 ) => Promise
<ResponseType
>;
79 this.internalSendMessage
= this.internalSendMessage
.bind(this) as (
80 chargingStation
: ChargingStation
,
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
,
90 messagePayload
: JsonType
| OCPPError
,
91 messageType
: MessageType
,
92 commandName
: RequestCommand
| IncomingRequestCommand
,
93 responseCallback
: ResponseCallback
,
94 errorCallback
: ErrorCallback
96 this.validateRequestPayload
= this.validateRequestPayload
.bind(this) as <T
extends JsonObject
>(
97 chargingStation
: ChargingStation
,
98 commandName
: RequestCommand
| IncomingRequestCommand
,
101 this.validateIncomingRequestResponsePayload
= this.validateIncomingRequestResponsePayload
.bind(
103 ) as <T
extends JsonObject
>(
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, {
142 public async sendError(
143 chargingStation
: ChargingStation
,
145 ocppError
: OCPPError
,
146 commandName
: RequestCommand
| IncomingRequestCommand
147 ): Promise
<ResponseType
> {
149 // Send error message
150 return await this.internalSendMessage(
154 MessageType
.CALL_ERROR_MESSAGE
,
158 handleSendMessageError(chargingStation
, commandName
, error
as Error);
162 protected async sendMessage(
163 chargingStation
: ChargingStation
,
165 messagePayload
: JsonType
,
166 commandName
: RequestCommand
,
167 params
: RequestParams
= defaultRequestParams
168 ): Promise
<ResponseType
> {
170 ...defaultRequestParams
,
174 return await this.internalSendMessage(
178 MessageType
.CALL_MESSAGE
,
183 handleSendMessageError(chargingStation
, commandName
, error
as Error, {
184 throwError
: params
.throwError
,
189 private validateRequestPayload
<T
extends JsonObject
>(
190 chargingStation
: ChargingStation
,
191 commandName
: RequestCommand
| IncomingRequestCommand
,
194 if (chargingStation
.getPayloadSchemaValidation() === false) {
197 if (this.jsonSchemas
.has(commandName
as RequestCommand
) === false) {
199 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: No JSON schema found for command '${commandName}' PDU validation`
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
)) {
210 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: Command '${commandName}' request PDU is invalid: %j`,
213 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
215 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
),
216 'Request PDU is invalid',
218 JSON
.stringify(validate
.errors
, null, 2)
222 private validateIncomingRequestResponsePayload
<T
extends JsonObject
>(
223 chargingStation
: ChargingStation
,
224 commandName
: RequestCommand
| IncomingRequestCommand
,
227 if (chargingStation
.getPayloadSchemaValidation() === false) {
231 this.ocppResponseService
.jsonIncomingRequestResponseSchemas
.has(
232 commandName
as IncomingRequestCommand
236 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: No JSON schema found for command '${commandName}' PDU validation`
240 const validate
= this.ajv
.compile(
241 this.ocppResponseService
.jsonIncomingRequestResponseSchemas
.get(
242 commandName
as IncomingRequestCommand
245 payload
= Utils
.cloneObject
<T
>(payload
);
246 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
);
247 if (validate(payload
)) {
251 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: Command '${commandName}' reponse PDU is invalid: %j`,
254 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
256 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
),
257 'Response PDU is invalid',
259 JSON
.stringify(validate
.errors
, null, 2)
263 private async internalSendMessage(
264 chargingStation
: ChargingStation
,
266 messagePayload
: JsonType
| OCPPError
,
267 messageType
: MessageType
,
268 commandName
: RequestCommand
| IncomingRequestCommand
,
269 params
: RequestParams
= defaultRequestParams
270 ): Promise
<ResponseType
> {
272 ...defaultRequestParams
,
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
))
284 // eslint-disable-next-line @typescript-eslint/no-this-alias
286 // Send a message through wsConnection
287 return Utils
.promiseWithTimeout(
288 new Promise((resolve
, reject
) => {
290 * Function that will receive the request's response
293 * @param requestPayload -
295 const responseCallback
= (payload
: JsonType
, requestPayload
: JsonType
): void => {
296 if (chargingStation
.getEnableStatistics() === true) {
297 chargingStation
.performanceStatistics
?.addRequestStatistic(
299 MessageType
.CALL_RESULT_MESSAGE
302 // Handle the request's response
303 self.ocppResponseService
306 commandName
as RequestCommand
,
317 chargingStation
.requests
.delete(messageId
);
322 * Function that will receive the request's error response
325 * @param requestStatistic -
327 const errorCallback
= (error
: OCPPError
, requestStatistic
= true): void => {
328 if (requestStatistic
=== true && chargingStation
.getEnableStatistics() === true) {
329 chargingStation
.performanceStatistics
?.addRequestStatistic(
331 MessageType
.CALL_ERROR_MESSAGE
335 `${chargingStation.logPrefix()} Error occurred at ${OCPPServiceUtils.getMessageTypeString(
337 )} command ${commandName} with PDU %j:`,
341 chargingStation
.requests
.delete(messageId
);
345 if (chargingStation
.getEnableStatistics() === true) {
346 chargingStation
.performanceStatistics
?.addRequestStatistic(commandName
, messageType
);
348 const messageToSend
= this.buildMessageToSend(
357 let sendError
= false;
358 // Check if wsConnection opened
359 const wsOpened
= chargingStation
.isWebSocketConnectionOpened() === true;
361 const beginId
= PerformanceStatistics
.beginMeasure(commandName
);
363 chargingStation
.wsConnection
?.send(messageToSend
);
365 `${chargingStation.logPrefix()} >> Command '${commandName}' sent ${OCPPServiceUtils.getMessageTypeString(
367 )} payload: ${messageToSend}`
371 `${chargingStation.logPrefix()} >> Command '${commandName}' failed to send ${OCPPServiceUtils.getMessageTypeString(
373 )} payload: ${messageToSend}:`,
378 PerformanceStatistics
.endMeasure(commandName
, beginId
);
380 const wsClosedOrErrored
= !wsOpened
|| sendError
=== true;
381 if (wsClosedOrErrored
&& params
.skipBufferingOnError
=== false) {
383 chargingStation
.bufferMessage(messageToSend
);
384 // Reject and keep request in the cache
387 ErrorType
.GENERIC_ERROR
,
388 `WebSocket closed or errored for buffered message id '${messageId}' with content '${messageToSend}'`,
390 (messagePayload
as JsonObject
)?.details
?? Constants
.EMPTY_FREEZED_OBJECT
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}'`,
398 (messagePayload
as JsonObject
)?.details
?? Constants
.EMPTY_FREEZED_OBJECT
401 if (messageType
!== MessageType
.CALL_MESSAGE
) {
402 return reject(ocppError
);
404 // Reject and remove request from the cache
405 return errorCallback(ocppError
, false);
408 if (messageType
!== MessageType
.CALL_MESSAGE
) {
409 return resolve(messagePayload
);
412 OCPPConstants
.OCPP_WEBSOCKET_TIMEOUT
,
414 ErrorType
.GENERIC_ERROR
,
415 `Timeout for message id '${messageId}'`,
417 (messagePayload
as JsonObject
)?.details
?? Constants
.EMPTY_FREEZED_OBJECT
420 messageType
=== MessageType
.CALL_MESSAGE
&& chargingStation
.requests
.delete(messageId
);
425 ErrorType
.SECURITY_ERROR
,
426 `Cannot send command ${commandName} PDU when the charging station is in ${chargingStation.getRegistrationStatus()} state on the central server`,
431 private buildMessageToSend(
432 chargingStation
: ChargingStation
,
434 messagePayload
: JsonType
| OCPPError
,
435 messageType
: MessageType
,
436 commandName
: RequestCommand
| IncomingRequestCommand
,
437 responseCallback
: ResponseCallback
,
438 errorCallback
: ErrorCallback
440 let messageToSend
: string;
442 switch (messageType
) {
444 case MessageType
.CALL_MESSAGE
:
446 this.validateRequestPayload(chargingStation
, commandName
, messagePayload
as JsonObject
);
447 chargingStation
.requests
.set(messageId
, [
451 messagePayload
as JsonType
,
453 messageToSend
= JSON
.stringify([
458 ] as OutgoingRequest
);
461 case MessageType
.CALL_RESULT_MESSAGE
:
463 this.validateIncomingRequestResponsePayload(
466 messagePayload
as JsonObject
468 messageToSend
= JSON
.stringify([messageType
, messageId
, messagePayload
] as Response
);
471 case MessageType
.CALL_ERROR_MESSAGE
:
472 // Build Error Message
473 messageToSend
= JSON
.stringify([
476 (messagePayload
as OCPPError
)?.code
?? ErrorType
.GENERIC_ERROR
,
477 (messagePayload
as OCPPError
)?.message
?? '',
478 (messagePayload
as OCPPError
)?.details
?? { commandName
},
482 return messageToSend
;
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