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 { getMessageTypeString
, 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(
100 MessageType
.CALL_RESULT_MESSAGE
,
110 public async sendError (
111 chargingStation
: ChargingStation
,
113 ocppError
: OCPPError
,
114 commandName
: RequestCommand
| IncomingRequestCommand
115 ): Promise
<ResponseType
> {
117 // Send error message
118 return await this.internalSendMessage(
122 MessageType
.CALL_ERROR_MESSAGE
,
126 handleSendMessageError(
129 MessageType
.CALL_ERROR_MESSAGE
,
136 protected async sendMessage (
137 chargingStation
: ChargingStation
,
139 messagePayload
: JsonType
,
140 commandName
: RequestCommand
,
141 params
?: RequestParams
142 ): Promise
<ResponseType
> {
144 ...defaultRequestParams
,
148 return await this.internalSendMessage(
152 MessageType
.CALL_MESSAGE
,
157 handleSendMessageError(
160 MessageType
.CALL_MESSAGE
,
163 throwError
: params
.throwError
170 private validateRequestPayload
<T
extends JsonType
>(
171 chargingStation
: ChargingStation
,
172 commandName
: RequestCommand
| IncomingRequestCommand
,
175 if (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false) {
178 if (!this.payloadValidateFunctions
.has(commandName
as RequestCommand
)) {
180 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: No JSON schema found for command '${commandName}' PDU validation`
184 const validate
= this.payloadValidateFunctions
.get(commandName
as RequestCommand
)
185 payload
= clone
<T
>(payload
)
186 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
)
187 if (validate
?.(payload
) === true) {
191 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: Command '${commandName}' request PDU is invalid: %j`,
194 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
196 OCPPServiceUtils
.ajvErrorsToErrorType(validate
?.errors
),
197 'Request PDU is invalid',
199 JSON
.stringify(validate
?.errors
, undefined, 2)
203 private validateIncomingRequestResponsePayload
<T
extends JsonType
>(
204 chargingStation
: ChargingStation
,
205 commandName
: RequestCommand
| IncomingRequestCommand
,
208 if (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false) {
212 !this.ocppResponseService
.incomingRequestResponsePayloadValidateFunctions
.has(
213 commandName
as IncomingRequestCommand
217 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: No JSON schema validation function found for command '${commandName}' PDU validation`
221 const validate
= this.ocppResponseService
.incomingRequestResponsePayloadValidateFunctions
.get(
222 commandName
as IncomingRequestCommand
224 payload
= clone
<T
>(payload
)
225 OCPPServiceUtils
.convertDateToISOString
<T
>(payload
)
226 if (validate
?.(payload
) === true) {
230 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: Command '${commandName}' response PDU is invalid: %j`,
233 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
235 OCPPServiceUtils
.ajvErrorsToErrorType(validate
?.errors
),
236 'Response PDU is invalid',
238 JSON
.stringify(validate
?.errors
, undefined, 2)
242 private async internalSendMessage (
243 chargingStation
: ChargingStation
,
245 messagePayload
: JsonType
| OCPPError
,
246 messageType
: MessageType
,
247 commandName
: RequestCommand
| IncomingRequestCommand
,
248 params
?: RequestParams
249 ): Promise
<ResponseType
> {
251 ...defaultRequestParams
,
255 (chargingStation
.inUnknownState() && commandName
=== RequestCommand
.BOOT_NOTIFICATION
) ||
256 (chargingStation
.stationInfo
?.ocppStrictCompliance
=== false &&
257 chargingStation
.inUnknownState()) ||
258 chargingStation
.inAcceptedState() ||
259 (chargingStation
.inPendingState() &&
260 (params
.triggerMessage
=== true || messageType
=== MessageType
.CALL_RESULT_MESSAGE
))
262 // eslint-disable-next-line @typescript-eslint/no-this-alias
264 // Send a message through wsConnection
265 return await new Promise
<ResponseType
>((resolve
, reject
: (reason
?: unknown
) => void) => {
267 * Function that will receive the request's response
270 * @param requestPayload -
272 const responseCallback
= (payload
: JsonType
, requestPayload
: JsonType
): void => {
273 if (chargingStation
.stationInfo
?.enableStatistics
=== true) {
274 chargingStation
.performanceStatistics
?.addRequestStatistic(
276 MessageType
.CALL_RESULT_MESSAGE
279 // Handle the request's response
280 self.ocppResponseService
283 commandName
as RequestCommand
,
292 chargingStation
.requests
.delete(messageId
)
293 chargingStation
.emit(ChargingStationEvents
.updated
)
298 * Function that will receive the request's error response
301 * @param requestStatistic -
303 const errorCallback
= (ocppError
: OCPPError
, requestStatistic
= true): void => {
304 if (requestStatistic
&& chargingStation
.stationInfo
?.enableStatistics
=== true) {
305 chargingStation
.performanceStatistics
?.addRequestStatistic(
307 MessageType
.CALL_ERROR_MESSAGE
311 `${chargingStation.logPrefix()} Error occurred at ${getMessageTypeString(
313 )} command ${commandName} with PDU %j:`,
317 chargingStation
.requests
.delete(messageId
)
318 chargingStation
.emit(ChargingStationEvents
.updated
)
322 const handleSendError
= (ocppError
: OCPPError
): void => {
323 if (params
.skipBufferingOnError
=== false) {
325 chargingStation
.bufferMessage(messageToSend
)
326 if (messageType
=== MessageType
.CALL_MESSAGE
) {
327 this.setCachedRequest(
330 messagePayload
as JsonType
,
337 params
.skipBufferingOnError
=== true &&
338 messageType
=== MessageType
.CALL_MESSAGE
340 // Remove request from the cache
341 chargingStation
.requests
.delete(messageId
)
346 if (chargingStation
.stationInfo
?.enableStatistics
=== true) {
347 chargingStation
.performanceStatistics
?.addRequestStatistic(commandName
, messageType
)
349 const messageToSend
= this.buildMessageToSend(
356 // Check if wsConnection opened
357 if (chargingStation
.isWebSocketConnectionOpened()) {
358 const beginId
= PerformanceStatistics
.beginMeasure(commandName
)
359 const sendTimeout
= setTimeout(() => {
362 ErrorType
.GENERIC_ERROR
,
363 `Timeout ${formatDurationMilliSeconds(
364 OCPPConstants.OCPP_WEBSOCKET_TIMEOUT
366 params.skipBufferingOnError === false ? '' : 'non '
367 }buffered message id '${messageId}' with content '${messageToSend}'`,
369 (messagePayload
as OCPPError
).details
372 }, OCPPConstants
.OCPP_WEBSOCKET_TIMEOUT
)
373 chargingStation
.wsConnection
?.send(messageToSend
, (error
?: Error) => {
374 PerformanceStatistics
.endMeasure(commandName
, beginId
)
375 clearTimeout(sendTimeout
)
378 `${chargingStation.logPrefix()} >> Command '${commandName}' sent ${getMessageTypeString(
380 )} payload: ${messageToSend}`
382 if (messageType
=== MessageType
.CALL_MESSAGE
) {
383 this.setCachedRequest(
386 messagePayload
as JsonType
,
393 resolve(messagePayload
)
398 ErrorType
.GENERIC_ERROR
,
399 `WebSocket errored for ${
400 params.skipBufferingOnError === false ? '' : 'non '
401 }buffered message id '${messageId}' with content '${messageToSend}'`,
405 message
: error
.message
,
415 ErrorType
.GENERIC_ERROR
,
416 `WebSocket closed for ${
417 params.skipBufferingOnError === false ? '' : 'non '
418 }buffered message id '${messageId}' with content '${messageToSend}'`,
420 (messagePayload
as OCPPError
).details
427 ErrorType
.SECURITY_ERROR
,
428 `Cannot send command ${commandName} PDU when the charging station is in ${chargingStation.bootNotificationResponse?.status} state on the central server`,
433 private buildMessageToSend (
434 chargingStation
: ChargingStation
,
436 messagePayload
: JsonType
| OCPPError
,
437 messageType
: MessageType
,
438 commandName
: RequestCommand
| IncomingRequestCommand
440 let messageToSend
: string
442 switch (messageType
) {
444 case MessageType
.CALL_MESSAGE
:
446 this.validateRequestPayload(chargingStation
, commandName
, messagePayload
as JsonType
)
447 messageToSend
= JSON
.stringify([
450 commandName
as RequestCommand
,
451 messagePayload
as JsonType
452 ] satisfies OutgoingRequest
)
455 case MessageType
.CALL_RESULT_MESSAGE
:
457 this.validateIncomingRequestResponsePayload(
460 messagePayload
as JsonType
462 messageToSend
= JSON
.stringify([
465 messagePayload
as JsonType
466 ] satisfies Response
)
469 case MessageType
.CALL_ERROR_MESSAGE
:
470 // Build Error Message
471 messageToSend
= JSON
.stringify([
474 (messagePayload
as OCPPError
).code
,
475 (messagePayload
as OCPPError
).message
,
476 (messagePayload
as OCPPError
).details
?? {
477 command
: (messagePayload
as OCPPError
).command
479 ] satisfies ErrorResponse
)
485 private setCachedRequest (
486 chargingStation
: ChargingStation
,
488 messagePayload
: JsonType
,
489 commandName
: RequestCommand
| IncomingRequestCommand
,
490 responseCallback
: ResponseCallback
,
491 errorCallback
: ErrorCallback
493 chargingStation
.requests
.set(messageId
, [
501 public abstract requestHandler
<ReqType
extends JsonType
, ResType
extends JsonType
>(
502 chargingStation
: ChargingStation
,
503 commandName
: RequestCommand
,
504 commandParams
?: ReqType
,
505 params
?: RequestParams