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
,
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 private readonly jsonValidateFunctions
: Map
<RequestCommand
, ValidateFunction
<JsonType
>>;
48 protected abstract jsonSchemas
: Map
<RequestCommand
, JSONSchemaType
<JsonType
>>;
50 protected constructor(version
: OCPPVersion
, ocppResponseService
: OCPPResponseService
) {
51 this.version
= version
;
53 keywords
: ['javaType'],
54 multipleOfPrecision
: 2,
57 this.jsonValidateFunctions
= new Map
<RequestCommand
, ValidateFunction
<JsonType
>>();
58 this.ocppResponseService
= ocppResponseService
;
59 this.requestHandler
= this.requestHandler
.bind(this) as <
60 // eslint-disable-next-line @typescript-eslint/no-unused-vars
61 ReqType
extends JsonType
,
62 ResType
extends JsonType
,
64 chargingStation
: ChargingStation
,
65 commandName
: RequestCommand
,
66 commandParams
?: JsonType
,
67 params
?: RequestParams
,
68 ) => Promise
<ResType
>;
69 this.sendMessage
= this.sendMessage
.bind(this) as (
70 chargingStation
: ChargingStation
,
72 messagePayload
: JsonType
,
73 commandName
: RequestCommand
,
74 params
?: RequestParams
,
75 ) => Promise
<ResponseType
>;
76 this.sendResponse
= this.sendResponse
.bind(this) as (
77 chargingStation
: ChargingStation
,
79 messagePayload
: JsonType
,
80 commandName
: IncomingRequestCommand
,
81 ) => Promise
<ResponseType
>;
82 this.sendError
= this.sendError
.bind(this) as (
83 chargingStation
: ChargingStation
,
86 commandName
: RequestCommand
| IncomingRequestCommand
,
87 ) => Promise
<ResponseType
>;
88 this.internalSendMessage
= this.internalSendMessage
.bind(this) as (
89 chargingStation
: ChargingStation
,
91 messagePayload
: JsonType
| OCPPError
,
92 messageType
: MessageType
,
93 commandName
: RequestCommand
| IncomingRequestCommand
,
94 params
?: RequestParams
,
95 ) => Promise
<ResponseType
>;
96 this.buildMessageToSend
= this.buildMessageToSend
.bind(this) as (
97 chargingStation
: ChargingStation
,
99 messagePayload
: JsonType
| OCPPError
,
100 messageType
: MessageType
,
101 commandName
: RequestCommand
| IncomingRequestCommand
,
102 responseCallback
: ResponseCallback
,
103 errorCallback
: ErrorCallback
,
105 this.validateRequestPayload
= this.validateRequestPayload
.bind(this) as <T
extends JsonType
>(
106 chargingStation
: ChargingStation
,
107 commandName
: RequestCommand
| IncomingRequestCommand
,
110 this.validateIncomingRequestResponsePayload
= this.validateIncomingRequestResponsePayload
.bind(
112 ) as <T
extends JsonType
>(
113 chargingStation
: ChargingStation
,
114 commandName
: RequestCommand
| IncomingRequestCommand
,
119 public static getInstance
<T
extends OCPPRequestService
>(
120 this: new (ocppResponseService
: OCPPResponseService
) => T
,
121 ocppResponseService
: OCPPResponseService
,
123 if (OCPPRequestService
.instance
=== null) {
124 OCPPRequestService
.instance
= new this(ocppResponseService
);
126 return OCPPRequestService
.instance
as T
;
129 public async sendResponse(
130 chargingStation
: ChargingStation
,
132 messagePayload
: JsonType
,
133 commandName
: IncomingRequestCommand
,
134 ): Promise
<ResponseType
> {
136 // Send response message
137 return await this.internalSendMessage(
141 MessageType
.CALL_RESULT_MESSAGE
,
145 handleSendMessageError(chargingStation
, commandName
, error
as Error, {
152 public async sendError(
153 chargingStation
: ChargingStation
,
155 ocppError
: OCPPError
,
156 commandName
: RequestCommand
| IncomingRequestCommand
,
157 ): Promise
<ResponseType
> {
159 // Send error message
160 return await this.internalSendMessage(
164 MessageType
.CALL_ERROR_MESSAGE
,
168 handleSendMessageError(chargingStation
, commandName
, error
as Error);
173 protected async sendMessage(
174 chargingStation
: ChargingStation
,
176 messagePayload
: JsonType
,
177 commandName
: RequestCommand
,
178 params
?: RequestParams
,
179 ): Promise
<ResponseType
> {
181 ...defaultRequestParams
,
185 return await this.internalSendMessage(
189 MessageType
.CALL_MESSAGE
,
194 handleSendMessageError(chargingStation
, commandName
, error
as Error, {
195 throwError
: params
.throwError
,
201 private validateRequestPayload
<T
extends JsonType
>(
202 chargingStation
: ChargingStation
,
203 commandName
: RequestCommand
| IncomingRequestCommand
,
206 if (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false) {
209 if (this.jsonSchemas
.has(commandName
as RequestCommand
) === false) {
211 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: No JSON schema found for command '${commandName}' PDU validation`,
215 const validate
= this.getJsonRequestValidateFunction
<T
>(commandName
as RequestCommand
);
216 payload
= cloneObject
<T
>(payload
);
217 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
);
218 if (validate(payload
)) {
222 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: Command '${commandName}' request PDU is invalid: %j`,
225 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
227 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
),
228 'Request PDU is invalid',
230 JSON
.stringify(validate
.errors
, undefined, 2),
234 private getJsonRequestValidateFunction
<T
extends JsonType
>(commandName
: RequestCommand
) {
235 if (this.jsonValidateFunctions
.has(commandName
) === false) {
236 this.jsonValidateFunctions
.set(
238 this.ajv
.compile
<T
>(this.jsonSchemas
.get(commandName
)!).bind(this),
241 return this.jsonValidateFunctions
.get(commandName
)!;
244 private validateIncomingRequestResponsePayload
<T
extends JsonType
>(
245 chargingStation
: ChargingStation
,
246 commandName
: RequestCommand
| IncomingRequestCommand
,
249 if (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false) {
253 this.ocppResponseService
.jsonIncomingRequestResponseSchemas
.has(
254 commandName
as IncomingRequestCommand
,
258 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: No JSON schema found for command '${commandName}' PDU validation`,
262 const validate
= this.getJsonRequestResponseValidateFunction
<T
>(
263 commandName
as IncomingRequestCommand
,
265 payload
= cloneObject
<T
>(payload
);
266 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
);
267 if (validate(payload
)) {
271 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: Command '${commandName}' reponse PDU is invalid: %j`,
274 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
276 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
),
277 'Response PDU is invalid',
279 JSON
.stringify(validate
.errors
, undefined, 2),
283 private getJsonRequestResponseValidateFunction
<T
extends JsonType
>(
284 commandName
: IncomingRequestCommand
,
287 this.ocppResponseService
.jsonIncomingRequestResponseValidateFunctions
.has(commandName
) ===
290 this.ocppResponseService
.jsonIncomingRequestResponseValidateFunctions
.set(
293 .compile
<T
>(this.ocppResponseService
.jsonIncomingRequestResponseSchemas
.get(commandName
)!)
297 return this.ocppResponseService
.jsonIncomingRequestResponseValidateFunctions
.get(commandName
)!;
300 private async internalSendMessage(
301 chargingStation
: ChargingStation
,
303 messagePayload
: JsonType
| OCPPError
,
304 messageType
: MessageType
,
305 commandName
: RequestCommand
| IncomingRequestCommand
,
306 params
?: RequestParams
,
307 ): Promise
<ResponseType
> {
309 ...defaultRequestParams
,
313 (chargingStation
.inUnknownState() === true &&
314 commandName
=== RequestCommand
.BOOT_NOTIFICATION
) ||
315 (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false &&
316 chargingStation
.inUnknownState() === true) ||
317 chargingStation
.inAcceptedState() === true ||
318 (chargingStation
.inPendingState() === true &&
319 (params
.triggerMessage
=== true || messageType
=== MessageType
.CALL_RESULT_MESSAGE
))
321 // eslint-disable-next-line @typescript-eslint/no-this-alias
323 // Send a message through wsConnection
324 return promiseWithTimeout(
325 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
,
354 chargingStation
.requests
.delete(messageId
);
359 * Function that will receive the request's error response
362 * @param requestStatistic -
364 const errorCallback
= (error
: OCPPError
, requestStatistic
= true): void => {
366 requestStatistic
=== true &&
367 chargingStation
.stationInfo
?.enableStatistics
=== true
369 chargingStation
.performanceStatistics
?.addRequestStatistic(
371 MessageType
.CALL_ERROR_MESSAGE
,
375 `${chargingStation.logPrefix()} Error occurred at ${OCPPServiceUtils.getMessageTypeString(
377 )} command ${commandName} with PDU %j:`,
381 chargingStation
.requests
.delete(messageId
);
385 if (chargingStation
.stationInfo
?.enableStatistics
=== true) {
386 chargingStation
.performanceStatistics
?.addRequestStatistic(commandName
, messageType
);
388 const messageToSend
= this.buildMessageToSend(
397 let sendError
= false;
398 // Check if wsConnection opened
399 const wsOpened
= chargingStation
.isWebSocketConnectionOpened() === true;
401 const beginId
= PerformanceStatistics
.beginMeasure(commandName
);
403 chargingStation
.wsConnection
?.send(messageToSend
);
405 `${chargingStation.logPrefix()} >> Command '${commandName}' sent ${OCPPServiceUtils.getMessageTypeString(
407 )} payload: ${messageToSend}`,
411 `${chargingStation.logPrefix()} >> Command '${commandName}' failed to send ${OCPPServiceUtils.getMessageTypeString(
413 )} payload: ${messageToSend}:`,
418 PerformanceStatistics
.endMeasure(commandName
, beginId
);
420 const wsClosedOrErrored
= !wsOpened
|| sendError
=== true;
421 if (wsClosedOrErrored
&& params
?.skipBufferingOnError
=== false) {
423 chargingStation
.bufferMessage(messageToSend
);
424 // Reject and keep request in the cache
427 ErrorType
.GENERIC_ERROR
,
428 `WebSocket closed or errored for buffered message id '${messageId}' with content '${messageToSend}'`,
430 (messagePayload
as JsonObject
)?.details
?? Constants
.EMPTY_FROZEN_OBJECT
,
433 } else if (wsClosedOrErrored
) {
434 const ocppError
= new OCPPError(
435 ErrorType
.GENERIC_ERROR
,
436 `WebSocket closed or errored for non buffered message id '${messageId}' with content '${messageToSend}'`,
438 (messagePayload
as JsonObject
)?.details
?? Constants
.EMPTY_FROZEN_OBJECT
,
441 if (messageType
!== MessageType
.CALL_MESSAGE
) {
442 return reject(ocppError
);
444 // Reject and remove request from the cache
445 return errorCallback(ocppError
, false);
448 if (messageType
!== MessageType
.CALL_MESSAGE
) {
449 return resolve(messagePayload
);
452 OCPPConstants
.OCPP_WEBSOCKET_TIMEOUT
,
454 ErrorType
.GENERIC_ERROR
,
455 `Timeout for message id '${messageId}'`,
457 (messagePayload
as JsonObject
)?.details
?? Constants
.EMPTY_FROZEN_OBJECT
,
460 messageType
=== MessageType
.CALL_MESSAGE
&& chargingStation
.requests
.delete(messageId
);
465 ErrorType
.SECURITY_ERROR
,
466 `Cannot send command ${commandName} PDU when the charging station is in ${chargingStation.getRegistrationStatus()} state on the central server`,
471 private buildMessageToSend(
472 chargingStation
: ChargingStation
,
474 messagePayload
: JsonType
| OCPPError
,
475 messageType
: MessageType
,
476 commandName
: RequestCommand
| IncomingRequestCommand
,
477 responseCallback
: ResponseCallback
,
478 errorCallback
: ErrorCallback
,
480 let messageToSend
: string;
482 switch (messageType
) {
484 case MessageType
.CALL_MESSAGE
:
486 this.validateRequestPayload(chargingStation
, commandName
, messagePayload
as JsonType
);
487 chargingStation
.requests
.set(messageId
, [
491 messagePayload
as JsonType
,
493 messageToSend
= JSON
.stringify([
498 ] as OutgoingRequest
);
501 case MessageType
.CALL_RESULT_MESSAGE
:
503 this.validateIncomingRequestResponsePayload(
506 messagePayload
as JsonType
,
508 messageToSend
= JSON
.stringify([messageType
, messageId
, messagePayload
] as Response
);
511 case MessageType
.CALL_ERROR_MESSAGE
:
512 // Build Error Message
513 messageToSend
= JSON
.stringify([
516 (messagePayload
as OCPPError
)?.code
?? ErrorType
.GENERIC_ERROR
,
517 (messagePayload
as OCPPError
)?.message
?? '',
518 (messagePayload
as OCPPError
)?.details
?? { commandName
},
522 return messageToSend
;
525 // eslint-disable-next-line @typescript-eslint/no-unused-vars
526 public abstract requestHandler
<ReqType
extends JsonType
, ResType
extends JsonType
>(
527 chargingStation
: ChargingStation
,
528 commandName
: RequestCommand
,
529 // FIXME: should be ReqType
530 commandParams
?: JsonType
,
531 params
?: RequestParams
,