1 import Ajv
, { type JSONSchemaType
} 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
, ErrorUtils
, Utils
, logger
} from
'../../utils';
28 const moduleName
= 'OCPPRequestService';
30 export abstract class OCPPRequestService
{
31 private static instance
: OCPPRequestService
| null = null;
32 private readonly version
: OCPPVersion
;
33 private readonly ajv
: Ajv
;
34 private readonly ocppResponseService
: OCPPResponseService
;
35 protected abstract jsonSchemas
: Map
<RequestCommand
, JSONSchemaType
<JsonObject
>>;
37 protected constructor(version
: OCPPVersion
, ocppResponseService
: OCPPResponseService
) {
38 this.version
= version
;
40 keywords
: ['javaType'],
41 multipleOfPrecision
: 2,
44 this.ocppResponseService
= ocppResponseService
;
45 this.requestHandler
= this.requestHandler
.bind(this) as <
46 ReqType
extends JsonType
,
47 ResType
extends JsonType
49 chargingStation
: ChargingStation
,
50 commandName
: RequestCommand
,
51 commandParams
?: JsonType
,
52 params
?: RequestParams
53 ) => Promise
<ResType
>;
54 this.sendMessage
= this.sendMessage
.bind(this) as (
55 chargingStation
: ChargingStation
,
57 messagePayload
: JsonType
,
58 commandName
: RequestCommand
,
59 params
?: RequestParams
60 ) => Promise
<ResponseType
>;
61 this.sendResponse
= this.sendResponse
.bind(this) as (
62 chargingStation
: ChargingStation
,
64 messagePayload
: JsonType
,
65 commandName
: IncomingRequestCommand
66 ) => Promise
<ResponseType
>;
67 this.sendError
= this.sendError
.bind(this) as (
68 chargingStation
: ChargingStation
,
71 commandName
: RequestCommand
| IncomingRequestCommand
72 ) => Promise
<ResponseType
>;
73 this.internalSendMessage
= this.internalSendMessage
.bind(this) as (
74 chargingStation
: ChargingStation
,
76 messagePayload
: JsonType
| OCPPError
,
77 messageType
: MessageType
,
78 commandName
: RequestCommand
| IncomingRequestCommand
,
79 params
?: RequestParams
80 ) => Promise
<ResponseType
>;
81 this.buildMessageToSend
= this.buildMessageToSend
.bind(this) as (
82 chargingStation
: ChargingStation
,
84 messagePayload
: JsonType
| OCPPError
,
85 messageType
: MessageType
,
86 commandName
: RequestCommand
| IncomingRequestCommand
,
87 responseCallback
: ResponseCallback
,
88 errorCallback
: ErrorCallback
90 this.validateRequestPayload
= this.validateRequestPayload
.bind(this) as <T
extends JsonObject
>(
91 chargingStation
: ChargingStation
,
92 commandName
: RequestCommand
| IncomingRequestCommand
,
95 this.validateIncomingRequestResponsePayload
= this.validateIncomingRequestResponsePayload
.bind(
97 ) as <T
extends JsonObject
>(
98 chargingStation
: ChargingStation
,
99 commandName
: RequestCommand
| IncomingRequestCommand
,
104 public static getInstance
<T
extends OCPPRequestService
>(
105 this: new (ocppResponseService
: OCPPResponseService
) => T
,
106 ocppResponseService
: OCPPResponseService
108 if (OCPPRequestService
.instance
=== null) {
109 OCPPRequestService
.instance
= new this(ocppResponseService
);
111 return OCPPRequestService
.instance
as T
;
114 public async sendResponse(
115 chargingStation
: ChargingStation
,
117 messagePayload
: JsonType
,
118 commandName
: IncomingRequestCommand
119 ): Promise
<ResponseType
> {
121 // Send response message
122 return await this.internalSendMessage(
126 MessageType
.CALL_RESULT_MESSAGE
,
130 ErrorUtils
.handleSendMessageError(chargingStation
, commandName
, error
as Error, {
136 public async sendError(
137 chargingStation
: ChargingStation
,
139 ocppError
: OCPPError
,
140 commandName
: RequestCommand
| IncomingRequestCommand
141 ): Promise
<ResponseType
> {
143 // Send error message
144 return await this.internalSendMessage(
148 MessageType
.CALL_ERROR_MESSAGE
,
152 ErrorUtils
.handleSendMessageError(chargingStation
, commandName
, error
as Error);
156 protected async sendMessage(
157 chargingStation
: ChargingStation
,
159 messagePayload
: JsonType
,
160 commandName
: RequestCommand
,
161 params
: RequestParams
= {
162 skipBufferingOnError
: false,
163 triggerMessage
: false,
166 ): Promise
<ResponseType
> {
168 ...{ skipBufferingOnError
: false, triggerMessage
: false, throwError
: false },
172 return await this.internalSendMessage(
176 MessageType
.CALL_MESSAGE
,
181 ErrorUtils
.handleSendMessageError(chargingStation
, commandName
, error
as Error, {
182 throwError
: params
.throwError
,
187 private validateRequestPayload
<T
extends JsonObject
>(
188 chargingStation
: ChargingStation
,
189 commandName
: RequestCommand
| IncomingRequestCommand
,
192 if (chargingStation
.getPayloadSchemaValidation() === false) {
195 if (this.jsonSchemas
.has(commandName
as RequestCommand
) === false) {
197 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: No JSON schema found for command '${commandName}' PDU validation`
201 const validate
= this.ajv
.compile(this.jsonSchemas
.get(commandName
as RequestCommand
));
202 payload
= Utils
.cloneObject
<T
>(payload
);
203 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
);
204 if (validate(payload
)) {
208 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: Command '${commandName}' request PDU is invalid: %j`,
211 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
213 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
),
214 'Request PDU is invalid',
216 JSON
.stringify(validate
.errors
, null, 2)
220 private validateIncomingRequestResponsePayload
<T
extends JsonObject
>(
221 chargingStation
: ChargingStation
,
222 commandName
: RequestCommand
| IncomingRequestCommand
,
225 if (chargingStation
.getPayloadSchemaValidation() === false) {
229 this.ocppResponseService
.jsonIncomingRequestResponseSchemas
.has(
230 commandName
as IncomingRequestCommand
234 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: No JSON schema found for command '${commandName}' PDU validation`
238 const validate
= this.ajv
.compile(
239 this.ocppResponseService
.jsonIncomingRequestResponseSchemas
.get(
240 commandName
as IncomingRequestCommand
243 payload
= Utils
.cloneObject
<T
>(payload
);
244 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
);
245 if (validate(payload
)) {
249 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: Command '${commandName}' reponse PDU is invalid: %j`,
252 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
254 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
),
255 'Response PDU is invalid',
257 JSON
.stringify(validate
.errors
, null, 2)
261 private async internalSendMessage(
262 chargingStation
: ChargingStation
,
264 messagePayload
: JsonType
| OCPPError
,
265 messageType
: MessageType
,
266 commandName
: RequestCommand
| IncomingRequestCommand
,
267 params
: RequestParams
= {
268 skipBufferingOnError
: false,
269 triggerMessage
: false,
272 ): Promise
<ResponseType
> {
274 ...{ skipBufferingOnError
: false, triggerMessage
: false, throwError
: false },
278 (chargingStation
.inUnknownState() === true &&
279 commandName
=== RequestCommand
.BOOT_NOTIFICATION
) ||
280 (chargingStation
.getOcppStrictCompliance() === false &&
281 chargingStation
.inUnknownState() === true) ||
282 chargingStation
.inAcceptedState() === true ||
283 (chargingStation
.inPendingState() === true &&
284 (params
.triggerMessage
=== true || messageType
=== MessageType
.CALL_RESULT_MESSAGE
))
286 // eslint-disable-next-line @typescript-eslint/no-this-alias
288 // Send a message through wsConnection
289 return Utils
.promiseWithTimeout(
290 new Promise((resolve
, reject
) => {
292 * Function that will receive the request's response
295 * @param requestPayload -
297 const responseCallback
= (payload
: JsonType
, requestPayload
: JsonType
): void => {
298 if (chargingStation
.getEnableStatistics() === true) {
299 chargingStation
.performanceStatistics
?.addRequestStatistic(
301 MessageType
.CALL_RESULT_MESSAGE
304 // Handle the request's response
305 self.ocppResponseService
308 commandName
as RequestCommand
,
319 chargingStation
.requests
.delete(messageId
);
324 * Function that will receive the request's error response
327 * @param requestStatistic -
329 const errorCallback
= (error
: OCPPError
, requestStatistic
= true): void => {
330 if (requestStatistic
=== true && chargingStation
.getEnableStatistics() === true) {
331 chargingStation
.performanceStatistics
?.addRequestStatistic(
333 MessageType
.CALL_ERROR_MESSAGE
337 `${chargingStation.logPrefix()} Error occurred at ${OCPPServiceUtils.getMessageTypeString(
339 )} command ${commandName} with PDU %j:`,
343 chargingStation
.requests
.delete(messageId
);
347 if (chargingStation
.getEnableStatistics() === true) {
348 chargingStation
.performanceStatistics
?.addRequestStatistic(commandName
, messageType
);
350 const messageToSend
= this.buildMessageToSend(
359 let sendError
= false;
360 // Check if wsConnection opened
361 const wsOpened
= chargingStation
.isWebSocketConnectionOpened() === true;
363 const beginId
= PerformanceStatistics
.beginMeasure(commandName
);
365 chargingStation
.wsConnection
?.send(messageToSend
);
367 `${chargingStation.logPrefix()} >> Command '${commandName}' sent ${OCPPServiceUtils.getMessageTypeString(
369 )} payload: ${messageToSend}`
373 `${chargingStation.logPrefix()} >> Command '${commandName}' failed to send ${OCPPServiceUtils.getMessageTypeString(
375 )} payload: ${messageToSend}:`,
380 PerformanceStatistics
.endMeasure(commandName
, beginId
);
382 const wsClosedOrErrored
= !wsOpened
|| sendError
=== true;
383 if (wsClosedOrErrored
&& params
.skipBufferingOnError
=== false) {
385 chargingStation
.bufferMessage(messageToSend
);
386 // Reject and keep request in the cache
389 ErrorType
.GENERIC_ERROR
,
390 `WebSocket closed or errored for buffered message id '${messageId}' with content '${messageToSend}'`,
392 (messagePayload
as JsonObject
)?.details
?? Constants
.EMPTY_FREEZED_OBJECT
395 } else if (wsClosedOrErrored
) {
396 const ocppError
= new OCPPError(
397 ErrorType
.GENERIC_ERROR
,
398 `WebSocket closed or errored for non buffered message id '${messageId}' with content '${messageToSend}'`,
400 (messagePayload
as JsonObject
)?.details
?? Constants
.EMPTY_FREEZED_OBJECT
403 if (messageType
!== MessageType
.CALL_MESSAGE
) {
404 return reject(ocppError
);
406 // Reject and remove request from the cache
407 return errorCallback(ocppError
, false);
410 if (messageType
!== MessageType
.CALL_MESSAGE
) {
411 return resolve(messagePayload
);
414 OCPPConstants
.OCPP_WEBSOCKET_TIMEOUT
,
416 ErrorType
.GENERIC_ERROR
,
417 `Timeout for message id '${messageId}'`,
419 (messagePayload
as JsonObject
)?.details
?? Constants
.EMPTY_FREEZED_OBJECT
422 messageType
=== MessageType
.CALL_MESSAGE
&& chargingStation
.requests
.delete(messageId
);
427 ErrorType
.SECURITY_ERROR
,
428 `Cannot send command ${commandName} PDU when the charging station is in ${chargingStation.getRegistrationStatus()} state on the central server`,
433 private buildMessageToSend(
434 chargingStation
: ChargingStation
,
436 messagePayload
: JsonType
| OCPPError
,
437 messageType
: MessageType
,
438 commandName
: RequestCommand
| IncomingRequestCommand
,
439 responseCallback
: ResponseCallback
,
440 errorCallback
: ErrorCallback
442 let messageToSend
: string;
444 switch (messageType
) {
446 case MessageType
.CALL_MESSAGE
:
448 this.validateRequestPayload(chargingStation
, commandName
, messagePayload
as JsonObject
);
449 chargingStation
.requests
.set(messageId
, [
453 messagePayload
as JsonType
,
455 messageToSend
= JSON
.stringify([
460 ] as OutgoingRequest
);
463 case MessageType
.CALL_RESULT_MESSAGE
:
465 this.validateIncomingRequestResponsePayload(
468 messagePayload
as JsonObject
470 messageToSend
= JSON
.stringify([messageType
, messageId
, messagePayload
] as Response
);
473 case MessageType
.CALL_ERROR_MESSAGE
:
474 // Build Error Message
475 messageToSend
= JSON
.stringify([
478 (messagePayload
as OCPPError
)?.code
?? ErrorType
.GENERIC_ERROR
,
479 (messagePayload
as OCPPError
)?.message
?? '',
480 (messagePayload
as OCPPError
)?.details
?? { commandName
},
484 return messageToSend
;
487 // eslint-disable-next-line @typescript-eslint/no-unused-vars
488 public abstract requestHandler
<ReqType
extends JsonType
, ResType
extends JsonType
>(
489 chargingStation
: ChargingStation
,
490 commandName
: RequestCommand
,
491 commandParams
?: JsonType
,
492 params
?: RequestParams