1 import _Ajv
, { type JSONSchemaType
, type ValidateFunction
} from
'ajv';
2 import _ajvFormats from
'ajv-formats';
4 import { OCPPConstants
} from
'./OCPPConstants.js';
5 import type { OCPPResponseService
} from
'./OCPPResponseService.js';
6 import { OCPPServiceUtils
} from
'./OCPPServiceUtils.js';
7 import type { ChargingStation
} from
'../../charging-station/index.js';
8 import { OCPPError
} from
'../../exception/index.js';
9 import { PerformanceStatistics
} from
'../../performance/index.js';
11 ChargingStationEvents
,
15 type IncomingRequestCommand
,
23 type ResponseCallback
,
25 } from
'../../types/index.js';
28 formatDurationMilliSeconds
,
29 handleSendMessageError
,
32 } from
'../../utils/index.js';
33 type Ajv
= _Ajv
.default;
34 const Ajv
= _Ajv
.default;
35 const ajvFormats
= _ajvFormats
.default;
37 const moduleName
= 'OCPPRequestService';
39 const defaultRequestParams
: RequestParams
= {
40 skipBufferingOnError
: false,
41 triggerMessage
: false,
45 export abstract class OCPPRequestService
{
46 private static instance
: OCPPRequestService
| null = null;
47 private readonly version
: OCPPVersion
;
48 private readonly ajv
: Ajv
;
49 private readonly ocppResponseService
: OCPPResponseService
;
50 private readonly jsonValidateFunctions
: Map
<RequestCommand
, ValidateFunction
<JsonType
>>;
51 protected abstract jsonSchemas
: Map
<RequestCommand
, JSONSchemaType
<JsonType
>>;
53 protected constructor(version
: OCPPVersion
, ocppResponseService
: OCPPResponseService
) {
54 this.version
= version
;
56 keywords
: ['javaType'],
57 multipleOfPrecision
: 2,
60 this.jsonValidateFunctions
= new Map
<RequestCommand
, ValidateFunction
<JsonType
>>();
61 this.ocppResponseService
= ocppResponseService
;
62 this.requestHandler
= this.requestHandler
.bind(this) as <
63 // eslint-disable-next-line @typescript-eslint/no-unused-vars
64 ReqType
extends JsonType
,
65 ResType
extends JsonType
,
67 chargingStation
: ChargingStation
,
68 commandName
: RequestCommand
,
69 commandParams
?: JsonType
,
70 params
?: RequestParams
,
71 ) => Promise
<ResType
>;
72 this.sendMessage
= this.sendMessage
.bind(this) as (
73 chargingStation
: ChargingStation
,
75 messagePayload
: JsonType
,
76 commandName
: RequestCommand
,
77 params
?: RequestParams
,
78 ) => Promise
<ResponseType
>;
79 this.sendResponse
= this.sendResponse
.bind(this) as (
80 chargingStation
: ChargingStation
,
82 messagePayload
: JsonType
,
83 commandName
: IncomingRequestCommand
,
84 ) => Promise
<ResponseType
>;
85 this.sendError
= this.sendError
.bind(this) as (
86 chargingStation
: ChargingStation
,
89 commandName
: RequestCommand
| IncomingRequestCommand
,
90 ) => Promise
<ResponseType
>;
91 this.internalSendMessage
= this.internalSendMessage
.bind(this) as (
92 chargingStation
: ChargingStation
,
94 messagePayload
: JsonType
| OCPPError
,
95 messageType
: MessageType
,
96 commandName
: RequestCommand
| IncomingRequestCommand
,
97 params
?: RequestParams
,
98 ) => Promise
<ResponseType
>;
99 this.buildMessageToSend
= this.buildMessageToSend
.bind(this) as (
100 chargingStation
: ChargingStation
,
102 messagePayload
: JsonType
| OCPPError
,
103 messageType
: MessageType
,
104 commandName
: RequestCommand
| IncomingRequestCommand
,
106 this.validateRequestPayload
= this.validateRequestPayload
.bind(this) as <T
extends JsonType
>(
107 chargingStation
: ChargingStation
,
108 commandName
: RequestCommand
| IncomingRequestCommand
,
111 this.validateIncomingRequestResponsePayload
= this.validateIncomingRequestResponsePayload
.bind(
113 ) as <T
extends JsonType
>(
114 chargingStation
: ChargingStation
,
115 commandName
: RequestCommand
| IncomingRequestCommand
,
120 public static getInstance
<T
extends OCPPRequestService
>(
121 this: new (ocppResponseService
: OCPPResponseService
) => T
,
122 ocppResponseService
: OCPPResponseService
,
124 if (OCPPRequestService
.instance
=== null) {
125 OCPPRequestService
.instance
= new this(ocppResponseService
);
127 return OCPPRequestService
.instance
as T
;
130 public async sendResponse(
131 chargingStation
: ChargingStation
,
133 messagePayload
: JsonType
,
134 commandName
: IncomingRequestCommand
,
135 ): Promise
<ResponseType
> {
137 // Send response message
138 return await this.internalSendMessage(
142 MessageType
.CALL_RESULT_MESSAGE
,
146 handleSendMessageError(chargingStation
, commandName
, error
as Error, {
153 public async sendError(
154 chargingStation
: ChargingStation
,
156 ocppError
: OCPPError
,
157 commandName
: RequestCommand
| IncomingRequestCommand
,
158 ): Promise
<ResponseType
> {
160 // Send error message
161 return await this.internalSendMessage(
165 MessageType
.CALL_ERROR_MESSAGE
,
169 handleSendMessageError(chargingStation
, commandName
, error
as Error);
174 protected async sendMessage(
175 chargingStation
: ChargingStation
,
177 messagePayload
: JsonType
,
178 commandName
: RequestCommand
,
179 params
?: RequestParams
,
180 ): Promise
<ResponseType
> {
182 ...defaultRequestParams
,
186 return await this.internalSendMessage(
190 MessageType
.CALL_MESSAGE
,
195 handleSendMessageError(chargingStation
, commandName
, error
as Error, {
196 throwError
: params
.throwError
,
202 private validateRequestPayload
<T
extends JsonType
>(
203 chargingStation
: ChargingStation
,
204 commandName
: RequestCommand
| IncomingRequestCommand
,
207 if (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false) {
210 if (this.jsonSchemas
.has(commandName
as RequestCommand
) === false) {
212 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: No JSON schema found for command '${commandName}' PDU validation`,
216 const validate
= this.getJsonRequestValidateFunction
<T
>(commandName
as RequestCommand
);
217 payload
= cloneObject
<T
>(payload
);
218 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
);
219 if (validate(payload
)) {
223 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: Command '${commandName}' request PDU is invalid: %j`,
226 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
228 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
),
229 'Request PDU is invalid',
231 JSON
.stringify(validate
.errors
, undefined, 2),
235 private getJsonRequestValidateFunction
<T
extends JsonType
>(commandName
: RequestCommand
) {
236 if (this.jsonValidateFunctions
.has(commandName
) === false) {
237 this.jsonValidateFunctions
.set(
239 this.ajv
.compile
<T
>(this.jsonSchemas
.get(commandName
)!).bind(this),
242 return this.jsonValidateFunctions
.get(commandName
)!;
245 private validateIncomingRequestResponsePayload
<T
extends JsonType
>(
246 chargingStation
: ChargingStation
,
247 commandName
: RequestCommand
| IncomingRequestCommand
,
250 if (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false) {
254 this.ocppResponseService
.jsonIncomingRequestResponseSchemas
.has(
255 commandName
as IncomingRequestCommand
,
259 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: No JSON schema found for command '${commandName}' PDU validation`,
263 const validate
= this.getJsonRequestResponseValidateFunction
<T
>(
264 commandName
as IncomingRequestCommand
,
266 payload
= cloneObject
<T
>(payload
);
267 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
);
268 if (validate(payload
)) {
272 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: Command '${commandName}' reponse PDU is invalid: %j`,
275 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
277 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
),
278 'Response PDU is invalid',
280 JSON
.stringify(validate
.errors
, undefined, 2),
284 private getJsonRequestResponseValidateFunction
<T
extends JsonType
>(
285 commandName
: IncomingRequestCommand
,
288 this.ocppResponseService
.jsonIncomingRequestResponseValidateFunctions
.has(commandName
) ===
291 this.ocppResponseService
.jsonIncomingRequestResponseValidateFunctions
.set(
294 .compile
<T
>(this.ocppResponseService
.jsonIncomingRequestResponseSchemas
.get(commandName
)!)
298 return this.ocppResponseService
.jsonIncomingRequestResponseValidateFunctions
.get(commandName
)!;
301 private async internalSendMessage(
302 chargingStation
: ChargingStation
,
304 messagePayload
: JsonType
| OCPPError
,
305 messageType
: MessageType
,
306 commandName
: RequestCommand
| IncomingRequestCommand
,
307 params
?: RequestParams
,
308 ): Promise
<ResponseType
> {
310 ...defaultRequestParams
,
314 (chargingStation
.inUnknownState() === true &&
315 commandName
=== RequestCommand
.BOOT_NOTIFICATION
) ||
316 (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false &&
317 chargingStation
.inUnknownState() === true) ||
318 chargingStation
.inAcceptedState() === true ||
319 (chargingStation
.inPendingState() === true &&
320 (params
.triggerMessage
=== true || messageType
=== MessageType
.CALL_RESULT_MESSAGE
))
322 // eslint-disable-next-line @typescript-eslint/no-this-alias
324 // Send a message through wsConnection
325 return new Promise
<ResponseType
>((resolve
, reject
) => {
327 * Function that will receive the request's response
330 * @param requestPayload -
332 const responseCallback
= (payload
: JsonType
, requestPayload
: JsonType
): void => {
333 if (chargingStation
.stationInfo
?.enableStatistics
=== true) {
334 chargingStation
.performanceStatistics
?.addRequestStatistic(
336 MessageType
.CALL_RESULT_MESSAGE
,
339 // Handle the request's response
340 self.ocppResponseService
343 commandName
as RequestCommand
,
352 chargingStation
.requests
.delete(messageId
);
353 chargingStation
.emit(ChargingStationEvents
.updated
);
358 * Function that will receive the request's error response
361 * @param requestStatistic -
363 const errorCallback
= (ocppError
: OCPPError
, requestStatistic
= true): void => {
364 if (requestStatistic
=== true && chargingStation
.stationInfo
?.enableStatistics
=== true) {
365 chargingStation
.performanceStatistics
?.addRequestStatistic(
367 MessageType
.CALL_ERROR_MESSAGE
,
371 `${chargingStation.logPrefix()} Error occurred at ${OCPPServiceUtils.getMessageTypeString(
373 )} command ${commandName} with PDU %j:`,
377 chargingStation
.requests
.delete(messageId
);
378 chargingStation
.emit(ChargingStationEvents
.updated
);
382 const handleSendError
= (ocppError
: OCPPError
): void => {
383 if (params
?.skipBufferingOnError
=== false) {
385 chargingStation
.bufferMessage(messageToSend
);
386 if (messageType
=== MessageType
.CALL_MESSAGE
) {
387 this.cacheRequestPromise(
390 messagePayload
as JsonType
,
397 params
?.skipBufferingOnError
=== true &&
398 messageType
=== MessageType
.CALL_MESSAGE
400 // Remove request from the cache
401 chargingStation
.requests
.delete(messageId
);
403 return reject(ocppError
);
406 if (chargingStation
.stationInfo
?.enableStatistics
=== true) {
407 chargingStation
.performanceStatistics
?.addRequestStatistic(commandName
, messageType
);
409 const messageToSend
= this.buildMessageToSend(
416 // Check if wsConnection opened
417 if (chargingStation
.isWebSocketConnectionOpened() === true) {
418 const beginId
= PerformanceStatistics
.beginMeasure(commandName
);
419 const sendTimeout
= setTimeout(() => {
420 return handleSendError(
422 ErrorType
.GENERIC_ERROR
,
423 `Timeout ${formatDurationMilliSeconds(
424 OCPPConstants.OCPP_WEBSOCKET_TIMEOUT,
426 params?.skipBufferingOnError === false ? '' : 'non '
427 }buffered message id '${messageId}' with content '${messageToSend}'`,
429 (messagePayload
as OCPPError
).details
,
432 }, OCPPConstants
.OCPP_WEBSOCKET_TIMEOUT
);
433 chargingStation
.wsConnection
?.send(messageToSend
, (error
?: Error) => {
434 PerformanceStatistics
.endMeasure(commandName
, beginId
);
435 clearTimeout(sendTimeout
);
436 if (isNullOrUndefined(error
)) {
438 `${chargingStation.logPrefix()} >> Command '${commandName}' sent ${OCPPServiceUtils.getMessageTypeString(
440 )} payload: ${messageToSend}`,
442 if (messageType
=== MessageType
.CALL_MESSAGE
) {
443 this.cacheRequestPromise(
446 messagePayload
as JsonType
,
453 return resolve(messagePayload
);
456 return handleSendError(
458 ErrorType
.GENERIC_ERROR
,
459 `WebSocket errored for ${
460 params?.skipBufferingOnError === false ? '' : 'non '
461 }buffered message id '${messageId}' with content '${messageToSend}'`,
463 { name
: error
.name
, message
: error
.message
, stack
: error
.stack
},
469 return handleSendError(
471 ErrorType
.GENERIC_ERROR
,
472 `WebSocket closed for ${
473 params?.skipBufferingOnError === false ? '' : 'non '
474 }buffered message id '${messageId}' with content '${messageToSend}'`,
476 (messagePayload
as OCPPError
).details
,
483 ErrorType
.SECURITY_ERROR
,
484 `Cannot send command ${commandName} PDU when the charging station is in ${chargingStation?.bootNotificationResponse?.status} state on the central server`,
489 private buildMessageToSend(
490 chargingStation
: ChargingStation
,
492 messagePayload
: JsonType
| OCPPError
,
493 messageType
: MessageType
,
494 commandName
: RequestCommand
| IncomingRequestCommand
,
496 let messageToSend
: string;
498 switch (messageType
) {
500 case MessageType
.CALL_MESSAGE
:
502 this.validateRequestPayload(chargingStation
, commandName
, messagePayload
as JsonType
);
503 messageToSend
= JSON
.stringify([
508 ] as OutgoingRequest
);
511 case MessageType
.CALL_RESULT_MESSAGE
:
513 this.validateIncomingRequestResponsePayload(
516 messagePayload
as JsonType
,
518 messageToSend
= JSON
.stringify([messageType
, messageId
, messagePayload
] as Response
);
521 case MessageType
.CALL_ERROR_MESSAGE
:
522 // Build Error Message
523 messageToSend
= JSON
.stringify([
526 (messagePayload
as OCPPError
).code
,
527 (messagePayload
as OCPPError
).message
,
528 (messagePayload
as OCPPError
).details
?? {
529 command
: (messagePayload
as OCPPError
).command
?? commandName
,
534 return messageToSend
;
537 private cacheRequestPromise(
538 chargingStation
: ChargingStation
,
540 messagePayload
: JsonType
,
541 commandName
: RequestCommand
| IncomingRequestCommand
,
542 responseCallback
: ResponseCallback
,
543 errorCallback
: ErrorCallback
,
545 chargingStation
.requests
.set(messageId
, [
553 // eslint-disable-next-line @typescript-eslint/no-unused-vars
554 public abstract requestHandler
<ReqType
extends JsonType
, ResType
extends JsonType
>(
555 chargingStation
: ChargingStation
,
556 commandName
: RequestCommand
,
557 // FIXME: should be ReqType
558 commandParams
?: JsonType
,
559 params
?: RequestParams
,