1 import Ajv
, { type JSONSchemaType
} from
'ajv';
2 import AjvDraft04 from
'ajv-draft-04';
3 import ajvFormats from
'ajv-formats';
5 import OCPPError from
'../../exception/OCPPError';
6 import PerformanceStatistics from
'../../performance/PerformanceStatistics';
7 import type { EmptyObject
} from
'../../types/EmptyObject';
8 import type { HandleErrorParams
} from
'../../types/Error';
9 import type { JsonObject
, JsonType
} from
'../../types/JsonType';
10 import { ErrorType
} from
'../../types/ocpp/ErrorType';
11 import { MessageType
} from
'../../types/ocpp/MessageType';
12 import { OCPPVersion
} from
'../../types/ocpp/OCPPVersion';
15 type IncomingRequestCommand
,
19 type ResponseCallback
,
21 } from
'../../types/ocpp/Requests';
22 import type { ErrorResponse
, Response
} from
'../../types/ocpp/Responses';
23 import Constants from
'../../utils/Constants';
24 import logger from
'../../utils/Logger';
25 import Utils from
'../../utils/Utils';
26 import type ChargingStation from
'../ChargingStation';
27 import type OCPPResponseService from
'./OCPPResponseService';
28 import { OCPPServiceUtils
} from
'./OCPPServiceUtils';
30 const moduleName
= 'OCPPRequestService';
32 export default abstract class OCPPRequestService
{
33 private static instance
: OCPPRequestService
| null = null;
34 private readonly version
: OCPPVersion
;
35 private readonly ajv
: Ajv
;
37 private readonly ocppResponseService
: OCPPResponseService
;
39 protected constructor(version
: OCPPVersion
, ocppResponseService
: OCPPResponseService
) {
40 this.version
= version
;
41 switch (this.version
) {
42 case OCPPVersion
.VERSION_16
:
43 this.ajv
= new AjvDraft04();
45 case OCPPVersion
.VERSION_20
:
46 case OCPPVersion
.VERSION_201
:
51 this.ocppResponseService
= ocppResponseService
;
52 this.requestHandler
.bind(this);
53 this.sendMessage
.bind(this);
54 this.sendResponse
.bind(this);
55 this.sendError
.bind(this);
56 this.internalSendMessage
.bind(this);
57 this.buildMessageToSend
.bind(this);
58 this.validateRequestPayload
.bind(this);
61 public static getInstance
<T
extends OCPPRequestService
>(
62 this: new (ocppResponseService
: OCPPResponseService
) => T
,
63 ocppResponseService
: OCPPResponseService
65 if (OCPPRequestService
.instance
=== null) {
66 OCPPRequestService
.instance
= new this(ocppResponseService
);
68 return OCPPRequestService
.instance
as T
;
71 public async sendResponse(
72 chargingStation
: ChargingStation
,
74 messagePayload
: JsonType
,
75 commandName
: IncomingRequestCommand
76 ): Promise
<ResponseType
> {
78 // Send response message
79 return await this.internalSendMessage(
83 MessageType
.CALL_RESULT_MESSAGE
,
87 this.handleSendMessageError(chargingStation
, commandName
, error
as Error, {
93 public async sendError(
94 chargingStation
: ChargingStation
,
97 commandName
: RequestCommand
| IncomingRequestCommand
98 ): Promise
<ResponseType
> {
100 // Send error message
101 return await this.internalSendMessage(
105 MessageType
.CALL_ERROR_MESSAGE
,
109 this.handleSendMessageError(chargingStation
, commandName
, error
as Error);
113 protected async sendMessage(
114 chargingStation
: ChargingStation
,
116 messagePayload
: JsonType
,
117 commandName
: RequestCommand
,
118 params
: RequestParams
= {
119 skipBufferingOnError
: false,
120 triggerMessage
: false,
122 ): Promise
<ResponseType
> {
124 return await this.internalSendMessage(
128 MessageType
.CALL_MESSAGE
,
133 this.handleSendMessageError(chargingStation
, commandName
, error
as Error);
137 protected validateRequestPayload
<T
extends JsonType
>(
138 chargingStation
: ChargingStation
,
139 commandName
: RequestCommand
,
140 schema
: JSONSchemaType
<T
>,
143 if (chargingStation
.getPayloadSchemaValidation() === false) {
146 const validate
= this.ajv
.compile(schema
);
147 if (validate(payload
)) {
151 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: Request PDU is invalid: %j`,
154 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
156 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
),
157 'Request PDU is invalid',
159 JSON
.stringify(validate
.errors
, null, 2)
163 private async internalSendMessage(
164 chargingStation
: ChargingStation
,
166 messagePayload
: JsonType
| OCPPError
,
167 messageType
: MessageType
,
168 commandName
?: RequestCommand
| IncomingRequestCommand
,
169 params
: RequestParams
= {
170 skipBufferingOnError
: false,
171 triggerMessage
: false,
173 ): Promise
<ResponseType
> {
175 (chargingStation
.isInUnknownState() === true &&
176 commandName
=== RequestCommand
.BOOT_NOTIFICATION
) ||
177 (chargingStation
.getOcppStrictCompliance() === false &&
178 chargingStation
.isInUnknownState() === true) ||
179 chargingStation
.isInAcceptedState() === true ||
180 (chargingStation
.isInPendingState() === true &&
181 (params
.triggerMessage
=== true || messageType
=== MessageType
.CALL_RESULT_MESSAGE
))
183 // eslint-disable-next-line @typescript-eslint/no-this-alias
185 // Send a message through wsConnection
186 return Utils
.promiseWithTimeout(
187 new Promise((resolve
, reject
) => {
188 const messageToSend
= this.buildMessageToSend(
197 if (chargingStation
.getEnableStatistics() === true) {
198 chargingStation
.performanceStatistics
.addRequestStatistic(commandName
, messageType
);
200 // Check if wsConnection opened
201 if (chargingStation
.isWebSocketConnectionOpened() === true) {
203 const beginId
= PerformanceStatistics
.beginMeasure(commandName
as string);
204 // FIXME: Handle sending error
205 chargingStation
.wsConnection
.send(messageToSend
);
206 PerformanceStatistics
.endMeasure(commandName
as string, beginId
);
208 `${chargingStation.logPrefix()} >> Command '${commandName}' sent ${this.getMessageTypeString(
210 )} payload: ${messageToSend}`
212 } else if (params
.skipBufferingOnError
=== false) {
214 chargingStation
.bufferMessage(messageToSend
);
215 const ocppError
= new OCPPError(
216 ErrorType
.GENERIC_ERROR
,
217 `WebSocket closed for buffered message id '${messageId}' with content '${messageToSend}'`,
219 (messagePayload
as JsonObject
)?.details
?? {}
221 if (messageType
=== MessageType
.CALL_MESSAGE
) {
222 // Reject it but keep the request in the cache
223 return reject(ocppError
);
225 return errorCallback(ocppError
, false);
228 return errorCallback(
230 ErrorType
.GENERIC_ERROR
,
231 `WebSocket closed for non buffered message id '${messageId}' with content '${messageToSend}'`,
233 (messagePayload
as JsonObject
)?.details
?? {}
239 if (messageType
!== MessageType
.CALL_MESSAGE
) {
241 return resolve(messagePayload
);
245 * Function that will receive the request's response
248 * @param requestPayload -
250 function responseCallback(payload
: JsonType
, requestPayload
: JsonType
): void {
251 if (chargingStation
.getEnableStatistics() === true) {
252 chargingStation
.performanceStatistics
.addRequestStatistic(
254 MessageType
.CALL_RESULT_MESSAGE
257 // Handle the request's response
258 self.ocppResponseService
261 commandName
as RequestCommand
,
272 chargingStation
.requests
.delete(messageId
);
277 * Function that will receive the request's error response
280 * @param requestStatistic -
282 function errorCallback(error
: OCPPError
, requestStatistic
= true): void {
283 if (requestStatistic
=== true && chargingStation
.getEnableStatistics() === true) {
284 chargingStation
.performanceStatistics
.addRequestStatistic(
286 MessageType
.CALL_ERROR_MESSAGE
290 `${chargingStation.logPrefix()} Error occurred when calling command ${commandName} with message data ${JSON.stringify(
295 chargingStation
.requests
.delete(messageId
);
299 Constants
.OCPP_WEBSOCKET_TIMEOUT
,
301 ErrorType
.GENERIC_ERROR
,
302 `Timeout for message id '${messageId}'`,
304 (messagePayload
as JsonObject
)?.details
?? {}
307 messageType
=== MessageType
.CALL_MESSAGE
&& chargingStation
.requests
.delete(messageId
);
312 ErrorType
.SECURITY_ERROR
,
313 `Cannot send command ${commandName} PDU when the charging station is in ${chargingStation.getRegistrationStatus()} state on the central server`,
318 private buildMessageToSend(
319 chargingStation
: ChargingStation
,
321 messagePayload
: JsonType
| OCPPError
,
322 messageType
: MessageType
,
323 commandName
?: RequestCommand
| IncomingRequestCommand
,
324 responseCallback
?: ResponseCallback
,
325 errorCallback
?: ErrorCallback
327 let messageToSend
: string;
329 switch (messageType
) {
331 case MessageType
.CALL_MESSAGE
:
333 chargingStation
.requests
.set(messageId
, [
337 messagePayload
as JsonType
,
339 messageToSend
= JSON
.stringify([
344 ] as OutgoingRequest
);
347 case MessageType
.CALL_RESULT_MESSAGE
:
349 messageToSend
= JSON
.stringify([messageType
, messageId
, messagePayload
] as Response
);
352 case MessageType
.CALL_ERROR_MESSAGE
:
353 // Build Error Message
354 messageToSend
= JSON
.stringify([
357 (messagePayload
as OCPPError
)?.code
?? ErrorType
.GENERIC_ERROR
,
358 (messagePayload
as OCPPError
)?.message
?? '',
359 (messagePayload
as OCPPError
)?.details
?? { commandName
},
363 return messageToSend
;
366 private getMessageTypeString(messageType
: MessageType
): string {
367 switch (messageType
) {
368 case MessageType
.CALL_MESSAGE
:
370 case MessageType
.CALL_RESULT_MESSAGE
:
372 case MessageType
.CALL_ERROR_MESSAGE
:
377 private handleSendMessageError(
378 chargingStation
: ChargingStation
,
379 commandName
: RequestCommand
| IncomingRequestCommand
,
381 params
: HandleErrorParams
<EmptyObject
> = { throwError
: false }
383 logger
.error(`${chargingStation.logPrefix()} Request command '${commandName}' error:`, error
);
384 if (params
?.throwError
=== true) {
389 // eslint-disable-next-line @typescript-eslint/no-unused-vars
390 public abstract requestHandler
<ReqType
extends JsonType
, ResType
extends JsonType
>(
391 chargingStation
: ChargingStation
,
392 commandName
: RequestCommand
,
393 commandParams
?: JsonType
,
394 params
?: RequestParams