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