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';
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
;
52 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
54 keywords
: ['javaType'],
55 multipleOfPrecision
: 2,
58 this.jsonValidateFunctions
= new Map
<RequestCommand
, ValidateFunction
<JsonType
>>();
59 this.ocppResponseService
= ocppResponseService
;
60 this.requestHandler
= this.requestHandler
.bind(this) as <
61 // eslint-disable-next-line @typescript-eslint/no-unused-vars
62 ReqType
extends JsonType
,
63 ResType
extends JsonType
,
65 chargingStation
: ChargingStation
,
66 commandName
: RequestCommand
,
67 commandParams
?: JsonType
,
68 params
?: RequestParams
,
69 ) => Promise
<ResType
>;
70 this.sendMessage
= this.sendMessage
.bind(this) as (
71 chargingStation
: ChargingStation
,
73 messagePayload
: JsonType
,
74 commandName
: RequestCommand
,
75 params
?: RequestParams
,
76 ) => Promise
<ResponseType
>;
77 this.sendResponse
= this.sendResponse
.bind(this) as (
78 chargingStation
: ChargingStation
,
80 messagePayload
: JsonType
,
81 commandName
: IncomingRequestCommand
,
82 ) => Promise
<ResponseType
>;
83 this.sendError
= this.sendError
.bind(this) as (
84 chargingStation
: ChargingStation
,
87 commandName
: RequestCommand
| IncomingRequestCommand
,
88 ) => Promise
<ResponseType
>;
89 this.internalSendMessage
= this.internalSendMessage
.bind(this) as (
90 chargingStation
: ChargingStation
,
92 messagePayload
: JsonType
| OCPPError
,
93 messageType
: MessageType
,
94 commandName
: RequestCommand
| IncomingRequestCommand
,
95 params
?: RequestParams
,
96 ) => Promise
<ResponseType
>;
97 this.buildMessageToSend
= this.buildMessageToSend
.bind(this) as (
98 chargingStation
: ChargingStation
,
100 messagePayload
: JsonType
| OCPPError
,
101 messageType
: MessageType
,
102 commandName
: RequestCommand
| IncomingRequestCommand
,
104 this.validateRequestPayload
= this.validateRequestPayload
.bind(this) as <T
extends JsonType
>(
105 chargingStation
: ChargingStation
,
106 commandName
: RequestCommand
| IncomingRequestCommand
,
109 this.validateIncomingRequestResponsePayload
= this.validateIncomingRequestResponsePayload
.bind(
111 ) as <T
extends JsonType
>(
112 chargingStation
: ChargingStation
,
113 commandName
: RequestCommand
| IncomingRequestCommand
,
118 public static getInstance
<T
extends OCPPRequestService
>(
119 this: new (ocppResponseService
: OCPPResponseService
) => T
,
120 ocppResponseService
: OCPPResponseService
,
122 if (OCPPRequestService
.instance
=== null) {
123 OCPPRequestService
.instance
= new this(ocppResponseService
);
125 return OCPPRequestService
.instance
as T
;
128 public async sendResponse(
129 chargingStation
: ChargingStation
,
131 messagePayload
: JsonType
,
132 commandName
: IncomingRequestCommand
,
133 ): Promise
<ResponseType
> {
135 // Send response message
136 return await this.internalSendMessage(
140 MessageType
.CALL_RESULT_MESSAGE
,
144 handleSendMessageError(chargingStation
, commandName
, error
as Error, {
151 public async sendError(
152 chargingStation
: ChargingStation
,
154 ocppError
: OCPPError
,
155 commandName
: RequestCommand
| IncomingRequestCommand
,
156 ): Promise
<ResponseType
> {
158 // Send error message
159 return await this.internalSendMessage(
163 MessageType
.CALL_ERROR_MESSAGE
,
167 handleSendMessageError(chargingStation
, commandName
, error
as Error);
172 protected async sendMessage(
173 chargingStation
: ChargingStation
,
175 messagePayload
: JsonType
,
176 commandName
: RequestCommand
,
177 params
?: RequestParams
,
178 ): Promise
<ResponseType
> {
180 ...defaultRequestParams
,
184 return await this.internalSendMessage(
188 MessageType
.CALL_MESSAGE
,
193 handleSendMessageError(chargingStation
, commandName
, error
as Error, {
194 throwError
: params
.throwError
,
200 private validateRequestPayload
<T
extends JsonType
>(
201 chargingStation
: ChargingStation
,
202 commandName
: RequestCommand
| IncomingRequestCommand
,
205 if (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false) {
208 if (this.jsonSchemas
.has(commandName
as RequestCommand
) === false) {
210 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: No JSON schema found for command '${commandName}' PDU validation`,
214 const validate
= this.getJsonRequestValidateFunction
<T
>(commandName
as RequestCommand
);
215 payload
= cloneObject
<T
>(payload
);
216 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
);
217 if (validate(payload
)) {
221 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: Command '${commandName}' request PDU is invalid: %j`,
224 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
226 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
),
227 'Request PDU is invalid',
229 JSON
.stringify(validate
.errors
, undefined, 2),
233 private getJsonRequestValidateFunction
<T
extends JsonType
>(commandName
: RequestCommand
) {
234 if (this.jsonValidateFunctions
.has(commandName
) === false) {
235 this.jsonValidateFunctions
.set(
237 // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
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(
292 // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call
294 // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
295 .compile
<T
>(this.ocppResponseService
.jsonIncomingRequestResponseSchemas
.get(commandName
)!)
296 // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
300 return this.ocppResponseService
.jsonIncomingRequestResponseValidateFunctions
.get(commandName
)!;
303 private async internalSendMessage(
304 chargingStation
: ChargingStation
,
306 messagePayload
: JsonType
| OCPPError
,
307 messageType
: MessageType
,
308 commandName
: RequestCommand
| IncomingRequestCommand
,
309 params
?: RequestParams
,
310 ): Promise
<ResponseType
> {
312 ...defaultRequestParams
,
316 (chargingStation
.inUnknownState() === true &&
317 commandName
=== RequestCommand
.BOOT_NOTIFICATION
) ||
318 (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false &&
319 chargingStation
.inUnknownState() === true) ||
320 chargingStation
.inAcceptedState() === true ||
321 (chargingStation
.inPendingState() === true &&
322 (params
.triggerMessage
=== true || messageType
=== MessageType
.CALL_RESULT_MESSAGE
))
324 // eslint-disable-next-line @typescript-eslint/no-this-alias
326 // Send a message through wsConnection
327 return new Promise
<ResponseType
>((resolve
, reject
) => {
329 * Function that will receive the request's response
332 * @param requestPayload -
334 const responseCallback
= (payload
: JsonType
, requestPayload
: JsonType
): void => {
335 if (chargingStation
.stationInfo
?.enableStatistics
=== true) {
336 chargingStation
.performanceStatistics
?.addRequestStatistic(
338 MessageType
.CALL_RESULT_MESSAGE
,
341 // Handle the request's response
342 self.ocppResponseService
345 commandName
as RequestCommand
,
354 chargingStation
.requests
.delete(messageId
);
355 chargingStation
.emit(ChargingStationEvents
.updated
);
360 * Function that will receive the request's error response
363 * @param requestStatistic -
365 const errorCallback
= (ocppError
: OCPPError
, requestStatistic
= true): void => {
366 if (requestStatistic
=== true && chargingStation
.stationInfo
?.enableStatistics
=== true) {
367 chargingStation
.performanceStatistics
?.addRequestStatistic(
369 MessageType
.CALL_ERROR_MESSAGE
,
373 `${chargingStation.logPrefix()} Error occurred at ${OCPPServiceUtils.getMessageTypeString(
375 )} command ${commandName} with PDU %j:`,
379 chargingStation
.requests
.delete(messageId
);
380 chargingStation
.emit(ChargingStationEvents
.updated
);
384 const handleSendError
= (ocppError
: OCPPError
): void => {
385 if (params
?.skipBufferingOnError
=== false) {
387 chargingStation
.bufferMessage(messageToSend
);
388 if (messageType
=== MessageType
.CALL_MESSAGE
) {
389 this.cacheRequestPromise(
392 messagePayload
as JsonType
,
399 params
?.skipBufferingOnError
=== true &&
400 messageType
=== MessageType
.CALL_MESSAGE
402 // Remove request from the cache
403 chargingStation
.requests
.delete(messageId
);
405 return reject(ocppError
);
408 if (chargingStation
.stationInfo
?.enableStatistics
=== true) {
409 chargingStation
.performanceStatistics
?.addRequestStatistic(commandName
, messageType
);
411 const messageToSend
= this.buildMessageToSend(
418 // Check if wsConnection opened
419 if (chargingStation
.isWebSocketConnectionOpened() === true) {
420 const beginId
= PerformanceStatistics
.beginMeasure(commandName
);
421 const sendTimeout
= setTimeout(() => {
422 return handleSendError(
424 ErrorType
.GENERIC_ERROR
,
425 `Timeout ${formatDurationMilliSeconds(
426 OCPPConstants.OCPP_WEBSOCKET_TIMEOUT,
428 params?.skipBufferingOnError === false ? '' : 'non '
429 }buffered message id '${messageId}' with content '${messageToSend}'`,
431 (messagePayload
as OCPPError
).details
,
434 }, OCPPConstants
.OCPP_WEBSOCKET_TIMEOUT
);
435 chargingStation
.wsConnection
?.send(messageToSend
, (error
?: Error) => {
436 PerformanceStatistics
.endMeasure(commandName
, beginId
);
437 clearTimeout(sendTimeout
);
438 if (isNullOrUndefined(error
)) {
440 `${chargingStation.logPrefix()} >> Command '${commandName}' sent ${OCPPServiceUtils.getMessageTypeString(
442 )} payload: ${messageToSend}`,
444 if (messageType
=== MessageType
.CALL_MESSAGE
) {
445 this.cacheRequestPromise(
448 messagePayload
as JsonType
,
455 return resolve(messagePayload
);
458 return handleSendError(
460 ErrorType
.GENERIC_ERROR
,
461 `WebSocket errored for ${
462 params?.skipBufferingOnError === false ? '' : 'non '
463 }buffered message id '${messageId}' with content '${messageToSend}'`,
465 { name
: error
.name
, message
: error
.message
, stack
: error
.stack
},
471 return handleSendError(
473 ErrorType
.GENERIC_ERROR
,
474 `WebSocket closed for ${
475 params?.skipBufferingOnError === false ? '' : 'non '
476 }buffered message id '${messageId}' with content '${messageToSend}'`,
478 (messagePayload
as OCPPError
).details
,
485 ErrorType
.SECURITY_ERROR
,
486 `Cannot send command ${commandName} PDU when the charging station is in ${chargingStation?.bootNotificationResponse?.status} state on the central server`,
491 private buildMessageToSend(
492 chargingStation
: ChargingStation
,
494 messagePayload
: JsonType
| OCPPError
,
495 messageType
: MessageType
,
496 commandName
: RequestCommand
| IncomingRequestCommand
,
498 let messageToSend
: string;
500 switch (messageType
) {
502 case MessageType
.CALL_MESSAGE
:
504 this.validateRequestPayload(chargingStation
, commandName
, messagePayload
as JsonType
);
505 messageToSend
= JSON
.stringify([
510 ] as OutgoingRequest
);
513 case MessageType
.CALL_RESULT_MESSAGE
:
515 this.validateIncomingRequestResponsePayload(
518 messagePayload
as JsonType
,
520 messageToSend
= JSON
.stringify([messageType
, messageId
, messagePayload
] as Response
);
523 case MessageType
.CALL_ERROR_MESSAGE
:
524 // Build Error Message
525 messageToSend
= JSON
.stringify([
528 (messagePayload
as OCPPError
).code
,
529 (messagePayload
as OCPPError
).message
,
530 (messagePayload
as OCPPError
).details
?? {
531 command
: (messagePayload
as OCPPError
).command
?? commandName
,
536 return messageToSend
;
539 private cacheRequestPromise(
540 chargingStation
: ChargingStation
,
542 messagePayload
: JsonType
,
543 commandName
: RequestCommand
| IncomingRequestCommand
,
544 responseCallback
: ResponseCallback
,
545 errorCallback
: ErrorCallback
,
547 chargingStation
.requests
.set(messageId
, [
555 // eslint-disable-next-line @typescript-eslint/no-unused-vars
556 public abstract requestHandler
<ReqType
extends JsonType
, ResType
extends JsonType
>(
557 chargingStation
: ChargingStation
,
558 commandName
: RequestCommand
,
559 // FIXME: should be ReqType
560 commandParams
?: JsonType
,
561 params
?: RequestParams
,