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) {
338 )} entered in transaction loop while a transaction ${connectorStatus.transactionId} 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) {
383 )} transaction loop waiting for started transaction ${connectorStatus.transactionId} 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 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
]
417 } else if (this.chargingStation
.getAutomaticTransactionGeneratorStatuses() != null) {
419 `${this.logPrefix(connectorId)} no status found for connector #${connectorId} in charging station configuration file. New status will be created`
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
)
429 (connectorStatus
.start
||
430 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()?.enable
!== true)
432 connectorStatus
.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
453 private async startTransaction (
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
,
466 const startTransactionLogMsg
= `${this.logPrefix(
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
)
477 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
478 Partial
<StartTransactionRequest
>,
479 StartTransactionResponse
480 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, {
484 this.handleStartTransactionResponse(connectorId
, startResponse
)
485 PerformanceStatistics
.endMeasure(measureId
, beginId
)
488 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
489 ++this.connectorsStatus
.get(connectorId
)!.rejectedAuthorizeRequests
490 PerformanceStatistics
.endMeasure(measureId
, beginId
)
493 logger
.info(startTransactionLogMsg
)
495 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
496 Partial
<StartTransactionRequest
>,
497 StartTransactionResponse
498 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, {
502 this.handleStartTransactionResponse(connectorId
, startResponse
)
503 PerformanceStatistics
.endMeasure(measureId
, beginId
)
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
)
516 private async stopTransaction (
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) {
525 `${this.logPrefix(connectorId)} stop transaction with id ${
526 this.chargingStation.getConnectorStatus(connectorId)?.transactionId
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
536 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
537 ++this.connectorsStatus
.get(connectorId
)!.rejectedStopTransactionRequests
540 const transactionId
= this.chargingStation
.getConnectorStatus(connectorId
)?.transactionId
542 `${this.logPrefix(connectorId)} stopping a not started transaction${
543 transactionId != null ? ` with id ${transactionId}
` : ''
547 PerformanceStatistics
.endMeasure(measureId
, beginId
)
551 private getRequireAuthorize (): boolean {
553 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()?.requireAuthorize
?? true
557 private readonly logPrefix
= (connectorId
?: number): string => {
559 ` ${this.chargingStation.stationInfo?.chargingStationId} | ATG${
560 connectorId != null ? ` on connector #${connectorId}
` : ''
565 private handleStartTransactionResponse (
567 startResponse
: StartTransactionResponse
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
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