1 import Ajv
, { type JSONSchemaType
} from
'ajv';
2 import ajvFormats from
'ajv-formats';
4 import { type OCPPResponseService
, OCPPServiceUtils
} from
'./internal';
5 import type { ChargingStation
} from
'../../charging-station';
6 import { OCPPError
} from
'../../exception';
7 import { PerformanceStatistics
} from
'../../performance';
13 type HandleErrorParams
,
14 type IncomingRequestCommand
,
23 type ResponseCallback
,
26 import { Constants
, Utils
, logger
} from
'../../utils';
28 const moduleName
= 'OCPPRequestService';
30 export abstract class OCPPRequestService
{
31 private static instance
: OCPPRequestService
| null = null;
32 private readonly version
: OCPPVersion
;
33 private readonly ajv
: Ajv
;
34 private readonly ocppResponseService
: OCPPResponseService
;
35 protected abstract jsonSchemas
: Map
<RequestCommand
, JSONSchemaType
<JsonObject
>>;
37 protected constructor(version
: OCPPVersion
, ocppResponseService
: OCPPResponseService
) {
38 this.version
= version
;
40 keywords
: ['javaType'],
41 multipleOfPrecision
: 2,
44 this.ocppResponseService
= ocppResponseService
;
45 this.requestHandler
.bind(this);
46 this.sendMessage
.bind(this);
47 this.sendResponse
.bind(this);
48 this.sendError
.bind(this);
49 this.internalSendMessage
.bind(this);
50 this.buildMessageToSend
.bind(this);
51 this.validateRequestPayload
.bind(this);
52 this.validateIncomingRequestResponsePayload
.bind(this);
55 public static getInstance
<T
extends OCPPRequestService
>(
56 this: new (ocppResponseService
: OCPPResponseService
) => T
,
57 ocppResponseService
: OCPPResponseService
59 if (OCPPRequestService
.instance
=== null) {
60 OCPPRequestService
.instance
= new this(ocppResponseService
);
62 return OCPPRequestService
.instance
as T
;
65 public async sendResponse(
66 chargingStation
: ChargingStation
,
68 messagePayload
: JsonType
,
69 commandName
: IncomingRequestCommand
70 ): Promise
<ResponseType
> {
72 // Send response message
73 return await this.internalSendMessage(
77 MessageType
.CALL_RESULT_MESSAGE
,
81 this.handleSendMessageError(chargingStation
, commandName
, error
as Error, {
87 public async sendError(
88 chargingStation
: ChargingStation
,
91 commandName
: RequestCommand
| IncomingRequestCommand
92 ): Promise
<ResponseType
> {
95 return await this.internalSendMessage(
99 MessageType
.CALL_ERROR_MESSAGE
,
103 this.handleSendMessageError(chargingStation
, commandName
, error
as Error);
107 protected async sendMessage(
108 chargingStation
: ChargingStation
,
110 messagePayload
: JsonType
,
111 commandName
: RequestCommand
,
112 params
: RequestParams
= {
113 skipBufferingOnError
: false,
114 triggerMessage
: false,
117 ): Promise
<ResponseType
> {
119 return await this.internalSendMessage(
123 MessageType
.CALL_MESSAGE
,
128 this.handleSendMessageError(chargingStation
, commandName
, error
as Error, {
129 throwError
: params
.throwError
,
134 private validateRequestPayload
<T
extends JsonObject
>(
135 chargingStation
: ChargingStation
,
136 commandName
: RequestCommand
| IncomingRequestCommand
,
139 if (chargingStation
.getPayloadSchemaValidation() === false) {
142 if (this.jsonSchemas
.has(commandName
as RequestCommand
) === false) {
144 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: No JSON schema found for command '${commandName}' PDU validation`
148 const validate
= this.ajv
.compile(this.jsonSchemas
.get(commandName
as RequestCommand
));
149 payload
= Utils
.cloneObject
<T
>(payload
);
150 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
);
151 if (validate(payload
)) {
155 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: Command '${commandName}' request PDU is invalid: %j`,
158 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
160 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
),
161 'Request PDU is invalid',
163 JSON
.stringify(validate
.errors
, null, 2)
167 private validateIncomingRequestResponsePayload
<T
extends JsonObject
>(
168 chargingStation
: ChargingStation
,
169 commandName
: RequestCommand
| IncomingRequestCommand
,
172 if (chargingStation
.getPayloadSchemaValidation() === false) {
176 this.ocppResponseService
.jsonIncomingRequestResponseSchemas
.has(
177 commandName
as IncomingRequestCommand
181 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: No JSON schema found for command '${commandName}' PDU validation`
185 const validate
= this.ajv
.compile(
186 this.ocppResponseService
.jsonIncomingRequestResponseSchemas
.get(
187 commandName
as IncomingRequestCommand
190 payload
= Utils
.cloneObject
<T
>(payload
);
191 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
);
192 if (validate(payload
)) {
196 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: Command '${commandName}' reponse PDU is invalid: %j`,
199 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
201 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
),
202 'Response PDU is invalid',
204 JSON
.stringify(validate
.errors
, null, 2)
208 private async internalSendMessage(
209 chargingStation
: ChargingStation
,
211 messagePayload
: JsonType
| OCPPError
,
212 messageType
: MessageType
,
213 commandName
: RequestCommand
| IncomingRequestCommand
,
214 params
: RequestParams
= {
215 skipBufferingOnError
: false,
216 triggerMessage
: false,
218 ): Promise
<ResponseType
> {
220 (chargingStation
.isInUnknownState() === true &&
221 commandName
=== RequestCommand
.BOOT_NOTIFICATION
) ||
222 (chargingStation
.getOcppStrictCompliance() === false &&
223 chargingStation
.isInUnknownState() === true) ||
224 chargingStation
.isInAcceptedState() === true ||
225 (chargingStation
.isInPendingState() === true &&
226 (params
.triggerMessage
=== true || messageType
=== MessageType
.CALL_RESULT_MESSAGE
))
228 // eslint-disable-next-line @typescript-eslint/no-this-alias
230 // Send a message through wsConnection
231 return Utils
.promiseWithTimeout(
232 new Promise((resolve
, reject
) => {
234 * Function that will receive the request's response
237 * @param requestPayload -
239 const responseCallback
= (payload
: JsonType
, requestPayload
: JsonType
): void => {
240 if (chargingStation
.getEnableStatistics() === true) {
241 chargingStation
.performanceStatistics
?.addRequestStatistic(
243 MessageType
.CALL_RESULT_MESSAGE
246 // Handle the request's response
247 self.ocppResponseService
250 commandName
as RequestCommand
,
261 chargingStation
.requests
.delete(messageId
);
266 * Function that will receive the request's error response
269 * @param requestStatistic -
271 const errorCallback
= (error
: OCPPError
, requestStatistic
= true): void => {
272 if (requestStatistic
=== true && chargingStation
.getEnableStatistics() === true) {
273 chargingStation
.performanceStatistics
?.addRequestStatistic(
275 MessageType
.CALL_ERROR_MESSAGE
279 `${chargingStation.logPrefix()} Error occurred at ${OCPPServiceUtils.getMessageTypeString(
281 )} command ${commandName} with PDU %j:`,
285 chargingStation
.requests
.delete(messageId
);
289 if (chargingStation
.getEnableStatistics() === true) {
290 chargingStation
.performanceStatistics
?.addRequestStatistic(commandName
, messageType
);
292 const messageToSend
= this.buildMessageToSend(
301 let sendError
= false;
302 // Check if wsConnection opened
303 const wsOpened
= chargingStation
.isWebSocketConnectionOpened() === true;
305 const beginId
= PerformanceStatistics
.beginMeasure(commandName
);
307 chargingStation
.wsConnection
?.send(messageToSend
);
309 `${chargingStation.logPrefix()} >> Command '${commandName}' sent ${OCPPServiceUtils.getMessageTypeString(
311 )} payload: ${messageToSend}`
315 `${chargingStation.logPrefix()} >> Command '${commandName}' failed to send ${OCPPServiceUtils.getMessageTypeString(
317 )} payload: ${messageToSend}:`,
322 PerformanceStatistics
.endMeasure(commandName
, beginId
);
324 const wsClosedOrErrored
= !wsOpened
|| sendError
=== true;
325 if (wsClosedOrErrored
&& params
.skipBufferingOnError
=== false) {
327 chargingStation
.bufferMessage(messageToSend
);
328 // Reject and keep request in the cache
331 ErrorType
.GENERIC_ERROR
,
332 `WebSocket closed or errored for buffered message id '${messageId}' with content '${messageToSend}'`,
334 (messagePayload
as JsonObject
)?.details
?? Constants
.EMPTY_FREEZED_OBJECT
337 } else if (wsClosedOrErrored
) {
338 const ocppError
= new OCPPError(
339 ErrorType
.GENERIC_ERROR
,
340 `WebSocket closed or errored for non buffered message id '${messageId}' with content '${messageToSend}'`,
342 (messagePayload
as JsonObject
)?.details
?? Constants
.EMPTY_FREEZED_OBJECT
345 if (messageType
!== MessageType
.CALL_MESSAGE
) {
346 return reject(ocppError
);
348 // Reject and remove request from the cache
349 return errorCallback(ocppError
, false);
352 if (messageType
!== MessageType
.CALL_MESSAGE
) {
353 return resolve(messagePayload
);
356 Constants
.OCPP_WEBSOCKET_TIMEOUT
,
358 ErrorType
.GENERIC_ERROR
,
359 `Timeout for message id '${messageId}'`,
361 (messagePayload
as JsonObject
)?.details
?? Constants
.EMPTY_FREEZED_OBJECT
364 messageType
=== MessageType
.CALL_MESSAGE
&& chargingStation
.requests
.delete(messageId
);
369 ErrorType
.SECURITY_ERROR
,
370 `Cannot send command ${commandName} PDU when the charging station is in ${chargingStation.getRegistrationStatus()} state on the central server`,
375 private buildMessageToSend(
376 chargingStation
: ChargingStation
,
378 messagePayload
: JsonType
| OCPPError
,
379 messageType
: MessageType
,
380 commandName
: RequestCommand
| IncomingRequestCommand
,
381 responseCallback
: ResponseCallback
,
382 errorCallback
: ErrorCallback
384 let messageToSend
: string;
386 switch (messageType
) {
388 case MessageType
.CALL_MESSAGE
:
390 this.validateRequestPayload(chargingStation
, commandName
, messagePayload
as JsonObject
);
391 chargingStation
.requests
.set(messageId
, [
395 messagePayload
as JsonType
,
397 messageToSend
= JSON
.stringify([
402 ] as OutgoingRequest
);
405 case MessageType
.CALL_RESULT_MESSAGE
:
407 this.validateIncomingRequestResponsePayload(
410 messagePayload
as JsonObject
412 messageToSend
= JSON
.stringify([messageType
, messageId
, messagePayload
] as Response
);
415 case MessageType
.CALL_ERROR_MESSAGE
:
416 // Build Error Message
417 messageToSend
= JSON
.stringify([
420 (messagePayload
as OCPPError
)?.code
?? ErrorType
.GENERIC_ERROR
,
421 (messagePayload
as OCPPError
)?.message
?? '',
422 (messagePayload
as OCPPError
)?.details
?? { commandName
},
426 return messageToSend
;
429 private handleSendMessageError(
430 chargingStation
: ChargingStation
,
431 commandName
: RequestCommand
| IncomingRequestCommand
,
433 params
: HandleErrorParams
<EmptyObject
> = { throwError
: false }
435 logger
.error(`${chargingStation.logPrefix()} Request command '${commandName}' error:`, error
);
436 if (params
?.throwError
=== true) {
441 // eslint-disable-next-line @typescript-eslint/no-unused-vars
442 public abstract requestHandler
<ReqType
extends JsonType
, ResType
extends JsonType
>(
443 chargingStation
: ChargingStation
,
444 commandName
: RequestCommand
,
445 commandParams
?: JsonType
,
446 params
?: RequestParams