1 import _Ajv
, { type JSONSchemaType
, type ValidateFunction
} from
'ajv'
2 import _ajvFormats from
'ajv-formats'
4 import { OCPPConstants
} from
'./OCPPConstants.js'
5 import type { OCPPResponseService
} from
'./OCPPResponseService.js'
6 import { OCPPServiceUtils
} from
'./OCPPServiceUtils.js'
7 import type { ChargingStation
} from
'../../charging-station/index.js'
8 import { OCPPError
} from
'../../exception/index.js'
9 import { PerformanceStatistics
} from
'../../performance/index.js'
11 ChargingStationEvents
,
15 type IncomingRequestCommand
,
23 type ResponseCallback
,
25 } from
'../../types/index.js'
28 formatDurationMilliSeconds
,
29 handleSendMessageError
,
31 } from
'../../utils/index.js'
32 type Ajv
= _Ajv
.default
33 // eslint-disable-next-line @typescript-eslint/no-redeclare
34 const Ajv
= _Ajv
.default
35 const ajvFormats
= _ajvFormats
.default
37 const moduleName
= 'OCPPRequestService'
39 const defaultRequestParams
: RequestParams
= {
40 skipBufferingOnError
: false,
41 triggerMessage
: false,
45 export abstract class OCPPRequestService
{
46 private static instance
: OCPPRequestService
| null = null
47 private readonly version
: OCPPVersion
48 private readonly ajv
: Ajv
49 private readonly ocppResponseService
: OCPPResponseService
50 private readonly jsonValidateFunctions
: Map
<RequestCommand
, ValidateFunction
<JsonType
>>
51 protected abstract jsonSchemas
: Map
<RequestCommand
, JSONSchemaType
<JsonType
>>
53 protected constructor (version
: OCPPVersion
, ocppResponseService
: OCPPResponseService
) {
54 this.version
= version
56 keywords
: ['javaType'],
57 multipleOfPrecision
: 2
60 this.jsonValidateFunctions
= new Map
<RequestCommand
, ValidateFunction
<JsonType
>>()
61 this.ocppResponseService
= ocppResponseService
62 this.requestHandler
= this.requestHandler
.bind(this)
63 this.sendMessage
= this.sendMessage
.bind(this)
64 this.sendResponse
= this.sendResponse
.bind(this)
65 this.sendError
= this.sendError
.bind(this)
66 this.internalSendMessage
= this.internalSendMessage
.bind(this)
67 this.buildMessageToSend
= this.buildMessageToSend
.bind(this)
68 this.validateRequestPayload
= this.validateRequestPayload
.bind(this)
69 this.validateIncomingRequestResponsePayload
=
70 this.validateIncomingRequestResponsePayload
.bind(this)
73 public static getInstance
<T
extends OCPPRequestService
>(
74 this: new (ocppResponseService
: OCPPResponseService
) => T
,
75 ocppResponseService
: OCPPResponseService
77 if (OCPPRequestService
.instance
=== null) {
78 OCPPRequestService
.instance
= new this(ocppResponseService
)
80 return OCPPRequestService
.instance
as T
83 public async sendResponse (
84 chargingStation
: ChargingStation
,
86 messagePayload
: JsonType
,
87 commandName
: IncomingRequestCommand
88 ): Promise
<ResponseType
> {
90 // Send response message
91 return await this.internalSendMessage(
95 MessageType
.CALL_RESULT_MESSAGE
,
99 handleSendMessageError(chargingStation
, commandName
, error
as Error, {
106 public async sendError (
107 chargingStation
: ChargingStation
,
109 ocppError
: OCPPError
,
110 commandName
: RequestCommand
| IncomingRequestCommand
111 ): Promise
<ResponseType
> {
113 // Send error message
114 return await this.internalSendMessage(
118 MessageType
.CALL_ERROR_MESSAGE
,
122 handleSendMessageError(chargingStation
, commandName
, error
as Error)
127 protected async sendMessage (
128 chargingStation
: ChargingStation
,
130 messagePayload
: JsonType
,
131 commandName
: RequestCommand
,
132 params
?: RequestParams
133 ): Promise
<ResponseType
> {
135 ...defaultRequestParams
,
139 return await this.internalSendMessage(
143 MessageType
.CALL_MESSAGE
,
148 handleSendMessageError(chargingStation
, commandName
, error
as Error, {
149 throwError
: params
.throwError
155 private validateRequestPayload
<T
extends JsonType
>(
156 chargingStation
: ChargingStation
,
157 commandName
: RequestCommand
| IncomingRequestCommand
,
160 if (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false) {
163 if (!this.jsonSchemas
.has(commandName
as RequestCommand
)) {
165 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: No JSON schema found for command '${commandName}' PDU validation`
169 const validate
= this.getJsonRequestValidateFunction
<T
>(commandName
as RequestCommand
)
170 payload
= clone
<T
>(payload
)
171 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
)
172 if (validate(payload
)) {
176 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: Command '${commandName}' request PDU is invalid: %j`,
179 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
181 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
),
182 'Request PDU is invalid',
184 JSON
.stringify(validate
.errors
, undefined, 2)
188 private getJsonRequestValidateFunction
<T
extends JsonType
>(
189 commandName
: RequestCommand
190 ): ValidateFunction
<JsonType
> {
191 if (!this.jsonValidateFunctions
.has(commandName
)) {
192 this.jsonValidateFunctions
.set(
194 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
195 this.ajv
.compile
<T
>(this.jsonSchemas
.get(commandName
)!).bind(this)
198 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
199 return this.jsonValidateFunctions
.get(commandName
)!
202 private validateIncomingRequestResponsePayload
<T
extends JsonType
>(
203 chargingStation
: ChargingStation
,
204 commandName
: RequestCommand
| IncomingRequestCommand
,
207 if (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false) {
211 !this.ocppResponseService
.jsonIncomingRequestResponseSchemas
.has(
212 commandName
as IncomingRequestCommand
216 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: No JSON schema found for command '${commandName}' PDU validation`
220 const validate
= this.getJsonRequestResponseValidateFunction
<T
>(
221 commandName
as IncomingRequestCommand
223 payload
= clone
<T
>(payload
)
224 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
)
225 if (validate(payload
)) {
229 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: Command '${commandName}' reponse PDU is invalid: %j`,
232 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
234 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
),
235 'Response PDU is invalid',
237 JSON
.stringify(validate
.errors
, undefined, 2)
241 private getJsonRequestResponseValidateFunction
<T
extends JsonType
>(
242 commandName
: IncomingRequestCommand
243 ): ValidateFunction
<JsonType
> {
244 if (!this.ocppResponseService
.jsonIncomingRequestResponseValidateFunctions
.has(commandName
)) {
245 this.ocppResponseService
.jsonIncomingRequestResponseValidateFunctions
.set(
248 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
249 .compile
<T
>(this.ocppResponseService
.jsonIncomingRequestResponseSchemas
.get(commandName
)!)
253 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
254 return this.ocppResponseService
.jsonIncomingRequestResponseValidateFunctions
.get(commandName
)!
257 private async internalSendMessage (
258 chargingStation
: ChargingStation
,
260 messagePayload
: JsonType
| OCPPError
,
261 messageType
: MessageType
,
262 commandName
: RequestCommand
| IncomingRequestCommand
,
263 params
?: RequestParams
264 ): Promise
<ResponseType
> {
266 ...defaultRequestParams
,
270 (chargingStation
.inUnknownState() && commandName
=== RequestCommand
.BOOT_NOTIFICATION
) ||
271 (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false &&
272 chargingStation
.inUnknownState()) ||
273 chargingStation
.inAcceptedState() ||
274 (chargingStation
.inPendingState() &&
275 (params
.triggerMessage
=== true || messageType
=== MessageType
.CALL_RESULT_MESSAGE
))
277 // eslint-disable-next-line @typescript-eslint/no-this-alias
279 // Send a message through wsConnection
280 return await new Promise
<ResponseType
>((resolve
, reject
) => {
282 * Function that will receive the request's response
285 * @param requestPayload -
287 const responseCallback
= (payload
: JsonType
, requestPayload
: JsonType
): void => {
288 if (chargingStation
.stationInfo
?.enableStatistics
=== true) {
289 chargingStation
.performanceStatistics
?.addRequestStatistic(
291 MessageType
.CALL_RESULT_MESSAGE
294 // Handle the request's response
295 self.ocppResponseService
298 commandName
as RequestCommand
,
307 chargingStation
.requests
.delete(messageId
)
308 chargingStation
.emit(ChargingStationEvents
.updated
)
313 * Function that will receive the request's error response
316 * @param requestStatistic -
318 const errorCallback
= (ocppError
: OCPPError
, requestStatistic
= true): void => {
319 if (requestStatistic
&& chargingStation
.stationInfo
?.enableStatistics
=== true) {
320 chargingStation
.performanceStatistics
?.addRequestStatistic(
322 MessageType
.CALL_ERROR_MESSAGE
326 `${chargingStation.logPrefix()} Error occurred at ${OCPPServiceUtils.getMessageTypeString(
328 )} command ${commandName} with PDU %j:`,
332 chargingStation
.requests
.delete(messageId
)
333 chargingStation
.emit(ChargingStationEvents
.updated
)
337 const handleSendError
= (ocppError
: OCPPError
): void => {
338 if (params
?.skipBufferingOnError
=== false) {
340 chargingStation
.bufferMessage(messageToSend
)
341 if (messageType
=== MessageType
.CALL_MESSAGE
) {
342 this.cacheRequestPromise(
345 messagePayload
as JsonType
,
352 params
?.skipBufferingOnError
=== true &&
353 messageType
=== MessageType
.CALL_MESSAGE
355 // Remove request from the cache
356 chargingStation
.requests
.delete(messageId
)
361 if (chargingStation
.stationInfo
?.enableStatistics
=== true) {
362 chargingStation
.performanceStatistics
?.addRequestStatistic(commandName
, messageType
)
364 const messageToSend
= this.buildMessageToSend(
371 // Check if wsConnection opened
372 if (chargingStation
.isWebSocketConnectionOpened()) {
373 const beginId
= PerformanceStatistics
.beginMeasure(commandName
)
374 const sendTimeout
= setTimeout(() => {
377 ErrorType
.GENERIC_ERROR
,
378 `Timeout ${formatDurationMilliSeconds(
379 OCPPConstants.OCPP_WEBSOCKET_TIMEOUT
381 params?.skipBufferingOnError === false ? '' : 'non '
382 }buffered message id '${messageId}' with content '${messageToSend}'`,
384 (messagePayload
as OCPPError
).details
387 }, OCPPConstants
.OCPP_WEBSOCKET_TIMEOUT
)
388 chargingStation
.wsConnection
?.send(messageToSend
, (error
?: Error) => {
389 PerformanceStatistics
.endMeasure(commandName
, beginId
)
390 clearTimeout(sendTimeout
)
393 `${chargingStation.logPrefix()} >> Command '${commandName}' sent ${OCPPServiceUtils.getMessageTypeString(
395 )} payload: ${messageToSend}`
397 if (messageType
=== MessageType
.CALL_MESSAGE
) {
398 this.cacheRequestPromise(
401 messagePayload
as JsonType
,
408 resolve(messagePayload
)
413 ErrorType
.GENERIC_ERROR
,
414 `WebSocket errored for ${
415 params?.skipBufferingOnError === false ? '' : 'non '
416 }buffered message id '${messageId}' with content '${messageToSend}'`,
418 { name
: error
.name
, message
: error
.message
, stack
: error
.stack
}
426 ErrorType
.GENERIC_ERROR
,
427 `WebSocket closed for ${
428 params?.skipBufferingOnError === false ? '' : 'non '
429 }buffered message id '${messageId}' with content '${messageToSend}'`,
431 (messagePayload
as OCPPError
).details
438 ErrorType
.SECURITY_ERROR
,
439 `Cannot send command ${commandName} PDU when the charging station is in ${chargingStation.bootNotificationResponse?.status} state on the central server`,
444 private buildMessageToSend (
445 chargingStation
: ChargingStation
,
447 messagePayload
: JsonType
| OCPPError
,
448 messageType
: MessageType
,
449 commandName
: RequestCommand
| IncomingRequestCommand
451 let messageToSend
: string
453 switch (messageType
) {
455 case MessageType
.CALL_MESSAGE
:
457 this.validateRequestPayload(chargingStation
, commandName
, messagePayload
as JsonType
)
458 messageToSend
= JSON
.stringify([
463 ] as OutgoingRequest
)
466 case MessageType
.CALL_RESULT_MESSAGE
:
468 this.validateIncomingRequestResponsePayload(
471 messagePayload
as JsonType
473 messageToSend
= JSON
.stringify([messageType
, messageId
, messagePayload
] as Response
)
476 case MessageType
.CALL_ERROR_MESSAGE
:
477 // Build Error Message
478 messageToSend
= JSON
.stringify([
481 (messagePayload
as OCPPError
).code
,
482 (messagePayload
as OCPPError
).message
,
483 (messagePayload
as OCPPError
).details
?? {
484 command
: (messagePayload
as OCPPError
).command
?? commandName
492 private cacheRequestPromise (
493 chargingStation
: ChargingStation
,
495 messagePayload
: JsonType
,
496 commandName
: RequestCommand
| IncomingRequestCommand
,
497 responseCallback
: ResponseCallback
,
498 errorCallback
: ErrorCallback
500 chargingStation
.requests
.set(messageId
, [
508 // eslint-disable-next-line @typescript-eslint/no-unused-vars
509 public abstract requestHandler
<ReqType
extends JsonType
, ResType
extends JsonType
>(
510 chargingStation
: ChargingStation
,
511 commandName
: RequestCommand
,
512 // FIXME: should be ReqType
513 commandParams
?: JsonType
,
514 params
?: RequestParams