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