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) as <
63 // eslint-disable-next-line @typescript-eslint/no-unused-vars
64 ReqType
extends JsonType
,
65 ResType
extends JsonType
,
67 chargingStation
: ChargingStation
,
68 commandName
: RequestCommand
,
69 commandParams
?: JsonType
,
70 params
?: RequestParams
72 this.sendMessage
= this.sendMessage
.bind(this) as (
73 chargingStation
: ChargingStation
,
75 messagePayload
: JsonType
,
76 commandName
: RequestCommand
,
77 params
?: RequestParams
78 ) => Promise
<ResponseType
>
79 this.sendResponse
= this.sendResponse
.bind(this) as (
80 chargingStation
: ChargingStation
,
82 messagePayload
: JsonType
,
83 commandName
: IncomingRequestCommand
84 ) => Promise
<ResponseType
>
85 this.sendError
= this.sendError
.bind(this) as (
86 chargingStation
: ChargingStation
,
89 commandName
: RequestCommand
| IncomingRequestCommand
90 ) => Promise
<ResponseType
>
91 this.internalSendMessage
= this.internalSendMessage
.bind(this) as (
92 chargingStation
: ChargingStation
,
94 messagePayload
: JsonType
| OCPPError
,
95 messageType
: MessageType
,
96 commandName
: RequestCommand
| IncomingRequestCommand
,
97 params
?: RequestParams
98 ) => Promise
<ResponseType
>
99 this.buildMessageToSend
= this.buildMessageToSend
.bind(this) as (
100 chargingStation
: ChargingStation
,
102 messagePayload
: JsonType
| OCPPError
,
103 messageType
: MessageType
,
104 commandName
: RequestCommand
| IncomingRequestCommand
106 this.validateRequestPayload
= this.validateRequestPayload
.bind(this) as <T
extends JsonType
>(
107 chargingStation
: ChargingStation
,
108 commandName
: RequestCommand
| IncomingRequestCommand
,
111 this.validateIncomingRequestResponsePayload
= this.validateIncomingRequestResponsePayload
.bind(
113 ) as <T
extends JsonType
>(
114 chargingStation
: ChargingStation
,
115 commandName
: RequestCommand
| IncomingRequestCommand
,
120 public static getInstance
<T
extends OCPPRequestService
>(
121 this: new (ocppResponseService
: OCPPResponseService
) => T
,
122 ocppResponseService
: OCPPResponseService
124 if (OCPPRequestService
.instance
=== null) {
125 OCPPRequestService
.instance
= new this(ocppResponseService
)
127 return OCPPRequestService
.instance
as T
130 public async sendResponse (
131 chargingStation
: ChargingStation
,
133 messagePayload
: JsonType
,
134 commandName
: IncomingRequestCommand
135 ): Promise
<ResponseType
> {
137 // Send response message
138 return await this.internalSendMessage(
142 MessageType
.CALL_RESULT_MESSAGE
,
146 handleSendMessageError(chargingStation
, commandName
, error
as Error, {
153 public async sendError (
154 chargingStation
: ChargingStation
,
156 ocppError
: OCPPError
,
157 commandName
: RequestCommand
| IncomingRequestCommand
158 ): Promise
<ResponseType
> {
160 // Send error message
161 return await this.internalSendMessage(
165 MessageType
.CALL_ERROR_MESSAGE
,
169 handleSendMessageError(chargingStation
, commandName
, error
as Error)
174 protected async sendMessage (
175 chargingStation
: ChargingStation
,
177 messagePayload
: JsonType
,
178 commandName
: RequestCommand
,
179 params
?: RequestParams
180 ): Promise
<ResponseType
> {
182 ...defaultRequestParams
,
186 return await this.internalSendMessage(
190 MessageType
.CALL_MESSAGE
,
195 handleSendMessageError(chargingStation
, commandName
, error
as Error, {
196 throwError
: params
.throwError
202 private validateRequestPayload
<T
extends JsonType
>(
203 chargingStation
: ChargingStation
,
204 commandName
: RequestCommand
| IncomingRequestCommand
,
207 if (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false) {
210 if (!this.jsonSchemas
.has(commandName
as RequestCommand
)) {
212 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: No JSON schema found for command '${commandName}' PDU validation`
216 const validate
= this.getJsonRequestValidateFunction
<T
>(commandName
as RequestCommand
)
217 payload
= cloneObject
<T
>(payload
)
218 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
)
219 if (validate(payload
)) {
223 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: Command '${commandName}' request PDU is invalid: %j`,
226 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
228 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
),
229 'Request PDU is invalid',
231 JSON
.stringify(validate
.errors
, undefined, 2)
235 private getJsonRequestValidateFunction
<T
extends JsonType
>(
236 commandName
: RequestCommand
237 ): ValidateFunction
<JsonType
> {
238 if (!this.jsonValidateFunctions
.has(commandName
)) {
239 this.jsonValidateFunctions
.set(
241 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
242 this.ajv
.compile
<T
>(this.jsonSchemas
.get(commandName
)!).bind(this)
245 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
246 return this.jsonValidateFunctions
.get(commandName
)!
249 private validateIncomingRequestResponsePayload
<T
extends JsonType
>(
250 chargingStation
: ChargingStation
,
251 commandName
: RequestCommand
| IncomingRequestCommand
,
254 if (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false) {
258 !this.ocppResponseService
.jsonIncomingRequestResponseSchemas
.has(
259 commandName
as IncomingRequestCommand
263 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: No JSON schema found for command '${commandName}' PDU validation`
267 const validate
= this.getJsonRequestResponseValidateFunction
<T
>(
268 commandName
as IncomingRequestCommand
270 payload
= cloneObject
<T
>(payload
)
271 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
)
272 if (validate(payload
)) {
276 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: Command '${commandName}' reponse PDU is invalid: %j`,
279 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
281 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
),
282 'Response PDU is invalid',
284 JSON
.stringify(validate
.errors
, undefined, 2)
288 private getJsonRequestResponseValidateFunction
<T
extends JsonType
>(
289 commandName
: IncomingRequestCommand
290 ): ValidateFunction
<JsonType
> {
291 if (!this.ocppResponseService
.jsonIncomingRequestResponseValidateFunctions
.has(commandName
)) {
292 this.ocppResponseService
.jsonIncomingRequestResponseValidateFunctions
.set(
295 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
296 .compile
<T
>(this.ocppResponseService
.jsonIncomingRequestResponseSchemas
.get(commandName
)!)
300 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
301 return this.ocppResponseService
.jsonIncomingRequestResponseValidateFunctions
.get(commandName
)!
304 private async internalSendMessage (
305 chargingStation
: ChargingStation
,
307 messagePayload
: JsonType
| OCPPError
,
308 messageType
: MessageType
,
309 commandName
: RequestCommand
| IncomingRequestCommand
,
310 params
?: RequestParams
311 ): Promise
<ResponseType
> {
313 ...defaultRequestParams
,
317 (chargingStation
.inUnknownState() && commandName
=== RequestCommand
.BOOT_NOTIFICATION
) ||
318 (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false &&
319 chargingStation
.inUnknownState()) ||
320 chargingStation
.inAcceptedState() ||
321 (chargingStation
.inPendingState() &&
322 (params
.triggerMessage
=== true || messageType
=== MessageType
.CALL_RESULT_MESSAGE
))
324 // eslint-disable-next-line @typescript-eslint/no-this-alias
326 // Send a message through wsConnection
327 return await new Promise
<ResponseType
>((resolve
, reject
) => {
329 * Function that will receive the request's response
332 * @param requestPayload -
334 const responseCallback
= (payload
: JsonType
, requestPayload
: JsonType
): void => {
335 if (chargingStation
.stationInfo
?.enableStatistics
=== true) {
336 chargingStation
.performanceStatistics
?.addRequestStatistic(
338 MessageType
.CALL_RESULT_MESSAGE
341 // Handle the request's response
342 self.ocppResponseService
345 commandName
as RequestCommand
,
354 chargingStation
.requests
.delete(messageId
)
355 chargingStation
.emit(ChargingStationEvents
.updated
)
360 * Function that will receive the request's error response
363 * @param requestStatistic -
365 const errorCallback
= (ocppError
: OCPPError
, requestStatistic
= true): void => {
366 if (requestStatistic
&& chargingStation
.stationInfo
?.enableStatistics
=== true) {
367 chargingStation
.performanceStatistics
?.addRequestStatistic(
369 MessageType
.CALL_ERROR_MESSAGE
373 `${chargingStation.logPrefix()} Error occurred at ${OCPPServiceUtils.getMessageTypeString(
375 )} command ${commandName} with PDU %j:`,
379 chargingStation
.requests
.delete(messageId
)
380 chargingStation
.emit(ChargingStationEvents
.updated
)
384 const handleSendError
= (ocppError
: OCPPError
): void => {
385 if (params
?.skipBufferingOnError
=== false) {
387 chargingStation
.bufferMessage(messageToSend
)
388 if (messageType
=== MessageType
.CALL_MESSAGE
) {
389 this.cacheRequestPromise(
392 messagePayload
as JsonType
,
399 params
?.skipBufferingOnError
=== true &&
400 messageType
=== MessageType
.CALL_MESSAGE
402 // Remove request from the cache
403 chargingStation
.requests
.delete(messageId
)
408 if (chargingStation
.stationInfo
?.enableStatistics
=== true) {
409 chargingStation
.performanceStatistics
?.addRequestStatistic(commandName
, messageType
)
411 const messageToSend
= this.buildMessageToSend(
418 // Check if wsConnection opened
419 if (chargingStation
.isWebSocketConnectionOpened()) {
420 const beginId
= PerformanceStatistics
.beginMeasure(commandName
)
421 const sendTimeout
= setTimeout(() => {
424 ErrorType
.GENERIC_ERROR
,
425 `Timeout ${formatDurationMilliSeconds(
426 OCPPConstants.OCPP_WEBSOCKET_TIMEOUT
428 params?.skipBufferingOnError === false ? '' : 'non '
429 }buffered message id '${messageId}' with content '${messageToSend}'`,
431 (messagePayload
as OCPPError
).details
434 }, OCPPConstants
.OCPP_WEBSOCKET_TIMEOUT
)
435 chargingStation
.wsConnection
?.send(messageToSend
, (error
?: Error) => {
436 PerformanceStatistics
.endMeasure(commandName
, beginId
)
437 clearTimeout(sendTimeout
)
440 `${chargingStation.logPrefix()} >> Command '${commandName}' sent ${OCPPServiceUtils.getMessageTypeString(
442 )} payload: ${messageToSend}`
444 if (messageType
=== MessageType
.CALL_MESSAGE
) {
445 this.cacheRequestPromise(
448 messagePayload
as JsonType
,
455 resolve(messagePayload
)
460 ErrorType
.GENERIC_ERROR
,
461 `WebSocket errored for ${
462 params?.skipBufferingOnError === false ? '' : 'non '
463 }buffered message id '${messageId}' with content '${messageToSend}'`,
465 { name
: error
.name
, message
: error
.message
, stack
: error
.stack
}
473 ErrorType
.GENERIC_ERROR
,
474 `WebSocket closed for ${
475 params?.skipBufferingOnError === false ? '' : 'non '
476 }buffered message id '${messageId}' with content '${messageToSend}'`,
478 (messagePayload
as OCPPError
).details
485 ErrorType
.SECURITY_ERROR
,
486 `Cannot send command ${commandName} PDU when the charging station is in ${chargingStation.bootNotificationResponse?.status} state on the central server`,
491 private buildMessageToSend (
492 chargingStation
: ChargingStation
,
494 messagePayload
: JsonType
| OCPPError
,
495 messageType
: MessageType
,
496 commandName
: RequestCommand
| IncomingRequestCommand
498 let messageToSend
: string
500 switch (messageType
) {
502 case MessageType
.CALL_MESSAGE
:
504 this.validateRequestPayload(chargingStation
, commandName
, messagePayload
as JsonType
)
505 messageToSend
= JSON
.stringify([
510 ] as OutgoingRequest
)
513 case MessageType
.CALL_RESULT_MESSAGE
:
515 this.validateIncomingRequestResponsePayload(
518 messagePayload
as JsonType
520 messageToSend
= JSON
.stringify([messageType
, messageId
, messagePayload
] as Response
)
523 case MessageType
.CALL_ERROR_MESSAGE
:
524 // Build Error Message
525 messageToSend
= JSON
.stringify([
528 (messagePayload
as OCPPError
).code
,
529 (messagePayload
as OCPPError
).message
,
530 (messagePayload
as OCPPError
).details
?? {
531 command
: (messagePayload
as OCPPError
).command
?? commandName
539 private cacheRequestPromise (
540 chargingStation
: ChargingStation
,
542 messagePayload
: JsonType
,
543 commandName
: RequestCommand
| IncomingRequestCommand
,
544 responseCallback
: ResponseCallback
,
545 errorCallback
: ErrorCallback
547 chargingStation
.requests
.set(messageId
, [
555 // eslint-disable-next-line @typescript-eslint/no-unused-vars
556 public abstract requestHandler
<ReqType
extends JsonType
, ResType
extends JsonType
>(
557 chargingStation
: ChargingStation
,
558 commandName
: RequestCommand
,
559 // FIXME: should be ReqType
560 commandParams
?: JsonType
,
561 params
?: RequestParams