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';
15 type HandleErrorParams
,
16 type IncomingRequestCommand
,
25 type ResponseCallback
,
28 import { Constants
, Utils
, logger
} from
'../../utils';
30 const moduleName
= 'OCPPRequestService';
32 export abstract class OCPPRequestService
{
33 private static instance
: OCPPRequestService
| null = null;
34 private readonly version
: OCPPVersion
;
35 private readonly ajv
: Ajv
;
36 private readonly ocppResponseService
: OCPPResponseService
;
37 protected abstract jsonSchemas
: Map
<RequestCommand
, JSONSchemaType
<JsonObject
>>;
39 protected constructor(version
: OCPPVersion
, ocppResponseService
: OCPPResponseService
) {
40 this.version
= version
;
42 keywords
: ['javaType'],
43 multipleOfPrecision
: 2,
46 this.ocppResponseService
= ocppResponseService
;
47 this.requestHandler
= this.requestHandler
.bind(this) as <
48 ReqType
extends JsonType
,
49 ResType
extends JsonType
51 chargingStation
: ChargingStation
,
52 commandName
: RequestCommand
,
53 commandParams
?: JsonType
,
54 params
?: RequestParams
55 ) => Promise
<ResType
>;
56 this.sendMessage
= this.sendMessage
.bind(this) as (
57 chargingStation
: ChargingStation
,
59 messagePayload
: JsonType
,
60 commandName
: RequestCommand
,
61 params
?: RequestParams
62 ) => Promise
<ResponseType
>;
63 this.sendResponse
= this.sendResponse
.bind(this) as (
64 chargingStation
: ChargingStation
,
66 messagePayload
: JsonType
,
67 commandName
: IncomingRequestCommand
68 ) => Promise
<ResponseType
>;
69 this.sendError
= this.sendError
.bind(this) as (
70 chargingStation
: ChargingStation
,
73 commandName
: RequestCommand
| IncomingRequestCommand
74 ) => Promise
<ResponseType
>;
75 this.internalSendMessage
= this.internalSendMessage
.bind(this) as (
76 chargingStation
: ChargingStation
,
78 messagePayload
: JsonType
| OCPPError
,
79 messageType
: MessageType
,
80 commandName
: RequestCommand
| IncomingRequestCommand
,
81 params
?: RequestParams
82 ) => Promise
<ResponseType
>;
83 this.buildMessageToSend
= this.buildMessageToSend
.bind(this) as (
84 chargingStation
: ChargingStation
,
86 messagePayload
: JsonType
| OCPPError
,
87 messageType
: MessageType
,
88 commandName
: RequestCommand
| IncomingRequestCommand
,
89 responseCallback
: ResponseCallback
,
90 errorCallback
: ErrorCallback
92 this.validateRequestPayload
= this.validateRequestPayload
.bind(this) as <T
extends JsonObject
>(
93 chargingStation
: ChargingStation
,
94 commandName
: RequestCommand
| IncomingRequestCommand
,
97 this.validateIncomingRequestResponsePayload
= this.validateIncomingRequestResponsePayload
.bind(
99 ) as <T
extends JsonObject
>(
100 chargingStation
: ChargingStation
,
101 commandName
: RequestCommand
| IncomingRequestCommand
,
106 public static getInstance
<T
extends OCPPRequestService
>(
107 this: new (ocppResponseService
: OCPPResponseService
) => T
,
108 ocppResponseService
: OCPPResponseService
110 if (OCPPRequestService
.instance
=== null) {
111 OCPPRequestService
.instance
= new this(ocppResponseService
);
113 return OCPPRequestService
.instance
as T
;
116 public async sendResponse(
117 chargingStation
: ChargingStation
,
119 messagePayload
: JsonType
,
120 commandName
: IncomingRequestCommand
121 ): Promise
<ResponseType
> {
123 // Send response message
124 return await this.internalSendMessage(
128 MessageType
.CALL_RESULT_MESSAGE
,
132 this.handleSendMessageError(chargingStation
, commandName
, error
as Error, {
138 public async sendError(
139 chargingStation
: ChargingStation
,
141 ocppError
: OCPPError
,
142 commandName
: RequestCommand
| IncomingRequestCommand
143 ): Promise
<ResponseType
> {
145 // Send error message
146 return await this.internalSendMessage(
150 MessageType
.CALL_ERROR_MESSAGE
,
154 this.handleSendMessageError(chargingStation
, commandName
, error
as Error);
158 protected async sendMessage(
159 chargingStation
: ChargingStation
,
161 messagePayload
: JsonType
,
162 commandName
: RequestCommand
,
163 params
: RequestParams
= {
164 skipBufferingOnError
: false,
165 triggerMessage
: false,
168 ): Promise
<ResponseType
> {
170 return await this.internalSendMessage(
174 MessageType
.CALL_MESSAGE
,
179 this.handleSendMessageError(chargingStation
, commandName
, error
as Error, {
180 throwError
: params
.throwError
,
185 private validateRequestPayload
<T
extends JsonObject
>(
186 chargingStation
: ChargingStation
,
187 commandName
: RequestCommand
| IncomingRequestCommand
,
190 if (chargingStation
.getPayloadSchemaValidation() === false) {
193 if (this.jsonSchemas
.has(commandName
as RequestCommand
) === false) {
195 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: No JSON schema found for command '${commandName}' PDU validation`
199 const validate
= this.ajv
.compile(this.jsonSchemas
.get(commandName
as RequestCommand
));
200 payload
= Utils
.cloneObject
<T
>(payload
);
201 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
);
202 if (validate(payload
)) {
206 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: Command '${commandName}' request PDU is invalid: %j`,
209 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
211 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
),
212 'Request PDU is invalid',
214 JSON
.stringify(validate
.errors
, null, 2)
218 private validateIncomingRequestResponsePayload
<T
extends JsonObject
>(
219 chargingStation
: ChargingStation
,
220 commandName
: RequestCommand
| IncomingRequestCommand
,
223 if (chargingStation
.getPayloadSchemaValidation() === false) {
227 this.ocppResponseService
.jsonIncomingRequestResponseSchemas
.has(
228 commandName
as IncomingRequestCommand
232 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: No JSON schema found for command '${commandName}' PDU validation`
236 const validate
= this.ajv
.compile(
237 this.ocppResponseService
.jsonIncomingRequestResponseSchemas
.get(
238 commandName
as IncomingRequestCommand
241 payload
= Utils
.cloneObject
<T
>(payload
);
242 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
);
243 if (validate(payload
)) {
247 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: Command '${commandName}' reponse PDU is invalid: %j`,
250 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
252 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
),
253 'Response PDU is invalid',
255 JSON
.stringify(validate
.errors
, null, 2)
259 private async internalSendMessage(
260 chargingStation
: ChargingStation
,
262 messagePayload
: JsonType
| OCPPError
,
263 messageType
: MessageType
,
264 commandName
: RequestCommand
| IncomingRequestCommand
,
265 params
: RequestParams
= {
266 skipBufferingOnError
: false,
267 triggerMessage
: false,
269 ): Promise
<ResponseType
> {
271 (chargingStation
.inUnknownState() === true &&
272 commandName
=== RequestCommand
.BOOT_NOTIFICATION
) ||
273 (chargingStation
.getOcppStrictCompliance() === false &&
274 chargingStation
.inUnknownState() === true) ||
275 chargingStation
.inAcceptedState() === true ||
276 (chargingStation
.inPendingState() === true &&
277 (params
.triggerMessage
=== true || messageType
=== MessageType
.CALL_RESULT_MESSAGE
))
279 // eslint-disable-next-line @typescript-eslint/no-this-alias
281 // Send a message through wsConnection
282 return Utils
.promiseWithTimeout(
283 new Promise((resolve
, reject
) => {
285 * Function that will receive the request's response
288 * @param requestPayload -
290 const responseCallback
= (payload
: JsonType
, requestPayload
: JsonType
): void => {
291 if (chargingStation
.getEnableStatistics() === true) {
292 chargingStation
.performanceStatistics
?.addRequestStatistic(
294 MessageType
.CALL_RESULT_MESSAGE
297 // Handle the request's response
298 self.ocppResponseService
301 commandName
as RequestCommand
,
312 chargingStation
.requests
.delete(messageId
);
317 * Function that will receive the request's error response
320 * @param requestStatistic -
322 const errorCallback
= (error
: OCPPError
, requestStatistic
= true): void => {
323 if (requestStatistic
=== true && chargingStation
.getEnableStatistics() === true) {
324 chargingStation
.performanceStatistics
?.addRequestStatistic(
326 MessageType
.CALL_ERROR_MESSAGE
330 `${chargingStation.logPrefix()} Error occurred at ${OCPPServiceUtils.getMessageTypeString(
332 )} command ${commandName} with PDU %j:`,
336 chargingStation
.requests
.delete(messageId
);
340 if (chargingStation
.getEnableStatistics() === true) {
341 chargingStation
.performanceStatistics
?.addRequestStatistic(commandName
, messageType
);
343 const messageToSend
= this.buildMessageToSend(
352 let sendError
= false;
353 // Check if wsConnection opened
354 const wsOpened
= chargingStation
.isWebSocketConnectionOpened() === true;
356 const beginId
= PerformanceStatistics
.beginMeasure(commandName
);
358 chargingStation
.wsConnection
?.send(messageToSend
);
360 `${chargingStation.logPrefix()} >> Command '${commandName}' sent ${OCPPServiceUtils.getMessageTypeString(
362 )} payload: ${messageToSend}`
366 `${chargingStation.logPrefix()} >> Command '${commandName}' failed to send ${OCPPServiceUtils.getMessageTypeString(
368 )} payload: ${messageToSend}:`,
373 PerformanceStatistics
.endMeasure(commandName
, beginId
);
375 const wsClosedOrErrored
= !wsOpened
|| sendError
=== true;
376 if (wsClosedOrErrored
&& params
.skipBufferingOnError
=== false) {
378 chargingStation
.bufferMessage(messageToSend
);
379 // Reject and keep request in the cache
382 ErrorType
.GENERIC_ERROR
,
383 `WebSocket closed or errored for buffered message id '${messageId}' with content '${messageToSend}'`,
385 (messagePayload
as JsonObject
)?.details
?? Constants
.EMPTY_FREEZED_OBJECT
388 } else if (wsClosedOrErrored
) {
389 const ocppError
= new OCPPError(
390 ErrorType
.GENERIC_ERROR
,
391 `WebSocket closed or errored for non buffered message id '${messageId}' with content '${messageToSend}'`,
393 (messagePayload
as JsonObject
)?.details
?? Constants
.EMPTY_FREEZED_OBJECT
396 if (messageType
!== MessageType
.CALL_MESSAGE
) {
397 return reject(ocppError
);
399 // Reject and remove request from the cache
400 return errorCallback(ocppError
, false);
403 if (messageType
!== MessageType
.CALL_MESSAGE
) {
404 return resolve(messagePayload
);
407 OCPPConstants
.OCPP_WEBSOCKET_TIMEOUT
,
409 ErrorType
.GENERIC_ERROR
,
410 `Timeout for message id '${messageId}'`,
412 (messagePayload
as JsonObject
)?.details
?? Constants
.EMPTY_FREEZED_OBJECT
415 messageType
=== MessageType
.CALL_MESSAGE
&& chargingStation
.requests
.delete(messageId
);
420 ErrorType
.SECURITY_ERROR
,
421 `Cannot send command ${commandName} PDU when the charging station is in ${chargingStation.getRegistrationStatus()} state on the central server`,
426 private buildMessageToSend(
427 chargingStation
: ChargingStation
,
429 messagePayload
: JsonType
| OCPPError
,
430 messageType
: MessageType
,
431 commandName
: RequestCommand
| IncomingRequestCommand
,
432 responseCallback
: ResponseCallback
,
433 errorCallback
: ErrorCallback
435 let messageToSend
: string;
437 switch (messageType
) {
439 case MessageType
.CALL_MESSAGE
:
441 this.validateRequestPayload(chargingStation
, commandName
, messagePayload
as JsonObject
);
442 chargingStation
.requests
.set(messageId
, [
446 messagePayload
as JsonType
,
448 messageToSend
= JSON
.stringify([
453 ] as OutgoingRequest
);
456 case MessageType
.CALL_RESULT_MESSAGE
:
458 this.validateIncomingRequestResponsePayload(
461 messagePayload
as JsonObject
463 messageToSend
= JSON
.stringify([messageType
, messageId
, messagePayload
] as Response
);
466 case MessageType
.CALL_ERROR_MESSAGE
:
467 // Build Error Message
468 messageToSend
= JSON
.stringify([
471 (messagePayload
as OCPPError
)?.code
?? ErrorType
.GENERIC_ERROR
,
472 (messagePayload
as OCPPError
)?.message
?? '',
473 (messagePayload
as OCPPError
)?.details
?? { commandName
},
477 return messageToSend
;
480 private handleSendMessageError(
481 chargingStation
: ChargingStation
,
482 commandName
: RequestCommand
| IncomingRequestCommand
,
484 params
: HandleErrorParams
<EmptyObject
> = { throwError
: false }
486 logger
.error(`${chargingStation.logPrefix()} Request command '${commandName}' error:`, error
);
487 if (params
?.throwError
=== true) {
492 // eslint-disable-next-line @typescript-eslint/no-unused-vars
493 public abstract requestHandler
<ReqType
extends JsonType
, ResType
extends JsonType
>(
494 chargingStation
: ChargingStation
,
495 commandName
: RequestCommand
,
496 commandParams
?: JsonType
,
497 params
?: RequestParams