1 import Ajv
, { type JSONSchemaType
} from
'ajv';
2 import ajvFormats from
'ajv-formats';
4 import type OCPPResponseService from
'./OCPPResponseService';
5 import { OCPPServiceUtils
} from
'./OCPPServiceUtils';
6 import OCPPError from
'../../exception/OCPPError';
7 import PerformanceStatistics from
'../../performance/PerformanceStatistics';
8 import type { EmptyObject
} from
'../../types/EmptyObject';
9 import type { HandleErrorParams
} from
'../../types/Error';
10 import type { JsonObject
, JsonType
} from
'../../types/JsonType';
11 import { ErrorType
} from
'../../types/ocpp/ErrorType';
12 import { MessageType
} from
'../../types/ocpp/MessageType';
13 import type { OCPPVersion
} from
'../../types/ocpp/OCPPVersion';
16 type IncomingRequestCommand
,
20 type ResponseCallback
,
22 } from
'../../types/ocpp/Requests';
23 import type { ErrorResponse
, Response
} from
'../../types/ocpp/Responses';
24 import Constants from
'../../utils/Constants';
25 import logger from
'../../utils/Logger';
26 import Utils from
'../../utils/Utils';
27 import type ChargingStation from
'../ChargingStation';
29 const moduleName
= 'OCPPRequestService';
31 export default abstract class OCPPRequestService
{
32 private static instance
: OCPPRequestService
| null = null;
33 private readonly version
: OCPPVersion
;
34 private readonly ajv
: Ajv
;
35 private readonly ocppResponseService
: OCPPResponseService
;
36 protected abstract jsonSchemas
: Map
<RequestCommand
, JSONSchemaType
<JsonObject
>>;
38 protected constructor(version
: OCPPVersion
, ocppResponseService
: OCPPResponseService
) {
39 this.version
= version
;
41 keywords
: ['javaType'],
42 multipleOfPrecision
: 2,
45 this.ocppResponseService
= ocppResponseService
;
46 this.requestHandler
.bind(this);
47 this.sendMessage
.bind(this);
48 this.sendResponse
.bind(this);
49 this.sendError
.bind(this);
50 this.internalSendMessage
.bind(this);
51 this.buildMessageToSend
.bind(this);
52 this.validateRequestPayload
.bind(this);
53 this.validateIncomingRequestResponsePayload
.bind(this);
56 public static getInstance
<T
extends OCPPRequestService
>(
57 this: new (ocppResponseService
: OCPPResponseService
) => T
,
58 ocppResponseService
: OCPPResponseService
60 if (OCPPRequestService
.instance
=== null) {
61 OCPPRequestService
.instance
= new this(ocppResponseService
);
63 return OCPPRequestService
.instance
as T
;
66 public async sendResponse(
67 chargingStation
: ChargingStation
,
69 messagePayload
: JsonType
,
70 commandName
: IncomingRequestCommand
71 ): Promise
<ResponseType
> {
73 // Send response message
74 return await this.internalSendMessage(
78 MessageType
.CALL_RESULT_MESSAGE
,
82 this.handleSendMessageError(chargingStation
, commandName
, error
as Error, {
88 public async sendError(
89 chargingStation
: ChargingStation
,
92 commandName
: RequestCommand
| IncomingRequestCommand
93 ): Promise
<ResponseType
> {
96 return await this.internalSendMessage(
100 MessageType
.CALL_ERROR_MESSAGE
,
104 this.handleSendMessageError(chargingStation
, commandName
, error
as Error);
108 protected async sendMessage(
109 chargingStation
: ChargingStation
,
111 messagePayload
: JsonType
,
112 commandName
: RequestCommand
,
113 params
: RequestParams
= {
114 skipBufferingOnError
: false,
115 triggerMessage
: false,
118 ): Promise
<ResponseType
> {
120 return await this.internalSendMessage(
124 MessageType
.CALL_MESSAGE
,
129 this.handleSendMessageError(chargingStation
, commandName
, error
as Error, {
130 throwError
: params
.throwError
,
135 private validateRequestPayload
<T
extends JsonObject
>(
136 chargingStation
: ChargingStation
,
137 commandName
: RequestCommand
| IncomingRequestCommand
,
140 if (chargingStation
.getPayloadSchemaValidation() === false) {
143 if (this.jsonSchemas
.has(commandName
as RequestCommand
) === false) {
145 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: No JSON schema found for command '${commandName}' PDU validation`
149 const validate
= this.ajv
.compile(this.jsonSchemas
.get(commandName
as RequestCommand
));
150 payload
= Utils
.cloneObject
<T
>(payload
);
151 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
);
152 if (validate(payload
)) {
156 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: Command '${commandName}' request PDU is invalid: %j`,
159 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
161 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
),
162 'Request PDU is invalid',
164 JSON
.stringify(validate
.errors
, null, 2)
168 private validateIncomingRequestResponsePayload
<T
extends JsonObject
>(
169 chargingStation
: ChargingStation
,
170 commandName
: RequestCommand
| IncomingRequestCommand
,
173 if (chargingStation
.getPayloadSchemaValidation() === false) {
177 this.ocppResponseService
.jsonIncomingRequestResponseSchemas
.has(
178 commandName
as IncomingRequestCommand
182 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: No JSON schema found for command '${commandName}' PDU validation`
186 const validate
= this.ajv
.compile(
187 this.ocppResponseService
.jsonIncomingRequestResponseSchemas
.get(
188 commandName
as IncomingRequestCommand
191 payload
= Utils
.cloneObject
<T
>(payload
);
192 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
);
193 if (validate(payload
)) {
197 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: Command '${commandName}' reponse PDU is invalid: %j`,
200 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
202 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
),
203 'Response PDU is invalid',
205 JSON
.stringify(validate
.errors
, null, 2)
209 private async internalSendMessage(
210 chargingStation
: ChargingStation
,
212 messagePayload
: JsonType
| OCPPError
,
213 messageType
: MessageType
,
214 commandName
?: RequestCommand
| IncomingRequestCommand
,
215 params
: RequestParams
= {
216 skipBufferingOnError
: false,
217 triggerMessage
: false,
219 ): Promise
<ResponseType
> {
221 (chargingStation
.isInUnknownState() === true &&
222 commandName
=== RequestCommand
.BOOT_NOTIFICATION
) ||
223 (chargingStation
.getOcppStrictCompliance() === false &&
224 chargingStation
.isInUnknownState() === true) ||
225 chargingStation
.isInAcceptedState() === true ||
226 (chargingStation
.isInPendingState() === true &&
227 (params
.triggerMessage
=== true || messageType
=== MessageType
.CALL_RESULT_MESSAGE
))
229 // eslint-disable-next-line @typescript-eslint/no-this-alias
231 // Send a message through wsConnection
232 return Utils
.promiseWithTimeout(
233 new Promise((resolve
, reject
) => {
234 const messageToSend
= this.buildMessageToSend(
243 if (chargingStation
.getEnableStatistics() === true) {
244 chargingStation
.performanceStatistics
.addRequestStatistic(commandName
, messageType
);
246 let sendError
= false;
247 // Check if wsConnection opened
248 if (chargingStation
.isWebSocketConnectionOpened() === true) {
249 const beginId
= PerformanceStatistics
.beginMeasure(commandName
as string);
251 chargingStation
.wsConnection
.send(messageToSend
);
253 `${chargingStation.logPrefix()} >> Command '${commandName}' sent ${this.getMessageTypeString(
255 )} payload: ${messageToSend}`
259 `${chargingStation.logPrefix()} >> Command '${commandName}' failed to send ${this.getMessageTypeString(
261 )} payload: ${messageToSend}:`,
266 PerformanceStatistics
.endMeasure(commandName
as string, beginId
);
268 const wsClosedOrErrored
=
269 chargingStation
.isWebSocketConnectionOpened() === false || sendError
=== true;
270 if (wsClosedOrErrored
&& params
.skipBufferingOnError
=== false) {
272 chargingStation
.bufferMessage(messageToSend
);
273 // Reject and keep request in the cache
276 ErrorType
.GENERIC_ERROR
,
277 `WebSocket closed or errored for buffered message id '${messageId}' with content '${messageToSend}'`,
279 (messagePayload
as JsonObject
)?.details
?? {}
282 } else if (wsClosedOrErrored
) {
283 const ocppError
= new OCPPError(
284 ErrorType
.GENERIC_ERROR
,
285 `WebSocket closed or errored for non buffered message id '${messageId}' with content '${messageToSend}'`,
287 (messagePayload
as JsonObject
)?.details
?? {}
290 if (messageType
!== MessageType
.CALL_MESSAGE
) {
291 return reject(ocppError
);
293 // Reject and remove request from the cache
294 return errorCallback(ocppError
, false);
297 if (messageType
!== MessageType
.CALL_MESSAGE
) {
298 return resolve(messagePayload
);
302 * Function that will receive the request's response
305 * @param requestPayload -
307 function responseCallback(payload
: JsonType
, requestPayload
: JsonType
): void {
308 if (chargingStation
.getEnableStatistics() === true) {
309 chargingStation
.performanceStatistics
.addRequestStatistic(
311 MessageType
.CALL_RESULT_MESSAGE
314 // Handle the request's response
315 self.ocppResponseService
318 commandName
as RequestCommand
,
329 chargingStation
.requests
.delete(messageId
);
334 * Function that will receive the request's error response
337 * @param requestStatistic -
339 function errorCallback(error
: OCPPError
, requestStatistic
= true): void {
340 if (requestStatistic
=== true && chargingStation
.getEnableStatistics() === true) {
341 chargingStation
.performanceStatistics
.addRequestStatistic(
343 MessageType
.CALL_ERROR_MESSAGE
347 `${chargingStation.logPrefix()} Error occurred at ${self.getMessageTypeString(
349 )} command ${commandName} with PDU %j:`,
353 chargingStation
.requests
.delete(messageId
);
357 Constants
.OCPP_WEBSOCKET_TIMEOUT
,
359 ErrorType
.GENERIC_ERROR
,
360 `Timeout for message id '${messageId}'`,
362 (messagePayload
as JsonObject
)?.details
?? {}
365 messageType
=== MessageType
.CALL_MESSAGE
&& chargingStation
.requests
.delete(messageId
);
370 ErrorType
.SECURITY_ERROR
,
371 `Cannot send command ${commandName} PDU when the charging station is in ${chargingStation.getRegistrationStatus()} state on the central server`,
376 private buildMessageToSend(
377 chargingStation
: ChargingStation
,
379 messagePayload
: JsonType
| OCPPError
,
380 messageType
: MessageType
,
381 commandName
?: RequestCommand
| IncomingRequestCommand
,
382 responseCallback
?: ResponseCallback
,
383 errorCallback
?: ErrorCallback
385 let messageToSend
: string;
387 switch (messageType
) {
389 case MessageType
.CALL_MESSAGE
:
391 this.validateRequestPayload(chargingStation
, commandName
, messagePayload
as JsonObject
);
392 chargingStation
.requests
.set(messageId
, [
396 messagePayload
as JsonType
,
398 messageToSend
= JSON
.stringify([
403 ] as OutgoingRequest
);
406 case MessageType
.CALL_RESULT_MESSAGE
:
408 this.validateIncomingRequestResponsePayload(
411 messagePayload
as JsonObject
413 messageToSend
= JSON
.stringify([messageType
, messageId
, messagePayload
] as Response
);
416 case MessageType
.CALL_ERROR_MESSAGE
:
417 // Build Error Message
418 messageToSend
= JSON
.stringify([
421 (messagePayload
as OCPPError
)?.code
?? ErrorType
.GENERIC_ERROR
,
422 (messagePayload
as OCPPError
)?.message
?? '',
423 (messagePayload
as OCPPError
)?.details
?? { commandName
},
427 return messageToSend
;
430 private getMessageTypeString(messageType
: MessageType
): string {
431 switch (messageType
) {
432 case MessageType
.CALL_MESSAGE
:
434 case MessageType
.CALL_RESULT_MESSAGE
:
436 case MessageType
.CALL_ERROR_MESSAGE
:
441 private handleSendMessageError(
442 chargingStation
: ChargingStation
,
443 commandName
: RequestCommand
| IncomingRequestCommand
,
445 params
: HandleErrorParams
<EmptyObject
> = { throwError
: false }
447 logger
.error(`${chargingStation.logPrefix()} Request command '${commandName}' error:`, error
);
448 if (params
?.throwError
=== true) {
453 // eslint-disable-next-line @typescript-eslint/no-unused-vars
454 public abstract requestHandler
<ReqType
extends JsonType
, ResType
extends JsonType
>(
455 chargingStation
: ChargingStation
,
456 commandName
: RequestCommand
,
457 commandParams
?: JsonType
,
458 params
?: RequestParams