1 import Ajv
, { type JSONSchemaType
} from
'ajv';
2 import ajvFormats from
'ajv-formats';
4 import { OCPPConstants
, 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
= this.requestHandler
.bind(this) as <
46 ReqType
extends JsonType
,
47 ResType
extends JsonType
49 chargingStation
: ChargingStation
,
50 commandName
: RequestCommand
,
51 commandParams
?: JsonType
,
52 params
?: RequestParams
53 ) => Promise
<ResType
>;
54 this.sendMessage
= this.sendMessage
.bind(this) as (
55 chargingStation
: ChargingStation
,
57 messagePayload
: JsonType
,
58 commandName
: RequestCommand
,
59 params
?: RequestParams
60 ) => Promise
<ResponseType
>;
61 this.sendResponse
= this.sendResponse
.bind(this) as (
62 chargingStation
: ChargingStation
,
64 messagePayload
: JsonType
,
65 commandName
: IncomingRequestCommand
66 ) => Promise
<ResponseType
>;
67 this.sendError
= this.sendError
.bind(this) as (
68 chargingStation
: ChargingStation
,
71 commandName
: RequestCommand
| IncomingRequestCommand
72 ) => Promise
<ResponseType
>;
73 this.internalSendMessage
= this.internalSendMessage
.bind(this) as (
74 chargingStation
: ChargingStation
,
76 messagePayload
: JsonType
| OCPPError
,
77 messageType
: MessageType
,
78 commandName
: RequestCommand
| IncomingRequestCommand
,
79 params
?: RequestParams
80 ) => Promise
<ResponseType
>;
81 this.buildMessageToSend
= this.buildMessageToSend
.bind(this) as (
82 chargingStation
: ChargingStation
,
84 messagePayload
: JsonType
| OCPPError
,
85 messageType
: MessageType
,
86 commandName
: RequestCommand
| IncomingRequestCommand
,
87 responseCallback
: ResponseCallback
,
88 errorCallback
: ErrorCallback
90 this.validateRequestPayload
= this.validateRequestPayload
.bind(this) as <T
extends JsonObject
>(
91 chargingStation
: ChargingStation
,
92 commandName
: RequestCommand
| IncomingRequestCommand
,
95 this.validateIncomingRequestResponsePayload
= this.validateIncomingRequestResponsePayload
.bind(
97 ) as <T
extends JsonObject
>(
98 chargingStation
: ChargingStation
,
99 commandName
: RequestCommand
| IncomingRequestCommand
,
104 public static getInstance
<T
extends OCPPRequestService
>(
105 this: new (ocppResponseService
: OCPPResponseService
) => T
,
106 ocppResponseService
: OCPPResponseService
108 if (OCPPRequestService
.instance
=== null) {
109 OCPPRequestService
.instance
= new this(ocppResponseService
);
111 return OCPPRequestService
.instance
as T
;
114 public async sendResponse(
115 chargingStation
: ChargingStation
,
117 messagePayload
: JsonType
,
118 commandName
: IncomingRequestCommand
119 ): Promise
<ResponseType
> {
121 // Send response message
122 return await this.internalSendMessage(
126 MessageType
.CALL_RESULT_MESSAGE
,
130 this.handleSendMessageError(chargingStation
, commandName
, error
as Error, {
136 public async sendError(
137 chargingStation
: ChargingStation
,
139 ocppError
: OCPPError
,
140 commandName
: RequestCommand
| IncomingRequestCommand
141 ): Promise
<ResponseType
> {
143 // Send error message
144 return await this.internalSendMessage(
148 MessageType
.CALL_ERROR_MESSAGE
,
152 this.handleSendMessageError(chargingStation
, commandName
, error
as Error);
156 protected async sendMessage(
157 chargingStation
: ChargingStation
,
159 messagePayload
: JsonType
,
160 commandName
: RequestCommand
,
161 params
: RequestParams
= {
162 skipBufferingOnError
: false,
163 triggerMessage
: false,
166 ): Promise
<ResponseType
> {
168 return await this.internalSendMessage(
172 MessageType
.CALL_MESSAGE
,
177 this.handleSendMessageError(chargingStation
, commandName
, error
as Error, {
178 throwError
: params
.throwError
,
183 private validateRequestPayload
<T
extends JsonObject
>(
184 chargingStation
: ChargingStation
,
185 commandName
: RequestCommand
| IncomingRequestCommand
,
188 if (chargingStation
.getPayloadSchemaValidation() === false) {
191 if (this.jsonSchemas
.has(commandName
as RequestCommand
) === false) {
193 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: No JSON schema found for command '${commandName}' PDU validation`
197 const validate
= this.ajv
.compile(this.jsonSchemas
.get(commandName
as RequestCommand
));
198 payload
= Utils
.cloneObject
<T
>(payload
);
199 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
);
200 if (validate(payload
)) {
204 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: Command '${commandName}' request PDU is invalid: %j`,
207 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
209 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
),
210 'Request PDU is invalid',
212 JSON
.stringify(validate
.errors
, null, 2)
216 private validateIncomingRequestResponsePayload
<T
extends JsonObject
>(
217 chargingStation
: ChargingStation
,
218 commandName
: RequestCommand
| IncomingRequestCommand
,
221 if (chargingStation
.getPayloadSchemaValidation() === false) {
225 this.ocppResponseService
.jsonIncomingRequestResponseSchemas
.has(
226 commandName
as IncomingRequestCommand
230 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: No JSON schema found for command '${commandName}' PDU validation`
234 const validate
= this.ajv
.compile(
235 this.ocppResponseService
.jsonIncomingRequestResponseSchemas
.get(
236 commandName
as IncomingRequestCommand
239 payload
= Utils
.cloneObject
<T
>(payload
);
240 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
);
241 if (validate(payload
)) {
245 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: Command '${commandName}' reponse PDU is invalid: %j`,
248 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
250 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
),
251 'Response PDU is invalid',
253 JSON
.stringify(validate
.errors
, null, 2)
257 private async internalSendMessage(
258 chargingStation
: ChargingStation
,
260 messagePayload
: JsonType
| OCPPError
,
261 messageType
: MessageType
,
262 commandName
: RequestCommand
| IncomingRequestCommand
,
263 params
: RequestParams
= {
264 skipBufferingOnError
: false,
265 triggerMessage
: false,
267 ): Promise
<ResponseType
> {
269 (chargingStation
.isInUnknownState() === true &&
270 commandName
=== RequestCommand
.BOOT_NOTIFICATION
) ||
271 (chargingStation
.getOcppStrictCompliance() === false &&
272 chargingStation
.isInUnknownState() === true) ||
273 chargingStation
.isInAcceptedState() === true ||
274 (chargingStation
.isInPendingState() === true &&
275 (params
.triggerMessage
=== true || messageType
=== MessageType
.CALL_RESULT_MESSAGE
))
277 // eslint-disable-next-line @typescript-eslint/no-this-alias
279 // Send a message through wsConnection
280 return Utils
.promiseWithTimeout(
281 new Promise((resolve
, reject
) => {
283 * Function that will receive the request's response
286 * @param requestPayload -
288 const responseCallback
= (payload
: JsonType
, requestPayload
: JsonType
): void => {
289 if (chargingStation
.getEnableStatistics() === true) {
290 chargingStation
.performanceStatistics
?.addRequestStatistic(
292 MessageType
.CALL_RESULT_MESSAGE
295 // Handle the request's response
296 self.ocppResponseService
299 commandName
as RequestCommand
,
310 chargingStation
.requests
.delete(messageId
);
315 * Function that will receive the request's error response
318 * @param requestStatistic -
320 const errorCallback
= (error
: OCPPError
, requestStatistic
= true): void => {
321 if (requestStatistic
=== true && chargingStation
.getEnableStatistics() === true) {
322 chargingStation
.performanceStatistics
?.addRequestStatistic(
324 MessageType
.CALL_ERROR_MESSAGE
328 `${chargingStation.logPrefix()} Error occurred at ${OCPPServiceUtils.getMessageTypeString(
330 )} command ${commandName} with PDU %j:`,
334 chargingStation
.requests
.delete(messageId
);
338 if (chargingStation
.getEnableStatistics() === true) {
339 chargingStation
.performanceStatistics
?.addRequestStatistic(commandName
, messageType
);
341 const messageToSend
= this.buildMessageToSend(
350 let sendError
= false;
351 // Check if wsConnection opened
352 const wsOpened
= chargingStation
.isWebSocketConnectionOpened() === true;
354 const beginId
= PerformanceStatistics
.beginMeasure(commandName
);
356 chargingStation
.wsConnection
?.send(messageToSend
);
358 `${chargingStation.logPrefix()} >> Command '${commandName}' sent ${OCPPServiceUtils.getMessageTypeString(
360 )} payload: ${messageToSend}`
364 `${chargingStation.logPrefix()} >> Command '${commandName}' failed to send ${OCPPServiceUtils.getMessageTypeString(
366 )} payload: ${messageToSend}:`,
371 PerformanceStatistics
.endMeasure(commandName
, beginId
);
373 const wsClosedOrErrored
= !wsOpened
|| sendError
=== true;
374 if (wsClosedOrErrored
&& params
.skipBufferingOnError
=== false) {
376 chargingStation
.bufferMessage(messageToSend
);
377 // Reject and keep request in the cache
380 ErrorType
.GENERIC_ERROR
,
381 `WebSocket closed or errored for buffered message id '${messageId}' with content '${messageToSend}'`,
383 (messagePayload
as JsonObject
)?.details
?? Constants
.EMPTY_FREEZED_OBJECT
386 } else if (wsClosedOrErrored
) {
387 const ocppError
= new OCPPError(
388 ErrorType
.GENERIC_ERROR
,
389 `WebSocket closed or errored for non buffered message id '${messageId}' with content '${messageToSend}'`,
391 (messagePayload
as JsonObject
)?.details
?? Constants
.EMPTY_FREEZED_OBJECT
394 if (messageType
!== MessageType
.CALL_MESSAGE
) {
395 return reject(ocppError
);
397 // Reject and remove request from the cache
398 return errorCallback(ocppError
, false);
401 if (messageType
!== MessageType
.CALL_MESSAGE
) {
402 return resolve(messagePayload
);
405 OCPPConstants
.OCPP_WEBSOCKET_TIMEOUT
,
407 ErrorType
.GENERIC_ERROR
,
408 `Timeout for message id '${messageId}'`,
410 (messagePayload
as JsonObject
)?.details
?? Constants
.EMPTY_FREEZED_OBJECT
413 messageType
=== MessageType
.CALL_MESSAGE
&& chargingStation
.requests
.delete(messageId
);
418 ErrorType
.SECURITY_ERROR
,
419 `Cannot send command ${commandName} PDU when the charging station is in ${chargingStation.getRegistrationStatus()} state on the central server`,
424 private buildMessageToSend(
425 chargingStation
: ChargingStation
,
427 messagePayload
: JsonType
| OCPPError
,
428 messageType
: MessageType
,
429 commandName
: RequestCommand
| IncomingRequestCommand
,
430 responseCallback
: ResponseCallback
,
431 errorCallback
: ErrorCallback
433 let messageToSend
: string;
435 switch (messageType
) {
437 case MessageType
.CALL_MESSAGE
:
439 this.validateRequestPayload(chargingStation
, commandName
, messagePayload
as JsonObject
);
440 chargingStation
.requests
.set(messageId
, [
444 messagePayload
as JsonType
,
446 messageToSend
= JSON
.stringify([
451 ] as OutgoingRequest
);
454 case MessageType
.CALL_RESULT_MESSAGE
:
456 this.validateIncomingRequestResponsePayload(
459 messagePayload
as JsonObject
461 messageToSend
= JSON
.stringify([messageType
, messageId
, messagePayload
] as Response
);
464 case MessageType
.CALL_ERROR_MESSAGE
:
465 // Build Error Message
466 messageToSend
= JSON
.stringify([
469 (messagePayload
as OCPPError
)?.code
?? ErrorType
.GENERIC_ERROR
,
470 (messagePayload
as OCPPError
)?.message
?? '',
471 (messagePayload
as OCPPError
)?.details
?? { commandName
},
475 return messageToSend
;
478 private handleSendMessageError(
479 chargingStation
: ChargingStation
,
480 commandName
: RequestCommand
| IncomingRequestCommand
,
482 params
: HandleErrorParams
<EmptyObject
> = { throwError
: false }
484 logger
.error(`${chargingStation.logPrefix()} Request command '${commandName}' error:`, error
);
485 if (params
?.throwError
=== true) {
490 // eslint-disable-next-line @typescript-eslint/no-unused-vars
491 public abstract requestHandler
<ReqType
extends JsonType
, ResType
extends JsonType
>(
492 chargingStation
: ChargingStation
,
493 commandName
: RequestCommand
,
494 commandParams
?: JsonType
,
495 params
?: RequestParams