1 import Ajv
, { type JSONSchemaType
, type ValidateFunction
} 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
,
22 type ResponseCallback
,
27 formatDurationMilliSeconds
,
28 handleSendMessageError
,
33 const moduleName
= 'OCPPRequestService';
35 const defaultRequestParams
: RequestParams
= {
36 skipBufferingOnError
: false,
37 triggerMessage
: false,
41 export abstract class OCPPRequestService
{
42 private static instance
: OCPPRequestService
| null = null;
43 private readonly version
: OCPPVersion
;
44 private readonly ajv
: Ajv
;
45 private readonly ocppResponseService
: OCPPResponseService
;
46 private readonly jsonValidateFunctions
: Map
<RequestCommand
, ValidateFunction
<JsonType
>>;
47 protected abstract jsonSchemas
: Map
<RequestCommand
, JSONSchemaType
<JsonType
>>;
49 protected constructor(version
: OCPPVersion
, ocppResponseService
: OCPPResponseService
) {
50 this.version
= version
;
52 keywords
: ['javaType'],
53 multipleOfPrecision
: 2,
56 this.jsonValidateFunctions
= new Map
<RequestCommand
, ValidateFunction
<JsonType
>>();
57 this.ocppResponseService
= ocppResponseService
;
58 this.requestHandler
= this.requestHandler
.bind(this) as <
59 // eslint-disable-next-line @typescript-eslint/no-unused-vars
60 ReqType
extends JsonType
,
61 ResType
extends JsonType
,
63 chargingStation
: ChargingStation
,
64 commandName
: RequestCommand
,
65 commandParams
?: JsonType
,
66 params
?: RequestParams
,
67 ) => Promise
<ResType
>;
68 this.sendMessage
= this.sendMessage
.bind(this) as (
69 chargingStation
: ChargingStation
,
71 messagePayload
: JsonType
,
72 commandName
: RequestCommand
,
73 params
?: RequestParams
,
74 ) => Promise
<ResponseType
>;
75 this.sendResponse
= this.sendResponse
.bind(this) as (
76 chargingStation
: ChargingStation
,
78 messagePayload
: JsonType
,
79 commandName
: IncomingRequestCommand
,
80 ) => Promise
<ResponseType
>;
81 this.sendError
= this.sendError
.bind(this) as (
82 chargingStation
: ChargingStation
,
85 commandName
: RequestCommand
| IncomingRequestCommand
,
86 ) => Promise
<ResponseType
>;
87 this.internalSendMessage
= this.internalSendMessage
.bind(this) as (
88 chargingStation
: ChargingStation
,
90 messagePayload
: JsonType
| OCPPError
,
91 messageType
: MessageType
,
92 commandName
: RequestCommand
| IncomingRequestCommand
,
93 params
?: RequestParams
,
94 ) => Promise
<ResponseType
>;
95 this.buildMessageToSend
= this.buildMessageToSend
.bind(this) as (
96 chargingStation
: ChargingStation
,
98 messagePayload
: JsonType
| OCPPError
,
99 messageType
: MessageType
,
100 commandName
: RequestCommand
| IncomingRequestCommand
,
102 this.validateRequestPayload
= this.validateRequestPayload
.bind(this) as <T
extends JsonType
>(
103 chargingStation
: ChargingStation
,
104 commandName
: RequestCommand
| IncomingRequestCommand
,
107 this.validateIncomingRequestResponsePayload
= this.validateIncomingRequestResponsePayload
.bind(
109 ) as <T
extends JsonType
>(
110 chargingStation
: ChargingStation
,
111 commandName
: RequestCommand
| IncomingRequestCommand
,
116 public static getInstance
<T
extends OCPPRequestService
>(
117 this: new (ocppResponseService
: OCPPResponseService
) => T
,
118 ocppResponseService
: OCPPResponseService
,
120 if (OCPPRequestService
.instance
=== null) {
121 OCPPRequestService
.instance
= new this(ocppResponseService
);
123 return OCPPRequestService
.instance
as T
;
126 public async sendResponse(
127 chargingStation
: ChargingStation
,
129 messagePayload
: JsonType
,
130 commandName
: IncomingRequestCommand
,
131 ): Promise
<ResponseType
> {
133 // Send response message
134 return await this.internalSendMessage(
138 MessageType
.CALL_RESULT_MESSAGE
,
142 handleSendMessageError(chargingStation
, commandName
, error
as Error, {
149 public async sendError(
150 chargingStation
: ChargingStation
,
152 ocppError
: OCPPError
,
153 commandName
: RequestCommand
| IncomingRequestCommand
,
154 ): Promise
<ResponseType
> {
156 // Send error message
157 return await this.internalSendMessage(
161 MessageType
.CALL_ERROR_MESSAGE
,
165 handleSendMessageError(chargingStation
, commandName
, error
as Error);
170 protected async sendMessage(
171 chargingStation
: ChargingStation
,
173 messagePayload
: JsonType
,
174 commandName
: RequestCommand
,
175 params
?: RequestParams
,
176 ): Promise
<ResponseType
> {
178 ...defaultRequestParams
,
182 return await this.internalSendMessage(
186 MessageType
.CALL_MESSAGE
,
191 handleSendMessageError(chargingStation
, commandName
, error
as Error, {
192 throwError
: params
.throwError
,
198 private validateRequestPayload
<T
extends JsonType
>(
199 chargingStation
: ChargingStation
,
200 commandName
: RequestCommand
| IncomingRequestCommand
,
203 if (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false) {
206 if (this.jsonSchemas
.has(commandName
as RequestCommand
) === false) {
208 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: No JSON schema found for command '${commandName}' PDU validation`,
212 const validate
= this.getJsonRequestValidateFunction
<T
>(commandName
as RequestCommand
);
213 payload
= cloneObject
<T
>(payload
);
214 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
);
215 if (validate(payload
)) {
219 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: Command '${commandName}' request PDU is invalid: %j`,
222 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
224 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
),
225 'Request PDU is invalid',
227 JSON
.stringify(validate
.errors
, undefined, 2),
231 private getJsonRequestValidateFunction
<T
extends JsonType
>(commandName
: RequestCommand
) {
232 if (this.jsonValidateFunctions
.has(commandName
) === false) {
233 this.jsonValidateFunctions
.set(
235 this.ajv
.compile
<T
>(this.jsonSchemas
.get(commandName
)!).bind(this),
238 return this.jsonValidateFunctions
.get(commandName
)!;
241 private validateIncomingRequestResponsePayload
<T
extends JsonType
>(
242 chargingStation
: ChargingStation
,
243 commandName
: RequestCommand
| IncomingRequestCommand
,
246 if (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false) {
250 this.ocppResponseService
.jsonIncomingRequestResponseSchemas
.has(
251 commandName
as IncomingRequestCommand
,
255 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: No JSON schema found for command '${commandName}' PDU validation`,
259 const validate
= this.getJsonRequestResponseValidateFunction
<T
>(
260 commandName
as IncomingRequestCommand
,
262 payload
= cloneObject
<T
>(payload
);
263 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
);
264 if (validate(payload
)) {
268 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: Command '${commandName}' reponse PDU is invalid: %j`,
271 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
273 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
),
274 'Response PDU is invalid',
276 JSON
.stringify(validate
.errors
, undefined, 2),
280 private getJsonRequestResponseValidateFunction
<T
extends JsonType
>(
281 commandName
: IncomingRequestCommand
,
284 this.ocppResponseService
.jsonIncomingRequestResponseValidateFunctions
.has(commandName
) ===
287 this.ocppResponseService
.jsonIncomingRequestResponseValidateFunctions
.set(
290 .compile
<T
>(this.ocppResponseService
.jsonIncomingRequestResponseSchemas
.get(commandName
)!)
294 return this.ocppResponseService
.jsonIncomingRequestResponseValidateFunctions
.get(commandName
)!;
297 private async internalSendMessage(
298 chargingStation
: ChargingStation
,
300 messagePayload
: JsonType
| OCPPError
,
301 messageType
: MessageType
,
302 commandName
: RequestCommand
| IncomingRequestCommand
,
303 params
?: RequestParams
,
304 ): Promise
<ResponseType
> {
306 ...defaultRequestParams
,
310 (chargingStation
.inUnknownState() === true &&
311 commandName
=== RequestCommand
.BOOT_NOTIFICATION
) ||
312 (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false &&
313 chargingStation
.inUnknownState() === true) ||
314 chargingStation
.inAcceptedState() === true ||
315 (chargingStation
.inPendingState() === true &&
316 (params
.triggerMessage
=== true || messageType
=== MessageType
.CALL_RESULT_MESSAGE
))
318 // eslint-disable-next-line @typescript-eslint/no-this-alias
320 // Send a message through wsConnection
321 return new Promise
<ResponseType
>((resolve
, reject
) => {
323 * Function that will receive the request's response
326 * @param requestPayload -
328 const responseCallback
= (payload
: JsonType
, requestPayload
: JsonType
): void => {
329 if (chargingStation
.stationInfo
?.enableStatistics
=== true) {
330 chargingStation
.performanceStatistics
?.addRequestStatistic(
332 MessageType
.CALL_RESULT_MESSAGE
,
335 // Handle the request's response
336 self.ocppResponseService
339 commandName
as RequestCommand
,
348 chargingStation
.requests
.delete(messageId
);
353 * Function that will receive the request's error response
356 * @param requestStatistic -
358 const errorCallback
= (ocppError
: OCPPError
, requestStatistic
= true): void => {
359 if (requestStatistic
=== true && chargingStation
.stationInfo
?.enableStatistics
=== true) {
360 chargingStation
.performanceStatistics
?.addRequestStatistic(
362 MessageType
.CALL_ERROR_MESSAGE
,
366 `${chargingStation.logPrefix()} Error occurred at ${OCPPServiceUtils.getMessageTypeString(
368 )} command ${commandName} with PDU %j:`,
372 chargingStation
.requests
.delete(messageId
);
376 const handleSendError
= (ocppError
: OCPPError
): void => {
377 if (params
?.skipBufferingOnError
=== false) {
379 chargingStation
.bufferMessage(messageToSend
);
380 if (messageType
=== MessageType
.CALL_MESSAGE
) {
381 this.cacheRequestPromise(
384 messagePayload
as JsonType
,
391 params
?.skipBufferingOnError
=== true &&
392 messageType
=== MessageType
.CALL_MESSAGE
394 // Remove request from the cache
395 chargingStation
.requests
.delete(messageId
);
397 return reject(ocppError
);
400 if (chargingStation
.stationInfo
?.enableStatistics
=== true) {
401 chargingStation
.performanceStatistics
?.addRequestStatistic(commandName
, messageType
);
403 const messageToSend
= this.buildMessageToSend(
410 // Check if wsConnection opened
411 if (chargingStation
.isWebSocketConnectionOpened() === true) {
412 const beginId
= PerformanceStatistics
.beginMeasure(commandName
);
413 const sendTimeout
= setTimeout(() => {
414 return handleSendError(
416 ErrorType
.GENERIC_ERROR
,
417 `Timeout ${formatDurationMilliSeconds(
418 OCPPConstants.OCPP_WEBSOCKET_TIMEOUT,
420 params?.skipBufferingOnError === false ? '' : 'non '
421 }buffered message id '${messageId}' with content '${messageToSend}'`,
423 (messagePayload
as OCPPError
).details
,
426 }, OCPPConstants
.OCPP_WEBSOCKET_TIMEOUT
);
427 chargingStation
.wsConnection
?.send(messageToSend
, (error
?: Error) => {
428 PerformanceStatistics
.endMeasure(commandName
, beginId
);
429 clearTimeout(sendTimeout
);
430 if (isNullOrUndefined(error
)) {
432 `${chargingStation.logPrefix()} >> Command '${commandName}' sent ${OCPPServiceUtils.getMessageTypeString(
434 )} payload: ${messageToSend}`,
436 if (messageType
=== MessageType
.CALL_MESSAGE
) {
437 this.cacheRequestPromise(
440 messagePayload
as JsonType
,
447 return resolve(messagePayload
);
450 return handleSendError(
452 ErrorType
.GENERIC_ERROR
,
453 `WebSocket errored for ${
454 params?.skipBufferingOnError === false ? '' : 'non '
455 }buffered message id '${messageId}' with content '${messageToSend}'`,
457 { name
: error
.name
, message
: error
.message
, stack
: error
.stack
},
463 return handleSendError(
465 ErrorType
.GENERIC_ERROR
,
466 `WebSocket closed for ${
467 params?.skipBufferingOnError === false ? '' : 'non '
468 }buffered message id '${messageId}' with content '${messageToSend}'`,
470 (messagePayload
as OCPPError
).details
,
477 ErrorType
.SECURITY_ERROR
,
478 `Cannot send command ${commandName} PDU when the charging station is in ${chargingStation.getRegistrationStatus()} state on the central server`,
483 private buildMessageToSend(
484 chargingStation
: ChargingStation
,
486 messagePayload
: JsonType
| OCPPError
,
487 messageType
: MessageType
,
488 commandName
: RequestCommand
| IncomingRequestCommand
,
490 let messageToSend
: string;
492 switch (messageType
) {
494 case MessageType
.CALL_MESSAGE
:
496 this.validateRequestPayload(chargingStation
, commandName
, messagePayload
as JsonType
);
497 messageToSend
= JSON
.stringify([
502 ] as OutgoingRequest
);
505 case MessageType
.CALL_RESULT_MESSAGE
:
507 this.validateIncomingRequestResponsePayload(
510 messagePayload
as JsonType
,
512 messageToSend
= JSON
.stringify([messageType
, messageId
, messagePayload
] as Response
);
515 case MessageType
.CALL_ERROR_MESSAGE
:
516 // Build Error Message
517 messageToSend
= JSON
.stringify([
520 (messagePayload
as OCPPError
).code
,
521 (messagePayload
as OCPPError
).message
,
522 (messagePayload
as OCPPError
).details
?? {
523 command
: (messagePayload
as OCPPError
).command
?? commandName
,
528 return messageToSend
;
531 private cacheRequestPromise(
532 chargingStation
: ChargingStation
,
534 messagePayload
: JsonType
,
535 commandName
: RequestCommand
| IncomingRequestCommand
,
536 responseCallback
: ResponseCallback
,
537 errorCallback
: ErrorCallback
,
539 chargingStation
.requests
.set(messageId
, [
547 // eslint-disable-next-line @typescript-eslint/no-unused-vars
548 public abstract requestHandler
<ReqType
extends JsonType
, ResType
extends JsonType
>(
549 chargingStation
: ChargingStation
,
550 commandName
: RequestCommand
,
551 // FIXME: should be ReqType
552 commandParams
?: JsonType
,
553 params
?: RequestParams
,