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