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