1 import _Ajv
, { 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 ocppResponseService
: OCPPResponseService
49 protected readonly ajv
: Ajv
50 protected abstract jsonSchemasValidateFunction
: 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.jsonSchemasValidateFunction
.has(commandName
as RequestCommand
)) {
163 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: No JSON schema found for command '${commandName}' PDU validation`
167 const validate
= this.jsonSchemasValidateFunction
.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
.jsonSchemasIncomingRequestResponseValidateFunction
.has(
196 commandName
as IncomingRequestCommand
200 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: No JSON schema validation function found for command '${commandName}' PDU validation`
205 this.ocppResponseService
.jsonSchemasIncomingRequestResponseValidateFunction
.get(
206 commandName
as IncomingRequestCommand
208 payload
= clone
<T
>(payload
)
209 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
)
210 if (validate
?.(payload
) === true) {
214 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: Command '${commandName}' response PDU is invalid: %j`,
217 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
219 OCPPServiceUtils
.ajvErrorsToErrorType(validate
?.errors
),
220 'Response PDU is invalid',
222 JSON
.stringify(validate
?.errors
, undefined, 2)
226 private async internalSendMessage (
227 chargingStation
: ChargingStation
,
229 messagePayload
: JsonType
| OCPPError
,
230 messageType
: MessageType
,
231 commandName
: RequestCommand
| IncomingRequestCommand
,
232 params
?: RequestParams
233 ): Promise
<ResponseType
> {
235 ...defaultRequestParams
,
239 (chargingStation
.inUnknownState() && commandName
=== RequestCommand
.BOOT_NOTIFICATION
) ||
240 (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false &&
241 chargingStation
.inUnknownState()) ||
242 chargingStation
.inAcceptedState() ||
243 (chargingStation
.inPendingState() &&
244 (params
.triggerMessage
=== true || messageType
=== MessageType
.CALL_RESULT_MESSAGE
))
246 // eslint-disable-next-line @typescript-eslint/no-this-alias
248 // Send a message through wsConnection
249 return await new Promise
<ResponseType
>((resolve
, reject
) => {
251 * Function that will receive the request's response
254 * @param requestPayload -
256 const responseCallback
= (payload
: JsonType
, requestPayload
: JsonType
): void => {
257 if (chargingStation
.stationInfo
?.enableStatistics
=== true) {
258 chargingStation
.performanceStatistics
?.addRequestStatistic(
260 MessageType
.CALL_RESULT_MESSAGE
263 // Handle the request's response
264 self.ocppResponseService
267 commandName
as RequestCommand
,
276 chargingStation
.requests
.delete(messageId
)
277 chargingStation
.emit(ChargingStationEvents
.updated
)
282 * Function that will receive the request's error response
285 * @param requestStatistic -
287 const errorCallback
= (ocppError
: OCPPError
, requestStatistic
= true): void => {
288 if (requestStatistic
&& chargingStation
.stationInfo
?.enableStatistics
=== true) {
289 chargingStation
.performanceStatistics
?.addRequestStatistic(
291 MessageType
.CALL_ERROR_MESSAGE
295 `${chargingStation.logPrefix()} Error occurred at ${OCPPServiceUtils.getMessageTypeString(
297 )} command ${commandName} with PDU %j:`,
301 chargingStation
.requests
.delete(messageId
)
302 chargingStation
.emit(ChargingStationEvents
.updated
)
306 const handleSendError
= (ocppError
: OCPPError
): void => {
307 if (params
?.skipBufferingOnError
=== false) {
309 chargingStation
.bufferMessage(messageToSend
)
310 if (messageType
=== MessageType
.CALL_MESSAGE
) {
311 this.cacheRequestPromise(
314 messagePayload
as JsonType
,
321 params
?.skipBufferingOnError
=== true &&
322 messageType
=== MessageType
.CALL_MESSAGE
324 // Remove request from the cache
325 chargingStation
.requests
.delete(messageId
)
330 if (chargingStation
.stationInfo
?.enableStatistics
=== true) {
331 chargingStation
.performanceStatistics
?.addRequestStatistic(commandName
, messageType
)
333 const messageToSend
= this.buildMessageToSend(
340 // Check if wsConnection opened
341 if (chargingStation
.isWebSocketConnectionOpened()) {
342 const beginId
= PerformanceStatistics
.beginMeasure(commandName
)
343 const sendTimeout
= setTimeout(() => {
346 ErrorType
.GENERIC_ERROR
,
347 `Timeout ${formatDurationMilliSeconds(
348 OCPPConstants.OCPP_WEBSOCKET_TIMEOUT
350 params?.skipBufferingOnError === false ? '' : 'non '
351 }buffered message id '${messageId}' with content '${messageToSend}'`,
353 (messagePayload
as OCPPError
).details
356 }, OCPPConstants
.OCPP_WEBSOCKET_TIMEOUT
)
357 chargingStation
.wsConnection
?.send(messageToSend
, (error
?: Error) => {
358 PerformanceStatistics
.endMeasure(commandName
, beginId
)
359 clearTimeout(sendTimeout
)
362 `${chargingStation.logPrefix()} >> Command '${commandName}' sent ${OCPPServiceUtils.getMessageTypeString(
364 )} payload: ${messageToSend}`
366 if (messageType
=== MessageType
.CALL_MESSAGE
) {
367 this.cacheRequestPromise(
370 messagePayload
as JsonType
,
377 resolve(messagePayload
)
382 ErrorType
.GENERIC_ERROR
,
383 `WebSocket errored for ${
384 params?.skipBufferingOnError === false ? '' : 'non '
385 }buffered message id '${messageId}' with content '${messageToSend}'`,
387 { name
: error
.name
, message
: error
.message
, stack
: error
.stack
}
395 ErrorType
.GENERIC_ERROR
,
396 `WebSocket closed for ${
397 params?.skipBufferingOnError === false ? '' : 'non '
398 }buffered message id '${messageId}' with content '${messageToSend}'`,
400 (messagePayload
as OCPPError
).details
407 ErrorType
.SECURITY_ERROR
,
408 `Cannot send command ${commandName} PDU when the charging station is in ${chargingStation.bootNotificationResponse?.status} state on the central server`,
413 private buildMessageToSend (
414 chargingStation
: ChargingStation
,
416 messagePayload
: JsonType
| OCPPError
,
417 messageType
: MessageType
,
418 commandName
: RequestCommand
| IncomingRequestCommand
420 let messageToSend
: string
422 switch (messageType
) {
424 case MessageType
.CALL_MESSAGE
:
426 this.validateRequestPayload(chargingStation
, commandName
, messagePayload
as JsonType
)
427 messageToSend
= JSON
.stringify([
430 commandName
as RequestCommand
,
431 messagePayload
as JsonType
432 ] satisfies OutgoingRequest
)
435 case MessageType
.CALL_RESULT_MESSAGE
:
437 this.validateIncomingRequestResponsePayload(
440 messagePayload
as JsonType
442 messageToSend
= JSON
.stringify([
445 messagePayload
as JsonType
446 ] satisfies Response
)
449 case MessageType
.CALL_ERROR_MESSAGE
:
450 // Build Error Message
451 messageToSend
= JSON
.stringify([
454 (messagePayload
as OCPPError
).code
,
455 (messagePayload
as OCPPError
).message
,
456 (messagePayload
as OCPPError
).details
?? {
457 command
: (messagePayload
as OCPPError
).command
?? commandName
459 ] satisfies ErrorResponse
)
465 private cacheRequestPromise (
466 chargingStation
: ChargingStation
,
468 messagePayload
: JsonType
,
469 commandName
: RequestCommand
| IncomingRequestCommand
,
470 responseCallback
: ResponseCallback
,
471 errorCallback
: ErrorCallback
473 chargingStation
.requests
.set(messageId
, [
481 // eslint-disable-next-line @typescript-eslint/no-unused-vars
482 public abstract requestHandler
<ReqType
extends JsonType
, ResType
extends JsonType
>(
483 chargingStation
: ChargingStation
,
484 commandName
: RequestCommand
,
485 // FIXME: should be ReqType
486 commandParams
?: JsonType
,
487 params
?: RequestParams