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'
33 convertDateToISOString
,
35 } from
'./OCPPServiceUtils.js'
36 type Ajv
= _Ajv
.default
37 // eslint-disable-next-line @typescript-eslint/no-redeclare
38 const Ajv
= _Ajv
.default
39 const ajvFormats
= _ajvFormats
.default
41 const moduleName
= 'OCPPRequestService'
43 const defaultRequestParams
: RequestParams
= {
44 skipBufferingOnError
: false,
45 triggerMessage
: false,
49 export abstract class OCPPRequestService
{
50 private static instance
: OCPPRequestService
| null = null
51 private readonly version
: OCPPVersion
52 private readonly ocppResponseService
: OCPPResponseService
53 protected readonly ajv
: Ajv
54 protected abstract payloadValidateFunctions
: Map
<RequestCommand
, ValidateFunction
<JsonType
>>
56 protected constructor (version
: OCPPVersion
, ocppResponseService
: OCPPResponseService
) {
57 this.version
= version
59 keywords
: ['javaType'],
60 multipleOfPrecision
: 2
63 this.ocppResponseService
= ocppResponseService
64 this.requestHandler
= this.requestHandler
.bind(this)
65 this.sendMessage
= this.sendMessage
.bind(this)
66 this.sendResponse
= this.sendResponse
.bind(this)
67 this.sendError
= this.sendError
.bind(this)
68 this.internalSendMessage
= this.internalSendMessage
.bind(this)
69 this.buildMessageToSend
= this.buildMessageToSend
.bind(this)
70 this.validateRequestPayload
= this.validateRequestPayload
.bind(this)
71 this.validateIncomingRequestResponsePayload
=
72 this.validateIncomingRequestResponsePayload
.bind(this)
75 public static getInstance
<T
extends OCPPRequestService
>(
76 this: new (ocppResponseService
: OCPPResponseService
) => T
,
77 ocppResponseService
: OCPPResponseService
79 if (OCPPRequestService
.instance
=== null) {
80 OCPPRequestService
.instance
= new this(ocppResponseService
)
82 return OCPPRequestService
.instance
as T
85 public async sendResponse (
86 chargingStation
: ChargingStation
,
88 messagePayload
: JsonType
,
89 commandName
: IncomingRequestCommand
90 ): Promise
<ResponseType
> {
92 // Send response message
93 return await this.internalSendMessage(
97 MessageType
.CALL_RESULT_MESSAGE
,
101 handleSendMessageError(
104 MessageType
.CALL_RESULT_MESSAGE
,
114 public async sendError (
115 chargingStation
: ChargingStation
,
117 ocppError
: OCPPError
,
118 commandName
: RequestCommand
| IncomingRequestCommand
119 ): Promise
<ResponseType
> {
121 // Send error message
122 return await this.internalSendMessage(
126 MessageType
.CALL_ERROR_MESSAGE
,
130 handleSendMessageError(
133 MessageType
.CALL_ERROR_MESSAGE
,
140 protected async sendMessage (
141 chargingStation
: ChargingStation
,
143 messagePayload
: JsonType
,
144 commandName
: RequestCommand
,
145 params
?: RequestParams
146 ): Promise
<ResponseType
> {
148 ...defaultRequestParams
,
152 return await this.internalSendMessage(
156 MessageType
.CALL_MESSAGE
,
161 handleSendMessageError(
164 MessageType
.CALL_MESSAGE
,
167 throwError
: params
.throwError
174 private validateRequestPayload
<T
extends JsonType
>(
175 chargingStation
: ChargingStation
,
176 commandName
: RequestCommand
| IncomingRequestCommand
,
179 if (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false) {
182 if (!this.payloadValidateFunctions
.has(commandName
as RequestCommand
)) {
184 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: No JSON schema found for command '${commandName}' PDU validation`
188 const validate
= this.payloadValidateFunctions
.get(commandName
as RequestCommand
)
189 payload
= clone
<T
>(payload
)
190 convertDateToISOString
<T
>(payload
)
191 if (validate
?.(payload
) === true) {
195 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: Command '${commandName}' request PDU is invalid: %j`,
198 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
200 ajvErrorsToErrorType(validate
?.errors
),
201 'Request PDU is invalid',
203 JSON
.stringify(validate
?.errors
, undefined, 2)
207 private validateIncomingRequestResponsePayload
<T
extends JsonType
>(
208 chargingStation
: ChargingStation
,
209 commandName
: RequestCommand
| IncomingRequestCommand
,
212 if (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false) {
216 !this.ocppResponseService
.incomingRequestResponsePayloadValidateFunctions
.has(
217 commandName
as IncomingRequestCommand
221 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: No JSON schema validation function found for command '${commandName}' PDU validation`
225 const validate
= this.ocppResponseService
.incomingRequestResponsePayloadValidateFunctions
.get(
226 commandName
as IncomingRequestCommand
228 payload
= clone
<T
>(payload
)
229 convertDateToISOString
<T
>(payload
)
230 if (validate
?.(payload
) === true) {
234 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: Command '${commandName}' response PDU is invalid: %j`,
237 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
239 ajvErrorsToErrorType(validate
?.errors
),
240 'Response PDU is invalid',
242 JSON
.stringify(validate
?.errors
, undefined, 2)
246 private async internalSendMessage (
247 chargingStation
: ChargingStation
,
249 messagePayload
: JsonType
| OCPPError
,
250 messageType
: MessageType
,
251 commandName
: RequestCommand
| IncomingRequestCommand
,
252 params
?: RequestParams
253 ): Promise
<ResponseType
> {
255 ...defaultRequestParams
,
259 (chargingStation
.inUnknownState() && commandName
=== RequestCommand
.BOOT_NOTIFICATION
) ||
260 (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false &&
261 chargingStation
.inUnknownState()) ||
262 chargingStation
.inAcceptedState() ||
263 (chargingStation
.inPendingState() &&
264 (params
.triggerMessage
=== true || messageType
=== MessageType
.CALL_RESULT_MESSAGE
))
266 // eslint-disable-next-line @typescript-eslint/no-this-alias
268 // Send a message through wsConnection
269 return await new Promise
<ResponseType
>((resolve
, reject
: (reason
?: unknown
) => void) => {
271 * Function that will receive the request's response
274 * @param requestPayload -
276 const responseCallback
= (payload
: JsonType
, requestPayload
: JsonType
): void => {
277 if (chargingStation
.stationInfo
?.enableStatistics
=== true) {
278 chargingStation
.performanceStatistics
?.addRequestStatistic(
280 MessageType
.CALL_RESULT_MESSAGE
283 // Handle the request's response
284 self.ocppResponseService
287 commandName
as RequestCommand
,
296 chargingStation
.requests
.delete(messageId
)
297 chargingStation
.emit(ChargingStationEvents
.updated
)
302 * Function that will receive the request's error response
305 * @param requestStatistic -
307 const errorCallback
= (ocppError
: OCPPError
, requestStatistic
= true): void => {
308 if (requestStatistic
&& chargingStation
.stationInfo
?.enableStatistics
=== true) {
309 chargingStation
.performanceStatistics
?.addRequestStatistic(
311 MessageType
.CALL_ERROR_MESSAGE
315 `${chargingStation.logPrefix()} Error occurred at ${getMessageTypeString(
317 )} command ${commandName} with PDU %j:`,
321 chargingStation
.requests
.delete(messageId
)
322 chargingStation
.emit(ChargingStationEvents
.updated
)
326 const handleSendError
= (ocppError
: OCPPError
): void => {
327 if (params
.skipBufferingOnError
=== false) {
329 chargingStation
.bufferMessage(messageToSend
)
330 if (messageType
=== MessageType
.CALL_MESSAGE
) {
331 this.setCachedRequest(
334 messagePayload
as JsonType
,
341 params
.skipBufferingOnError
=== true &&
342 messageType
=== MessageType
.CALL_MESSAGE
344 // Remove request from the cache
345 chargingStation
.requests
.delete(messageId
)
350 if (chargingStation
.stationInfo
?.enableStatistics
=== true) {
351 chargingStation
.performanceStatistics
?.addRequestStatistic(commandName
, messageType
)
353 const messageToSend
= this.buildMessageToSend(
360 // Check if wsConnection opened
361 if (chargingStation
.isWebSocketConnectionOpened()) {
362 const beginId
= PerformanceStatistics
.beginMeasure(commandName
)
363 const sendTimeout
= setTimeout(() => {
366 ErrorType
.GENERIC_ERROR
,
367 `Timeout ${formatDurationMilliSeconds(
368 OCPPConstants.OCPP_WEBSOCKET_TIMEOUT
370 params.skipBufferingOnError === false ? '' : 'non '
371 }buffered message id '${messageId}' with content '${messageToSend}'`,
373 (messagePayload
as OCPPError
).details
376 }, OCPPConstants
.OCPP_WEBSOCKET_TIMEOUT
)
377 chargingStation
.wsConnection
?.send(messageToSend
, (error
?: Error) => {
378 PerformanceStatistics
.endMeasure(commandName
, beginId
)
379 clearTimeout(sendTimeout
)
382 `${chargingStation.logPrefix()} >> Command '${commandName}' sent ${getMessageTypeString(
384 )} payload: ${messageToSend}`
386 if (messageType
=== MessageType
.CALL_MESSAGE
) {
387 this.setCachedRequest(
390 messagePayload
as JsonType
,
397 resolve(messagePayload
)
402 ErrorType
.GENERIC_ERROR
,
403 `WebSocket errored for ${
404 params.skipBufferingOnError === false ? '' : 'non '
405 }buffered message id '${messageId}' with content '${messageToSend}'`,
409 message
: error
.message
,
419 ErrorType
.GENERIC_ERROR
,
420 `WebSocket closed for ${
421 params.skipBufferingOnError === false ? '' : 'non '
422 }buffered message id '${messageId}' with content '${messageToSend}'`,
424 (messagePayload
as OCPPError
).details
431 ErrorType
.SECURITY_ERROR
,
432 `Cannot send command ${commandName} PDU when the charging station is in ${chargingStation.bootNotificationResponse?.status} state on the central server`,
437 private buildMessageToSend (
438 chargingStation
: ChargingStation
,
440 messagePayload
: JsonType
| OCPPError
,
441 messageType
: MessageType
,
442 commandName
: RequestCommand
| IncomingRequestCommand
444 let messageToSend
: string
446 switch (messageType
) {
448 case MessageType
.CALL_MESSAGE
:
450 this.validateRequestPayload(chargingStation
, commandName
, messagePayload
as JsonType
)
451 messageToSend
= JSON
.stringify([
454 commandName
as RequestCommand
,
455 messagePayload
as JsonType
456 ] satisfies OutgoingRequest
)
459 case MessageType
.CALL_RESULT_MESSAGE
:
461 this.validateIncomingRequestResponsePayload(
464 messagePayload
as JsonType
466 messageToSend
= JSON
.stringify([
469 messagePayload
as JsonType
470 ] satisfies Response
)
473 case MessageType
.CALL_ERROR_MESSAGE
:
474 // Build Error Message
475 messageToSend
= JSON
.stringify([
478 (messagePayload
as OCPPError
).code
,
479 (messagePayload
as OCPPError
).message
,
480 (messagePayload
as OCPPError
).details
?? {
481 command
: (messagePayload
as OCPPError
).command
483 ] satisfies ErrorResponse
)
489 private setCachedRequest (
490 chargingStation
: ChargingStation
,
492 messagePayload
: JsonType
,
493 commandName
: RequestCommand
| IncomingRequestCommand
,
494 responseCallback
: ResponseCallback
,
495 errorCallback
: ErrorCallback
497 chargingStation
.requests
.set(messageId
, [
505 public abstract requestHandler
<ReqType
extends JsonType
, ResType
extends JsonType
>(
506 chargingStation
: ChargingStation
,
507 commandName
: RequestCommand
,
508 commandParams
?: ReqType
,
509 params
?: RequestParams