1 import _Ajv
, { type ValidateFunction
} from
'ajv'
2 import _ajvFormats from
'ajv-formats'
4 import type { ChargingStation
} from
'../../charging-station/index.js'
5 import type { OCPPResponseService
} from
'./OCPPResponseService.js'
7 import { OCPPError
} from
'../../exception/index.js'
8 import { PerformanceStatistics
} from
'../../performance/index.js'
10 ChargingStationEvents
,
14 type IncomingRequestCommand
,
22 type ResponseCallback
,
24 } from
'../../types/index.js'
27 formatDurationMilliSeconds
,
28 handleSendMessageError
,
30 } from
'../../utils/index.js'
31 import { OCPPConstants
} from
'./OCPPConstants.js'
34 convertDateToISOString
,
36 } from
'./OCPPServiceUtils.js'
37 type Ajv
= _Ajv
.default
38 // eslint-disable-next-line @typescript-eslint/no-redeclare
39 const Ajv
= _Ajv
.default
40 const ajvFormats
= _ajvFormats
.default
42 const moduleName
= 'OCPPRequestService'
44 const defaultRequestParams
: RequestParams
= {
45 skipBufferingOnError
: false,
47 triggerMessage
: false,
50 export abstract class OCPPRequestService
{
51 private static instance
: null | OCPPRequestService
= null
52 protected readonly ajv
: Ajv
53 protected abstract payloadValidateFunctions
: Map
<RequestCommand
, ValidateFunction
<JsonType
>>
54 private readonly ocppResponseService
: OCPPResponseService
55 private readonly version
: OCPPVersion
57 protected constructor (version
: OCPPVersion
, ocppResponseService
: OCPPResponseService
) {
58 this.version
= version
60 keywords
: ['javaType'],
61 multipleOfPrecision
: 2,
64 this.ocppResponseService
= ocppResponseService
65 this.requestHandler
= this.requestHandler
.bind(this)
66 this.sendMessage
= this.sendMessage
.bind(this)
67 this.sendResponse
= this.sendResponse
.bind(this)
68 this.sendError
= this.sendError
.bind(this)
69 this.internalSendMessage
= this.internalSendMessage
.bind(this)
70 this.buildMessageToSend
= this.buildMessageToSend
.bind(this)
71 this.validateRequestPayload
= this.validateRequestPayload
.bind(this)
72 this.validateIncomingRequestResponsePayload
=
73 this.validateIncomingRequestResponsePayload
.bind(this)
76 public static getInstance
<T
extends OCPPRequestService
>(
77 this: new (ocppResponseService
: OCPPResponseService
) => T
,
78 ocppResponseService
: OCPPResponseService
80 OCPPRequestService
.instance
??= new this(ocppResponseService
)
81 return OCPPRequestService
.instance
as T
84 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
85 public abstract requestHandler
<ReqType
extends JsonType
, ResType
extends JsonType
>(
86 chargingStation
: ChargingStation
,
87 commandName
: RequestCommand
,
88 commandParams
?: ReqType
,
89 params
?: RequestParams
92 public async sendError (
93 chargingStation
: ChargingStation
,
96 commandName
: IncomingRequestCommand
| RequestCommand
97 ): Promise
<ResponseType
> {
100 return await this.internalSendMessage(
104 MessageType
.CALL_ERROR_MESSAGE
,
108 handleSendMessageError(
111 MessageType
.CALL_ERROR_MESSAGE
,
118 public async sendResponse (
119 chargingStation
: ChargingStation
,
121 messagePayload
: JsonType
,
122 commandName
: IncomingRequestCommand
123 ): Promise
<ResponseType
> {
125 // Send response message
126 return await this.internalSendMessage(
130 MessageType
.CALL_RESULT_MESSAGE
,
134 handleSendMessageError(
137 MessageType
.CALL_RESULT_MESSAGE
,
147 protected async sendMessage (
148 chargingStation
: ChargingStation
,
150 messagePayload
: JsonType
,
151 commandName
: RequestCommand
,
152 params
?: RequestParams
153 ): Promise
<ResponseType
> {
155 ...defaultRequestParams
,
159 return await this.internalSendMessage(
163 MessageType
.CALL_MESSAGE
,
168 handleSendMessageError(
171 MessageType
.CALL_MESSAGE
,
174 throwError
: params
.throwError
,
181 private buildMessageToSend (
182 chargingStation
: ChargingStation
,
184 messagePayload
: JsonType
| OCPPError
,
185 messageType
: MessageType
,
186 commandName
: IncomingRequestCommand
| RequestCommand
188 let messageToSend
: string
190 switch (messageType
) {
192 case MessageType
.CALL_ERROR_MESSAGE
:
193 // Build Error Message
194 messageToSend
= JSON
.stringify([
197 (messagePayload
as OCPPError
).code
,
198 (messagePayload
as OCPPError
).message
,
199 (messagePayload
as OCPPError
).details
?? {
200 command
: (messagePayload
as OCPPError
).command
,
202 ] satisfies ErrorResponse
)
205 case MessageType
.CALL_MESSAGE
:
207 this.validateRequestPayload(chargingStation
, commandName
, messagePayload
as JsonType
)
208 messageToSend
= JSON
.stringify([
211 commandName
as RequestCommand
,
212 messagePayload
as JsonType
,
213 ] satisfies OutgoingRequest
)
216 case MessageType
.CALL_RESULT_MESSAGE
:
218 this.validateIncomingRequestResponsePayload(
221 messagePayload
as JsonType
223 messageToSend
= JSON
.stringify([
226 messagePayload
as JsonType
,
227 ] satisfies Response
)
233 private async internalSendMessage (
234 chargingStation
: ChargingStation
,
236 messagePayload
: JsonType
| OCPPError
,
237 messageType
: MessageType
,
238 commandName
: IncomingRequestCommand
| RequestCommand
,
239 params
?: RequestParams
240 ): Promise
<ResponseType
> {
242 ...defaultRequestParams
,
246 ((chargingStation
.inUnknownState() || chargingStation
.inPendingState()) &&
247 commandName
=== RequestCommand
.BOOT_NOTIFICATION
) ||
248 (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false &&
249 (chargingStation
.inUnknownState() || chargingStation
.inPendingState())) ||
250 chargingStation
.inAcceptedState() ||
251 (chargingStation
.inPendingState() &&
252 (params
.triggerMessage
=== true || messageType
=== MessageType
.CALL_RESULT_MESSAGE
))
254 // eslint-disable-next-line @typescript-eslint/no-this-alias
256 // Send a message through wsConnection
257 return await new Promise
<ResponseType
>((resolve
, reject
: (reason
?: unknown
) => void) => {
259 * Function that will receive the request's response
261 * @param requestPayload -
263 const responseCallback
= (payload
: JsonType
, requestPayload
: JsonType
): void => {
264 if (chargingStation
.stationInfo
?.enableStatistics
=== true) {
265 chargingStation
.performanceStatistics
?.addRequestStatistic(
267 MessageType
.CALL_RESULT_MESSAGE
270 // Handle the request's response
271 self.ocppResponseService
274 commandName
as RequestCommand
,
283 chargingStation
.requests
.delete(messageId
)
284 chargingStation
.emit(ChargingStationEvents
.updated
)
290 * Function that will receive the request's error response
292 * @param requestStatistic -
294 const errorCallback
= (ocppError
: OCPPError
, requestStatistic
= true): void => {
295 if (requestStatistic
&& chargingStation
.stationInfo
?.enableStatistics
=== true) {
296 chargingStation
.performanceStatistics
?.addRequestStatistic(
298 MessageType
.CALL_ERROR_MESSAGE
302 `${chargingStation.logPrefix()} Error occurred at ${getMessageTypeString(
304 )} command ${commandName} with PDU %j:`,
308 chargingStation
.requests
.delete(messageId
)
309 chargingStation
.emit(ChargingStationEvents
.updated
)
313 const handleSendError
= (ocppError
: OCPPError
): void => {
314 if (params
.skipBufferingOnError
=== false) {
316 chargingStation
.bufferMessage(messageToSend
)
317 if (messageType
=== MessageType
.CALL_MESSAGE
) {
318 this.setCachedRequest(
321 messagePayload
as JsonType
,
328 params
.skipBufferingOnError
=== true &&
329 messageType
=== MessageType
.CALL_MESSAGE
331 // Remove request from the cache
332 chargingStation
.requests
.delete(messageId
)
337 if (chargingStation
.stationInfo
?.enableStatistics
=== true) {
338 chargingStation
.performanceStatistics
?.addRequestStatistic(commandName
, messageType
)
340 const messageToSend
= this.buildMessageToSend(
347 // Check if wsConnection opened
348 if (chargingStation
.isWebSocketConnectionOpened()) {
349 const beginId
= PerformanceStatistics
.beginMeasure(commandName
)
350 const sendTimeout
= setTimeout(() => {
353 ErrorType
.GENERIC_ERROR
,
354 `Timeout ${formatDurationMilliSeconds(
355 OCPPConstants.OCPP_WEBSOCKET_TIMEOUT
357 params.skipBufferingOnError === false ? '' : 'non '
358 }buffered message id '${messageId}' with content '${messageToSend}'`,
360 (messagePayload
as OCPPError
).details
363 }, OCPPConstants
.OCPP_WEBSOCKET_TIMEOUT
)
364 chargingStation
.wsConnection
?.send(messageToSend
, (error
?: Error) => {
365 PerformanceStatistics
.endMeasure(commandName
, beginId
)
366 clearTimeout(sendTimeout
)
369 `${chargingStation.logPrefix()} >> Command '${commandName}' sent ${getMessageTypeString(
371 )} payload: ${messageToSend}`
373 if (messageType
=== MessageType
.CALL_MESSAGE
) {
374 this.setCachedRequest(
377 messagePayload
as JsonType
,
384 resolve(messagePayload
)
389 ErrorType
.GENERIC_ERROR
,
390 `WebSocket errored for ${
391 params.skipBufferingOnError === false ? '' : 'non '
392 }buffered message id '${messageId}' with content '${messageToSend}'`,
395 message
: error
.message
,
406 ErrorType
.GENERIC_ERROR
,
407 `WebSocket closed for ${
408 params.skipBufferingOnError === false ? '' : 'non '
409 }buffered message id '${messageId}' with content '${messageToSend}'`,
411 (messagePayload
as OCPPError
).details
418 ErrorType
.SECURITY_ERROR
,
419 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
420 `Cannot send command ${commandName} PDU when the charging station is in ${chargingStation.bootNotificationResponse?.status} state on the central server`,
425 private setCachedRequest (
426 chargingStation
: ChargingStation
,
428 messagePayload
: JsonType
,
429 commandName
: IncomingRequestCommand
| RequestCommand
,
430 responseCallback
: ResponseCallback
,
431 errorCallback
: ErrorCallback
433 chargingStation
.requests
.set(messageId
, [
441 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
442 private validateIncomingRequestResponsePayload
<T
extends JsonType
>(
443 chargingStation
: ChargingStation
,
444 commandName
: IncomingRequestCommand
| RequestCommand
,
447 if (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false) {
451 !this.ocppResponseService
.incomingRequestResponsePayloadValidateFunctions
.has(
452 commandName
as IncomingRequestCommand
456 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: No JSON schema validation function found for command '${commandName}' PDU validation`
460 const validate
= this.ocppResponseService
.incomingRequestResponsePayloadValidateFunctions
.get(
461 commandName
as IncomingRequestCommand
463 payload
= clone
<T
>(payload
)
464 convertDateToISOString
<T
>(payload
)
465 if (validate
?.(payload
) === true) {
469 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: Command '${commandName}' incoming request response PDU is invalid: %j`,
472 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
474 ajvErrorsToErrorType(validate
?.errors
),
475 'Incoming request response PDU is invalid',
477 JSON
.stringify(validate
?.errors
, undefined, 2)
481 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
482 private validateRequestPayload
<T
extends JsonType
>(
483 chargingStation
: ChargingStation
,
484 commandName
: IncomingRequestCommand
| RequestCommand
,
487 if (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false) {
490 if (!this.payloadValidateFunctions
.has(commandName
as RequestCommand
)) {
492 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: No JSON schema found for command '${commandName}' PDU validation`
496 const validate
= this.payloadValidateFunctions
.get(commandName
as RequestCommand
)
497 payload
= clone
<T
>(payload
)
498 convertDateToISOString
<T
>(payload
)
499 if (validate
?.(payload
) === true) {
503 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: Command '${commandName}' request PDU is invalid: %j`,
506 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
508 ajvErrorsToErrorType(validate
?.errors
),
509 'Request PDU is invalid',
511 JSON
.stringify(validate
?.errors
, undefined, 2)