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';
10 import { PerformanceStatistics
} from
'../../performance/PerformanceStatistics';
16 type HandleErrorParams
,
17 type IncomingRequestCommand
,
26 type ResponseCallback
,
29 import { Constants
, Utils
, logger
} from
'../../utils';
31 const moduleName
= 'OCPPRequestService';
33 export abstract class OCPPRequestService
{
34 private static instance
: OCPPRequestService
| null = null;
35 private readonly version
: OCPPVersion
;
36 private readonly ajv
: Ajv
;
37 private readonly ocppResponseService
: OCPPResponseService
;
38 protected abstract jsonSchemas
: Map
<RequestCommand
, JSONSchemaType
<JsonObject
>>;
40 protected constructor(version
: OCPPVersion
, ocppResponseService
: OCPPResponseService
) {
41 this.version
= version
;
43 keywords
: ['javaType'],
44 multipleOfPrecision
: 2,
47 this.ocppResponseService
= ocppResponseService
;
48 this.requestHandler
= this.requestHandler
.bind(this) as <
49 ReqType
extends JsonType
,
50 ResType
extends JsonType
52 chargingStation
: ChargingStation
,
53 commandName
: RequestCommand
,
54 commandParams
?: JsonType
,
55 params
?: RequestParams
56 ) => Promise
<ResType
>;
57 this.sendMessage
= this.sendMessage
.bind(this) as (
58 chargingStation
: ChargingStation
,
60 messagePayload
: JsonType
,
61 commandName
: RequestCommand
,
62 params
?: RequestParams
63 ) => Promise
<ResponseType
>;
64 this.sendResponse
= this.sendResponse
.bind(this) as (
65 chargingStation
: ChargingStation
,
67 messagePayload
: JsonType
,
68 commandName
: IncomingRequestCommand
69 ) => Promise
<ResponseType
>;
70 this.sendError
= this.sendError
.bind(this) as (
71 chargingStation
: ChargingStation
,
74 commandName
: RequestCommand
| IncomingRequestCommand
75 ) => Promise
<ResponseType
>;
76 this.internalSendMessage
= this.internalSendMessage
.bind(this) as (
77 chargingStation
: ChargingStation
,
79 messagePayload
: JsonType
| OCPPError
,
80 messageType
: MessageType
,
81 commandName
: RequestCommand
| IncomingRequestCommand
,
82 params
?: RequestParams
83 ) => Promise
<ResponseType
>;
84 this.buildMessageToSend
= this.buildMessageToSend
.bind(this) as (
85 chargingStation
: ChargingStation
,
87 messagePayload
: JsonType
| OCPPError
,
88 messageType
: MessageType
,
89 commandName
: RequestCommand
| IncomingRequestCommand
,
90 responseCallback
: ResponseCallback
,
91 errorCallback
: ErrorCallback
93 this.validateRequestPayload
= this.validateRequestPayload
.bind(this) as <T
extends JsonObject
>(
94 chargingStation
: ChargingStation
,
95 commandName
: RequestCommand
| IncomingRequestCommand
,
98 this.validateIncomingRequestResponsePayload
= this.validateIncomingRequestResponsePayload
.bind(
100 ) as <T
extends JsonObject
>(
101 chargingStation
: ChargingStation
,
102 commandName
: RequestCommand
| IncomingRequestCommand
,
107 public static getInstance
<T
extends OCPPRequestService
>(
108 this: new (ocppResponseService
: OCPPResponseService
) => T
,
109 ocppResponseService
: OCPPResponseService
111 if (OCPPRequestService
.instance
=== null) {
112 OCPPRequestService
.instance
= new this(ocppResponseService
);
114 return OCPPRequestService
.instance
as T
;
117 public async sendResponse(
118 chargingStation
: ChargingStation
,
120 messagePayload
: JsonType
,
121 commandName
: IncomingRequestCommand
122 ): Promise
<ResponseType
> {
124 // Send response message
125 return await this.internalSendMessage(
129 MessageType
.CALL_RESULT_MESSAGE
,
133 this.handleSendMessageError(chargingStation
, commandName
, error
as Error, {
139 public async sendError(
140 chargingStation
: ChargingStation
,
142 ocppError
: OCPPError
,
143 commandName
: RequestCommand
| IncomingRequestCommand
144 ): Promise
<ResponseType
> {
146 // Send error message
147 return await this.internalSendMessage(
151 MessageType
.CALL_ERROR_MESSAGE
,
155 this.handleSendMessageError(chargingStation
, commandName
, error
as Error);
159 protected async sendMessage(
160 chargingStation
: ChargingStation
,
162 messagePayload
: JsonType
,
163 commandName
: RequestCommand
,
164 params
: RequestParams
= {
165 skipBufferingOnError
: false,
166 triggerMessage
: false,
169 ): Promise
<ResponseType
> {
171 return await this.internalSendMessage(
175 MessageType
.CALL_MESSAGE
,
180 this.handleSendMessageError(chargingStation
, commandName
, error
as Error, {
181 throwError
: params
.throwError
,
186 private validateRequestPayload
<T
extends JsonObject
>(
187 chargingStation
: ChargingStation
,
188 commandName
: RequestCommand
| IncomingRequestCommand
,
191 if (chargingStation
.getPayloadSchemaValidation() === false) {
194 if (this.jsonSchemas
.has(commandName
as RequestCommand
) === false) {
196 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: No JSON schema found for command '${commandName}' PDU validation`
200 const validate
= this.ajv
.compile(this.jsonSchemas
.get(commandName
as RequestCommand
));
201 payload
= Utils
.cloneObject
<T
>(payload
);
202 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
);
203 if (validate(payload
)) {
207 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: Command '${commandName}' request PDU is invalid: %j`,
210 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
212 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
),
213 'Request PDU is invalid',
215 JSON
.stringify(validate
.errors
, null, 2)
219 private validateIncomingRequestResponsePayload
<T
extends JsonObject
>(
220 chargingStation
: ChargingStation
,
221 commandName
: RequestCommand
| IncomingRequestCommand
,
224 if (chargingStation
.getPayloadSchemaValidation() === false) {
228 this.ocppResponseService
.jsonIncomingRequestResponseSchemas
.has(
229 commandName
as IncomingRequestCommand
233 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: No JSON schema found for command '${commandName}' PDU validation`
237 const validate
= this.ajv
.compile(
238 this.ocppResponseService
.jsonIncomingRequestResponseSchemas
.get(
239 commandName
as IncomingRequestCommand
242 payload
= Utils
.cloneObject
<T
>(payload
);
243 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
);
244 if (validate(payload
)) {
248 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: Command '${commandName}' reponse PDU is invalid: %j`,
251 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
253 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
),
254 'Response PDU is invalid',
256 JSON
.stringify(validate
.errors
, null, 2)
260 private async internalSendMessage(
261 chargingStation
: ChargingStation
,
263 messagePayload
: JsonType
| OCPPError
,
264 messageType
: MessageType
,
265 commandName
: RequestCommand
| IncomingRequestCommand
,
266 params
: RequestParams
= {
267 skipBufferingOnError
: false,
268 triggerMessage
: false,
270 ): Promise
<ResponseType
> {
272 (chargingStation
.inUnknownState() === true &&
273 commandName
=== RequestCommand
.BOOT_NOTIFICATION
) ||
274 (chargingStation
.getOcppStrictCompliance() === false &&
275 chargingStation
.inUnknownState() === true) ||
276 chargingStation
.inAcceptedState() === true ||
277 (chargingStation
.inPendingState() === true &&
278 (params
.triggerMessage
=== true || messageType
=== MessageType
.CALL_RESULT_MESSAGE
))
280 // eslint-disable-next-line @typescript-eslint/no-this-alias
282 // Send a message through wsConnection
283 return Utils
.promiseWithTimeout(
284 new Promise((resolve
, reject
) => {
286 * Function that will receive the request's response
289 * @param requestPayload -
291 const responseCallback
= (payload
: JsonType
, requestPayload
: JsonType
): void => {
292 if (chargingStation
.getEnableStatistics() === true) {
293 chargingStation
.performanceStatistics
?.addRequestStatistic(
295 MessageType
.CALL_RESULT_MESSAGE
298 // Handle the request's response
299 self.ocppResponseService
302 commandName
as RequestCommand
,
313 chargingStation
.requests
.delete(messageId
);
318 * Function that will receive the request's error response
321 * @param requestStatistic -
323 const errorCallback
= (error
: OCPPError
, requestStatistic
= true): void => {
324 if (requestStatistic
=== true && chargingStation
.getEnableStatistics() === true) {
325 chargingStation
.performanceStatistics
?.addRequestStatistic(
327 MessageType
.CALL_ERROR_MESSAGE
331 `${chargingStation.logPrefix()} Error occurred at ${OCPPServiceUtils.getMessageTypeString(
333 )} command ${commandName} with PDU %j:`,
337 chargingStation
.requests
.delete(messageId
);
341 if (chargingStation
.getEnableStatistics() === true) {
342 chargingStation
.performanceStatistics
?.addRequestStatistic(commandName
, messageType
);
344 const messageToSend
= this.buildMessageToSend(
353 let sendError
= false;
354 // Check if wsConnection opened
355 const wsOpened
= chargingStation
.isWebSocketConnectionOpened() === true;
357 const beginId
= PerformanceStatistics
.beginMeasure(commandName
);
359 chargingStation
.wsConnection
?.send(messageToSend
);
361 `${chargingStation.logPrefix()} >> Command '${commandName}' sent ${OCPPServiceUtils.getMessageTypeString(
363 )} payload: ${messageToSend}`
367 `${chargingStation.logPrefix()} >> Command '${commandName}' failed to send ${OCPPServiceUtils.getMessageTypeString(
369 )} payload: ${messageToSend}:`,
374 PerformanceStatistics
.endMeasure(commandName
, beginId
);
376 const wsClosedOrErrored
= !wsOpened
|| sendError
=== true;
377 if (wsClosedOrErrored
&& params
.skipBufferingOnError
=== false) {
379 chargingStation
.bufferMessage(messageToSend
);
380 // Reject and keep request in the cache
383 ErrorType
.GENERIC_ERROR
,
384 `WebSocket closed or errored for buffered message id '${messageId}' with content '${messageToSend}'`,
386 (messagePayload
as JsonObject
)?.details
?? Constants
.EMPTY_FREEZED_OBJECT
389 } else if (wsClosedOrErrored
) {
390 const ocppError
= new OCPPError(
391 ErrorType
.GENERIC_ERROR
,
392 `WebSocket closed or errored for non buffered message id '${messageId}' with content '${messageToSend}'`,
394 (messagePayload
as JsonObject
)?.details
?? Constants
.EMPTY_FREEZED_OBJECT
397 if (messageType
!== MessageType
.CALL_MESSAGE
) {
398 return reject(ocppError
);
400 // Reject and remove request from the cache
401 return errorCallback(ocppError
, false);
404 if (messageType
!== MessageType
.CALL_MESSAGE
) {
405 return resolve(messagePayload
);
408 OCPPConstants
.OCPP_WEBSOCKET_TIMEOUT
,
410 ErrorType
.GENERIC_ERROR
,
411 `Timeout for message id '${messageId}'`,
413 (messagePayload
as JsonObject
)?.details
?? Constants
.EMPTY_FREEZED_OBJECT
416 messageType
=== MessageType
.CALL_MESSAGE
&& chargingStation
.requests
.delete(messageId
);
421 ErrorType
.SECURITY_ERROR
,
422 `Cannot send command ${commandName} PDU when the charging station is in ${chargingStation.getRegistrationStatus()} state on the central server`,
427 private buildMessageToSend(
428 chargingStation
: ChargingStation
,
430 messagePayload
: JsonType
| OCPPError
,
431 messageType
: MessageType
,
432 commandName
: RequestCommand
| IncomingRequestCommand
,
433 responseCallback
: ResponseCallback
,
434 errorCallback
: ErrorCallback
436 let messageToSend
: string;
438 switch (messageType
) {
440 case MessageType
.CALL_MESSAGE
:
442 this.validateRequestPayload(chargingStation
, commandName
, messagePayload
as JsonObject
);
443 chargingStation
.requests
.set(messageId
, [
447 messagePayload
as JsonType
,
449 messageToSend
= JSON
.stringify([
454 ] as OutgoingRequest
);
457 case MessageType
.CALL_RESULT_MESSAGE
:
459 this.validateIncomingRequestResponsePayload(
462 messagePayload
as JsonObject
464 messageToSend
= JSON
.stringify([messageType
, messageId
, messagePayload
] as Response
);
467 case MessageType
.CALL_ERROR_MESSAGE
:
468 // Build Error Message
469 messageToSend
= JSON
.stringify([
472 (messagePayload
as OCPPError
)?.code
?? ErrorType
.GENERIC_ERROR
,
473 (messagePayload
as OCPPError
)?.message
?? '',
474 (messagePayload
as OCPPError
)?.details
?? { commandName
},
478 return messageToSend
;
481 private handleSendMessageError(
482 chargingStation
: ChargingStation
,
483 commandName
: RequestCommand
| IncomingRequestCommand
,
485 params
: HandleErrorParams
<EmptyObject
> = { throwError
: false }
487 logger
.error(`${chargingStation.logPrefix()} Request command '${commandName}' error:`, error
);
488 if (params
?.throwError
=== true) {
493 // eslint-disable-next-line @typescript-eslint/no-unused-vars
494 public abstract requestHandler
<ReqType
extends JsonType
, ResType
extends JsonType
>(
495 chargingStation
: ChargingStation
,
496 commandName
: RequestCommand
,
497 commandParams
?: JsonType
,
498 params
?: RequestParams