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
,
29 handleSendMessageError
,
34 const moduleName
= 'OCPPRequestService';
36 const defaultRequestParams
: RequestParams
= {
37 skipBufferingOnError
: false,
38 triggerMessage
: false,
42 export abstract class OCPPRequestService
{
43 private static instance
: OCPPRequestService
| null = null;
44 private readonly version
: OCPPVersion
;
45 private readonly ajv
: Ajv
;
46 private readonly ocppResponseService
: OCPPResponseService
;
47 protected abstract jsonSchemas
: Map
<RequestCommand
, JSONSchemaType
<JsonObject
>>;
49 protected constructor(version
: OCPPVersion
, ocppResponseService
: OCPPResponseService
) {
50 this.version
= version
;
52 keywords
: ['javaType'],
53 multipleOfPrecision
: 2,
56 this.ocppResponseService
= ocppResponseService
;
57 this.requestHandler
= this.requestHandler
.bind(this) as <
58 // eslint-disable-next-line @typescript-eslint/no-unused-vars
59 ReqType
extends JsonType
,
60 ResType
extends JsonType
,
62 chargingStation
: ChargingStation
,
63 commandName
: RequestCommand
,
64 commandParams
?: JsonType
,
65 params
?: RequestParams
,
66 ) => Promise
<ResType
>;
67 this.sendMessage
= this.sendMessage
.bind(this) as (
68 chargingStation
: ChargingStation
,
70 messagePayload
: JsonType
,
71 commandName
: RequestCommand
,
72 params
?: RequestParams
,
73 ) => Promise
<ResponseType
>;
74 this.sendResponse
= this.sendResponse
.bind(this) as (
75 chargingStation
: ChargingStation
,
77 messagePayload
: JsonType
,
78 commandName
: IncomingRequestCommand
,
79 ) => Promise
<ResponseType
>;
80 this.sendError
= this.sendError
.bind(this) as (
81 chargingStation
: ChargingStation
,
84 commandName
: RequestCommand
| IncomingRequestCommand
,
85 ) => Promise
<ResponseType
>;
86 this.internalSendMessage
= this.internalSendMessage
.bind(this) as (
87 chargingStation
: ChargingStation
,
89 messagePayload
: JsonType
| OCPPError
,
90 messageType
: MessageType
,
91 commandName
: RequestCommand
| IncomingRequestCommand
,
92 params
?: RequestParams
,
93 ) => Promise
<ResponseType
>;
94 this.buildMessageToSend
= this.buildMessageToSend
.bind(this) as (
95 chargingStation
: ChargingStation
,
97 messagePayload
: JsonType
| OCPPError
,
98 messageType
: MessageType
,
99 commandName
: RequestCommand
| IncomingRequestCommand
,
100 responseCallback
: ResponseCallback
,
101 errorCallback
: ErrorCallback
,
103 this.validateRequestPayload
= this.validateRequestPayload
.bind(this) as <T
extends JsonObject
>(
104 chargingStation
: ChargingStation
,
105 commandName
: RequestCommand
| IncomingRequestCommand
,
108 this.validateIncomingRequestResponsePayload
= this.validateIncomingRequestResponsePayload
.bind(
110 ) as <T
extends JsonObject
>(
111 chargingStation
: ChargingStation
,
112 commandName
: RequestCommand
| IncomingRequestCommand
,
117 public static getInstance
<T
extends OCPPRequestService
>(
118 this: new (ocppResponseService
: OCPPResponseService
) => T
,
119 ocppResponseService
: OCPPResponseService
,
121 if (OCPPRequestService
.instance
=== null) {
122 OCPPRequestService
.instance
= new this(ocppResponseService
);
124 return OCPPRequestService
.instance
as T
;
127 public async sendResponse(
128 chargingStation
: ChargingStation
,
130 messagePayload
: JsonType
,
131 commandName
: IncomingRequestCommand
,
132 ): Promise
<ResponseType
> {
134 // Send response message
135 return await this.internalSendMessage(
139 MessageType
.CALL_RESULT_MESSAGE
,
143 handleSendMessageError(chargingStation
, commandName
, error
as Error, {
150 public async sendError(
151 chargingStation
: ChargingStation
,
153 ocppError
: OCPPError
,
154 commandName
: RequestCommand
| IncomingRequestCommand
,
155 ): Promise
<ResponseType
> {
157 // Send error message
158 return await this.internalSendMessage(
162 MessageType
.CALL_ERROR_MESSAGE
,
166 handleSendMessageError(chargingStation
, commandName
, error
as Error);
171 protected async sendMessage(
172 chargingStation
: ChargingStation
,
174 messagePayload
: JsonType
,
175 commandName
: RequestCommand
,
176 params
?: RequestParams
,
177 ): Promise
<ResponseType
> {
179 ...defaultRequestParams
,
183 return await this.internalSendMessage(
187 MessageType
.CALL_MESSAGE
,
192 handleSendMessageError(chargingStation
, commandName
, error
as Error, {
193 throwError
: params
.throwError
,
199 private validateRequestPayload
<T
extends JsonObject
>(
200 chargingStation
: ChargingStation
,
201 commandName
: RequestCommand
| IncomingRequestCommand
,
204 if (chargingStation
.getOcppStrictCompliance() === false) {
207 if (this.jsonSchemas
.has(commandName
as RequestCommand
) === false) {
209 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: No JSON schema found for command '${commandName}' PDU validation`,
213 const validate
= this.ajv
.compile(this.jsonSchemas
.get(commandName
as RequestCommand
)!);
214 payload
= cloneObject
<T
>(payload
);
215 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
);
216 if (validate(payload
)) {
220 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: Command '${commandName}' request PDU is invalid: %j`,
223 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
225 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
!),
226 'Request PDU is invalid',
228 JSON
.stringify(validate
.errors
, undefined, 2),
232 private validateIncomingRequestResponsePayload
<T
extends JsonObject
>(
233 chargingStation
: ChargingStation
,
234 commandName
: RequestCommand
| IncomingRequestCommand
,
237 if (chargingStation
.getOcppStrictCompliance() === false) {
241 this.ocppResponseService
.jsonIncomingRequestResponseSchemas
.has(
242 commandName
as IncomingRequestCommand
,
246 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: No JSON schema found for command '${commandName}' PDU validation`,
250 const validate
= this.ajv
.compile(
251 this.ocppResponseService
.jsonIncomingRequestResponseSchemas
.get(
252 commandName
as IncomingRequestCommand
,
255 payload
= cloneObject
<T
>(payload
);
256 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
);
257 if (validate(payload
)) {
261 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: Command '${commandName}' reponse PDU is invalid: %j`,
264 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
266 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
!),
267 'Response PDU is invalid',
269 JSON
.stringify(validate
.errors
, undefined, 2),
273 private async internalSendMessage(
274 chargingStation
: ChargingStation
,
276 messagePayload
: JsonType
| OCPPError
,
277 messageType
: MessageType
,
278 commandName
: RequestCommand
| IncomingRequestCommand
,
279 params
?: RequestParams
,
280 ): Promise
<ResponseType
> {
282 ...defaultRequestParams
,
286 (chargingStation
.inUnknownState() === true &&
287 commandName
=== RequestCommand
.BOOT_NOTIFICATION
) ||
288 (chargingStation
.getOcppStrictCompliance() === false &&
289 chargingStation
.inUnknownState() === true) ||
290 chargingStation
.inAcceptedState() === true ||
291 (chargingStation
.inPendingState() === true &&
292 (params
.triggerMessage
=== true || messageType
=== MessageType
.CALL_RESULT_MESSAGE
))
294 // eslint-disable-next-line @typescript-eslint/no-this-alias
296 // Send a message through wsConnection
297 return promiseWithTimeout(
298 new Promise
<ResponseType
>((resolve
, reject
) => {
300 * Function that will receive the request's response
303 * @param requestPayload -
305 const responseCallback
= (payload
: JsonType
, requestPayload
: JsonType
): void => {
306 if (chargingStation
.getEnableStatistics() === true) {
307 chargingStation
.performanceStatistics
?.addRequestStatistic(
309 MessageType
.CALL_RESULT_MESSAGE
,
312 // Handle the request's response
313 self.ocppResponseService
316 commandName
as RequestCommand
,
327 chargingStation
.requests
.delete(messageId
);
332 * Function that will receive the request's error response
335 * @param requestStatistic -
337 const errorCallback
= (error
: OCPPError
, requestStatistic
= true): void => {
338 if (requestStatistic
=== true && chargingStation
.getEnableStatistics() === true) {
339 chargingStation
.performanceStatistics
?.addRequestStatistic(
341 MessageType
.CALL_ERROR_MESSAGE
,
345 `${chargingStation.logPrefix()} Error occurred at ${OCPPServiceUtils.getMessageTypeString(
347 )} command ${commandName} with PDU %j:`,
351 chargingStation
.requests
.delete(messageId
);
355 if (chargingStation
.getEnableStatistics() === true) {
356 chargingStation
.performanceStatistics
?.addRequestStatistic(commandName
, messageType
);
358 const messageToSend
= this.buildMessageToSend(
367 let sendError
= false;
368 // Check if wsConnection opened
369 const wsOpened
= chargingStation
.isWebSocketConnectionOpened() === true;
371 const beginId
= PerformanceStatistics
.beginMeasure(commandName
);
373 chargingStation
.wsConnection
?.send(messageToSend
);
375 `${chargingStation.logPrefix()} >> Command '${commandName}' sent ${OCPPServiceUtils.getMessageTypeString(
377 )} payload: ${messageToSend}`,
381 `${chargingStation.logPrefix()} >> Command '${commandName}' failed to send ${OCPPServiceUtils.getMessageTypeString(
383 )} payload: ${messageToSend}:`,
388 PerformanceStatistics
.endMeasure(commandName
, beginId
);
390 const wsClosedOrErrored
= !wsOpened
|| sendError
=== true;
391 if (wsClosedOrErrored
&& params
?.skipBufferingOnError
=== false) {
393 chargingStation
.bufferMessage(messageToSend
);
394 // Reject and keep request in the cache
397 ErrorType
.GENERIC_ERROR
,
398 `WebSocket closed or errored for buffered message id '${messageId}' with content '${messageToSend}'`,
400 (messagePayload
as JsonObject
)?.details
?? Constants
.EMPTY_FROZEN_OBJECT
,
403 } else if (wsClosedOrErrored
) {
404 const ocppError
= new OCPPError(
405 ErrorType
.GENERIC_ERROR
,
406 `WebSocket closed or errored for non buffered message id '${messageId}' with content '${messageToSend}'`,
408 (messagePayload
as JsonObject
)?.details
?? Constants
.EMPTY_FROZEN_OBJECT
,
411 if (messageType
!== MessageType
.CALL_MESSAGE
) {
412 return reject(ocppError
);
414 // Reject and remove request from the cache
415 return errorCallback(ocppError
, false);
418 if (messageType
!== MessageType
.CALL_MESSAGE
) {
419 return resolve(messagePayload
);
422 OCPPConstants
.OCPP_WEBSOCKET_TIMEOUT
,
424 ErrorType
.GENERIC_ERROR
,
425 `Timeout for message id '${messageId}'`,
427 (messagePayload
as JsonObject
)?.details
?? Constants
.EMPTY_FROZEN_OBJECT
,
430 messageType
=== MessageType
.CALL_MESSAGE
&& chargingStation
.requests
.delete(messageId
);
435 ErrorType
.SECURITY_ERROR
,
436 `Cannot send command ${commandName} PDU when the charging station is in ${chargingStation.getRegistrationStatus()} state on the central server`,
441 private buildMessageToSend(
442 chargingStation
: ChargingStation
,
444 messagePayload
: JsonType
| OCPPError
,
445 messageType
: MessageType
,
446 commandName
: RequestCommand
| IncomingRequestCommand
,
447 responseCallback
: ResponseCallback
,
448 errorCallback
: ErrorCallback
,
450 let messageToSend
: string;
452 switch (messageType
) {
454 case MessageType
.CALL_MESSAGE
:
456 this.validateRequestPayload(chargingStation
, commandName
, messagePayload
as JsonObject
);
457 chargingStation
.requests
.set(messageId
, [
461 messagePayload
as JsonType
,
463 messageToSend
= JSON
.stringify([
468 ] as OutgoingRequest
);
471 case MessageType
.CALL_RESULT_MESSAGE
:
473 this.validateIncomingRequestResponsePayload(
476 messagePayload
as JsonObject
,
478 messageToSend
= JSON
.stringify([messageType
, messageId
, messagePayload
] as Response
);
481 case MessageType
.CALL_ERROR_MESSAGE
:
482 // Build Error Message
483 messageToSend
= JSON
.stringify([
486 (messagePayload
as OCPPError
)?.code
?? ErrorType
.GENERIC_ERROR
,
487 (messagePayload
as OCPPError
)?.message
?? '',
488 (messagePayload
as OCPPError
)?.details
?? { commandName
},
492 return messageToSend
;
495 // eslint-disable-next-line @typescript-eslint/no-unused-vars
496 public abstract requestHandler
<ReqType
extends JsonType
, ResType
extends JsonType
>(
497 chargingStation
: ChargingStation
,
498 commandName
: RequestCommand
,
499 // FIXME: should be ReqType
500 commandParams
?: JsonType
,
501 params
?: RequestParams
,