]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/blame - src/charging-station/ocpp/OCPPRequestService.ts
Merge branch 'main' of github.com:SAP/e-mobility-charging-stations-simulator
[e-mobility-charging-stations-simulator.git] / src / charging-station / ocpp / OCPPRequestService.ts
CommitLineData
24d15716 1import _Ajv, { type ValidateFunction } from 'ajv'
66a7748d 2import _ajvFormats from 'ajv-formats'
b52c969d 3
66a7748d 4import type { ChargingStation } from '../../charging-station/index.js'
0749233f
JB
5import type { OCPPResponseService } from './OCPPResponseService.js'
6
66a7748d
JB
7import { OCPPError } from '../../exception/index.js'
8import { PerformanceStatistics } from '../../performance/index.js'
e7aeea18 9import {
8baae8ee 10 ChargingStationEvents,
27782dbc 11 type ErrorCallback,
268a74bb
JB
12 type ErrorResponse,
13 ErrorType,
27782dbc 14 type IncomingRequestCommand,
268a74bb
JB
15 type JsonType,
16 MessageType,
17 type OCPPVersion,
27782dbc 18 type OutgoingRequest,
e7aeea18 19 RequestCommand,
27782dbc 20 type RequestParams,
268a74bb 21 type Response,
27782dbc 22 type ResponseCallback,
d1f5bfd8 23 type ResponseType,
66a7748d 24} from '../../types/index.js'
2b94ad12 25import {
40615072 26 clone,
2b94ad12
JB
27 formatDurationMilliSeconds,
28 handleSendMessageError,
d1f5bfd8 29 logger,
66a7748d 30} from '../../utils/index.js'
4c3f6c20 31import { OCPPConstants } from './OCPPConstants.js'
01b82de5
JB
32import {
33 ajvErrorsToErrorType,
34 convertDateToISOString,
d1f5bfd8 35 getMessageTypeString,
01b82de5 36} from './OCPPServiceUtils.js'
66a7748d
JB
37type Ajv = _Ajv.default
38// eslint-disable-next-line @typescript-eslint/no-redeclare
39const Ajv = _Ajv.default
40const ajvFormats = _ajvFormats.default
c0560973 41
66a7748d 42const moduleName = 'OCPPRequestService'
e3018bc4 43
b9da1bc2
JB
44const defaultRequestParams: RequestParams = {
45 skipBufferingOnError: false,
d1f5bfd8 46 throwError: false,
0749233f 47 triggerMessage: false,
66a7748d 48}
b9da1bc2 49
268a74bb 50export abstract class OCPPRequestService {
0749233f 51 private static instance: null | OCPPRequestService = null
24d15716 52 protected readonly ajv: Ajv
d5490a13 53 protected abstract payloadValidateFunctions: Map<RequestCommand, ValidateFunction<JsonType>>
0749233f
JB
54 private readonly ocppResponseService: OCPPResponseService
55 private readonly version: OCPPVersion
c0560973 56
66a7748d
JB
57 protected constructor (version: OCPPVersion, ocppResponseService: OCPPResponseService) {
58 this.version = version
45988780 59 this.ajv = new Ajv({
98fc1389 60 keywords: ['javaType'],
d1f5bfd8 61 multipleOfPrecision: 2,
66a7748d
JB
62 })
63 ajvFormats(this.ajv)
66a7748d 64 this.ocppResponseService = ocppResponseService
ba9a56a6
JB
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)
c0560973
JB
74 }
75
e7aeea18 76 public static getInstance<T extends OCPPRequestService>(
08f130a0 77 this: new (ocppResponseService: OCPPResponseService) => T,
66a7748d 78 ocppResponseService: OCPPResponseService
e7aeea18 79 ): T {
678ffc93 80 OCPPRequestService.instance ??= new this(ocppResponseService)
66a7748d 81 return OCPPRequestService.instance as T
9f2e3130
JB
82 }
83
c4a89082
JB
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
90 ): Promise<ResType>
91
92 public async sendError (
93 chargingStation: ChargingStation,
94 messageId: string,
95 ocppError: OCPPError,
96 commandName: IncomingRequestCommand | RequestCommand
97 ): Promise<ResponseType> {
98 try {
99 // Send error message
100 return await this.internalSendMessage(
101 chargingStation,
102 messageId,
103 ocppError,
104 MessageType.CALL_ERROR_MESSAGE,
105 commandName
106 )
107 } catch (error) {
108 handleSendMessageError(
109 chargingStation,
110 commandName,
111 MessageType.CALL_ERROR_MESSAGE,
112 error as Error
113 )
114 return null
115 }
116 }
117
118 public async sendResponse (
119 chargingStation: ChargingStation,
120 messageId: string,
121 messagePayload: JsonType,
122 commandName: IncomingRequestCommand
123 ): Promise<ResponseType> {
124 try {
125 // Send response message
126 return await this.internalSendMessage(
127 chargingStation,
128 messageId,
129 messagePayload,
130 MessageType.CALL_RESULT_MESSAGE,
131 commandName
132 )
133 } catch (error) {
134 handleSendMessageError(
135 chargingStation,
136 commandName,
137 MessageType.CALL_RESULT_MESSAGE,
138 error as Error,
139 {
140 throwError: true,
141 }
142 )
143 return null
144 }
145 }
146
66a7748d 147 protected async sendMessage (
08f130a0 148 chargingStation: ChargingStation,
e7aeea18 149 messageId: string,
5cc4b63b 150 messagePayload: JsonType,
e7aeea18 151 commandName: RequestCommand,
66a7748d 152 params?: RequestParams
e7aeea18 153 ): Promise<ResponseType> {
7b5dbe91 154 params = {
b9da1bc2 155 ...defaultRequestParams,
d1f5bfd8 156 ...params,
66a7748d 157 }
5e0c67e8 158 try {
e7aeea18 159 return await this.internalSendMessage(
08f130a0 160 chargingStation,
e7aeea18
JB
161 messageId,
162 messagePayload,
163 MessageType.CALL_MESSAGE,
164 commandName,
66a7748d
JB
165 params
166 )
5e0c67e8 167 } catch (error) {
436569b1
JB
168 handleSendMessageError(
169 chargingStation,
170 commandName,
171 MessageType.CALL_MESSAGE,
172 error as Error,
173 {
d1f5bfd8 174 throwError: params.throwError,
436569b1
JB
175 }
176 )
66a7748d 177 return null
5e0c67e8
JB
178 }
179 }
180
0749233f 181 private buildMessageToSend (
b3fc3ff5 182 chargingStation: ChargingStation,
0749233f
JB
183 messageId: string,
184 messagePayload: JsonType | OCPPError,
185 messageType: MessageType,
186 commandName: IncomingRequestCommand | RequestCommand
187 ): string {
188 let messageToSend: string
189 // Type of message
190 switch (messageType) {
191 // Error Message
192 case MessageType.CALL_ERROR_MESSAGE:
193 // Build Error Message
194 messageToSend = JSON.stringify([
195 messageType,
196 messageId,
197 (messagePayload as OCPPError).code,
198 (messagePayload as OCPPError).message,
199 (messagePayload as OCPPError).details ?? {
200 command: (messagePayload as OCPPError).command,
201 },
202 ] satisfies ErrorResponse)
203 break
204 // Request
205 case MessageType.CALL_MESSAGE:
206 // Build request
207 this.validateRequestPayload(chargingStation, commandName, messagePayload as JsonType)
208 messageToSend = JSON.stringify([
209 messageType,
210 messageId,
211 commandName as RequestCommand,
212 messagePayload as JsonType,
213 ] satisfies OutgoingRequest)
214 break
215 // Response
216 case MessageType.CALL_RESULT_MESSAGE:
217 // Build response
218 this.validateIncomingRequestResponsePayload(
219 chargingStation,
220 commandName,
221 messagePayload as JsonType
222 )
223 messageToSend = JSON.stringify([
224 messageType,
225 messageId,
226 messagePayload as JsonType,
227 ] satisfies Response)
228 break
b3fc3ff5 229 }
0749233f 230 return messageToSend
b3fc3ff5
JB
231 }
232
66a7748d 233 private async internalSendMessage (
08f130a0 234 chargingStation: ChargingStation,
e7aeea18 235 messageId: string,
5cc4b63b 236 messagePayload: JsonType | OCPPError,
e7aeea18 237 messageType: MessageType,
0749233f 238 commandName: IncomingRequestCommand | RequestCommand,
66a7748d 239 params?: RequestParams
e7aeea18 240 ): Promise<ResponseType> {
7b5dbe91 241 params = {
b9da1bc2 242 ...defaultRequestParams,
d1f5bfd8 243 ...params,
66a7748d 244 }
e7aeea18 245 if (
b72d1749
JB
246 ((chargingStation.inUnknownState() || chargingStation.inPendingState()) &&
247 commandName === RequestCommand.BOOT_NOTIFICATION) ||
5398cecf 248 (chargingStation.stationInfo?.ocppStrictCompliance === false &&
b72d1749 249 (chargingStation.inUnknownState() || chargingStation.inPendingState())) ||
66a7748d
JB
250 chargingStation.inAcceptedState() ||
251 (chargingStation.inPendingState() &&
3a13fc92 252 (params.triggerMessage === true || messageType === MessageType.CALL_RESULT_MESSAGE))
e7aeea18 253 ) {
caad9d6b 254 // eslint-disable-next-line @typescript-eslint/no-this-alias
66a7748d 255 const self = this
caad9d6b 256 // Send a message through wsConnection
ea32ea05 257 return await new Promise<ResponseType>((resolve, reject: (reason?: unknown) => void) => {
1b2acf4e
JB
258 /**
259 * Function that will receive the request's response
1b2acf4e
JB
260 * @param payload -
261 * @param requestPayload -
262 */
263 const responseCallback = (payload: JsonType, requestPayload: JsonType): void => {
264 if (chargingStation.stationInfo?.enableStatistics === true) {
265 chargingStation.performanceStatistics?.addRequestStatistic(
266 commandName,
66a7748d
JB
267 MessageType.CALL_RESULT_MESSAGE
268 )
1b2acf4e
JB
269 }
270 // Handle the request's response
271 self.ocppResponseService
272 .responseHandler(
273 chargingStation,
274 commandName as RequestCommand,
275 payload,
66a7748d 276 requestPayload
1b2acf4e
JB
277 )
278 .then(() => {
66a7748d 279 resolve(payload)
d1f5bfd8 280 return undefined
1b2acf4e 281 })
1b2acf4e 282 .finally(() => {
66a7748d
JB
283 chargingStation.requests.delete(messageId)
284 chargingStation.emit(ChargingStationEvents.updated)
285 })
d1f5bfd8 286 .catch(reject)
66a7748d 287 }
e8a92d57 288
1b2acf4e
JB
289 /**
290 * Function that will receive the request's error response
9d7b5fa3 291 * @param ocppError -
1b2acf4e
JB
292 * @param requestStatistic -
293 */
9d7b5fa3 294 const errorCallback = (ocppError: OCPPError, requestStatistic = true): void => {
66a7748d 295 if (requestStatistic && chargingStation.stationInfo?.enableStatistics === true) {
1b2acf4e
JB
296 chargingStation.performanceStatistics?.addRequestStatistic(
297 commandName,
66a7748d
JB
298 MessageType.CALL_ERROR_MESSAGE
299 )
764d2c91 300 }
1b2acf4e 301 logger.error(
436569b1 302 `${chargingStation.logPrefix()} Error occurred at ${getMessageTypeString(
66a7748d 303 messageType
1b2acf4e 304 )} command ${commandName} with PDU %j:`,
e7aeea18 305 messagePayload,
66a7748d
JB
306 ocppError
307 )
308 chargingStation.requests.delete(messageId)
309 chargingStation.emit(ChargingStationEvents.updated)
310 reject(ocppError)
311 }
9d7b5fa3 312
3c80de96 313 const handleSendError = (ocppError: OCPPError): void => {
e2baeffc 314 if (params.skipBufferingOnError === false) {
3c80de96 315 // Buffer
66a7748d 316 chargingStation.bufferMessage(messageToSend)
3c80de96 317 if (messageType === MessageType.CALL_MESSAGE) {
f10f20c9 318 this.setCachedRequest(
3c80de96
JB
319 chargingStation,
320 messageId,
321 messagePayload as JsonType,
322 commandName,
323 responseCallback,
66a7748d
JB
324 errorCallback
325 )
3c80de96 326 }
9aa1a33f 327 } else if (
e2baeffc 328 params.skipBufferingOnError === true &&
9aa1a33f
JB
329 messageType === MessageType.CALL_MESSAGE
330 ) {
2b94ad12 331 // Remove request from the cache
66a7748d 332 chargingStation.requests.delete(messageId)
d42379d8 333 }
66a7748d
JB
334 reject(ocppError)
335 }
d42379d8 336
1b2acf4e 337 if (chargingStation.stationInfo?.enableStatistics === true) {
66a7748d 338 chargingStation.performanceStatistics?.addRequestStatistic(commandName, messageType)
1b2acf4e
JB
339 }
340 const messageToSend = this.buildMessageToSend(
341 chargingStation,
342 messageId,
343 messagePayload,
344 messageType,
66a7748d
JB
345 commandName
346 )
1b2acf4e 347 // Check if wsConnection opened
66a7748d
JB
348 if (chargingStation.isWebSocketConnectionOpened()) {
349 const beginId = PerformanceStatistics.beginMeasure(commandName)
1a32c36b 350 const sendTimeout = setTimeout(() => {
66a7748d 351 handleSendError(
1a32c36b
JB
352 new OCPPError(
353 ErrorType.GENERIC_ERROR,
2b94ad12 354 `Timeout ${formatDurationMilliSeconds(
66a7748d 355 OCPPConstants.OCPP_WEBSOCKET_TIMEOUT
2b94ad12 356 )} reached for ${
e2baeffc 357 params.skipBufferingOnError === false ? '' : 'non '
42b8cf5c 358 }buffered message id '${messageId}' with content '${messageToSend}'`,
1a32c36b 359 commandName,
66a7748d
JB
360 (messagePayload as OCPPError).details
361 )
362 )
363 }, OCPPConstants.OCPP_WEBSOCKET_TIMEOUT)
1a32c36b 364 chargingStation.wsConnection?.send(messageToSend, (error?: Error) => {
66a7748d
JB
365 PerformanceStatistics.endMeasure(commandName, beginId)
366 clearTimeout(sendTimeout)
aa63c9b7 367 if (error == null) {
d42379d8 368 logger.debug(
436569b1 369 `${chargingStation.logPrefix()} >> Command '${commandName}' sent ${getMessageTypeString(
66a7748d
JB
370 messageType
371 )} payload: ${messageToSend}`
372 )
d42379d8 373 if (messageType === MessageType.CALL_MESSAGE) {
f10f20c9 374 this.setCachedRequest(
d42379d8
JB
375 chargingStation,
376 messageId,
377 messagePayload as JsonType,
378 commandName,
379 responseCallback,
66a7748d
JB
380 errorCallback
381 )
69dae411
JB
382 } else {
383 // Resolve response
66a7748d 384 resolve(messagePayload)
d42379d8 385 }
5199f9fd 386 } else {
66a7748d 387 handleSendError(
3c80de96
JB
388 new OCPPError(
389 ErrorType.GENERIC_ERROR,
390 `WebSocket errored for ${
e2baeffc 391 params.skipBufferingOnError === false ? '' : 'non '
3c80de96
JB
392 }buffered message id '${messageId}' with content '${messageToSend}'`,
393 commandName,
48847bc0 394 {
48847bc0 395 message: error.message,
0749233f 396 name: error.name,
d1f5bfd8 397 stack: error.stack,
48847bc0 398 }
66a7748d
JB
399 )
400 )
1a32c36b 401 }
66a7748d 402 })
82fa1110 403 } else {
66a7748d 404 handleSendError(
3c80de96
JB
405 new OCPPError(
406 ErrorType.GENERIC_ERROR,
407 `WebSocket closed for ${
e2baeffc 408 params.skipBufferingOnError === false ? '' : 'non '
3c80de96
JB
409 }buffered message id '${messageId}' with content '${messageToSend}'`,
410 commandName,
66a7748d
JB
411 (messagePayload as OCPPError).details
412 )
413 )
1b2acf4e 414 }
66a7748d 415 })
caad9d6b 416 }
e7aeea18
JB
417 throw new OCPPError(
418 ErrorType.SECURITY_ERROR,
d1f5bfd8 419 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
5199f9fd 420 `Cannot send command ${commandName} PDU when the charging station is in ${chargingStation.bootNotificationResponse?.status} state on the central server`,
66a7748d
JB
421 commandName
422 )
c0560973
JB
423 }
424
f10f20c9 425 private setCachedRequest (
82fa1110 426 chargingStation: ChargingStation,
82fa1110
JB
427 messageId: string,
428 messagePayload: JsonType,
0749233f 429 commandName: IncomingRequestCommand | RequestCommand,
54a8fbc7 430 responseCallback: ResponseCallback,
66a7748d 431 errorCallback: ErrorCallback
82fa1110
JB
432 ): void {
433 chargingStation.requests.set(messageId, [
434 responseCallback,
435 errorCallback,
436 commandName,
d1f5bfd8 437 messagePayload,
66a7748d 438 ])
82fa1110
JB
439 }
440
0749233f
JB
441 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
442 private validateIncomingRequestResponsePayload<T extends JsonType>(
443 chargingStation: ChargingStation,
444 commandName: IncomingRequestCommand | RequestCommand,
445 payload: T
446 ): boolean {
447 if (chargingStation.stationInfo?.ocppStrictCompliance === false) {
448 return true
449 }
450 if (
451 !this.ocppResponseService.incomingRequestResponsePayloadValidateFunctions.has(
452 commandName as IncomingRequestCommand
453 )
454 ) {
455 logger.warn(
456 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: No JSON schema validation function found for command '${commandName}' PDU validation`
457 )
458 return true
459 }
460 const validate = this.ocppResponseService.incomingRequestResponsePayloadValidateFunctions.get(
461 commandName as IncomingRequestCommand
462 )
463 payload = clone<T>(payload)
464 convertDateToISOString<T>(payload)
465 if (validate?.(payload) === true) {
466 return true
467 }
468 logger.error(
469 `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: Command '${commandName}' incoming request response PDU is invalid: %j`,
470 validate?.errors
471 )
472 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
473 throw new OCPPError(
474 ajvErrorsToErrorType(validate?.errors),
475 'Incoming request response PDU is invalid',
476 commandName,
477 JSON.stringify(validate?.errors, undefined, 2)
478 )
479 }
480
481 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
482 private validateRequestPayload<T extends JsonType>(
483 chargingStation: ChargingStation,
484 commandName: IncomingRequestCommand | RequestCommand,
485 payload: T
486 ): boolean {
487 if (chargingStation.stationInfo?.ocppStrictCompliance === false) {
488 return true
489 }
490 if (!this.payloadValidateFunctions.has(commandName as RequestCommand)) {
491 logger.warn(
492 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: No JSON schema found for command '${commandName}' PDU validation`
493 )
494 return true
495 }
496 const validate = this.payloadValidateFunctions.get(commandName as RequestCommand)
497 payload = clone<T>(payload)
498 convertDateToISOString<T>(payload)
499 if (validate?.(payload) === true) {
500 return true
501 }
502 logger.error(
503 `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: Command '${commandName}' request PDU is invalid: %j`,
504 validate?.errors
505 )
506 // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
507 throw new OCPPError(
508 ajvErrorsToErrorType(validate?.errors),
509 'Request PDU is invalid',
510 commandName,
511 JSON.stringify(validate?.errors, undefined, 2)
512 )
513 }
c0560973 514}