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