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
,
26 import { Constants
, cloneObject
, handleSendMessageError
, logger
} from
'../../utils';
28 const moduleName
= 'OCPPRequestService';
30 const defaultRequestParams
: RequestParams
= {
31 skipBufferingOnError
: false,
32 triggerMessage
: false,
36 export abstract class OCPPRequestService
{
37 private static instance
: OCPPRequestService
| null = null;
38 private readonly version
: OCPPVersion
;
39 private readonly ajv
: Ajv
;
40 private readonly ocppResponseService
: OCPPResponseService
;
41 private readonly jsonValidateFunctions
: Map
<RequestCommand
, ValidateFunction
<JsonType
>>;
42 protected abstract jsonSchemas
: Map
<RequestCommand
, JSONSchemaType
<JsonType
>>;
44 protected constructor(version
: OCPPVersion
, ocppResponseService
: OCPPResponseService
) {
45 this.version
= version
;
47 keywords
: ['javaType'],
48 multipleOfPrecision
: 2,
51 this.jsonValidateFunctions
= new Map
<RequestCommand
, ValidateFunction
<JsonType
>>();
52 this.ocppResponseService
= ocppResponseService
;
53 this.requestHandler
= this.requestHandler
.bind(this) as <
54 // eslint-disable-next-line @typescript-eslint/no-unused-vars
55 ReqType
extends JsonType
,
56 ResType
extends JsonType
,
58 chargingStation
: ChargingStation
,
59 commandName
: RequestCommand
,
60 commandParams
?: JsonType
,
61 params
?: RequestParams
,
62 ) => Promise
<ResType
>;
63 this.sendMessage
= this.sendMessage
.bind(this) as (
64 chargingStation
: ChargingStation
,
66 messagePayload
: JsonType
,
67 commandName
: RequestCommand
,
68 params
?: RequestParams
,
69 ) => Promise
<ResponseType
>;
70 this.sendResponse
= this.sendResponse
.bind(this) as (
71 chargingStation
: ChargingStation
,
73 messagePayload
: JsonType
,
74 commandName
: IncomingRequestCommand
,
75 ) => Promise
<ResponseType
>;
76 this.sendError
= this.sendError
.bind(this) as (
77 chargingStation
: ChargingStation
,
80 commandName
: RequestCommand
| IncomingRequestCommand
,
81 ) => Promise
<ResponseType
>;
82 this.internalSendMessage
= this.internalSendMessage
.bind(this) as (
83 chargingStation
: ChargingStation
,
85 messagePayload
: JsonType
| OCPPError
,
86 messageType
: MessageType
,
87 commandName
: RequestCommand
| IncomingRequestCommand
,
88 params
?: RequestParams
,
89 ) => Promise
<ResponseType
>;
90 this.buildMessageToSend
= this.buildMessageToSend
.bind(this) as (
91 chargingStation
: ChargingStation
,
93 messagePayload
: JsonType
| OCPPError
,
94 messageType
: MessageType
,
95 commandName
: RequestCommand
| IncomingRequestCommand
,
97 this.validateRequestPayload
= this.validateRequestPayload
.bind(this) as <T
extends JsonType
>(
98 chargingStation
: ChargingStation
,
99 commandName
: RequestCommand
| IncomingRequestCommand
,
102 this.validateIncomingRequestResponsePayload
= this.validateIncomingRequestResponsePayload
.bind(
104 ) as <T
extends JsonType
>(
105 chargingStation
: ChargingStation
,
106 commandName
: RequestCommand
| IncomingRequestCommand
,
111 public static getInstance
<T
extends OCPPRequestService
>(
112 this: new (ocppResponseService
: OCPPResponseService
) => T
,
113 ocppResponseService
: OCPPResponseService
,
115 if (OCPPRequestService
.instance
=== null) {
116 OCPPRequestService
.instance
= new this(ocppResponseService
);
118 return OCPPRequestService
.instance
as T
;
121 public async sendResponse(
122 chargingStation
: ChargingStation
,
124 messagePayload
: JsonType
,
125 commandName
: IncomingRequestCommand
,
126 ): Promise
<ResponseType
> {
128 // Send response message
129 return await this.internalSendMessage(
133 MessageType
.CALL_RESULT_MESSAGE
,
137 handleSendMessageError(chargingStation
, commandName
, error
as Error, {
144 public async sendError(
145 chargingStation
: ChargingStation
,
147 ocppError
: OCPPError
,
148 commandName
: RequestCommand
| IncomingRequestCommand
,
149 ): Promise
<ResponseType
> {
151 // Send error message
152 return await this.internalSendMessage(
156 MessageType
.CALL_ERROR_MESSAGE
,
160 handleSendMessageError(chargingStation
, commandName
, error
as Error);
165 protected async sendMessage(
166 chargingStation
: ChargingStation
,
168 messagePayload
: JsonType
,
169 commandName
: RequestCommand
,
170 params
?: RequestParams
,
171 ): Promise
<ResponseType
> {
173 ...defaultRequestParams
,
177 return await this.internalSendMessage(
181 MessageType
.CALL_MESSAGE
,
186 handleSendMessageError(chargingStation
, commandName
, error
as Error, {
187 throwError
: params
.throwError
,
193 private validateRequestPayload
<T
extends JsonType
>(
194 chargingStation
: ChargingStation
,
195 commandName
: RequestCommand
| IncomingRequestCommand
,
198 if (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false) {
201 if (this.jsonSchemas
.has(commandName
as RequestCommand
) === false) {
203 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: No JSON schema found for command '${commandName}' PDU validation`,
207 const validate
= this.getJsonRequestValidateFunction
<T
>(commandName
as RequestCommand
);
208 payload
= cloneObject
<T
>(payload
);
209 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
);
210 if (validate(payload
)) {
214 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: Command '${commandName}' request PDU is invalid: %j`,
217 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
219 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
),
220 'Request PDU is invalid',
222 JSON
.stringify(validate
.errors
, undefined, 2),
226 private getJsonRequestValidateFunction
<T
extends JsonType
>(commandName
: RequestCommand
) {
227 if (this.jsonValidateFunctions
.has(commandName
) === false) {
228 this.jsonValidateFunctions
.set(
230 this.ajv
.compile
<T
>(this.jsonSchemas
.get(commandName
)!).bind(this),
233 return this.jsonValidateFunctions
.get(commandName
)!;
236 private validateIncomingRequestResponsePayload
<T
extends JsonType
>(
237 chargingStation
: ChargingStation
,
238 commandName
: RequestCommand
| IncomingRequestCommand
,
241 if (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false) {
245 this.ocppResponseService
.jsonIncomingRequestResponseSchemas
.has(
246 commandName
as IncomingRequestCommand
,
250 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: No JSON schema found for command '${commandName}' PDU validation`,
254 const validate
= this.getJsonRequestResponseValidateFunction
<T
>(
255 commandName
as IncomingRequestCommand
,
257 payload
= cloneObject
<T
>(payload
);
258 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
);
259 if (validate(payload
)) {
263 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: Command '${commandName}' reponse PDU is invalid: %j`,
266 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
268 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
),
269 'Response PDU is invalid',
271 JSON
.stringify(validate
.errors
, undefined, 2),
275 private getJsonRequestResponseValidateFunction
<T
extends JsonType
>(
276 commandName
: IncomingRequestCommand
,
279 this.ocppResponseService
.jsonIncomingRequestResponseValidateFunctions
.has(commandName
) ===
282 this.ocppResponseService
.jsonIncomingRequestResponseValidateFunctions
.set(
285 .compile
<T
>(this.ocppResponseService
.jsonIncomingRequestResponseSchemas
.get(commandName
)!)
289 return this.ocppResponseService
.jsonIncomingRequestResponseValidateFunctions
.get(commandName
)!;
292 private async internalSendMessage(
293 chargingStation
: ChargingStation
,
295 messagePayload
: JsonType
| OCPPError
,
296 messageType
: MessageType
,
297 commandName
: RequestCommand
| IncomingRequestCommand
,
298 params
?: RequestParams
,
299 ): Promise
<ResponseType
> {
301 ...defaultRequestParams
,
305 (chargingStation
.inUnknownState() === true &&
306 commandName
=== RequestCommand
.BOOT_NOTIFICATION
) ||
307 (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false &&
308 chargingStation
.inUnknownState() === true) ||
309 chargingStation
.inAcceptedState() === true ||
310 (chargingStation
.inPendingState() === true &&
311 (params
.triggerMessage
=== true || messageType
=== MessageType
.CALL_RESULT_MESSAGE
))
313 // eslint-disable-next-line @typescript-eslint/no-this-alias
315 // Send a message through wsConnection
316 return await new Promise
<ResponseType
>((resolve
, reject
) => {
318 * Function that will receive the request's response
321 * @param requestPayload -
323 const responseCallback
= (payload
: JsonType
, requestPayload
: JsonType
): void => {
324 if (chargingStation
.stationInfo
?.enableStatistics
=== true) {
325 chargingStation
.performanceStatistics
?.addRequestStatistic(
327 MessageType
.CALL_RESULT_MESSAGE
,
330 // Handle the request's response
331 self.ocppResponseService
334 commandName
as RequestCommand
,
345 chargingStation
.requests
.delete(messageId
);
350 * Function that will receive the request's error response
353 * @param requestStatistic -
355 const errorCallback
= (error
: OCPPError
, requestStatistic
= true): void => {
356 if (requestStatistic
=== true && chargingStation
.stationInfo
?.enableStatistics
=== true) {
357 chargingStation
.performanceStatistics
?.addRequestStatistic(
359 MessageType
.CALL_ERROR_MESSAGE
,
363 `${chargingStation.logPrefix()} Error occurred at ${OCPPServiceUtils.getMessageTypeString(
365 )} command ${commandName} with PDU %j:`,
369 chargingStation
.requests
.delete(messageId
);
373 if (chargingStation
.stationInfo
?.enableStatistics
=== true) {
374 chargingStation
.performanceStatistics
?.addRequestStatistic(commandName
, messageType
);
376 const messageToSend
= this.buildMessageToSend(
383 // Check if wsConnection opened
384 if (chargingStation
.isWebSocketConnectionOpened() === true) {
385 const beginId
= PerformanceStatistics
.beginMeasure(commandName
);
386 const sendTimeout
= setTimeout(() => {
387 return errorCallback(
389 ErrorType
.GENERIC_ERROR
,
390 `Timeout for message id '${messageId}'`,
392 (messagePayload
as JsonObject
)?.details
?? Constants
.EMPTY_FROZEN_OBJECT
,
396 }, OCPPConstants
.OCPP_WEBSOCKET_TIMEOUT
);
397 chargingStation
.wsConnection
?.send(messageToSend
, (error
?: Error) => {
398 clearTimeout(sendTimeout
);
400 const ocppError
= new OCPPError(
401 ErrorType
.GENERIC_ERROR
,
402 `WebSocket errored for ${
403 params?.skipBufferingOnError === false ? '' : 'non '
404 }buffered message id '${messageId}' with content '${messageToSend}'`,
406 { name
: error
.name
, message
: error
.message
, stack
: error
.stack
},
408 if (params
?.skipBufferingOnError
=== false) {
410 chargingStation
.bufferMessage(messageToSend
);
411 // Reject and keep request in the cache
412 return reject(ocppError
);
415 if (messageType
!== MessageType
.CALL_MESSAGE
) {
416 return reject(ocppError
);
418 // Reject and remove request from the cache
419 return errorCallback(ocppError
, false);
422 if (messageType
=== MessageType
.CALL_MESSAGE
) {
423 this.cacheRequestPromise(
426 messagePayload
as JsonType
,
433 `${chargingStation.logPrefix()} >> Command '${commandName}' sent ${OCPPServiceUtils.getMessageTypeString(
435 )} payload: ${messageToSend}`,
437 PerformanceStatistics
.endMeasure(commandName
, beginId
);
439 if (params
?.skipBufferingOnError
=== false) {
441 chargingStation
.bufferMessage(messageToSend
);
442 if (messageType
=== MessageType
.CALL_MESSAGE
) {
443 this.cacheRequestPromise(
446 messagePayload
as JsonType
,
453 // Reject and keep request in the cache
456 ErrorType
.GENERIC_ERROR
,
457 `WebSocket closed for ${
458 params?.skipBufferingOnError === false ? '' : 'non '
459 }buffered message id '${messageId}' with content '${messageToSend}'`,
461 (messagePayload
as JsonObject
)?.details
?? Constants
.EMPTY_FROZEN_OBJECT
,
466 if (messageType
!== MessageType
.CALL_MESSAGE
) {
467 return resolve(messagePayload
);
472 ErrorType
.SECURITY_ERROR
,
473 `Cannot send command ${commandName} PDU when the charging station is in ${chargingStation.getRegistrationStatus()} state on the central server`,
478 private buildMessageToSend(
479 chargingStation
: ChargingStation
,
481 messagePayload
: JsonType
| OCPPError
,
482 messageType
: MessageType
,
483 commandName
: RequestCommand
| IncomingRequestCommand
,
485 let messageToSend
: string;
487 switch (messageType
) {
489 case MessageType
.CALL_MESSAGE
:
491 this.validateRequestPayload(chargingStation
, commandName
, messagePayload
as JsonType
);
492 messageToSend
= JSON
.stringify([
497 ] as OutgoingRequest
);
500 case MessageType
.CALL_RESULT_MESSAGE
:
502 this.validateIncomingRequestResponsePayload(
505 messagePayload
as JsonType
,
507 messageToSend
= JSON
.stringify([messageType
, messageId
, messagePayload
] as Response
);
510 case MessageType
.CALL_ERROR_MESSAGE
:
511 // Build Error Message
512 messageToSend
= JSON
.stringify([
515 (messagePayload
as OCPPError
)?.code
?? ErrorType
.GENERIC_ERROR
,
516 (messagePayload
as OCPPError
)?.message
?? '',
517 (messagePayload
as OCPPError
)?.details
?? { commandName
},
521 return messageToSend
;
524 private cacheRequestPromise(
525 chargingStation
: ChargingStation
,
527 messagePayload
: JsonType
,
528 commandName
: RequestCommand
| IncomingRequestCommand
,
529 responseCallback
: ResponseCallback
,
530 errorCallback
: ErrorCallback
,
532 chargingStation
.requests
.set(messageId
, [
540 // eslint-disable-next-line @typescript-eslint/no-unused-vars
541 public abstract requestHandler
<ReqType
extends JsonType
, ResType
extends JsonType
>(
542 chargingStation
: ChargingStation
,
543 commandName
: RequestCommand
,
544 // FIXME: should be ReqType
545 commandParams
?: JsonType
,
546 params
?: RequestParams
,