1 import _Ajv
, { type ValidateFunction
} from
'ajv'
2 import _ajvFormats from
'ajv-formats'
4 import type { ChargingStation
} from
'../../charging-station/index.js'
5 import { OCPPError
} from
'../../exception/index.js'
6 import { PerformanceStatistics
} from
'../../performance/index.js'
12 type IncomingRequestCommand
,
20 type ResponseCallback
,
22 } from
'../../types/index.js'
25 formatDurationMilliSeconds
,
26 handleSendMessageError
,
28 } from
'../../utils/index.js'
29 import { OCPPConstants
} from
'./OCPPConstants.js'
30 import type { OCPPResponseService
} from
'./OCPPResponseService.js'
31 import { OCPPServiceUtils
} from
'./OCPPServiceUtils.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 ocppResponseService
: OCPPResponseService
49 protected readonly ajv
: Ajv
50 protected abstract payloadValidateFunctions
: Map
<RequestCommand
, ValidateFunction
<JsonType
>>
52 protected constructor (version
: OCPPVersion
, ocppResponseService
: OCPPResponseService
) {
53 this.version
= version
55 keywords
: ['javaType'],
56 multipleOfPrecision
: 2
59 this.ocppResponseService
= ocppResponseService
60 this.requestHandler
= this.requestHandler
.bind(this)
61 this.sendMessage
= this.sendMessage
.bind(this)
62 this.sendResponse
= this.sendResponse
.bind(this)
63 this.sendError
= this.sendError
.bind(this)
64 this.internalSendMessage
= this.internalSendMessage
.bind(this)
65 this.buildMessageToSend
= this.buildMessageToSend
.bind(this)
66 this.validateRequestPayload
= this.validateRequestPayload
.bind(this)
67 this.validateIncomingRequestResponsePayload
=
68 this.validateIncomingRequestResponsePayload
.bind(this)
71 public static getInstance
<T
extends OCPPRequestService
>(
72 this: new (ocppResponseService
: OCPPResponseService
) => T
,
73 ocppResponseService
: OCPPResponseService
75 if (OCPPRequestService
.instance
=== null) {
76 OCPPRequestService
.instance
= new this(ocppResponseService
)
78 return OCPPRequestService
.instance
as T
81 public async sendResponse (
82 chargingStation
: ChargingStation
,
84 messagePayload
: JsonType
,
85 commandName
: IncomingRequestCommand
86 ): Promise
<ResponseType
> {
88 // Send response message
89 return await this.internalSendMessage(
93 MessageType
.CALL_RESULT_MESSAGE
,
97 handleSendMessageError(chargingStation
, commandName
, error
as Error, {
104 public async sendError (
105 chargingStation
: ChargingStation
,
107 ocppError
: OCPPError
,
108 commandName
: RequestCommand
| IncomingRequestCommand
109 ): Promise
<ResponseType
> {
111 // Send error message
112 return await this.internalSendMessage(
116 MessageType
.CALL_ERROR_MESSAGE
,
120 handleSendMessageError(chargingStation
, commandName
, error
as Error)
125 protected async sendMessage (
126 chargingStation
: ChargingStation
,
128 messagePayload
: JsonType
,
129 commandName
: RequestCommand
,
130 params
?: RequestParams
131 ): Promise
<ResponseType
> {
133 ...defaultRequestParams
,
137 return await this.internalSendMessage(
141 MessageType
.CALL_MESSAGE
,
146 handleSendMessageError(chargingStation
, commandName
, error
as Error, {
147 throwError
: params
.throwError
153 private validateRequestPayload
<T
extends JsonType
>(
154 chargingStation
: ChargingStation
,
155 commandName
: RequestCommand
| IncomingRequestCommand
,
158 if (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false) {
161 if (!this.payloadValidateFunctions
.has(commandName
as RequestCommand
)) {
163 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: No JSON schema found for command '${commandName}' PDU validation`
167 const validate
= this.payloadValidateFunctions
.get(commandName
as RequestCommand
)
168 payload
= clone
<T
>(payload
)
169 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
)
170 if (validate
?.(payload
) === true) {
174 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: Command '${commandName}' request PDU is invalid: %j`,
177 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
179 OCPPServiceUtils
.ajvErrorsToErrorType(validate
?.errors
),
180 'Request PDU is invalid',
182 JSON
.stringify(validate
?.errors
, undefined, 2)
186 private validateIncomingRequestResponsePayload
<T
extends JsonType
>(
187 chargingStation
: ChargingStation
,
188 commandName
: RequestCommand
| IncomingRequestCommand
,
191 if (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false) {
195 !this.ocppResponseService
.incomingRequestResponsePayloadValidateFunctions
.has(
196 commandName
as IncomingRequestCommand
200 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: No JSON schema validation function found for command '${commandName}' PDU validation`
204 const validate
= this.ocppResponseService
.incomingRequestResponsePayloadValidateFunctions
.get(
205 commandName
as IncomingRequestCommand
207 payload
= clone
<T
>(payload
)
208 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
)
209 if (validate
?.(payload
) === true) {
213 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: Command '${commandName}' response PDU is invalid: %j`,
216 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
218 OCPPServiceUtils
.ajvErrorsToErrorType(validate
?.errors
),
219 'Response PDU is invalid',
221 JSON
.stringify(validate
?.errors
, undefined, 2)
225 private async internalSendMessage (
226 chargingStation
: ChargingStation
,
228 messagePayload
: JsonType
| OCPPError
,
229 messageType
: MessageType
,
230 commandName
: RequestCommand
| IncomingRequestCommand
,
231 params
?: RequestParams
232 ): Promise
<ResponseType
> {
234 ...defaultRequestParams
,
238 (chargingStation
.inUnknownState() && commandName
=== RequestCommand
.BOOT_NOTIFICATION
) ||
239 (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false &&
240 chargingStation
.inUnknownState()) ||
241 chargingStation
.inAcceptedState() ||
242 (chargingStation
.inPendingState() &&
243 (params
.triggerMessage
=== true || messageType
=== MessageType
.CALL_RESULT_MESSAGE
))
245 // eslint-disable-next-line @typescript-eslint/no-this-alias
247 // Send a message through wsConnection
248 return await new Promise
<ResponseType
>((resolve
, reject
: (reason
?: unknown
) => void) => {
250 * Function that will receive the request's response
253 * @param requestPayload -
255 const responseCallback
= (payload
: JsonType
, requestPayload
: JsonType
): void => {
256 if (chargingStation
.stationInfo
?.enableStatistics
=== true) {
257 chargingStation
.performanceStatistics
?.addRequestStatistic(
259 MessageType
.CALL_RESULT_MESSAGE
262 // Handle the request's response
263 self.ocppResponseService
266 commandName
as RequestCommand
,
275 chargingStation
.requests
.delete(messageId
)
276 chargingStation
.emit(ChargingStationEvents
.updated
)
281 * Function that will receive the request's error response
284 * @param requestStatistic -
286 const errorCallback
= (ocppError
: OCPPError
, requestStatistic
= true): void => {
287 if (requestStatistic
&& chargingStation
.stationInfo
?.enableStatistics
=== true) {
288 chargingStation
.performanceStatistics
?.addRequestStatistic(
290 MessageType
.CALL_ERROR_MESSAGE
294 `${chargingStation.logPrefix()} Error occurred at ${OCPPServiceUtils.getMessageTypeString(
296 )} command ${commandName} with PDU %j:`,
300 chargingStation
.requests
.delete(messageId
)
301 chargingStation
.emit(ChargingStationEvents
.updated
)
305 const handleSendError
= (ocppError
: OCPPError
): void => {
306 if (params
.skipBufferingOnError
=== false) {
308 chargingStation
.bufferMessage(messageToSend
)
309 if (messageType
=== MessageType
.CALL_MESSAGE
) {
310 this.setCachedRequest(
313 messagePayload
as JsonType
,
320 params
.skipBufferingOnError
=== true &&
321 messageType
=== MessageType
.CALL_MESSAGE
323 // Remove request from the cache
324 chargingStation
.requests
.delete(messageId
)
329 if (chargingStation
.stationInfo
?.enableStatistics
=== true) {
330 chargingStation
.performanceStatistics
?.addRequestStatistic(commandName
, messageType
)
332 const messageToSend
= this.buildMessageToSend(
339 // Check if wsConnection opened
340 if (chargingStation
.isWebSocketConnectionOpened()) {
341 const beginId
= PerformanceStatistics
.beginMeasure(commandName
)
342 const sendTimeout
= setTimeout(() => {
345 ErrorType
.GENERIC_ERROR
,
346 `Timeout ${formatDurationMilliSeconds(
347 OCPPConstants.OCPP_WEBSOCKET_TIMEOUT
349 params.skipBufferingOnError === false ? '' : 'non '
350 }buffered message id '${messageId}' with content '${messageToSend}'`,
352 (messagePayload
as OCPPError
).details
355 }, OCPPConstants
.OCPP_WEBSOCKET_TIMEOUT
)
356 chargingStation
.wsConnection
?.send(messageToSend
, (error
?: Error) => {
357 PerformanceStatistics
.endMeasure(commandName
, beginId
)
358 clearTimeout(sendTimeout
)
361 `${chargingStation.logPrefix()} >> Command '${commandName}' sent ${OCPPServiceUtils.getMessageTypeString(
363 )} payload: ${messageToSend}`
365 if (messageType
=== MessageType
.CALL_MESSAGE
) {
366 this.setCachedRequest(
369 messagePayload
as JsonType
,
376 resolve(messagePayload
)
381 ErrorType
.GENERIC_ERROR
,
382 `WebSocket errored for ${
383 params.skipBufferingOnError === false ? '' : 'non '
384 }buffered message id '${messageId}' with content '${messageToSend}'`,
386 { name
: error
.name
, message
: error
.message
, stack
: error
.stack
}
394 ErrorType
.GENERIC_ERROR
,
395 `WebSocket closed for ${
396 params.skipBufferingOnError === false ? '' : 'non '
397 }buffered message id '${messageId}' with content '${messageToSend}'`,
399 (messagePayload
as OCPPError
).details
406 ErrorType
.SECURITY_ERROR
,
407 `Cannot send command ${commandName} PDU when the charging station is in ${chargingStation.bootNotificationResponse?.status} state on the central server`,
412 private buildMessageToSend (
413 chargingStation
: ChargingStation
,
415 messagePayload
: JsonType
| OCPPError
,
416 messageType
: MessageType
,
417 commandName
: RequestCommand
| IncomingRequestCommand
419 let messageToSend
: string
421 switch (messageType
) {
423 case MessageType
.CALL_MESSAGE
:
425 this.validateRequestPayload(chargingStation
, commandName
, messagePayload
as JsonType
)
426 messageToSend
= JSON
.stringify([
429 commandName
as RequestCommand
,
430 messagePayload
as JsonType
431 ] satisfies OutgoingRequest
)
434 case MessageType
.CALL_RESULT_MESSAGE
:
436 this.validateIncomingRequestResponsePayload(
439 messagePayload
as JsonType
441 messageToSend
= JSON
.stringify([
444 messagePayload
as JsonType
445 ] satisfies Response
)
448 case MessageType
.CALL_ERROR_MESSAGE
:
449 // Build Error Message
450 messageToSend
= JSON
.stringify([
453 (messagePayload
as OCPPError
).code
,
454 (messagePayload
as OCPPError
).message
,
455 (messagePayload
as OCPPError
).details
?? {
456 command
: (messagePayload
as OCPPError
).command
458 ] satisfies ErrorResponse
)
464 private setCachedRequest (
465 chargingStation
: ChargingStation
,
467 messagePayload
: JsonType
,
468 commandName
: RequestCommand
| IncomingRequestCommand
,
469 responseCallback
: ResponseCallback
,
470 errorCallback
: ErrorCallback
472 chargingStation
.requests
.set(messageId
, [
480 // eslint-disable-next-line @typescript-eslint/no-unused-vars
481 public abstract requestHandler
<ReqType
extends JsonType
, ResType
extends JsonType
>(
482 chargingStation
: ChargingStation
,
483 commandName
: RequestCommand
,
484 // FIXME: should be ReqType
485 commandParams
?: JsonType
,
486 params
?: RequestParams