1 import Ajv
, { type JSONSchemaType
} from
'ajv';
2 import ajvFormats from
'ajv-formats';
4 import OCPPError from
'../../exception/OCPPError';
5 import PerformanceStatistics from
'../../performance/PerformanceStatistics';
6 import type { EmptyObject
} from
'../../types/EmptyObject';
7 import type { HandleErrorParams
} from
'../../types/Error';
8 import type { JsonObject
, JsonType
} from
'../../types/JsonType';
9 import { ErrorType
} from
'../../types/ocpp/ErrorType';
10 import { MessageType
} from
'../../types/ocpp/MessageType';
11 import type { OCPPVersion
} from
'../../types/ocpp/OCPPVersion';
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 version
: OCPPVersion
;
34 private readonly ajv
: Ajv
;
36 private readonly ocppResponseService
: OCPPResponseService
;
38 protected constructor(version
: OCPPVersion
, ocppResponseService
: OCPPResponseService
) {
39 this.version
= version
;
41 multipleOfPrecision
: 2,
44 this.ocppResponseService
= ocppResponseService
;
45 this.requestHandler
.bind(this);
46 this.sendMessage
.bind(this);
47 this.sendResponse
.bind(this);
48 this.sendError
.bind(this);
49 this.internalSendMessage
.bind(this);
50 this.buildMessageToSend
.bind(this);
51 this.validateRequestPayload
.bind(this);
54 public static getInstance
<T
extends OCPPRequestService
>(
55 this: new (ocppResponseService
: OCPPResponseService
) => T
,
56 ocppResponseService
: OCPPResponseService
58 if (OCPPRequestService
.instance
=== null) {
59 OCPPRequestService
.instance
= new this(ocppResponseService
);
61 return OCPPRequestService
.instance
as T
;
64 public async sendResponse(
65 chargingStation
: ChargingStation
,
67 messagePayload
: JsonType
,
68 commandName
: IncomingRequestCommand
69 ): Promise
<ResponseType
> {
71 // Send response message
72 return await this.internalSendMessage(
76 MessageType
.CALL_RESULT_MESSAGE
,
80 this.handleSendMessageError(chargingStation
, commandName
, error
as Error, {
86 public async sendError(
87 chargingStation
: ChargingStation
,
90 commandName
: RequestCommand
| IncomingRequestCommand
91 ): Promise
<ResponseType
> {
94 return await this.internalSendMessage(
98 MessageType
.CALL_ERROR_MESSAGE
,
102 this.handleSendMessageError(chargingStation
, commandName
, error
as Error);
106 protected async sendMessage(
107 chargingStation
: ChargingStation
,
109 messagePayload
: JsonType
,
110 commandName
: RequestCommand
,
111 params
: RequestParams
= {
112 skipBufferingOnError
: false,
113 triggerMessage
: false,
115 ): Promise
<ResponseType
> {
117 return await this.internalSendMessage(
121 MessageType
.CALL_MESSAGE
,
126 this.handleSendMessageError(chargingStation
, commandName
, error
as Error);
130 protected validateRequestPayload
<T
extends JsonType
>(
131 chargingStation
: ChargingStation
,
132 commandName
: RequestCommand
| IncomingRequestCommand
,
135 if (chargingStation
.getPayloadSchemaValidation() === false) {
138 const schema
= this.getRequestPayloadValidationSchema(chargingStation
, commandName
);
139 if (schema
=== false) {
142 const validate
= this.ajv
.compile(schema
);
143 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
);
144 if (validate(payload
)) {
148 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: Command '${commandName}' request PDU is invalid: %j`,
151 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
153 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
),
154 'Request PDU is invalid',
156 JSON
.stringify(validate
.errors
, null, 2)
160 private async internalSendMessage(
161 chargingStation
: ChargingStation
,
163 messagePayload
: JsonType
| OCPPError
,
164 messageType
: MessageType
,
165 commandName
?: RequestCommand
| IncomingRequestCommand
,
166 params
: RequestParams
= {
167 skipBufferingOnError
: false,
168 triggerMessage
: false,
170 ): Promise
<ResponseType
> {
172 (chargingStation
.isInUnknownState() === true &&
173 commandName
=== RequestCommand
.BOOT_NOTIFICATION
) ||
174 (chargingStation
.getOcppStrictCompliance() === false &&
175 chargingStation
.isInUnknownState() === true) ||
176 chargingStation
.isInAcceptedState() === true ||
177 (chargingStation
.isInPendingState() === true &&
178 (params
.triggerMessage
=== true || messageType
=== MessageType
.CALL_RESULT_MESSAGE
))
180 // eslint-disable-next-line @typescript-eslint/no-this-alias
182 // Send a message through wsConnection
183 return Utils
.promiseWithTimeout(
184 new Promise((resolve
, reject
) => {
185 const messageToSend
= this.buildMessageToSend(
194 if (chargingStation
.getEnableStatistics() === true) {
195 chargingStation
.performanceStatistics
.addRequestStatistic(commandName
, messageType
);
197 // Check if wsConnection opened
198 if (chargingStation
.isWebSocketConnectionOpened() === true) {
200 const beginId
= PerformanceStatistics
.beginMeasure(commandName
as string);
201 // FIXME: Handle sending error
202 chargingStation
.wsConnection
.send(messageToSend
);
203 PerformanceStatistics
.endMeasure(commandName
as string, beginId
);
205 `${chargingStation.logPrefix()} >> Command '${commandName}' sent ${this.getMessageTypeString(
207 )} payload: ${messageToSend}`
209 } else if (params
.skipBufferingOnError
=== false) {
211 chargingStation
.bufferMessage(messageToSend
);
212 const ocppError
= new OCPPError(
213 ErrorType
.GENERIC_ERROR
,
214 `WebSocket closed for buffered message id '${messageId}' with content '${messageToSend}'`,
216 (messagePayload
as JsonObject
)?.details
?? {}
218 if (messageType
=== MessageType
.CALL_MESSAGE
) {
219 // Reject it but keep the request in the cache
220 return reject(ocppError
);
222 return errorCallback(ocppError
, false);
225 return errorCallback(
227 ErrorType
.GENERIC_ERROR
,
228 `WebSocket closed for non buffered message id '${messageId}' with content '${messageToSend}'`,
230 (messagePayload
as JsonObject
)?.details
?? {}
236 if (messageType
!== MessageType
.CALL_MESSAGE
) {
238 return resolve(messagePayload
);
242 * Function that will receive the request's response
245 * @param requestPayload -
247 function responseCallback(payload
: JsonType
, requestPayload
: JsonType
): void {
248 if (chargingStation
.getEnableStatistics() === true) {
249 chargingStation
.performanceStatistics
.addRequestStatistic(
251 MessageType
.CALL_RESULT_MESSAGE
254 // Handle the request's response
255 self.ocppResponseService
258 commandName
as RequestCommand
,
269 chargingStation
.requests
.delete(messageId
);
274 * Function that will receive the request's error response
277 * @param requestStatistic -
279 function errorCallback(error
: OCPPError
, requestStatistic
= true): void {
280 if (requestStatistic
=== true && chargingStation
.getEnableStatistics() === true) {
281 chargingStation
.performanceStatistics
.addRequestStatistic(
283 MessageType
.CALL_ERROR_MESSAGE
287 `${chargingStation.logPrefix()} Error occurred when calling command ${commandName} with message data ${JSON.stringify(
292 chargingStation
.requests
.delete(messageId
);
296 Constants
.OCPP_WEBSOCKET_TIMEOUT
,
298 ErrorType
.GENERIC_ERROR
,
299 `Timeout for message id '${messageId}'`,
301 (messagePayload
as JsonObject
)?.details
?? {}
304 messageType
=== MessageType
.CALL_MESSAGE
&& chargingStation
.requests
.delete(messageId
);
309 ErrorType
.SECURITY_ERROR
,
310 `Cannot send command ${commandName} PDU when the charging station is in ${chargingStation.getRegistrationStatus()} state on the central server`,
315 private buildMessageToSend(
316 chargingStation
: ChargingStation
,
318 messagePayload
: JsonType
| OCPPError
,
319 messageType
: MessageType
,
320 commandName
?: RequestCommand
| IncomingRequestCommand
,
321 responseCallback
?: ResponseCallback
,
322 errorCallback
?: ErrorCallback
324 let messageToSend
: string;
326 switch (messageType
) {
328 case MessageType
.CALL_MESSAGE
:
330 chargingStation
.requests
.set(messageId
, [
334 messagePayload
as JsonType
,
336 this.validateRequestPayload(chargingStation
, commandName
, messagePayload
as JsonType
);
337 messageToSend
= JSON
.stringify([
342 ] as OutgoingRequest
);
345 case MessageType
.CALL_RESULT_MESSAGE
:
347 // FIXME: Validate response payload
348 messageToSend
= JSON
.stringify([messageType
, messageId
, messagePayload
] as Response
);
351 case MessageType
.CALL_ERROR_MESSAGE
:
352 // Build Error Message
353 messageToSend
= JSON
.stringify([
356 (messagePayload
as OCPPError
)?.code
?? ErrorType
.GENERIC_ERROR
,
357 (messagePayload
as OCPPError
)?.message
?? '',
358 (messagePayload
as OCPPError
)?.details
?? { commandName
},
362 return messageToSend
;
365 private getMessageTypeString(messageType
: MessageType
): string {
366 switch (messageType
) {
367 case MessageType
.CALL_MESSAGE
:
369 case MessageType
.CALL_RESULT_MESSAGE
:
371 case MessageType
.CALL_ERROR_MESSAGE
:
376 private handleSendMessageError(
377 chargingStation
: ChargingStation
,
378 commandName
: RequestCommand
| IncomingRequestCommand
,
380 params
: HandleErrorParams
<EmptyObject
> = { throwError
: false }
382 logger
.error(`${chargingStation.logPrefix()} Request command '${commandName}' error:`, error
);
383 if (params
?.throwError
=== true) {
388 // eslint-disable-next-line @typescript-eslint/no-unused-vars
389 public abstract requestHandler
<ReqType
extends JsonType
, ResType
extends JsonType
>(
390 chargingStation
: ChargingStation
,
391 commandName
: RequestCommand
,
392 commandParams
?: JsonType
,
393 params
?: RequestParams
396 protected abstract getRequestPayloadValidationSchema(
397 chargingStation
: ChargingStation
,
398 commandName
: RequestCommand
| IncomingRequestCommand
399 ): JSONSchemaType
<JsonObject
> | false;