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'
11 ChargingStationEvents
,
13 type StartTransactionRequest
,
14 type StartTransactionResponse
,
16 StopTransactionReason
,
17 type StopTransactionResponse
18 } from
'../types/index.js'
23 formatDurationMilliSeconds
,
29 } from
'../utils/index.js'
30 import type { ChargingStation
} from
'./ChargingStation.js'
31 import { checkChargingStation
} from
'./Helpers.js'
32 import { IdTagsCache
} from
'./IdTagsCache.js'
33 import { isIdTagAuthorized
} from
'./ocpp/index.js'
35 export class AutomaticTransactionGenerator
{
36 private static readonly instances
: Map
<string, AutomaticTransactionGenerator
> = new Map
<
38 AutomaticTransactionGenerator
41 public readonly connectorsStatus
: Map
<number, Status
>
42 public started
: boolean
43 private starting
: boolean
44 private stopping
: boolean
45 private readonly chargingStation
: ChargingStation
47 private constructor (chargingStation
: ChargingStation
) {
51 this.chargingStation
= chargingStation
52 this.connectorsStatus
= new Map
<number, Status
>()
53 this.initializeConnectorsStatus()
56 public static getInstance (
57 chargingStation
: ChargingStation
58 ): AutomaticTransactionGenerator
| undefined {
59 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
60 if (!AutomaticTransactionGenerator
.instances
.has(chargingStation
.stationInfo
!.hashId
)) {
61 AutomaticTransactionGenerator
.instances
.set(
62 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
63 chargingStation
.stationInfo
!.hashId
,
64 new AutomaticTransactionGenerator(chargingStation
)
67 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
68 return AutomaticTransactionGenerator
.instances
.get(chargingStation
.stationInfo
!.hashId
)
71 public static deleteInstance (chargingStation
: ChargingStation
): boolean {
72 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
73 return AutomaticTransactionGenerator
.instances
.delete(chargingStation
.stationInfo
!.hashId
)
76 public start (stopAbsoluteDuration
?: boolean): void {
77 if (!checkChargingStation(this.chargingStation
, this.logPrefix())) {
81 logger
.warn(`${this.logPrefix()} is already started`)
85 logger
.warn(`${this.logPrefix()} is already starting`)
89 this.startConnectors(stopAbsoluteDuration
)
94 public stop (): void {
96 logger
.warn(`${this.logPrefix()} is already stopped`)
100 logger
.warn(`${this.logPrefix()} is already stopping`)
104 this.stopConnectors()
106 this.stopping
= false
109 public startConnector (connectorId
: number, stopAbsoluteDuration
?: boolean): void {
110 if (!checkChargingStation(this.chargingStation
, this.logPrefix(connectorId
))) {
113 if (!this.connectorsStatus
.has(connectorId
)) {
114 logger
.error(`${this.logPrefix(connectorId)} starting on non existing connector`)
115 throw new BaseError(`Connector ${connectorId} does not exist`)
117 if (this.connectorsStatus
.get(connectorId
)?.start
=== false) {
118 this.internalStartConnector(connectorId
, stopAbsoluteDuration
).catch(Constants
.EMPTY_FUNCTION
)
119 } else if (this.connectorsStatus
.get(connectorId
)?.start
=== true) {
120 logger
.warn(`${this.logPrefix(connectorId)} is already started on connector`)
124 public stopConnector (connectorId
: number): void {
125 if (!this.connectorsStatus
.has(connectorId
)) {
126 logger
.error(`${this.logPrefix(connectorId)} stopping on non existing connector`)
127 throw new BaseError(`Connector ${connectorId} does not exist`)
129 if (this.connectorsStatus
.get(connectorId
)?.start
=== true) {
130 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
131 this.connectorsStatus
.get(connectorId
)!.start
= false
132 } else if (this.connectorsStatus
.get(connectorId
)?.start
=== false) {
133 logger
.warn(`${this.logPrefix(connectorId)} is already stopped on connector`)
137 private startConnectors (stopAbsoluteDuration
?: boolean): void {
139 this.connectorsStatus
.size
> 0 &&
140 this.connectorsStatus
.size
!== this.chargingStation
.getNumberOfConnectors()
142 this.connectorsStatus
.clear()
143 this.initializeConnectorsStatus()
145 if (this.chargingStation
.hasEvses
) {
146 for (const [evseId
, evseStatus
] of this.chargingStation
.evses
) {
148 for (const connectorId
of evseStatus
.connectors
.keys()) {
149 this.startConnector(connectorId
, stopAbsoluteDuration
)
154 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
155 if (connectorId
> 0) {
156 this.startConnector(connectorId
, stopAbsoluteDuration
)
162 private stopConnectors (): void {
163 if (this.chargingStation
.hasEvses
) {
164 for (const [evseId
, evseStatus
] of this.chargingStation
.evses
) {
166 for (const connectorId
of evseStatus
.connectors
.keys()) {
167 this.stopConnector(connectorId
)
172 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
173 if (connectorId
> 0) {
174 this.stopConnector(connectorId
)
180 private async internalStartConnector (
182 stopAbsoluteDuration
?: boolean
184 this.setStartConnectorStatus(connectorId
, stopAbsoluteDuration
)
188 )} started on connector and will run for ${formatDurationMilliSeconds(
189 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
190 this.connectorsStatus.get(connectorId)!.stopDate!.getTime() -
191 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
192 this.connectorsStatus.get(connectorId)!.startDate!.getTime()
195 while (this.connectorsStatus
.get(connectorId
)?.start
=== true) {
196 await this.waitChargingStationAvailable(connectorId
)
197 await this.waitConnectorAvailable(connectorId
)
198 await this.waitRunningTransactionStopped(connectorId
)
199 if (!this.canStartConnector(connectorId
)) {
200 this.stopConnector(connectorId
)
203 const wait
= secondsToMilliseconds(
205 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()
206 ?.minDelayBetweenTwoTransactions
,
207 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()
208 ?.maxDelayBetweenTwoTransactions
211 logger
.info(`${this.logPrefix(connectorId)} waiting for ${formatDurationMilliSeconds(wait)}`)
213 const start
= secureRandom()
216 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
217 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()!.probabilityOfStart
219 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
220 this.connectorsStatus
.get(connectorId
)!.skippedConsecutiveTransactions
= 0
222 const startResponse
= await this.startTransaction(connectorId
)
223 if (startResponse
?.idTagInfo
.status === AuthorizationStatus
.ACCEPTED
) {
224 // Wait until end of transaction
225 const waitTrxEnd
= secondsToMilliseconds(
227 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()?.minDuration
,
228 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()?.maxDuration
232 `${this.logPrefix(connectorId)} transaction started with id ${
233 this.chargingStation.getConnectorStatus(connectorId)?.transactionId
234 } and will stop in ${formatDurationMilliSeconds(waitTrxEnd)}`
236 await sleep(waitTrxEnd
)
237 await this.stopTransaction(connectorId
)
240 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
241 ++this.connectorsStatus
.get(connectorId
)!.skippedConsecutiveTransactions
242 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
243 ++this.connectorsStatus
.get(connectorId
)!.skippedTransactions
245 `${this.logPrefix(connectorId)} skipped consecutively ${
246 this.connectorsStatus.get(connectorId)?.skippedConsecutiveTransactions
247 }/${this.connectorsStatus.get(connectorId)?.skippedTransactions} transaction(s)`
250 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
251 this.connectorsStatus
.get(connectorId
)!.lastRunDate
= new Date()
253 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
254 this.connectorsStatus
.get(connectorId
)!.stoppedDate
= new Date()
258 )} stopped on connector and lasted for ${formatDurationMilliSeconds(
259 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
260 this.connectorsStatus.get(connectorId)!.stoppedDate!.getTime() -
261 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
262 this.connectorsStatus.get(connectorId)!.startDate!.getTime()
266 `${this.logPrefix(connectorId)} stopped with connector status: %j`,
267 this.connectorsStatus
.get(connectorId
)
269 this.chargingStation
.emit(ChargingStationEvents
.updated
)
272 private setStartConnectorStatus (
274 stopAbsoluteDuration
= this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()
275 ?.stopAbsoluteDuration
277 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
278 this.connectorsStatus
.get(connectorId
)!.startDate
= new Date()
280 stopAbsoluteDuration
=== false ||
281 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
282 !isValidDate(this.connectorsStatus
.get(connectorId
)!.stopDate
)
284 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
285 this.connectorsStatus
.get(connectorId
)!.stopDate
= new Date(
286 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
287 this.connectorsStatus
.get(connectorId
)!.startDate
!.getTime() +
289 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
290 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()!.stopAfterHours
294 delete this.connectorsStatus
.get(connectorId
)?.stoppedDate
295 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
296 this.connectorsStatus
.get(connectorId
)!.skippedConsecutiveTransactions
= 0
297 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
298 this.connectorsStatus
.get(connectorId
)!.start
= true
299 this.chargingStation
.emit(ChargingStationEvents
.updated
)
302 private canStartConnector (connectorId
: number): boolean {
303 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
304 if (new Date() > this.connectorsStatus
.get(connectorId
)!.stopDate
!) {
308 )} entered in transaction loop while the ATG stop date has been reached`
312 if (!this.chargingStation
.inAcceptedState()) {
316 )} entered in transaction loop while the charging station is not in accepted state`
320 if (!this.chargingStation
.isChargingStationAvailable()) {
324 )} entered in transaction loop while the charging station is unavailable`
328 if (!this.chargingStation
.isConnectorAvailable(connectorId
)) {
332 )} entered in transaction loop while the connector ${connectorId} is unavailable`
336 const connectorStatus
= this.chargingStation
.getConnectorStatus(connectorId
)
337 if (connectorStatus
?.transactionStarted
=== true) {
339 `${this.logPrefix(connectorId)} entered in transaction loop while a transaction ${
340 connectorStatus.transactionId
341 } is already started on connector ${connectorId}`
348 private async waitChargingStationAvailable (connectorId
: number): Promise
<void> {
350 while (!this.chargingStation
.isChargingStationAvailable()) {
355 )} transaction loop waiting for charging station to be available`
359 await sleep(Constants
.DEFAULT_ATG_WAIT_TIME
)
363 private async waitConnectorAvailable (connectorId
: number): Promise
<void> {
365 while (!this.chargingStation
.isConnectorAvailable(connectorId
)) {
370 )} transaction loop waiting for connector ${connectorId} to be available`
374 await sleep(Constants
.DEFAULT_ATG_WAIT_TIME
)
378 private async waitRunningTransactionStopped (connectorId
: number): Promise
<void> {
379 const connectorStatus
= this.chargingStation
.getConnectorStatus(connectorId
)
381 while (connectorStatus
?.transactionStarted
=== true) {
384 `${this.logPrefix(connectorId)} transaction loop waiting for started transaction ${
385 connectorStatus.transactionId
386 } on connector ${connectorId} to be stopped`
390 await sleep(Constants
.DEFAULT_ATG_WAIT_TIME
)
394 private initializeConnectorsStatus (): void {
395 if (this.chargingStation
.hasEvses
) {
396 for (const [evseId
, evseStatus
] of this.chargingStation
.evses
) {
398 for (const connectorId
of evseStatus
.connectors
.keys()) {
399 this.connectorsStatus
.set(connectorId
, this.getConnectorStatus(connectorId
))
404 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
405 if (connectorId
> 0) {
406 this.connectorsStatus
.set(connectorId
, this.getConnectorStatus(connectorId
))
412 private getConnectorStatus (connectorId
: number): Status
{
413 const statusIndex
= connectorId
- 1
414 if (statusIndex
< 0) {
415 logger
.error(`${this.logPrefix(connectorId)} invalid connector id`)
416 throw new BaseError(`Invalid connector id ${connectorId}`)
418 let connectorStatus
: Status
| undefined
419 if (this.chargingStation
.getAutomaticTransactionGeneratorStatuses()?.[statusIndex
] != null) {
420 connectorStatus
= clone
<Status
>(
421 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
422 this.chargingStation
.getAutomaticTransactionGeneratorStatuses()![statusIndex
]
428 )} no status found for connector #${connectorId} in charging station configuration file. New status will be created`
431 if (connectorStatus
!= null) {
432 connectorStatus
.startDate
= convertToDate(connectorStatus
.startDate
)
433 connectorStatus
.lastRunDate
= convertToDate(connectorStatus
.lastRunDate
)
434 connectorStatus
.stopDate
= convertToDate(connectorStatus
.stopDate
)
435 connectorStatus
.stoppedDate
= convertToDate(connectorStatus
.stoppedDate
)
438 (connectorStatus
.start
||
439 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()?.enable
!== true)
441 connectorStatus
.start
= false
447 authorizeRequests
: 0,
448 acceptedAuthorizeRequests
: 0,
449 rejectedAuthorizeRequests
: 0,
450 startTransactionRequests
: 0,
451 acceptedStartTransactionRequests
: 0,
452 rejectedStartTransactionRequests
: 0,
453 stopTransactionRequests
: 0,
454 acceptedStopTransactionRequests
: 0,
455 rejectedStopTransactionRequests
: 0,
456 skippedConsecutiveTransactions
: 0,
457 skippedTransactions
: 0
462 private async startTransaction (
464 ): Promise
<StartTransactionResponse
| undefined> {
465 const measureId
= 'StartTransaction with ATG'
466 const beginId
= PerformanceStatistics
.beginMeasure(measureId
)
467 let startResponse
: StartTransactionResponse
| undefined
468 if (this.chargingStation
.hasIdTags()) {
469 const idTag
= IdTagsCache
.getInstance().getIdTag(
470 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
471 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()!.idTagDistribution
!,
472 this.chargingStation
,
475 const startTransactionLogMsg
= `${this.logPrefix(
477 )} start transaction with an idTag '${idTag}'`
478 if (this.getRequireAuthorize()) {
479 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
480 ++this.connectorsStatus
.get(connectorId
)!.authorizeRequests
481 if (await isIdTagAuthorized(this.chargingStation
, connectorId
, idTag
)) {
482 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
483 ++this.connectorsStatus
.get(connectorId
)!.acceptedAuthorizeRequests
484 logger
.info(startTransactionLogMsg
)
486 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
487 Partial
<StartTransactionRequest
>,
488 StartTransactionResponse
489 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, {
493 this.handleStartTransactionResponse(connectorId
, startResponse
)
494 PerformanceStatistics
.endMeasure(measureId
, beginId
)
497 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
498 ++this.connectorsStatus
.get(connectorId
)!.rejectedAuthorizeRequests
499 PerformanceStatistics
.endMeasure(measureId
, beginId
)
502 logger
.info(startTransactionLogMsg
)
504 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
505 Partial
<StartTransactionRequest
>,
506 StartTransactionResponse
507 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, {
511 this.handleStartTransactionResponse(connectorId
, startResponse
)
512 PerformanceStatistics
.endMeasure(measureId
, beginId
)
515 logger
.info(`${this.logPrefix(connectorId)} start transaction without an idTag`)
516 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
517 Partial
<StartTransactionRequest
>,
518 StartTransactionResponse
519 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, {
522 this.handleStartTransactionResponse(connectorId
, startResponse
)
523 PerformanceStatistics
.endMeasure(measureId
, beginId
)
527 private async stopTransaction (
529 reason
= StopTransactionReason
.LOCAL
530 ): Promise
<StopTransactionResponse
| undefined> {
531 const measureId
= 'StopTransaction with ATG'
532 const beginId
= PerformanceStatistics
.beginMeasure(measureId
)
533 let stopResponse
: StopTransactionResponse
| undefined
534 if (this.chargingStation
.getConnectorStatus(connectorId
)?.transactionStarted
=== true) {
536 `${this.logPrefix(connectorId)} stop transaction with id ${
537 this.chargingStation.getConnectorStatus(connectorId)?.transactionId
540 stopResponse
= await this.chargingStation
.stopTransactionOnConnector(connectorId
, reason
)
541 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
542 ++this.connectorsStatus
.get(connectorId
)!.stopTransactionRequests
543 if (stopResponse
.idTagInfo
?.status === AuthorizationStatus
.ACCEPTED
) {
544 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
545 ++this.connectorsStatus
.get(connectorId
)!.acceptedStopTransactionRequests
547 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
548 ++this.connectorsStatus
.get(connectorId
)!.rejectedStopTransactionRequests
551 const transactionId
= this.chargingStation
.getConnectorStatus(connectorId
)?.transactionId
553 `${this.logPrefix(connectorId)} stopping a not started transaction${
554 transactionId != null ? ` with id ${transactionId}
` : ''
558 PerformanceStatistics
.endMeasure(measureId
, beginId
)
562 private getRequireAuthorize (): boolean {
564 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()?.requireAuthorize
?? true
568 private readonly logPrefix
= (connectorId
?: number): string => {
570 ` ${this.chargingStation.stationInfo?.chargingStationId} | ATG${
571 connectorId != null ? ` on connector #${connectorId}
` : ''
576 private handleStartTransactionResponse (
578 startResponse
: StartTransactionResponse
580 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
581 ++this.connectorsStatus
.get(connectorId
)!.startTransactionRequests
582 if (startResponse
.idTagInfo
.status === AuthorizationStatus
.ACCEPTED
) {
583 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
584 ++this.connectorsStatus
.get(connectorId
)!.acceptedStartTransactionRequests
586 logger
.warn(`${this.logPrefix(connectorId)} start transaction rejected`)
587 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
588 ++this.connectorsStatus
.get(connectorId
)!.rejectedStartTransactionRequests