1 // Partial Copyright Jerome Benoit. 2021-2024. All Rights Reserved.
3 import { hoursToMilliseconds
, secondsToMilliseconds
} from
'date-fns'
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'
14 type StartTransactionRequest
,
15 type StartTransactionResponse
,
17 StopTransactionReason
,
18 type StopTransactionResponse
19 } from
'../types/index.js'
24 formatDurationMilliSeconds
,
31 } from
'../utils/index.js'
33 export class AutomaticTransactionGenerator
{
34 private static readonly instances
: Map
<string, AutomaticTransactionGenerator
> = new Map
<
36 AutomaticTransactionGenerator
39 public readonly connectorsStatus
: Map
<number, Status
>
40 public started
: boolean
41 private starting
: boolean
42 private stopping
: boolean
43 private readonly chargingStation
: ChargingStation
45 private constructor (chargingStation
: ChargingStation
) {
49 this.chargingStation
= chargingStation
50 this.connectorsStatus
= new Map
<number, Status
>()
51 this.initializeConnectorsStatus()
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
)
65 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
66 return AutomaticTransactionGenerator
.instances
.get(chargingStation
.stationInfo
!.hashId
)
69 public start (stopAbsoluteDuration
?: boolean): void {
70 if (!checkChargingStation(this.chargingStation
, this.logPrefix())) {
74 logger
.warn(`${this.logPrefix()} is already started`)
78 logger
.warn(`${this.logPrefix()} is already starting`)
82 this.startConnectors(stopAbsoluteDuration
)
87 public stop (): void {
89 logger
.warn(`${this.logPrefix()} is already stopped`)
93 logger
.warn(`${this.logPrefix()} is already stopping`)
102 public startConnector (connectorId
: number, stopAbsoluteDuration
?: boolean): void {
103 if (!checkChargingStation(this.chargingStation
, this.logPrefix(connectorId
))) {
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`)
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`)
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`)
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`)
130 private startConnectors (stopAbsoluteDuration
?: boolean): void {
132 this.connectorsStatus
.size
> 0 &&
133 this.connectorsStatus
.size
!== this.chargingStation
.getNumberOfConnectors()
135 this.connectorsStatus
.clear()
136 this.initializeConnectorsStatus()
138 if (this.chargingStation
.hasEvses
) {
139 for (const [evseId
, evseStatus
] of this.chargingStation
.evses
) {
141 for (const connectorId
of evseStatus
.connectors
.keys()) {
142 this.startConnector(connectorId
, stopAbsoluteDuration
)
147 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
148 if (connectorId
> 0) {
149 this.startConnector(connectorId
, stopAbsoluteDuration
)
155 private stopConnectors (): void {
156 if (this.chargingStation
.hasEvses
) {
157 for (const [evseId
, evseStatus
] of this.chargingStation
.evses
) {
159 for (const connectorId
of evseStatus
.connectors
.keys()) {
160 this.stopConnector(connectorId
)
165 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
166 if (connectorId
> 0) {
167 this.stopConnector(connectorId
)
173 private async internalStartConnector (
175 stopAbsoluteDuration
?: boolean
177 this.setStartConnectorStatus(connectorId
, stopAbsoluteDuration
)
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()
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
)
196 const wait
= secondsToMilliseconds(
198 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()
199 ?.maxDelayBetweenTwoTransactions
,
200 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()
201 ?.minDelayBetweenTwoTransactions
204 logger
.info(`${this.logPrefix(connectorId)} waiting for ${formatDurationMilliSeconds(wait)}`)
206 const start
= secureRandom()
209 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
210 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()!.probabilityOfStart
212 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
213 this.connectorsStatus
.get(connectorId
)!.skippedConsecutiveTransactions
= 0
215 const startResponse
= await this.startTransaction(connectorId
)
216 if (startResponse
?.idTagInfo
.status === AuthorizationStatus
.ACCEPTED
) {
217 // Wait until end of transaction
218 const waitTrxEnd
= secondsToMilliseconds(
220 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()?.maxDuration
,
221 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()?.minDuration
225 `${this.logPrefix(connectorId)} transaction started with id ${
226 this.chargingStation.getConnectorStatus(connectorId)?.transactionId
227 } and will stop in ${formatDurationMilliSeconds(waitTrxEnd)}`
229 await sleep(waitTrxEnd
)
230 await this.stopTransaction(connectorId
)
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
238 `${this.logPrefix(connectorId)} skipped consecutively ${
239 this.connectorsStatus.get(connectorId)?.skippedConsecutiveTransactions
240 }/${this.connectorsStatus.get(connectorId)?.skippedTransactions} transaction(s)`
243 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
244 this.connectorsStatus
.get(connectorId
)!.lastRunDate
= new Date()
246 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
247 this.connectorsStatus
.get(connectorId
)!.stoppedDate
= new Date()
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()
259 `${this.logPrefix(connectorId)} connector status: %j`,
260 this.connectorsStatus
.get(connectorId
)
264 private setStartConnectorStatus (
266 stopAbsoluteDuration
= this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()
267 ?.stopAbsoluteDuration
269 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
270 this.connectorsStatus
.get(connectorId
)!.startDate
= new Date()
272 stopAbsoluteDuration
=== false ||
273 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
274 !isValidDate(this.connectorsStatus
.get(connectorId
)!.stopDate
)
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() +
281 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
282 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()!.stopAfterHours
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
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
!) {
299 )} entered in transaction loop while the ATG stop date has been reached`
303 if (!this.chargingStation
.inAcceptedState()) {
307 )} entered in transaction loop while the charging station is not in accepted state`
311 if (!this.chargingStation
.isChargingStationAvailable()) {
315 )} entered in transaction loop while the charging station is unavailable`
319 if (!this.chargingStation
.isConnectorAvailable(connectorId
)) {
323 )} entered in transaction loop while the connector ${connectorId} is unavailable`
327 const connectorStatus
= this.chargingStation
.getConnectorStatus(connectorId
)
328 if (connectorStatus
?.transactionStarted
=== true) {
332 )} entered in transaction loop while a transaction ${connectorStatus.transactionId} is already started on connector ${connectorId}`
339 private async waitChargingStationAvailable (connectorId
: number): Promise
<void> {
341 while (!this.chargingStation
.isChargingStationAvailable()) {
346 )} transaction loop waiting for charging station to be available`
350 await sleep(Constants
.DEFAULT_ATG_WAIT_TIME
)
354 private async waitConnectorAvailable (connectorId
: number): Promise
<void> {
356 while (!this.chargingStation
.isConnectorAvailable(connectorId
)) {
361 )} transaction loop waiting for connector ${connectorId} to be available`
365 await sleep(Constants
.DEFAULT_ATG_WAIT_TIME
)
369 private async waitRunningTransactionStopped (connectorId
: number): Promise
<void> {
370 const connectorStatus
= this.chargingStation
.getConnectorStatus(connectorId
)
372 while (connectorStatus
?.transactionStarted
=== true) {
377 )} transaction loop waiting for started transaction ${connectorStatus.transactionId} on connector ${connectorId} to be stopped`
381 await sleep(Constants
.DEFAULT_ATG_WAIT_TIME
)
385 private initializeConnectorsStatus (): void {
386 if (this.chargingStation
.hasEvses
) {
387 for (const [evseId
, evseStatus
] of this.chargingStation
.evses
) {
389 for (const connectorId
of evseStatus
.connectors
.keys()) {
390 this.connectorsStatus
.set(connectorId
, this.getConnectorStatus(connectorId
))
395 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
396 if (connectorId
> 0) {
397 this.connectorsStatus
.set(connectorId
, this.getConnectorStatus(connectorId
))
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
]
411 } else if (this.chargingStation
.getAutomaticTransactionGeneratorStatuses() != null) {
413 `${this.logPrefix(connectorId)} no status found for connector #${connectorId} in charging station configuration file. New status will be created`
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
)
423 (connectorStatus
.start
||
424 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()?.enable
!== true)
426 connectorStatus
.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
447 private async startTransaction (
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
,
460 const startTransactionLogMsg
= `${this.logPrefix(
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
)
471 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
472 StartTransactionRequest
,
473 StartTransactionResponse
474 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, {
478 this.handleStartTransactionResponse(connectorId
, startResponse
)
479 PerformanceStatistics
.endMeasure(measureId
, beginId
)
482 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
483 ++this.connectorsStatus
.get(connectorId
)!.rejectedAuthorizeRequests
484 PerformanceStatistics
.endMeasure(measureId
, beginId
)
487 logger
.info(startTransactionLogMsg
)
489 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
490 StartTransactionRequest
,
491 StartTransactionResponse
492 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, {
496 this.handleStartTransactionResponse(connectorId
, startResponse
)
497 PerformanceStatistics
.endMeasure(measureId
, beginId
)
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
)
510 private async stopTransaction (
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) {
519 `${this.logPrefix(connectorId)} stop transaction with id ${
520 this.chargingStation.getConnectorStatus(connectorId)?.transactionId
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
530 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
531 ++this.connectorsStatus
.get(connectorId
)!.rejectedStopTransactionRequests
534 const transactionId
= this.chargingStation
.getConnectorStatus(connectorId
)?.transactionId
536 `${this.logPrefix(connectorId)} stopping a not started transaction${
537 transactionId != null ? ` with id ${transactionId}
` : ''
541 PerformanceStatistics
.endMeasure(measureId
, beginId
)
545 private getRequireAuthorize (): boolean {
547 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()?.requireAuthorize
?? true
551 private readonly logPrefix
= (connectorId
?: number): string => {
553 ` ${this.chargingStation.stationInfo?.chargingStationId} | ATG${
554 connectorId != null ? ` on connector #${connectorId}
` : ''
559 private handleStartTransactionResponse (
561 startResponse
: StartTransactionResponse
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
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