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
,
32 } from
'../../utils/index.js'
33 type Ajv
= _Ajv
.default
34 // eslint-disable-next-line @typescript-eslint/no-redeclare
35 const Ajv
= _Ajv
.default
36 const ajvFormats
= _ajvFormats
.default
38 const moduleName
= 'OCPPRequestService'
40 const defaultRequestParams
: RequestParams
= {
41 skipBufferingOnError
: false,
42 triggerMessage
: false,
46 export abstract class OCPPRequestService
{
47 private static instance
: OCPPRequestService
| null = null
48 private readonly version
: OCPPVersion
49 private readonly ajv
: Ajv
50 private readonly ocppResponseService
: OCPPResponseService
51 private readonly jsonValidateFunctions
: Map
<RequestCommand
, ValidateFunction
<JsonType
>>
52 protected abstract jsonSchemas
: Map
<RequestCommand
, JSONSchemaType
<JsonType
>>
54 protected constructor (version
: OCPPVersion
, ocppResponseService
: OCPPResponseService
) {
55 this.version
= version
57 keywords
: ['javaType'],
58 multipleOfPrecision
: 2
61 this.jsonValidateFunctions
= new Map
<RequestCommand
, ValidateFunction
<JsonType
>>()
62 this.ocppResponseService
= ocppResponseService
63 this.requestHandler
= this.requestHandler
.bind(this) as <
64 // eslint-disable-next-line @typescript-eslint/no-unused-vars
65 ReqType
extends JsonType
,
66 ResType
extends JsonType
68 chargingStation
: ChargingStation
,
69 commandName
: RequestCommand
,
70 commandParams
?: JsonType
,
71 params
?: RequestParams
73 this.sendMessage
= this.sendMessage
.bind(this) as (
74 chargingStation
: ChargingStation
,
76 messagePayload
: JsonType
,
77 commandName
: RequestCommand
,
78 params
?: RequestParams
79 ) => Promise
<ResponseType
>
80 this.sendResponse
= this.sendResponse
.bind(this) as (
81 chargingStation
: ChargingStation
,
83 messagePayload
: JsonType
,
84 commandName
: IncomingRequestCommand
85 ) => Promise
<ResponseType
>
86 this.sendError
= this.sendError
.bind(this) as (
87 chargingStation
: ChargingStation
,
90 commandName
: RequestCommand
| IncomingRequestCommand
91 ) => Promise
<ResponseType
>
92 this.internalSendMessage
= this.internalSendMessage
.bind(this) as (
93 chargingStation
: ChargingStation
,
95 messagePayload
: JsonType
| OCPPError
,
96 messageType
: MessageType
,
97 commandName
: RequestCommand
| IncomingRequestCommand
,
98 params
?: RequestParams
99 ) => Promise
<ResponseType
>
100 this.buildMessageToSend
= this.buildMessageToSend
.bind(this) as (
101 chargingStation
: ChargingStation
,
103 messagePayload
: JsonType
| OCPPError
,
104 messageType
: MessageType
,
105 commandName
: RequestCommand
| IncomingRequestCommand
107 this.validateRequestPayload
= this.validateRequestPayload
.bind(this) as <T
extends JsonType
>(
108 chargingStation
: ChargingStation
,
109 commandName
: RequestCommand
| IncomingRequestCommand
,
112 this.validateIncomingRequestResponsePayload
= this.validateIncomingRequestResponsePayload
.bind(
114 ) as <T
extends JsonType
>(
115 chargingStation
: ChargingStation
,
116 commandName
: RequestCommand
| IncomingRequestCommand
,
121 public static getInstance
<T
extends OCPPRequestService
>(
122 this: new (ocppResponseService
: OCPPResponseService
) => T
,
123 ocppResponseService
: OCPPResponseService
125 if (OCPPRequestService
.instance
=== null) {
126 OCPPRequestService
.instance
= new this(ocppResponseService
)
128 return OCPPRequestService
.instance
as T
131 public async sendResponse (
132 chargingStation
: ChargingStation
,
134 messagePayload
: JsonType
,
135 commandName
: IncomingRequestCommand
136 ): Promise
<ResponseType
> {
138 // Send response message
139 return await this.internalSendMessage(
143 MessageType
.CALL_RESULT_MESSAGE
,
147 handleSendMessageError(chargingStation
, commandName
, error
as Error, {
154 public async sendError (
155 chargingStation
: ChargingStation
,
157 ocppError
: OCPPError
,
158 commandName
: RequestCommand
| IncomingRequestCommand
159 ): Promise
<ResponseType
> {
161 // Send error message
162 return await this.internalSendMessage(
166 MessageType
.CALL_ERROR_MESSAGE
,
170 handleSendMessageError(chargingStation
, commandName
, error
as Error)
175 protected async sendMessage (
176 chargingStation
: ChargingStation
,
178 messagePayload
: JsonType
,
179 commandName
: RequestCommand
,
180 params
?: RequestParams
181 ): Promise
<ResponseType
> {
183 ...defaultRequestParams
,
187 return await this.internalSendMessage(
191 MessageType
.CALL_MESSAGE
,
196 handleSendMessageError(chargingStation
, commandName
, error
as Error, {
197 throwError
: params
.throwError
203 private validateRequestPayload
<T
extends JsonType
>(
204 chargingStation
: ChargingStation
,
205 commandName
: RequestCommand
| IncomingRequestCommand
,
208 if (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false) {
211 if (!this.jsonSchemas
.has(commandName
as RequestCommand
)) {
213 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: No JSON schema found for command '${commandName}' PDU validation`
217 const validate
= this.getJsonRequestValidateFunction
<T
>(commandName
as RequestCommand
)
218 payload
= cloneObject
<T
>(payload
)
219 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
)
220 if (validate(payload
)) {
224 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: Command '${commandName}' request PDU is invalid: %j`,
227 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
229 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
),
230 'Request PDU is invalid',
232 JSON
.stringify(validate
.errors
, undefined, 2)
236 private getJsonRequestValidateFunction
<T
extends JsonType
>(
237 commandName
: RequestCommand
238 ): ValidateFunction
<JsonType
> {
239 if (!this.jsonValidateFunctions
.has(commandName
)) {
240 this.jsonValidateFunctions
.set(
242 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
243 this.ajv
.compile
<T
>(this.jsonSchemas
.get(commandName
)!).bind(this)
246 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
247 return this.jsonValidateFunctions
.get(commandName
)!
250 private validateIncomingRequestResponsePayload
<T
extends JsonType
>(
251 chargingStation
: ChargingStation
,
252 commandName
: RequestCommand
| IncomingRequestCommand
,
255 if (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false) {
259 !this.ocppResponseService
.jsonIncomingRequestResponseSchemas
.has(
260 commandName
as IncomingRequestCommand
264 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: No JSON schema found for command '${commandName}' PDU validation`
268 const validate
= this.getJsonRequestResponseValidateFunction
<T
>(
269 commandName
as IncomingRequestCommand
271 payload
= cloneObject
<T
>(payload
)
272 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
)
273 if (validate(payload
)) {
277 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: Command '${commandName}' reponse PDU is invalid: %j`,
280 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
282 OCPPServiceUtils
.ajvErrorsToErrorType(validate
.errors
),
283 'Response PDU is invalid',
285 JSON
.stringify(validate
.errors
, undefined, 2)
289 private getJsonRequestResponseValidateFunction
<T
extends JsonType
>(
290 commandName
: IncomingRequestCommand
291 ): ValidateFunction
<JsonType
> {
292 if (!this.ocppResponseService
.jsonIncomingRequestResponseValidateFunctions
.has(commandName
)) {
293 this.ocppResponseService
.jsonIncomingRequestResponseValidateFunctions
.set(
296 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
297 .compile
<T
>(this.ocppResponseService
.jsonIncomingRequestResponseSchemas
.get(commandName
)!)
301 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
302 return this.ocppResponseService
.jsonIncomingRequestResponseValidateFunctions
.get(commandName
)!
305 private async internalSendMessage (
306 chargingStation
: ChargingStation
,
308 messagePayload
: JsonType
| OCPPError
,
309 messageType
: MessageType
,
310 commandName
: RequestCommand
| IncomingRequestCommand
,
311 params
?: RequestParams
312 ): Promise
<ResponseType
> {
314 ...defaultRequestParams
,
318 (chargingStation
.inUnknownState() && commandName
=== RequestCommand
.BOOT_NOTIFICATION
) ||
319 (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false &&
320 chargingStation
.inUnknownState()) ||
321 chargingStation
.inAcceptedState() ||
322 (chargingStation
.inPendingState() &&
323 (params
.triggerMessage
=== true || messageType
=== MessageType
.CALL_RESULT_MESSAGE
))
325 // eslint-disable-next-line @typescript-eslint/no-this-alias
327 // Send a message through wsConnection
328 return await new Promise
<ResponseType
>((resolve
, reject
) => {
330 * Function that will receive the request's response
333 * @param requestPayload -
335 const responseCallback
= (payload
: JsonType
, requestPayload
: JsonType
): void => {
336 if (chargingStation
.stationInfo
?.enableStatistics
=== true) {
337 chargingStation
.performanceStatistics
?.addRequestStatistic(
339 MessageType
.CALL_RESULT_MESSAGE
342 // Handle the request's response
343 self.ocppResponseService
346 commandName
as RequestCommand
,
355 chargingStation
.requests
.delete(messageId
)
356 chargingStation
.emit(ChargingStationEvents
.updated
)
361 * Function that will receive the request's error response
364 * @param requestStatistic -
366 const errorCallback
= (ocppError
: OCPPError
, requestStatistic
= true): void => {
367 if (requestStatistic
&& chargingStation
.stationInfo
?.enableStatistics
=== true) {
368 chargingStation
.performanceStatistics
?.addRequestStatistic(
370 MessageType
.CALL_ERROR_MESSAGE
374 `${chargingStation.logPrefix()} Error occurred at ${OCPPServiceUtils.getMessageTypeString(
376 )} command ${commandName} with PDU %j:`,
380 chargingStation
.requests
.delete(messageId
)
381 chargingStation
.emit(ChargingStationEvents
.updated
)
385 const handleSendError
= (ocppError
: OCPPError
): void => {
386 if (params
?.skipBufferingOnError
=== false) {
388 chargingStation
.bufferMessage(messageToSend
)
389 if (messageType
=== MessageType
.CALL_MESSAGE
) {
390 this.cacheRequestPromise(
393 messagePayload
as JsonType
,
400 params
?.skipBufferingOnError
=== true &&
401 messageType
=== MessageType
.CALL_MESSAGE
403 // Remove request from the cache
404 chargingStation
.requests
.delete(messageId
)
409 if (chargingStation
.stationInfo
?.enableStatistics
=== true) {
410 chargingStation
.performanceStatistics
?.addRequestStatistic(commandName
, messageType
)
412 const messageToSend
= this.buildMessageToSend(
419 // Check if wsConnection opened
420 if (chargingStation
.isWebSocketConnectionOpened()) {
421 const beginId
= PerformanceStatistics
.beginMeasure(commandName
)
422 const sendTimeout
= setTimeout(() => {
425 ErrorType
.GENERIC_ERROR
,
426 `Timeout ${formatDurationMilliSeconds(
427 OCPPConstants.OCPP_WEBSOCKET_TIMEOUT
429 params?.skipBufferingOnError === false ? '' : 'non '
430 }buffered message id '${messageId}' with content '${messageToSend}'`,
432 (messagePayload
as OCPPError
).details
435 }, OCPPConstants
.OCPP_WEBSOCKET_TIMEOUT
)
436 chargingStation
.wsConnection
?.send(messageToSend
, (error
?: Error) => {
437 PerformanceStatistics
.endMeasure(commandName
, beginId
)
438 clearTimeout(sendTimeout
)
439 if (isNullOrUndefined(error
)) {
441 `${chargingStation.logPrefix()} >> Command '${commandName}' sent ${OCPPServiceUtils.getMessageTypeString(
443 )} payload: ${messageToSend}`
445 if (messageType
=== MessageType
.CALL_MESSAGE
) {
446 this.cacheRequestPromise(
449 messagePayload
as JsonType
,
456 resolve(messagePayload
)
458 } else if (error
!= null) {
461 ErrorType
.GENERIC_ERROR
,
462 `WebSocket errored for ${
463 params?.skipBufferingOnError === false ? '' : 'non '
464 }buffered message id '${messageId}' with content '${messageToSend}'`,
466 { name
: error
.name
, message
: error
.message
, stack
: error
.stack
}
474 ErrorType
.GENERIC_ERROR
,
475 `WebSocket closed for ${
476 params?.skipBufferingOnError === false ? '' : 'non '
477 }buffered message id '${messageId}' with content '${messageToSend}'`,
479 (messagePayload
as OCPPError
).details
486 ErrorType
.SECURITY_ERROR
,
487 `Cannot send command ${commandName} PDU when the charging station is in ${chargingStation?.bootNotificationResponse?.status} state on the central server`,
492 private buildMessageToSend (
493 chargingStation
: ChargingStation
,
495 messagePayload
: JsonType
| OCPPError
,
496 messageType
: MessageType
,
497 commandName
: RequestCommand
| IncomingRequestCommand
499 let messageToSend
: string
501 switch (messageType
) {
503 case MessageType
.CALL_MESSAGE
:
505 this.validateRequestPayload(chargingStation
, commandName
, messagePayload
as JsonType
)
506 messageToSend
= JSON
.stringify([
511 ] as OutgoingRequest
)
514 case MessageType
.CALL_RESULT_MESSAGE
:
516 this.validateIncomingRequestResponsePayload(
519 messagePayload
as JsonType
521 messageToSend
= JSON
.stringify([messageType
, messageId
, messagePayload
] as Response
)
524 case MessageType
.CALL_ERROR_MESSAGE
:
525 // Build Error Message
526 messageToSend
= JSON
.stringify([
529 (messagePayload
as OCPPError
).code
,
530 (messagePayload
as OCPPError
).message
,
531 (messagePayload
as OCPPError
).details
?? {
532 command
: (messagePayload
as OCPPError
).command
?? commandName
540 private cacheRequestPromise (
541 chargingStation
: ChargingStation
,
543 messagePayload
: JsonType
,
544 commandName
: RequestCommand
| IncomingRequestCommand
,
545 responseCallback
: ResponseCallback
,
546 errorCallback
: ErrorCallback
548 chargingStation
.requests
.set(messageId
, [
556 // eslint-disable-next-line @typescript-eslint/no-unused-vars
557 public abstract requestHandler
<ReqType
extends JsonType
, ResType
extends JsonType
>(
558 chargingStation
: ChargingStation
,
559 commandName
: RequestCommand
,
560 // FIXME: should be ReqType
561 commandParams
?: JsonType
,
562 params
?: RequestParams