1 // Partial Copyright Jerome Benoit. 2021-2024. All Rights Reserved.
3 import { randomInt
} from
'node:crypto'
5 import { hoursToMilliseconds
, secondsToMilliseconds
} from
'date-fns'
7 import { BaseError
} from
'../exception/index.js'
8 import { PerformanceStatistics
} from
'../performance/index.js'
12 type StartTransactionRequest
,
13 type StartTransactionResponse
,
15 StopTransactionReason
,
16 type StopTransactionResponse
17 } from
'../types/index.js'
22 formatDurationMilliSeconds
,
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'
34 export class AutomaticTransactionGenerator
{
35 private static readonly instances
: Map
<string, AutomaticTransactionGenerator
> = new Map
<
37 AutomaticTransactionGenerator
40 public readonly connectorsStatus
: Map
<number, Status
>
41 public started
: boolean
42 private starting
: boolean
43 private stopping
: boolean
44 private readonly chargingStation
: ChargingStation
46 private constructor (chargingStation
: ChargingStation
) {
50 this.chargingStation
= chargingStation
51 this.connectorsStatus
= new Map
<number, Status
>()
52 this.initializeConnectorsStatus()
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
)
66 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
67 return AutomaticTransactionGenerator
.instances
.get(chargingStation
.stationInfo
!.hashId
)
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
)
75 public start (stopAbsoluteDuration
?: boolean): void {
76 if (!checkChargingStation(this.chargingStation
, this.logPrefix())) {
80 logger
.warn(`${this.logPrefix()} is already started`)
84 logger
.warn(`${this.logPrefix()} is already starting`)
88 this.startConnectors(stopAbsoluteDuration
)
93 public stop (): void {
95 logger
.warn(`${this.logPrefix()} is already stopped`)
99 logger
.warn(`${this.logPrefix()} is already stopping`)
103 this.stopConnectors()
105 this.stopping
= false
108 public startConnector (connectorId
: number, stopAbsoluteDuration
?: boolean): void {
109 if (!checkChargingStation(this.chargingStation
, this.logPrefix(connectorId
))) {
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`)
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`)
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`)
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`)
136 private startConnectors (stopAbsoluteDuration
?: boolean): void {
138 this.connectorsStatus
.size
> 0 &&
139 this.connectorsStatus
.size
!== this.chargingStation
.getNumberOfConnectors()
141 this.connectorsStatus
.clear()
142 this.initializeConnectorsStatus()
144 if (this.chargingStation
.hasEvses
) {
145 for (const [evseId
, evseStatus
] of this.chargingStation
.evses
) {
147 for (const connectorId
of evseStatus
.connectors
.keys()) {
148 this.startConnector(connectorId
, stopAbsoluteDuration
)
153 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
154 if (connectorId
> 0) {
155 this.startConnector(connectorId
, stopAbsoluteDuration
)
161 private stopConnectors (): void {
162 if (this.chargingStation
.hasEvses
) {
163 for (const [evseId
, evseStatus
] of this.chargingStation
.evses
) {
165 for (const connectorId
of evseStatus
.connectors
.keys()) {
166 this.stopConnector(connectorId
)
171 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
172 if (connectorId
> 0) {
173 this.stopConnector(connectorId
)
179 private async internalStartConnector (
181 stopAbsoluteDuration
?: boolean
183 this.setStartConnectorStatus(connectorId
, stopAbsoluteDuration
)
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()
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
)
202 const wait
= secondsToMilliseconds(
204 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()
205 ?.minDelayBetweenTwoTransactions
,
206 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()
207 ?.maxDelayBetweenTwoTransactions
210 logger
.info(`${this.logPrefix(connectorId)} waiting for ${formatDurationMilliSeconds(wait)}`)
212 const start
= secureRandom()
215 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
216 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()!.probabilityOfStart
218 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
219 this.connectorsStatus
.get(connectorId
)!.skippedConsecutiveTransactions
= 0
221 const startResponse
= await this.startTransaction(connectorId
)
222 if (startResponse
?.idTagInfo
.status === AuthorizationStatus
.ACCEPTED
) {
223 // Wait until end of transaction
224 const waitTrxEnd
= secondsToMilliseconds(
226 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()?.minDuration
,
227 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()?.maxDuration
231 `${this.logPrefix(connectorId)} transaction started with id ${
232 this.chargingStation.getConnectorStatus(connectorId)?.transactionId
233 } and will stop in ${formatDurationMilliSeconds(waitTrxEnd)}`
235 await sleep(waitTrxEnd
)
236 await this.stopTransaction(connectorId
)
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
244 `${this.logPrefix(connectorId)} skipped consecutively ${
245 this.connectorsStatus.get(connectorId)?.skippedConsecutiveTransactions
246 }/${this.connectorsStatus.get(connectorId)?.skippedTransactions} transaction(s)`
249 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
250 this.connectorsStatus
.get(connectorId
)!.lastRunDate
= new Date()
252 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
253 this.connectorsStatus
.get(connectorId
)!.stoppedDate
= new Date()
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()
265 `${this.logPrefix(connectorId)} connector status: %j`,
266 this.connectorsStatus
.get(connectorId
)
270 private setStartConnectorStatus (
272 stopAbsoluteDuration
= this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()
273 ?.stopAbsoluteDuration
275 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
276 this.connectorsStatus
.get(connectorId
)!.startDate
= new Date()
278 stopAbsoluteDuration
=== false ||
279 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
280 !isValidDate(this.connectorsStatus
.get(connectorId
)!.stopDate
)
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() +
287 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
288 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()!.stopAfterHours
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
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
!) {
305 )} entered in transaction loop while the ATG stop date has been reached`
309 if (!this.chargingStation
.inAcceptedState()) {
313 )} entered in transaction loop while the charging station is not in accepted state`
317 if (!this.chargingStation
.isChargingStationAvailable()) {
321 )} entered in transaction loop while the charging station is unavailable`
325 if (!this.chargingStation
.isConnectorAvailable(connectorId
)) {
329 )} entered in transaction loop while the connector ${connectorId} is unavailable`
333 const connectorStatus
= this.chargingStation
.getConnectorStatus(connectorId
)
334 if (connectorStatus
?.transactionStarted
=== true) {
336 `${this.logPrefix(connectorId)} entered in transaction loop while a transaction ${
337 connectorStatus.transactionId
338 } is already started on connector ${connectorId}`
345 private async waitChargingStationAvailable (connectorId
: number): Promise
<void> {
347 while (!this.chargingStation
.isChargingStationAvailable()) {
352 )} transaction loop waiting for charging station to be available`
356 await sleep(Constants
.DEFAULT_ATG_WAIT_TIME
)
360 private async waitConnectorAvailable (connectorId
: number): Promise
<void> {
362 while (!this.chargingStation
.isConnectorAvailable(connectorId
)) {
367 )} transaction loop waiting for connector ${connectorId} to be available`
371 await sleep(Constants
.DEFAULT_ATG_WAIT_TIME
)
375 private async waitRunningTransactionStopped (connectorId
: number): Promise
<void> {
376 const connectorStatus
= this.chargingStation
.getConnectorStatus(connectorId
)
378 while (connectorStatus
?.transactionStarted
=== true) {
381 `${this.logPrefix(connectorId)} transaction loop waiting for started transaction ${
382 connectorStatus.transactionId
383 } on connector ${connectorId} to be stopped`
387 await sleep(Constants
.DEFAULT_ATG_WAIT_TIME
)
391 private initializeConnectorsStatus (): void {
392 if (this.chargingStation
.hasEvses
) {
393 for (const [evseId
, evseStatus
] of this.chargingStation
.evses
) {
395 for (const connectorId
of evseStatus
.connectors
.keys()) {
396 this.connectorsStatus
.set(connectorId
, this.getConnectorStatus(connectorId
))
401 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
402 if (connectorId
> 0) {
403 this.connectorsStatus
.set(connectorId
, this.getConnectorStatus(connectorId
))
409 private getConnectorStatus (connectorId
: number): Status
{
410 const statusIndex
= connectorId
- 1
411 if (statusIndex
< 0) {
412 logger
.error(`${this.logPrefix(connectorId)} invalid connector id`)
413 throw new BaseError(`Invalid connector id ${connectorId}`)
415 let connectorStatus
: Status
| undefined
416 if (this.chargingStation
.getAutomaticTransactionGeneratorStatuses()?.[statusIndex
] != null) {
417 connectorStatus
= clone
<Status
>(
418 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
419 this.chargingStation
.getAutomaticTransactionGeneratorStatuses()![statusIndex
]
425 )} no status found for connector #${connectorId} in charging station configuration file. New status will be created`
428 if (connectorStatus
!= null) {
429 connectorStatus
.startDate
= convertToDate(connectorStatus
.startDate
)
430 connectorStatus
.lastRunDate
= convertToDate(connectorStatus
.lastRunDate
)
431 connectorStatus
.stopDate
= convertToDate(connectorStatus
.stopDate
)
432 connectorStatus
.stoppedDate
= convertToDate(connectorStatus
.stoppedDate
)
435 (connectorStatus
.start
||
436 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()?.enable
!== true)
438 connectorStatus
.start
= false
444 authorizeRequests
: 0,
445 acceptedAuthorizeRequests
: 0,
446 rejectedAuthorizeRequests
: 0,
447 startTransactionRequests
: 0,
448 acceptedStartTransactionRequests
: 0,
449 rejectedStartTransactionRequests
: 0,
450 stopTransactionRequests
: 0,
451 acceptedStopTransactionRequests
: 0,
452 rejectedStopTransactionRequests
: 0,
453 skippedConsecutiveTransactions
: 0,
454 skippedTransactions
: 0
459 private async startTransaction (
461 ): Promise
<StartTransactionResponse
| undefined> {
462 const measureId
= 'StartTransaction with ATG'
463 const beginId
= PerformanceStatistics
.beginMeasure(measureId
)
464 let startResponse
: StartTransactionResponse
| undefined
465 if (this.chargingStation
.hasIdTags()) {
466 const idTag
= IdTagsCache
.getInstance().getIdTag(
467 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
468 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()!.idTagDistribution
!,
469 this.chargingStation
,
472 const startTransactionLogMsg
= `${this.logPrefix(
474 )} start transaction with an idTag '${idTag}'`
475 if (this.getRequireAuthorize()) {
476 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
477 ++this.connectorsStatus
.get(connectorId
)!.authorizeRequests
478 if (await isIdTagAuthorized(this.chargingStation
, connectorId
, idTag
)) {
479 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
480 ++this.connectorsStatus
.get(connectorId
)!.acceptedAuthorizeRequests
481 logger
.info(startTransactionLogMsg
)
483 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
484 Partial
<StartTransactionRequest
>,
485 StartTransactionResponse
486 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, {
490 this.handleStartTransactionResponse(connectorId
, startResponse
)
491 PerformanceStatistics
.endMeasure(measureId
, beginId
)
494 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
495 ++this.connectorsStatus
.get(connectorId
)!.rejectedAuthorizeRequests
496 PerformanceStatistics
.endMeasure(measureId
, beginId
)
499 logger
.info(startTransactionLogMsg
)
501 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
502 Partial
<StartTransactionRequest
>,
503 StartTransactionResponse
504 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, {
508 this.handleStartTransactionResponse(connectorId
, startResponse
)
509 PerformanceStatistics
.endMeasure(measureId
, beginId
)
512 logger
.info(`${this.logPrefix(connectorId)} start transaction without an idTag`)
513 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
514 Partial
<StartTransactionRequest
>,
515 StartTransactionResponse
516 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, {
519 this.handleStartTransactionResponse(connectorId
, startResponse
)
520 PerformanceStatistics
.endMeasure(measureId
, beginId
)
524 private async stopTransaction (
526 reason
= StopTransactionReason
.LOCAL
527 ): Promise
<StopTransactionResponse
| undefined> {
528 const measureId
= 'StopTransaction with ATG'
529 const beginId
= PerformanceStatistics
.beginMeasure(measureId
)
530 let stopResponse
: StopTransactionResponse
| undefined
531 if (this.chargingStation
.getConnectorStatus(connectorId
)?.transactionStarted
=== true) {
533 `${this.logPrefix(connectorId)} stop transaction with id ${
534 this.chargingStation.getConnectorStatus(connectorId)?.transactionId
537 stopResponse
= await this.chargingStation
.stopTransactionOnConnector(connectorId
, reason
)
538 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
539 ++this.connectorsStatus
.get(connectorId
)!.stopTransactionRequests
540 if (stopResponse
.idTagInfo
?.status === AuthorizationStatus
.ACCEPTED
) {
541 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
542 ++this.connectorsStatus
.get(connectorId
)!.acceptedStopTransactionRequests
544 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
545 ++this.connectorsStatus
.get(connectorId
)!.rejectedStopTransactionRequests
548 const transactionId
= this.chargingStation
.getConnectorStatus(connectorId
)?.transactionId
550 `${this.logPrefix(connectorId)} stopping a not started transaction${
551 transactionId != null ? ` with id ${transactionId}
` : ''
555 PerformanceStatistics
.endMeasure(measureId
, beginId
)
559 private getRequireAuthorize (): boolean {
561 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()?.requireAuthorize
?? true
565 private readonly logPrefix
= (connectorId
?: number): string => {
567 ` ${this.chargingStation.stationInfo?.chargingStationId} | ATG${
568 connectorId != null ? ` on connector #${connectorId}
` : ''
573 private handleStartTransactionResponse (
575 startResponse
: StartTransactionResponse
577 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
578 ++this.connectorsStatus
.get(connectorId
)!.startTransactionRequests
579 if (startResponse
.idTagInfo
.status === AuthorizationStatus
.ACCEPTED
) {
580 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
581 ++this.connectorsStatus
.get(connectorId
)!.acceptedStartTransactionRequests
583 logger
.warn(`${this.logPrefix(connectorId)} start transaction rejected`)
584 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
585 ++this.connectorsStatus
.get(connectorId
)!.rejectedStartTransactionRequests