refactor: silence typing errors
[e-mobility-charging-stations-simulator.git] / src / charging-station / AutomaticTransactionGenerator.ts
1 // Partial Copyright Jerome Benoit. 2021-2024. All Rights Reserved.
2
3 import { randomInt } from 'node:crypto'
4
5 import { hoursToMilliseconds, secondsToMilliseconds } from 'date-fns'
6
7 import { BaseError } from '../exception/index.js'
8 import { PerformanceStatistics } from '../performance/index.js'
9 import {
10 AuthorizationStatus,
11 RequestCommand,
12 type StartTransactionRequest,
13 type StartTransactionResponse,
14 type Status,
15 StopTransactionReason,
16 type StopTransactionResponse
17 } from '../types/index.js'
18 import {
19 clone,
20 Constants,
21 convertToDate,
22 formatDurationMilliSeconds,
23 isValidDate,
24 logger,
25 logPrefix,
26 secureRandom,
27 sleep
28 } from '../utils/index.js'
29 import type { ChargingStation } from './ChargingStation.js'
30 import { checkChargingStation } from './Helpers.js'
31 import { IdTagsCache } from './IdTagsCache.js'
32 import { isIdTagAuthorized } from './ocpp/index.js'
33
34 export class AutomaticTransactionGenerator {
35 private static readonly instances: Map<string, AutomaticTransactionGenerator> = new Map<
36 string,
37 AutomaticTransactionGenerator
38 >()
39
40 public readonly connectorsStatus: Map<number, Status>
41 public started: boolean
42 private starting: boolean
43 private stopping: boolean
44 private readonly chargingStation: ChargingStation
45
46 private constructor (chargingStation: ChargingStation) {
47 this.started = false
48 this.starting = false
49 this.stopping = false
50 this.chargingStation = chargingStation
51 this.connectorsStatus = new Map<number, Status>()
52 this.initializeConnectorsStatus()
53 }
54
55 public static getInstance (
56 chargingStation: ChargingStation
57 ): AutomaticTransactionGenerator | undefined {
58 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
59 if (!AutomaticTransactionGenerator.instances.has(chargingStation.stationInfo!.hashId)) {
60 AutomaticTransactionGenerator.instances.set(
61 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
62 chargingStation.stationInfo!.hashId,
63 new AutomaticTransactionGenerator(chargingStation)
64 )
65 }
66 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
67 return AutomaticTransactionGenerator.instances.get(chargingStation.stationInfo!.hashId)
68 }
69
70 public static deleteInstance (chargingStation: ChargingStation): boolean {
71 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
72 return AutomaticTransactionGenerator.instances.delete(chargingStation.stationInfo!.hashId)
73 }
74
75 public start (stopAbsoluteDuration?: boolean): void {
76 if (!checkChargingStation(this.chargingStation, this.logPrefix())) {
77 return
78 }
79 if (this.started) {
80 logger.warn(`${this.logPrefix()} is already started`)
81 return
82 }
83 if (this.starting) {
84 logger.warn(`${this.logPrefix()} is already starting`)
85 return
86 }
87 this.starting = true
88 this.startConnectors(stopAbsoluteDuration)
89 this.started = true
90 this.starting = false
91 }
92
93 public stop (): void {
94 if (!this.started) {
95 logger.warn(`${this.logPrefix()} is already stopped`)
96 return
97 }
98 if (this.stopping) {
99 logger.warn(`${this.logPrefix()} is already stopping`)
100 return
101 }
102 this.stopping = true
103 this.stopConnectors()
104 this.started = false
105 this.stopping = false
106 }
107
108 public startConnector (connectorId: number, stopAbsoluteDuration?: boolean): void {
109 if (!checkChargingStation(this.chargingStation, this.logPrefix(connectorId))) {
110 return
111 }
112 if (!this.connectorsStatus.has(connectorId)) {
113 logger.error(`${this.logPrefix(connectorId)} starting on non existing connector`)
114 throw new BaseError(`Connector ${connectorId} does not exist`)
115 }
116 if (this.connectorsStatus.get(connectorId)?.start === false) {
117 this.internalStartConnector(connectorId, stopAbsoluteDuration).catch(Constants.EMPTY_FUNCTION)
118 } else if (this.connectorsStatus.get(connectorId)?.start === true) {
119 logger.warn(`${this.logPrefix(connectorId)} is already started on connector`)
120 }
121 }
122
123 public stopConnector (connectorId: number): void {
124 if (!this.connectorsStatus.has(connectorId)) {
125 logger.error(`${this.logPrefix(connectorId)} stopping on non existing connector`)
126 throw new BaseError(`Connector ${connectorId} does not exist`)
127 }
128 if (this.connectorsStatus.get(connectorId)?.start === true) {
129 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
130 this.connectorsStatus.get(connectorId)!.start = false
131 } else if (this.connectorsStatus.get(connectorId)?.start === false) {
132 logger.warn(`${this.logPrefix(connectorId)} is already stopped on connector`)
133 }
134 }
135
136 private startConnectors (stopAbsoluteDuration?: boolean): void {
137 if (
138 this.connectorsStatus.size > 0 &&
139 this.connectorsStatus.size !== this.chargingStation.getNumberOfConnectors()
140 ) {
141 this.connectorsStatus.clear()
142 this.initializeConnectorsStatus()
143 }
144 if (this.chargingStation.hasEvses) {
145 for (const [evseId, evseStatus] of this.chargingStation.evses) {
146 if (evseId > 0) {
147 for (const connectorId of evseStatus.connectors.keys()) {
148 this.startConnector(connectorId, stopAbsoluteDuration)
149 }
150 }
151 }
152 } else {
153 for (const connectorId of this.chargingStation.connectors.keys()) {
154 if (connectorId > 0) {
155 this.startConnector(connectorId, stopAbsoluteDuration)
156 }
157 }
158 }
159 }
160
161 private stopConnectors (): void {
162 if (this.chargingStation.hasEvses) {
163 for (const [evseId, evseStatus] of this.chargingStation.evses) {
164 if (evseId > 0) {
165 for (const connectorId of evseStatus.connectors.keys()) {
166 this.stopConnector(connectorId)
167 }
168 }
169 }
170 } else {
171 for (const connectorId of this.chargingStation.connectors.keys()) {
172 if (connectorId > 0) {
173 this.stopConnector(connectorId)
174 }
175 }
176 }
177 }
178
179 private async internalStartConnector (
180 connectorId: number,
181 stopAbsoluteDuration?: boolean
182 ): Promise<void> {
183 this.setStartConnectorStatus(connectorId, stopAbsoluteDuration)
184 logger.info(
185 `${this.logPrefix(
186 connectorId
187 )} started on connector and will run for ${formatDurationMilliSeconds(
188 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
189 this.connectorsStatus.get(connectorId)!.stopDate!.getTime() -
190 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
191 this.connectorsStatus.get(connectorId)!.startDate!.getTime()
192 )}`
193 )
194 while (this.connectorsStatus.get(connectorId)?.start === true) {
195 await this.waitChargingStationAvailable(connectorId)
196 await this.waitConnectorAvailable(connectorId)
197 await this.waitRunningTransactionStopped(connectorId)
198 if (!this.canStartConnector(connectorId)) {
199 this.stopConnector(connectorId)
200 break
201 }
202 const wait = secondsToMilliseconds(
203 randomInt(
204 this.chargingStation.getAutomaticTransactionGeneratorConfiguration()
205 ?.minDelayBetweenTwoTransactions,
206 this.chargingStation.getAutomaticTransactionGeneratorConfiguration()
207 ?.maxDelayBetweenTwoTransactions
208 )
209 )
210 logger.info(`${this.logPrefix(connectorId)} waiting for ${formatDurationMilliSeconds(wait)}`)
211 await sleep(wait)
212 const start = secureRandom()
213 if (
214 start <
215 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
216 this.chargingStation.getAutomaticTransactionGeneratorConfiguration()!.probabilityOfStart
217 ) {
218 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
219 this.connectorsStatus.get(connectorId)!.skippedConsecutiveTransactions = 0
220 // Start transaction
221 const startResponse = await this.startTransaction(connectorId)
222 if (startResponse?.idTagInfo.status === AuthorizationStatus.ACCEPTED) {
223 // Wait until end of transaction
224 const waitTrxEnd = secondsToMilliseconds(
225 randomInt(
226 this.chargingStation.getAutomaticTransactionGeneratorConfiguration()?.minDuration,
227 this.chargingStation.getAutomaticTransactionGeneratorConfiguration()?.maxDuration
228 )
229 )
230 logger.info(
231 `${this.logPrefix(connectorId)} transaction started with id ${
232 this.chargingStation.getConnectorStatus(connectorId)?.transactionId
233 } and will stop in ${formatDurationMilliSeconds(waitTrxEnd)}`
234 )
235 await sleep(waitTrxEnd)
236 await this.stopTransaction(connectorId)
237 }
238 } else {
239 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
240 ++this.connectorsStatus.get(connectorId)!.skippedConsecutiveTransactions
241 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
242 ++this.connectorsStatus.get(connectorId)!.skippedTransactions
243 logger.info(
244 `${this.logPrefix(connectorId)} skipped consecutively ${
245 this.connectorsStatus.get(connectorId)?.skippedConsecutiveTransactions
246 }/${this.connectorsStatus.get(connectorId)?.skippedTransactions} transaction(s)`
247 )
248 }
249 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
250 this.connectorsStatus.get(connectorId)!.lastRunDate = new Date()
251 }
252 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
253 this.connectorsStatus.get(connectorId)!.stoppedDate = new Date()
254 logger.info(
255 `${this.logPrefix(
256 connectorId
257 )} stopped on connector and lasted for ${formatDurationMilliSeconds(
258 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
259 this.connectorsStatus.get(connectorId)!.stoppedDate!.getTime() -
260 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
261 this.connectorsStatus.get(connectorId)!.startDate!.getTime()
262 )}`
263 )
264 logger.debug(
265 `${this.logPrefix(connectorId)} connector status: %j`,
266 this.connectorsStatus.get(connectorId)
267 )
268 }
269
270 private setStartConnectorStatus (
271 connectorId: number,
272 stopAbsoluteDuration = this.chargingStation.getAutomaticTransactionGeneratorConfiguration()
273 ?.stopAbsoluteDuration
274 ): void {
275 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
276 this.connectorsStatus.get(connectorId)!.startDate = new Date()
277 if (
278 stopAbsoluteDuration === false ||
279 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
280 !isValidDate(this.connectorsStatus.get(connectorId)!.stopDate)
281 ) {
282 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
283 this.connectorsStatus.get(connectorId)!.stopDate = new Date(
284 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
285 this.connectorsStatus.get(connectorId)!.startDate!.getTime() +
286 hoursToMilliseconds(
287 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
288 this.chargingStation.getAutomaticTransactionGeneratorConfiguration()!.stopAfterHours
289 )
290 )
291 }
292 delete this.connectorsStatus.get(connectorId)?.stoppedDate
293 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
294 this.connectorsStatus.get(connectorId)!.skippedConsecutiveTransactions = 0
295 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
296 this.connectorsStatus.get(connectorId)!.start = true
297 }
298
299 private canStartConnector (connectorId: number): boolean {
300 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
301 if (new Date() > this.connectorsStatus.get(connectorId)!.stopDate!) {
302 logger.info(
303 `${this.logPrefix(
304 connectorId
305 )} entered in transaction loop while the ATG stop date has been reached`
306 )
307 return false
308 }
309 if (!this.chargingStation.inAcceptedState()) {
310 logger.error(
311 `${this.logPrefix(
312 connectorId
313 )} entered in transaction loop while the charging station is not in accepted state`
314 )
315 return false
316 }
317 if (!this.chargingStation.isChargingStationAvailable()) {
318 logger.info(
319 `${this.logPrefix(
320 connectorId
321 )} entered in transaction loop while the charging station is unavailable`
322 )
323 return false
324 }
325 if (!this.chargingStation.isConnectorAvailable(connectorId)) {
326 logger.info(
327 `${this.logPrefix(
328 connectorId
329 )} entered in transaction loop while the connector ${connectorId} is unavailable`
330 )
331 return false
332 }
333 const connectorStatus = this.chargingStation.getConnectorStatus(connectorId)
334 if (connectorStatus?.transactionStarted === true) {
335 logger.info(
336 `${this.logPrefix(
337 connectorId
338 )} entered in transaction loop while a transaction ${connectorStatus.transactionId} is already started on connector ${connectorId}`
339 )
340 return false
341 }
342 return true
343 }
344
345 private async waitChargingStationAvailable (connectorId: number): Promise<void> {
346 let logged = false
347 while (!this.chargingStation.isChargingStationAvailable()) {
348 if (!logged) {
349 logger.info(
350 `${this.logPrefix(
351 connectorId
352 )} transaction loop waiting for charging station to be available`
353 )
354 logged = true
355 }
356 await sleep(Constants.DEFAULT_ATG_WAIT_TIME)
357 }
358 }
359
360 private async waitConnectorAvailable (connectorId: number): Promise<void> {
361 let logged = false
362 while (!this.chargingStation.isConnectorAvailable(connectorId)) {
363 if (!logged) {
364 logger.info(
365 `${this.logPrefix(
366 connectorId
367 )} transaction loop waiting for connector ${connectorId} to be available`
368 )
369 logged = true
370 }
371 await sleep(Constants.DEFAULT_ATG_WAIT_TIME)
372 }
373 }
374
375 private async waitRunningTransactionStopped (connectorId: number): Promise<void> {
376 const connectorStatus = this.chargingStation.getConnectorStatus(connectorId)
377 let logged = false
378 while (connectorStatus?.transactionStarted === true) {
379 if (!logged) {
380 logger.info(
381 `${this.logPrefix(
382 connectorId
383 )} transaction loop waiting for started transaction ${connectorStatus.transactionId} on connector ${connectorId} to be stopped`
384 )
385 logged = true
386 }
387 await sleep(Constants.DEFAULT_ATG_WAIT_TIME)
388 }
389 }
390
391 private initializeConnectorsStatus (): void {
392 if (this.chargingStation.hasEvses) {
393 for (const [evseId, evseStatus] of this.chargingStation.evses) {
394 if (evseId > 0) {
395 for (const connectorId of evseStatus.connectors.keys()) {
396 this.connectorsStatus.set(connectorId, this.getConnectorStatus(connectorId))
397 }
398 }
399 }
400 } else {
401 for (const connectorId of this.chargingStation.connectors.keys()) {
402 if (connectorId > 0) {
403 this.connectorsStatus.set(connectorId, this.getConnectorStatus(connectorId))
404 }
405 }
406 }
407 }
408
409 private getConnectorStatus (connectorId: number): Status {
410 const statusIndex = connectorId - 1
411 let connectorStatus: Status | undefined
412 if (this.chargingStation.getAutomaticTransactionGeneratorStatuses()?.[statusIndex] != null) {
413 connectorStatus = clone<Status>(
414 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
415 this.chargingStation.getAutomaticTransactionGeneratorStatuses()![statusIndex]
416 )
417 } else if (this.chargingStation.getAutomaticTransactionGeneratorStatuses() != null) {
418 logger.warn(
419 `${this.logPrefix(connectorId)} no status found for connector #${connectorId} in charging station configuration file. New status will be created`
420 )
421 }
422 if (connectorStatus != null) {
423 connectorStatus.startDate = convertToDate(connectorStatus.startDate)
424 connectorStatus.lastRunDate = convertToDate(connectorStatus.lastRunDate)
425 connectorStatus.stopDate = convertToDate(connectorStatus.stopDate)
426 connectorStatus.stoppedDate = convertToDate(connectorStatus.stoppedDate)
427 if (
428 !this.started &&
429 (connectorStatus.start ||
430 this.chargingStation.getAutomaticTransactionGeneratorConfiguration()?.enable !== true)
431 ) {
432 connectorStatus.start = false
433 }
434 }
435 return (
436 connectorStatus ?? {
437 start: false,
438 authorizeRequests: 0,
439 acceptedAuthorizeRequests: 0,
440 rejectedAuthorizeRequests: 0,
441 startTransactionRequests: 0,
442 acceptedStartTransactionRequests: 0,
443 rejectedStartTransactionRequests: 0,
444 stopTransactionRequests: 0,
445 acceptedStopTransactionRequests: 0,
446 rejectedStopTransactionRequests: 0,
447 skippedConsecutiveTransactions: 0,
448 skippedTransactions: 0
449 }
450 )
451 }
452
453 private async startTransaction (
454 connectorId: number
455 ): Promise<StartTransactionResponse | undefined> {
456 const measureId = 'StartTransaction with ATG'
457 const beginId = PerformanceStatistics.beginMeasure(measureId)
458 let startResponse: StartTransactionResponse | undefined
459 if (this.chargingStation.hasIdTags()) {
460 const idTag = IdTagsCache.getInstance().getIdTag(
461 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
462 this.chargingStation.getAutomaticTransactionGeneratorConfiguration()!.idTagDistribution!,
463 this.chargingStation,
464 connectorId
465 )
466 const startTransactionLogMsg = `${this.logPrefix(
467 connectorId
468 )} start transaction with an idTag '${idTag}'`
469 if (this.getRequireAuthorize()) {
470 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
471 ++this.connectorsStatus.get(connectorId)!.authorizeRequests
472 if (await isIdTagAuthorized(this.chargingStation, connectorId, idTag)) {
473 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
474 ++this.connectorsStatus.get(connectorId)!.acceptedAuthorizeRequests
475 logger.info(startTransactionLogMsg)
476 // Start transaction
477 startResponse = await this.chargingStation.ocppRequestService.requestHandler<
478 Partial<StartTransactionRequest>,
479 StartTransactionResponse
480 >(this.chargingStation, RequestCommand.START_TRANSACTION, {
481 connectorId,
482 idTag
483 })
484 this.handleStartTransactionResponse(connectorId, startResponse)
485 PerformanceStatistics.endMeasure(measureId, beginId)
486 return startResponse
487 }
488 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
489 ++this.connectorsStatus.get(connectorId)!.rejectedAuthorizeRequests
490 PerformanceStatistics.endMeasure(measureId, beginId)
491 return startResponse
492 }
493 logger.info(startTransactionLogMsg)
494 // Start transaction
495 startResponse = await this.chargingStation.ocppRequestService.requestHandler<
496 Partial<StartTransactionRequest>,
497 StartTransactionResponse
498 >(this.chargingStation, RequestCommand.START_TRANSACTION, {
499 connectorId,
500 idTag
501 })
502 this.handleStartTransactionResponse(connectorId, startResponse)
503 PerformanceStatistics.endMeasure(measureId, beginId)
504 return startResponse
505 }
506 logger.info(`${this.logPrefix(connectorId)} start transaction without an idTag`)
507 startResponse = await this.chargingStation.ocppRequestService.requestHandler<
508 Partial<StartTransactionRequest>,
509 StartTransactionResponse
510 >(this.chargingStation, RequestCommand.START_TRANSACTION, { connectorId })
511 this.handleStartTransactionResponse(connectorId, startResponse)
512 PerformanceStatistics.endMeasure(measureId, beginId)
513 return startResponse
514 }
515
516 private async stopTransaction (
517 connectorId: number,
518 reason = StopTransactionReason.LOCAL
519 ): Promise<StopTransactionResponse | undefined> {
520 const measureId = 'StopTransaction with ATG'
521 const beginId = PerformanceStatistics.beginMeasure(measureId)
522 let stopResponse: StopTransactionResponse | undefined
523 if (this.chargingStation.getConnectorStatus(connectorId)?.transactionStarted === true) {
524 logger.info(
525 `${this.logPrefix(connectorId)} stop transaction with id ${
526 this.chargingStation.getConnectorStatus(connectorId)?.transactionId
527 }`
528 )
529 stopResponse = await this.chargingStation.stopTransactionOnConnector(connectorId, reason)
530 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
531 ++this.connectorsStatus.get(connectorId)!.stopTransactionRequests
532 if (stopResponse.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
533 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
534 ++this.connectorsStatus.get(connectorId)!.acceptedStopTransactionRequests
535 } else {
536 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
537 ++this.connectorsStatus.get(connectorId)!.rejectedStopTransactionRequests
538 }
539 } else {
540 const transactionId = this.chargingStation.getConnectorStatus(connectorId)?.transactionId
541 logger.debug(
542 `${this.logPrefix(connectorId)} stopping a not started transaction${
543 transactionId != null ? ` with id ${transactionId}` : ''
544 }`
545 )
546 }
547 PerformanceStatistics.endMeasure(measureId, beginId)
548 return stopResponse
549 }
550
551 private getRequireAuthorize (): boolean {
552 return (
553 this.chargingStation.getAutomaticTransactionGeneratorConfiguration()?.requireAuthorize ?? true
554 )
555 }
556
557 private readonly logPrefix = (connectorId?: number): string => {
558 return logPrefix(
559 ` ${this.chargingStation.stationInfo?.chargingStationId} | ATG${
560 connectorId != null ? ` on connector #${connectorId}` : ''
561 }:`
562 )
563 }
564
565 private handleStartTransactionResponse (
566 connectorId: number,
567 startResponse: StartTransactionResponse
568 ): void {
569 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
570 ++this.connectorsStatus.get(connectorId)!.startTransactionRequests
571 if (startResponse.idTagInfo.status === AuthorizationStatus.ACCEPTED) {
572 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
573 ++this.connectorsStatus.get(connectorId)!.acceptedStartTransactionRequests
574 } else {
575 logger.warn(`${this.logPrefix(connectorId)} start transaction rejected`)
576 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
577 ++this.connectorsStatus.get(connectorId)!.rejectedStartTransactionRequests
578 }
579 }
580 }