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
;
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);
55 public static getInstance
<T
extends OCPPRequestService
>(
56 this: new (ocppResponseService
: OCPPResponseService
) => T
,
57 ocppResponseService
: OCPPResponseService
59 if (OCPPRequestService
.instance
=== null) {
60 OCPPRequestService
.instance
= new this(ocppResponseService
);
62 return OCPPRequestService
.instance
as T
;
65 public async sendResponse(
66 chargingStation
: ChargingStation
,
68 messagePayload
: JsonType
,
69 commandName
: IncomingRequestCommand
70 ): Promise
<ResponseType
> {
72 // Send response message
73 return await this.internalSendMessage(
77 MessageType
.CALL_RESULT_MESSAGE
,
81 this.handleSendMessageError(chargingStation
, commandName
, error
as Error, {
87 public async sendError(
88 chargingStation
: ChargingStation
,
91 commandName
: RequestCommand
| IncomingRequestCommand
92 ): Promise
<ResponseType
> {
95 return await this.internalSendMessage(
99 MessageType
.CALL_ERROR_MESSAGE
,
103 this.handleSendMessageError(chargingStation
, commandName
, error
as Error);
107 protected async sendMessage(
108 chargingStation
: ChargingStation
,
110 messagePayload
: JsonType
,
111 commandName
: RequestCommand
,
112 params
: RequestParams
= {
113 skipBufferingOnError
: false,
114 triggerMessage
: false,
116 ): Promise
<ResponseType
> {
118 return await this.internalSendMessage(
122 MessageType
.CALL_MESSAGE
,
127 this.handleSendMessageError(chargingStation
, commandName
, error
as Error);
131 protected validateRequestPayload
<T
extends JsonObject
>(
132 chargingStation
: ChargingStation
,
133 commandName
: RequestCommand
| IncomingRequestCommand
,
136 if (chargingStation
.getPayloadSchemaValidation() === false) {
139 if (this.jsonSchemas
.has(commandName
as RequestCommand
) === false) {
141 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: No JSON schema found for command '${commandName}' PDU validation`
145 const validate
= this.ajv
.compile(this.jsonSchemas
.get(commandName
as RequestCommand
));
146 payload
= Utils
.cloneObject
<T
>(payload
);
147 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
);
148 if (validate(payload
)) {
152 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: Command '${commandName}' request PDU is invalid: %j`,
155 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
157 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
),
158 'Request PDU is invalid',
160 JSON
.stringify(validate
.errors
, null, 2)
164 protected validateResponsePayload
<T
extends JsonObject
>(
165 chargingStation
: ChargingStation
,
166 commandName
: RequestCommand
| IncomingRequestCommand
,
169 if (chargingStation
.getPayloadSchemaValidation() === false) {
173 this.ocppResponseService
.jsonIncomingRequestResponseSchemas
.has(
174 commandName
as IncomingRequestCommand
178 `${chargingStation.logPrefix()} ${moduleName}.validateResponsePayload: No JSON schema found for command '${commandName}' PDU validation`
182 const validate
= this.ajv
.compile(
183 this.ocppResponseService
.jsonIncomingRequestResponseSchemas
.get(
184 commandName
as IncomingRequestCommand
187 payload
= Utils
.cloneObject
<T
>(payload
);
188 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
);
189 if (validate(payload
)) {
193 `${chargingStation.logPrefix()} ${moduleName}.validateResponsePayload: Command '${commandName}' reponse PDU is invalid: %j`,
196 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
198 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
),
199 'Response PDU is invalid',
201 JSON
.stringify(validate
.errors
, null, 2)
205 private async internalSendMessage(
206 chargingStation
: ChargingStation
,
208 messagePayload
: JsonType
| OCPPError
,
209 messageType
: MessageType
,
210 commandName
?: RequestCommand
| IncomingRequestCommand
,
211 params
: RequestParams
= {
212 skipBufferingOnError
: false,
213 triggerMessage
: false,
215 ): Promise
<ResponseType
> {
217 (chargingStation
.isInUnknownState() === true &&
218 commandName
=== RequestCommand
.BOOT_NOTIFICATION
) ||
219 (chargingStation
.getOcppStrictCompliance() === false &&
220 chargingStation
.isInUnknownState() === true) ||
221 chargingStation
.isInAcceptedState() === true ||
222 (chargingStation
.isInPendingState() === true &&
223 (params
.triggerMessage
=== true || messageType
=== MessageType
.CALL_RESULT_MESSAGE
))
225 // eslint-disable-next-line @typescript-eslint/no-this-alias
227 // Send a message through wsConnection
228 return Utils
.promiseWithTimeout(
229 new Promise((resolve
, reject
) => {
230 const messageToSend
= this.buildMessageToSend(
239 if (chargingStation
.getEnableStatistics() === true) {
240 chargingStation
.performanceStatistics
.addRequestStatistic(commandName
, messageType
);
242 // Check if wsConnection opened
243 if (chargingStation
.isWebSocketConnectionOpened() === true) {
245 const beginId
= PerformanceStatistics
.beginMeasure(commandName
as string);
246 // FIXME: Handle sending error
247 chargingStation
.wsConnection
.send(messageToSend
);
248 PerformanceStatistics
.endMeasure(commandName
as string, beginId
);
250 `${chargingStation.logPrefix()} >> Command '${commandName}' sent ${this.getMessageTypeString(
252 )} payload: ${messageToSend}`
254 } else if (params
.skipBufferingOnError
=== false) {
256 chargingStation
.bufferMessage(messageToSend
);
257 const ocppError
= new OCPPError(
258 ErrorType
.GENERIC_ERROR
,
259 `WebSocket closed for buffered message id '${messageId}' with content '${messageToSend}'`,
261 (messagePayload
as JsonObject
)?.details
?? {}
263 if (messageType
=== MessageType
.CALL_MESSAGE
) {
264 // Reject it but keep the request in the cache
265 return reject(ocppError
);
267 return errorCallback(ocppError
, false);
270 return errorCallback(
272 ErrorType
.GENERIC_ERROR
,
273 `WebSocket closed for non buffered message id '${messageId}' with content '${messageToSend}'`,
275 (messagePayload
as JsonObject
)?.details
?? {}
281 if (messageType
!== MessageType
.CALL_MESSAGE
) {
283 return resolve(messagePayload
);
287 * Function that will receive the request's response
290 * @param requestPayload -
292 function responseCallback(payload
: JsonType
, requestPayload
: JsonType
): void {
293 if (chargingStation
.getEnableStatistics() === true) {
294 chargingStation
.performanceStatistics
.addRequestStatistic(
296 MessageType
.CALL_RESULT_MESSAGE
299 // Handle the request's response
300 self.ocppResponseService
303 commandName
as RequestCommand
,
314 chargingStation
.requests
.delete(messageId
);
319 * Function that will receive the request's error response
322 * @param requestStatistic -
324 function errorCallback(error
: OCPPError
, requestStatistic
= true): void {
325 if (requestStatistic
=== true && chargingStation
.getEnableStatistics() === true) {
326 chargingStation
.performanceStatistics
.addRequestStatistic(
328 MessageType
.CALL_ERROR_MESSAGE
332 `${chargingStation.logPrefix()} Error occurred when calling command ${commandName} with message data ${JSON.stringify(
337 chargingStation
.requests
.delete(messageId
);
341 Constants
.OCPP_WEBSOCKET_TIMEOUT
,
343 ErrorType
.GENERIC_ERROR
,
344 `Timeout for message id '${messageId}'`,
346 (messagePayload
as JsonObject
)?.details
?? {}
349 messageType
=== MessageType
.CALL_MESSAGE
&& chargingStation
.requests
.delete(messageId
);
354 ErrorType
.SECURITY_ERROR
,
355 `Cannot send command ${commandName} PDU when the charging station is in ${chargingStation.getRegistrationStatus()} state on the central server`,
360 private buildMessageToSend(
361 chargingStation
: ChargingStation
,
363 messagePayload
: JsonType
| OCPPError
,
364 messageType
: MessageType
,
365 commandName
?: RequestCommand
| IncomingRequestCommand
,
366 responseCallback
?: ResponseCallback
,
367 errorCallback
?: ErrorCallback
369 let messageToSend
: string;
371 switch (messageType
) {
373 case MessageType
.CALL_MESSAGE
:
375 chargingStation
.requests
.set(messageId
, [
379 messagePayload
as JsonType
,
381 this.validateRequestPayload(chargingStation
, commandName
, messagePayload
as JsonObject
);
382 messageToSend
= JSON
.stringify([
387 ] as OutgoingRequest
);
390 case MessageType
.CALL_RESULT_MESSAGE
:
392 this.validateResponsePayload(chargingStation
, commandName
, messagePayload
as JsonObject
);
393 messageToSend
= JSON
.stringify([messageType
, messageId
, messagePayload
] as Response
);
396 case MessageType
.CALL_ERROR_MESSAGE
:
397 // Build Error Message
398 messageToSend
= JSON
.stringify([
401 (messagePayload
as OCPPError
)?.code
?? ErrorType
.GENERIC_ERROR
,
402 (messagePayload
as OCPPError
)?.message
?? '',
403 (messagePayload
as OCPPError
)?.details
?? { commandName
},
407 return messageToSend
;
410 private getMessageTypeString(messageType
: MessageType
): string {
411 switch (messageType
) {
412 case MessageType
.CALL_MESSAGE
:
414 case MessageType
.CALL_RESULT_MESSAGE
:
416 case MessageType
.CALL_ERROR_MESSAGE
:
421 private handleSendMessageError(
422 chargingStation
: ChargingStation
,
423 commandName
: RequestCommand
| IncomingRequestCommand
,
425 params
: HandleErrorParams
<EmptyObject
> = { throwError
: false }
427 logger
.error(`${chargingStation.logPrefix()} Request command '${commandName}' error:`, error
);
428 if (params
?.throwError
=== true) {
433 // eslint-disable-next-line @typescript-eslint/no-unused-vars
434 public abstract requestHandler
<ReqType
extends JsonType
, ResType
extends JsonType
>(
435 chargingStation
: ChargingStation
,
436 commandName
: RequestCommand
,
437 commandParams
?: JsonType
,
438 params
?: RequestParams