1 import Ajv
, { type JSONSchemaType
} from
'ajv';
2 import ajvFormats from
'ajv-formats';
4 import type { OCPPResponseService
} from
'./OCPPResponseService';
5 import { OCPPServiceUtils
} from
'./OCPPServiceUtils';
6 import { OCPPError
} from
'../../exception';
7 import { PerformanceStatistics
} from
'../../performance/PerformanceStatistics';
13 type HandleErrorParams
,
14 type IncomingRequestCommand
,
23 type ResponseCallback
,
26 import { Constants
} from
'../../utils/Constants';
27 import { logger
} from
'../../utils/Logger';
28 import { Utils
} from
'../../utils/Utils';
29 import type { ChargingStation
} from
'../ChargingStation';
31 const moduleName
= 'OCPPRequestService';
33 export abstract class OCPPRequestService
{
34 private static instance
: OCPPRequestService
| null = null;
35 private readonly version
: OCPPVersion
;
36 private readonly ajv
: Ajv
;
37 private readonly ocppResponseService
: OCPPResponseService
;
38 protected abstract jsonSchemas
: Map
<RequestCommand
, JSONSchemaType
<JsonObject
>>;
40 protected constructor(version
: OCPPVersion
, ocppResponseService
: OCPPResponseService
) {
41 this.version
= version
;
43 keywords
: ['javaType'],
44 multipleOfPrecision
: 2,
47 this.ocppResponseService
= ocppResponseService
;
48 this.requestHandler
.bind(this);
49 this.sendMessage
.bind(this);
50 this.sendResponse
.bind(this);
51 this.sendError
.bind(this);
52 this.internalSendMessage
.bind(this);
53 this.buildMessageToSend
.bind(this);
54 this.validateRequestPayload
.bind(this);
55 this.validateIncomingRequestResponsePayload
.bind(this);
58 public static getInstance
<T
extends OCPPRequestService
>(
59 this: new (ocppResponseService
: OCPPResponseService
) => T
,
60 ocppResponseService
: OCPPResponseService
62 if (OCPPRequestService
.instance
=== null) {
63 OCPPRequestService
.instance
= new this(ocppResponseService
);
65 return OCPPRequestService
.instance
as T
;
68 public async sendResponse(
69 chargingStation
: ChargingStation
,
71 messagePayload
: JsonType
,
72 commandName
: IncomingRequestCommand
73 ): Promise
<ResponseType
> {
75 // Send response message
76 return await this.internalSendMessage(
80 MessageType
.CALL_RESULT_MESSAGE
,
84 this.handleSendMessageError(chargingStation
, commandName
, error
as Error, {
90 public async sendError(
91 chargingStation
: ChargingStation
,
94 commandName
: RequestCommand
| IncomingRequestCommand
95 ): Promise
<ResponseType
> {
98 return await this.internalSendMessage(
102 MessageType
.CALL_ERROR_MESSAGE
,
106 this.handleSendMessageError(chargingStation
, commandName
, error
as Error);
110 protected async sendMessage(
111 chargingStation
: ChargingStation
,
113 messagePayload
: JsonType
,
114 commandName
: RequestCommand
,
115 params
: RequestParams
= {
116 skipBufferingOnError
: false,
117 triggerMessage
: false,
120 ): Promise
<ResponseType
> {
122 return await this.internalSendMessage(
126 MessageType
.CALL_MESSAGE
,
131 this.handleSendMessageError(chargingStation
, commandName
, error
as Error, {
132 throwError
: params
.throwError
,
137 private validateRequestPayload
<T
extends JsonObject
>(
138 chargingStation
: ChargingStation
,
139 commandName
: RequestCommand
| IncomingRequestCommand
,
142 if (chargingStation
.getPayloadSchemaValidation() === false) {
145 if (this.jsonSchemas
.has(commandName
as RequestCommand
) === false) {
147 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: No JSON schema found for command '${commandName}' PDU validation`
151 const validate
= this.ajv
.compile(this.jsonSchemas
.get(commandName
as RequestCommand
));
152 payload
= Utils
.cloneObject
<T
>(payload
);
153 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
);
154 if (validate(payload
)) {
158 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: Command '${commandName}' request PDU is invalid: %j`,
161 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
163 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
),
164 'Request PDU is invalid',
166 JSON
.stringify(validate
.errors
, null, 2)
170 private validateIncomingRequestResponsePayload
<T
extends JsonObject
>(
171 chargingStation
: ChargingStation
,
172 commandName
: RequestCommand
| IncomingRequestCommand
,
175 if (chargingStation
.getPayloadSchemaValidation() === false) {
179 this.ocppResponseService
.jsonIncomingRequestResponseSchemas
.has(
180 commandName
as IncomingRequestCommand
184 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: No JSON schema found for command '${commandName}' PDU validation`
188 const validate
= this.ajv
.compile(
189 this.ocppResponseService
.jsonIncomingRequestResponseSchemas
.get(
190 commandName
as IncomingRequestCommand
193 payload
= Utils
.cloneObject
<T
>(payload
);
194 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
);
195 if (validate(payload
)) {
199 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: Command '${commandName}' reponse PDU is invalid: %j`,
202 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
204 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
),
205 'Response PDU is invalid',
207 JSON
.stringify(validate
.errors
, null, 2)
211 private async internalSendMessage(
212 chargingStation
: ChargingStation
,
214 messagePayload
: JsonType
| OCPPError
,
215 messageType
: MessageType
,
216 commandName
: RequestCommand
| IncomingRequestCommand
,
217 params
: RequestParams
= {
218 skipBufferingOnError
: false,
219 triggerMessage
: false,
221 ): Promise
<ResponseType
> {
223 (chargingStation
.isInUnknownState() === true &&
224 commandName
=== RequestCommand
.BOOT_NOTIFICATION
) ||
225 (chargingStation
.getOcppStrictCompliance() === false &&
226 chargingStation
.isInUnknownState() === true) ||
227 chargingStation
.isInAcceptedState() === true ||
228 (chargingStation
.isInPendingState() === true &&
229 (params
.triggerMessage
=== true || messageType
=== MessageType
.CALL_RESULT_MESSAGE
))
231 // eslint-disable-next-line @typescript-eslint/no-this-alias
233 // Send a message through wsConnection
234 return Utils
.promiseWithTimeout(
235 new Promise((resolve
, reject
) => {
236 if (chargingStation
.getEnableStatistics() === true) {
237 chargingStation
.performanceStatistics
?.addRequestStatistic(commandName
, messageType
);
239 const messageToSend
= this.buildMessageToSend(
248 let sendError
= false;
249 // Check if wsConnection opened
250 const wsOpened
= chargingStation
.isWebSocketConnectionOpened() === true;
252 const beginId
= PerformanceStatistics
.beginMeasure(commandName
);
254 chargingStation
.wsConnection
?.send(messageToSend
);
256 `${chargingStation.logPrefix()} >> Command '${commandName}' sent ${OCPPServiceUtils.getMessageTypeString(
258 )} payload: ${messageToSend}`
262 `${chargingStation.logPrefix()} >> Command '${commandName}' failed to send ${OCPPServiceUtils.getMessageTypeString(
264 )} payload: ${messageToSend}:`,
269 PerformanceStatistics
.endMeasure(commandName
, beginId
);
271 const wsClosedOrErrored
= !wsOpened
|| sendError
=== true;
272 if (wsClosedOrErrored
&& params
.skipBufferingOnError
=== false) {
274 chargingStation
.bufferMessage(messageToSend
);
275 // Reject and keep request in the cache
278 ErrorType
.GENERIC_ERROR
,
279 `WebSocket closed or errored for buffered message id '${messageId}' with content '${messageToSend}'`,
281 (messagePayload
as JsonObject
)?.details
?? {}
284 } else if (wsClosedOrErrored
) {
285 const ocppError
= new OCPPError(
286 ErrorType
.GENERIC_ERROR
,
287 `WebSocket closed or errored for non buffered message id '${messageId}' with content '${messageToSend}'`,
289 (messagePayload
as JsonObject
)?.details
?? {}
292 if (messageType
!== MessageType
.CALL_MESSAGE
) {
293 return reject(ocppError
);
295 // Reject and remove request from the cache
296 return errorCallback(ocppError
, false);
299 if (messageType
!== MessageType
.CALL_MESSAGE
) {
300 return resolve(messagePayload
);
304 * Function that will receive the request's response
307 * @param requestPayload -
309 function responseCallback(payload
: JsonType
, requestPayload
: JsonType
): void {
310 if (chargingStation
.getEnableStatistics() === true) {
311 chargingStation
.performanceStatistics
?.addRequestStatistic(
313 MessageType
.CALL_RESULT_MESSAGE
316 // Handle the request's response
317 self.ocppResponseService
320 commandName
as RequestCommand
,
331 chargingStation
.requests
.delete(messageId
);
336 * Function that will receive the request's error response
339 * @param requestStatistic -
341 function errorCallback(error
: OCPPError
, requestStatistic
= true): void {
342 if (requestStatistic
=== true && chargingStation
.getEnableStatistics() === true) {
343 chargingStation
.performanceStatistics
?.addRequestStatistic(
345 MessageType
.CALL_ERROR_MESSAGE
349 `${chargingStation.logPrefix()} Error occurred at ${OCPPServiceUtils.getMessageTypeString(
351 )} command ${commandName} with PDU %j:`,
355 chargingStation
.requests
.delete(messageId
);
359 Constants
.OCPP_WEBSOCKET_TIMEOUT
,
361 ErrorType
.GENERIC_ERROR
,
362 `Timeout for message id '${messageId}'`,
364 (messagePayload
as JsonObject
)?.details
?? {}
367 messageType
=== MessageType
.CALL_MESSAGE
&& chargingStation
.requests
.delete(messageId
);
372 ErrorType
.SECURITY_ERROR
,
373 `Cannot send command ${commandName} PDU when the charging station is in ${chargingStation.getRegistrationStatus()} state on the central server`,
378 private buildMessageToSend(
379 chargingStation
: ChargingStation
,
381 messagePayload
: JsonType
| OCPPError
,
382 messageType
: MessageType
,
383 commandName
: RequestCommand
| IncomingRequestCommand
,
384 responseCallback
: ResponseCallback
,
385 errorCallback
: ErrorCallback
387 let messageToSend
: string;
389 switch (messageType
) {
391 case MessageType
.CALL_MESSAGE
:
393 this.validateRequestPayload(chargingStation
, commandName
, messagePayload
as JsonObject
);
394 chargingStation
.requests
.set(messageId
, [
398 messagePayload
as JsonType
,
400 messageToSend
= JSON
.stringify([
405 ] as OutgoingRequest
);
408 case MessageType
.CALL_RESULT_MESSAGE
:
410 this.validateIncomingRequestResponsePayload(
413 messagePayload
as JsonObject
415 messageToSend
= JSON
.stringify([messageType
, messageId
, messagePayload
] as Response
);
418 case MessageType
.CALL_ERROR_MESSAGE
:
419 // Build Error Message
420 messageToSend
= JSON
.stringify([
423 (messagePayload
as OCPPError
)?.code
?? ErrorType
.GENERIC_ERROR
,
424 (messagePayload
as OCPPError
)?.message
?? '',
425 (messagePayload
as OCPPError
)?.details
?? { commandName
},
429 return messageToSend
;
432 private handleSendMessageError(
433 chargingStation
: ChargingStation
,
434 commandName
: RequestCommand
| IncomingRequestCommand
,
436 params
: HandleErrorParams
<EmptyObject
> = { throwError
: false }
438 logger
.error(`${chargingStation.logPrefix()} Request command '${commandName}' error:`, error
);
439 if (params
?.throwError
=== true) {
444 // eslint-disable-next-line @typescript-eslint/no-unused-vars
445 public abstract requestHandler
<ReqType
extends JsonType
, ResType
extends JsonType
>(
446 chargingStation
: ChargingStation
,
447 commandName
: RequestCommand
,
448 commandParams
?: JsonType
,
449 params
?: RequestParams