1 import type { JSONSchemaType
} from
'ajv';
2 import Ajv 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';
14 type IncomingRequestCommand
,
18 type ResponseCallback
,
20 } from
'../../types/ocpp/Requests';
21 import type { ErrorResponse
, Response
} from
'../../types/ocpp/Responses';
22 import Constants from
'../../utils/Constants';
23 import logger from
'../../utils/Logger';
24 import Utils from
'../../utils/Utils';
25 import type ChargingStation from
'../ChargingStation';
26 import type OCPPResponseService from
'./OCPPResponseService';
27 import { OCPPServiceUtils
} from
'./OCPPServiceUtils';
29 const moduleName
= 'OCPPRequestService';
31 export default abstract class OCPPRequestService
{
32 private static instance
: OCPPRequestService
| null = null;
33 private readonly ajv
: Ajv
;
35 private readonly ocppResponseService
: OCPPResponseService
;
37 protected constructor(ocppResponseService
: OCPPResponseService
) {
38 this.ocppResponseService
= ocppResponseService
;
41 this.requestHandler
.bind(this);
42 this.sendMessage
.bind(this);
43 this.sendResponse
.bind(this);
44 this.sendError
.bind(this);
45 this.internalSendMessage
.bind(this);
46 this.buildMessageToSend
.bind(this);
47 this.validateRequestPayload
.bind(this);
50 public static getInstance
<T
extends OCPPRequestService
>(
51 this: new (ocppResponseService
: OCPPResponseService
) => T
,
52 ocppResponseService
: OCPPResponseService
54 if (OCPPRequestService
.instance
=== null) {
55 OCPPRequestService
.instance
= new this(ocppResponseService
);
57 return OCPPRequestService
.instance
as T
;
60 public async sendResponse(
61 chargingStation
: ChargingStation
,
63 messagePayload
: JsonType
,
64 commandName
: IncomingRequestCommand
65 ): Promise
<ResponseType
> {
67 // Send response message
68 return await this.internalSendMessage(
72 MessageType
.CALL_RESULT_MESSAGE
,
76 this.handleSendMessageError(chargingStation
, commandName
, error
as Error, {
82 public async sendError(
83 chargingStation
: ChargingStation
,
86 commandName
: RequestCommand
| IncomingRequestCommand
87 ): Promise
<ResponseType
> {
90 return await this.internalSendMessage(
94 MessageType
.CALL_ERROR_MESSAGE
,
98 this.handleSendMessageError(chargingStation
, commandName
, error
as Error);
102 protected async sendMessage(
103 chargingStation
: ChargingStation
,
105 messagePayload
: JsonType
,
106 commandName
: RequestCommand
,
107 params
: RequestParams
= {
108 skipBufferingOnError
: false,
109 triggerMessage
: false,
111 ): Promise
<ResponseType
> {
113 return await this.internalSendMessage(
117 MessageType
.CALL_MESSAGE
,
122 this.handleSendMessageError(chargingStation
, commandName
, error
as Error);
126 protected validateRequestPayload
<T
extends JsonType
>(
127 chargingStation
: ChargingStation
,
128 commandName
: RequestCommand
,
129 schema
: JSONSchemaType
<T
>,
132 if (chargingStation
.getPayloadSchemaValidation() === false) {
135 const validate
= this.ajv
.compile(schema
);
136 if (validate(payload
)) {
140 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: Request PDU is invalid: %j`,
143 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
145 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
),
146 'Request PDU is invalid',
148 JSON
.stringify(validate
.errors
, null, 2)
152 private async internalSendMessage(
153 chargingStation
: ChargingStation
,
155 messagePayload
: JsonType
| OCPPError
,
156 messageType
: MessageType
,
157 commandName
?: RequestCommand
| IncomingRequestCommand
,
158 params
: RequestParams
= {
159 skipBufferingOnError
: false,
160 triggerMessage
: false,
162 ): Promise
<ResponseType
> {
164 (chargingStation
.isInUnknownState() === true &&
165 commandName
=== RequestCommand
.BOOT_NOTIFICATION
) ||
166 (chargingStation
.getOcppStrictCompliance() === false &&
167 chargingStation
.isInUnknownState() === true) ||
168 chargingStation
.isInAcceptedState() === true ||
169 (chargingStation
.isInPendingState() === true &&
170 (params
.triggerMessage
=== true || messageType
=== MessageType
.CALL_RESULT_MESSAGE
))
172 // eslint-disable-next-line @typescript-eslint/no-this-alias
174 // Send a message through wsConnection
175 return Utils
.promiseWithTimeout(
176 new Promise((resolve
, reject
) => {
177 const messageToSend
= this.buildMessageToSend(
186 if (chargingStation
.getEnableStatistics() === true) {
187 chargingStation
.performanceStatistics
.addRequestStatistic(commandName
, messageType
);
189 // Check if wsConnection opened
190 if (chargingStation
.isWebSocketConnectionOpened() === true) {
192 const beginId
= PerformanceStatistics
.beginMeasure(commandName
as string);
193 // FIXME: Handle sending error
194 chargingStation
.wsConnection
.send(messageToSend
);
195 PerformanceStatistics
.endMeasure(commandName
as string, beginId
);
197 `${chargingStation.logPrefix()} >> Command '${commandName}' sent ${this.getMessageTypeString(
199 )} payload: ${messageToSend}`
201 } else if (params
.skipBufferingOnError
=== false) {
203 chargingStation
.bufferMessage(messageToSend
);
204 const ocppError
= new OCPPError(
205 ErrorType
.GENERIC_ERROR
,
206 `WebSocket closed for buffered message id '${messageId}' with content '${messageToSend}'`,
208 (messagePayload
as JsonObject
)?.details
?? {}
210 if (messageType
=== MessageType
.CALL_MESSAGE
) {
211 // Reject it but keep the request in the cache
212 return reject(ocppError
);
214 return errorCallback(ocppError
, false);
217 return errorCallback(
219 ErrorType
.GENERIC_ERROR
,
220 `WebSocket closed for non buffered message id '${messageId}' with content '${messageToSend}'`,
222 (messagePayload
as JsonObject
)?.details
?? {}
228 if (messageType
!== MessageType
.CALL_MESSAGE
) {
230 return resolve(messagePayload
);
234 * Function that will receive the request's response
237 * @param requestPayload -
239 function responseCallback(payload
: JsonType
, requestPayload
: JsonType
): void {
240 if (chargingStation
.getEnableStatistics() === true) {
241 chargingStation
.performanceStatistics
.addRequestStatistic(
243 MessageType
.CALL_RESULT_MESSAGE
246 // Handle the request's response
247 self.ocppResponseService
250 commandName
as RequestCommand
,
261 chargingStation
.requests
.delete(messageId
);
266 * Function that will receive the request's error response
269 * @param requestStatistic -
271 function errorCallback(error
: OCPPError
, requestStatistic
= true): void {
272 if (requestStatistic
=== true && chargingStation
.getEnableStatistics() === true) {
273 chargingStation
.performanceStatistics
.addRequestStatistic(
275 MessageType
.CALL_ERROR_MESSAGE
279 `${chargingStation.logPrefix()} Error occurred when calling command ${commandName} with message data ${JSON.stringify(
284 chargingStation
.requests
.delete(messageId
);
288 Constants
.OCPP_WEBSOCKET_TIMEOUT
,
290 ErrorType
.GENERIC_ERROR
,
291 `Timeout for message id '${messageId}'`,
293 (messagePayload
as JsonObject
)?.details
?? {}
296 messageType
=== MessageType
.CALL_MESSAGE
&& chargingStation
.requests
.delete(messageId
);
301 ErrorType
.SECURITY_ERROR
,
302 `Cannot send command ${commandName} PDU when the charging station is in ${chargingStation.getRegistrationStatus()} state on the central server`,
307 private buildMessageToSend(
308 chargingStation
: ChargingStation
,
310 messagePayload
: JsonType
| OCPPError
,
311 messageType
: MessageType
,
312 commandName
?: RequestCommand
| IncomingRequestCommand
,
313 responseCallback
?: ResponseCallback
,
314 errorCallback
?: ErrorCallback
316 let messageToSend
: string;
318 switch (messageType
) {
320 case MessageType
.CALL_MESSAGE
:
322 chargingStation
.requests
.set(messageId
, [
326 messagePayload
as JsonType
,
328 messageToSend
= JSON
.stringify([
333 ] as OutgoingRequest
);
336 case MessageType
.CALL_RESULT_MESSAGE
:
338 messageToSend
= JSON
.stringify([messageType
, messageId
, messagePayload
] as Response
);
341 case MessageType
.CALL_ERROR_MESSAGE
:
342 // Build Error Message
343 messageToSend
= JSON
.stringify([
346 (messagePayload
as OCPPError
)?.code
?? ErrorType
.GENERIC_ERROR
,
347 (messagePayload
as OCPPError
)?.message
?? '',
348 (messagePayload
as OCPPError
)?.details
?? { commandName
},
352 return messageToSend
;
355 private getMessageTypeString(messageType
: MessageType
): string {
356 switch (messageType
) {
357 case MessageType
.CALL_MESSAGE
:
359 case MessageType
.CALL_RESULT_MESSAGE
:
361 case MessageType
.CALL_ERROR_MESSAGE
:
366 private handleSendMessageError(
367 chargingStation
: ChargingStation
,
368 commandName
: RequestCommand
| IncomingRequestCommand
,
370 params
: HandleErrorParams
<EmptyObject
> = { throwError
: false }
372 logger
.error(`${chargingStation.logPrefix()} Request command '${commandName}' error:`, error
);
373 if (params
?.throwError
=== true) {
378 // eslint-disable-next-line @typescript-eslint/no-unused-vars
379 public abstract requestHandler
<ReqType
extends JsonType
, ResType
extends JsonType
>(
380 chargingStation
: ChargingStation
,
381 commandName
: RequestCommand
,
382 commandParams
?: JsonType
,
383 params
?: RequestParams