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
,
96 responseCallback
: ResponseCallback
,
97 errorCallback
: ErrorCallback
,
99 this.validateRequestPayload
= this.validateRequestPayload
.bind(this) as <T
extends JsonType
>(
100 chargingStation
: ChargingStation
,
101 commandName
: RequestCommand
| IncomingRequestCommand
,
104 this.validateIncomingRequestResponsePayload
= this.validateIncomingRequestResponsePayload
.bind(
106 ) as <T
extends JsonType
>(
107 chargingStation
: ChargingStation
,
108 commandName
: RequestCommand
| IncomingRequestCommand
,
113 public static getInstance
<T
extends OCPPRequestService
>(
114 this: new (ocppResponseService
: OCPPResponseService
) => T
,
115 ocppResponseService
: OCPPResponseService
,
117 if (OCPPRequestService
.instance
=== null) {
118 OCPPRequestService
.instance
= new this(ocppResponseService
);
120 return OCPPRequestService
.instance
as T
;
123 public async sendResponse(
124 chargingStation
: ChargingStation
,
126 messagePayload
: JsonType
,
127 commandName
: IncomingRequestCommand
,
128 ): Promise
<ResponseType
> {
130 // Send response message
131 return await this.internalSendMessage(
135 MessageType
.CALL_RESULT_MESSAGE
,
139 handleSendMessageError(chargingStation
, commandName
, error
as Error, {
146 public async sendError(
147 chargingStation
: ChargingStation
,
149 ocppError
: OCPPError
,
150 commandName
: RequestCommand
| IncomingRequestCommand
,
151 ): Promise
<ResponseType
> {
153 // Send error message
154 return await this.internalSendMessage(
158 MessageType
.CALL_ERROR_MESSAGE
,
162 handleSendMessageError(chargingStation
, commandName
, error
as Error);
167 protected async sendMessage(
168 chargingStation
: ChargingStation
,
170 messagePayload
: JsonType
,
171 commandName
: RequestCommand
,
172 params
?: RequestParams
,
173 ): Promise
<ResponseType
> {
175 ...defaultRequestParams
,
179 return await this.internalSendMessage(
183 MessageType
.CALL_MESSAGE
,
188 handleSendMessageError(chargingStation
, commandName
, error
as Error, {
189 throwError
: params
.throwError
,
195 private validateRequestPayload
<T
extends JsonType
>(
196 chargingStation
: ChargingStation
,
197 commandName
: RequestCommand
| IncomingRequestCommand
,
200 if (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false) {
203 if (this.jsonSchemas
.has(commandName
as RequestCommand
) === false) {
205 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: No JSON schema found for command '${commandName}' PDU validation`,
209 const validate
= this.getJsonRequestValidateFunction
<T
>(commandName
as RequestCommand
);
210 payload
= cloneObject
<T
>(payload
);
211 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
);
212 if (validate(payload
)) {
216 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: Command '${commandName}' request PDU is invalid: %j`,
219 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
221 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
),
222 'Request PDU is invalid',
224 JSON
.stringify(validate
.errors
, undefined, 2),
228 private getJsonRequestValidateFunction
<T
extends JsonType
>(commandName
: RequestCommand
) {
229 if (this.jsonValidateFunctions
.has(commandName
) === false) {
230 this.jsonValidateFunctions
.set(
232 this.ajv
.compile
<T
>(this.jsonSchemas
.get(commandName
)!).bind(this),
235 return this.jsonValidateFunctions
.get(commandName
)!;
238 private validateIncomingRequestResponsePayload
<T
extends JsonType
>(
239 chargingStation
: ChargingStation
,
240 commandName
: RequestCommand
| IncomingRequestCommand
,
243 if (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false) {
247 this.ocppResponseService
.jsonIncomingRequestResponseSchemas
.has(
248 commandName
as IncomingRequestCommand
,
252 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: No JSON schema found for command '${commandName}' PDU validation`,
256 const validate
= this.getJsonRequestResponseValidateFunction
<T
>(
257 commandName
as IncomingRequestCommand
,
259 payload
= cloneObject
<T
>(payload
);
260 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
);
261 if (validate(payload
)) {
265 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: Command '${commandName}' reponse PDU is invalid: %j`,
268 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
270 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
),
271 'Response PDU is invalid',
273 JSON
.stringify(validate
.errors
, undefined, 2),
277 private getJsonRequestResponseValidateFunction
<T
extends JsonType
>(
278 commandName
: IncomingRequestCommand
,
281 this.ocppResponseService
.jsonIncomingRequestResponseValidateFunctions
.has(commandName
) ===
284 this.ocppResponseService
.jsonIncomingRequestResponseValidateFunctions
.set(
287 .compile
<T
>(this.ocppResponseService
.jsonIncomingRequestResponseSchemas
.get(commandName
)!)
291 return this.ocppResponseService
.jsonIncomingRequestResponseValidateFunctions
.get(commandName
)!;
294 private async internalSendMessage(
295 chargingStation
: ChargingStation
,
297 messagePayload
: JsonType
| OCPPError
,
298 messageType
: MessageType
,
299 commandName
: RequestCommand
| IncomingRequestCommand
,
300 params
?: RequestParams
,
301 ): Promise
<ResponseType
> {
303 ...defaultRequestParams
,
307 (chargingStation
.inUnknownState() === true &&
308 commandName
=== RequestCommand
.BOOT_NOTIFICATION
) ||
309 (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false &&
310 chargingStation
.inUnknownState() === true) ||
311 chargingStation
.inAcceptedState() === true ||
312 (chargingStation
.inPendingState() === true &&
313 (params
.triggerMessage
=== true || messageType
=== MessageType
.CALL_RESULT_MESSAGE
))
315 // eslint-disable-next-line @typescript-eslint/no-this-alias
317 // Send a message through wsConnection
318 return await new Promise
<ResponseType
>((resolve
, reject
) => {
320 * Function that will receive the request's response
323 * @param requestPayload -
325 const responseCallback
= (payload
: JsonType
, requestPayload
: JsonType
): void => {
326 if (chargingStation
.stationInfo
?.enableStatistics
=== true) {
327 chargingStation
.performanceStatistics
?.addRequestStatistic(
329 MessageType
.CALL_RESULT_MESSAGE
,
332 // Handle the request's response
333 self.ocppResponseService
336 commandName
as RequestCommand
,
347 chargingStation
.requests
.delete(messageId
);
352 * Function that will receive the request's error response
355 * @param requestStatistic -
357 const errorCallback
= (error
: OCPPError
, requestStatistic
= true): void => {
358 if (requestStatistic
=== true && chargingStation
.stationInfo
?.enableStatistics
=== true) {
359 chargingStation
.performanceStatistics
?.addRequestStatistic(
361 MessageType
.CALL_ERROR_MESSAGE
,
365 `${chargingStation.logPrefix()} Error occurred at ${OCPPServiceUtils.getMessageTypeString(
367 )} command ${commandName} with PDU %j:`,
371 chargingStation
.requests
.delete(messageId
);
375 if (chargingStation
.stationInfo
?.enableStatistics
=== true) {
376 chargingStation
.performanceStatistics
?.addRequestStatistic(commandName
, messageType
);
378 const messageToSend
= this.buildMessageToSend(
387 // Check if wsConnection opened
388 if (chargingStation
.isWebSocketConnectionOpened() === true) {
389 const beginId
= PerformanceStatistics
.beginMeasure(commandName
);
390 const sendTimeout
= setTimeout(() => {
391 return errorCallback(
393 ErrorType
.GENERIC_ERROR
,
394 `Timeout for message id '${messageId}'`,
396 (messagePayload
as JsonObject
)?.details
?? Constants
.EMPTY_FROZEN_OBJECT
,
400 }, OCPPConstants
.OCPP_WEBSOCKET_TIMEOUT
);
401 chargingStation
.wsConnection
?.send(messageToSend
, (error
?: Error) => {
402 if (error
&& params
?.skipBufferingOnError
=== false) {
404 chargingStation
.bufferMessage(messageToSend
);
405 // Reject and keep request in the cache
408 ErrorType
.GENERIC_ERROR
,
409 `WebSocket errored for buffered message id '${messageId}' with content '${messageToSend}'`,
411 { name
: error
.name
, message
: error
.message
, stack
: error
.stack
} ??
412 Constants
.EMPTY_FROZEN_OBJECT
,
416 const ocppError
= new OCPPError(
417 ErrorType
.GENERIC_ERROR
,
418 `WebSocket errored for non buffered message id '${messageId}' with content '${messageToSend}'`,
420 { name
: error
.name
, message
: error
.message
, stack
: error
.stack
} ??
421 Constants
.EMPTY_FROZEN_OBJECT
,
424 if (messageType
!== MessageType
.CALL_MESSAGE
) {
425 return reject(ocppError
);
427 // Reject and remove request from the cache
428 return errorCallback(ocppError
, false);
430 clearTimeout(sendTimeout
);
433 `${chargingStation.logPrefix()} >> Command '${commandName}' sent ${OCPPServiceUtils.getMessageTypeString(
435 )} payload: ${messageToSend}`,
437 PerformanceStatistics
.endMeasure(commandName
, beginId
);
438 } else if (params
?.skipBufferingOnError
=== false) {
440 chargingStation
.bufferMessage(messageToSend
);
441 // Reject and keep request in the cache
444 ErrorType
.GENERIC_ERROR
,
445 `WebSocket closed for buffered message id '${messageId}' with content '${messageToSend}'`,
447 (messagePayload
as JsonObject
)?.details
?? Constants
.EMPTY_FROZEN_OBJECT
,
451 const ocppError
= new OCPPError(
452 ErrorType
.GENERIC_ERROR
,
453 `WebSocket closed for non buffered message id '${messageId}' with content '${messageToSend}'`,
455 (messagePayload
as JsonObject
)?.details
?? Constants
.EMPTY_FROZEN_OBJECT
,
458 if (messageType
!== MessageType
.CALL_MESSAGE
) {
459 return reject(ocppError
);
461 // Reject and remove request from the cache
462 return errorCallback(ocppError
, false);
465 if (messageType
!== MessageType
.CALL_MESSAGE
) {
466 return resolve(messagePayload
);
471 ErrorType
.SECURITY_ERROR
,
472 `Cannot send command ${commandName} PDU when the charging station is in ${chargingStation.getRegistrationStatus()} state on the central server`,
477 private buildMessageToSend(
478 chargingStation
: ChargingStation
,
480 messagePayload
: JsonType
| OCPPError
,
481 messageType
: MessageType
,
482 commandName
: RequestCommand
| IncomingRequestCommand
,
483 responseCallback
: ResponseCallback
,
484 errorCallback
: ErrorCallback
,
486 let messageToSend
: string;
488 switch (messageType
) {
490 case MessageType
.CALL_MESSAGE
:
492 this.validateRequestPayload(chargingStation
, commandName
, messagePayload
as JsonType
);
493 chargingStation
.requests
.set(messageId
, [
497 messagePayload
as JsonType
,
499 messageToSend
= JSON
.stringify([
504 ] as OutgoingRequest
);
507 case MessageType
.CALL_RESULT_MESSAGE
:
509 this.validateIncomingRequestResponsePayload(
512 messagePayload
as JsonType
,
514 messageToSend
= JSON
.stringify([messageType
, messageId
, messagePayload
] as Response
);
517 case MessageType
.CALL_ERROR_MESSAGE
:
518 // Build Error Message
519 messageToSend
= JSON
.stringify([
522 (messagePayload
as OCPPError
)?.code
?? ErrorType
.GENERIC_ERROR
,
523 (messagePayload
as OCPPError
)?.message
?? '',
524 (messagePayload
as OCPPError
)?.details
?? { commandName
},
528 return messageToSend
;
531 // eslint-disable-next-line @typescript-eslint/no-unused-vars
532 public abstract requestHandler
<ReqType
extends JsonType
, ResType
extends JsonType
>(
533 chargingStation
: ChargingStation
,
534 commandName
: RequestCommand
,
535 // FIXME: should be ReqType
536 commandParams
?: JsonType
,
537 params
?: RequestParams
,