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 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
]
421 )} no status found for connector #${connectorId} in charging station configuration file. New status will be created`
424 if (connectorStatus
!= null) {
425 connectorStatus
.startDate
= convertToDate(connectorStatus
.startDate
)
426 connectorStatus
.lastRunDate
= convertToDate(connectorStatus
.lastRunDate
)
427 connectorStatus
.stopDate
= convertToDate(connectorStatus
.stopDate
)
428 connectorStatus
.stoppedDate
= convertToDate(connectorStatus
.stoppedDate
)
431 (connectorStatus
.start
||
432 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()?.enable
!== true)
434 connectorStatus
.start
= false
440 authorizeRequests
: 0,
441 acceptedAuthorizeRequests
: 0,
442 rejectedAuthorizeRequests
: 0,
443 startTransactionRequests
: 0,
444 acceptedStartTransactionRequests
: 0,
445 rejectedStartTransactionRequests
: 0,
446 stopTransactionRequests
: 0,
447 acceptedStopTransactionRequests
: 0,
448 rejectedStopTransactionRequests
: 0,
449 skippedConsecutiveTransactions
: 0,
450 skippedTransactions
: 0
455 private async startTransaction (
457 ): Promise
<StartTransactionResponse
| undefined> {
458 const measureId
= 'StartTransaction with ATG'
459 const beginId
= PerformanceStatistics
.beginMeasure(measureId
)
460 let startResponse
: StartTransactionResponse
| undefined
461 if (this.chargingStation
.hasIdTags()) {
462 const idTag
= IdTagsCache
.getInstance().getIdTag(
463 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
464 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()!.idTagDistribution
!,
465 this.chargingStation
,
468 const startTransactionLogMsg
= `${this.logPrefix(
470 )} start transaction with an idTag '${idTag}'`
471 if (this.getRequireAuthorize()) {
472 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
473 ++this.connectorsStatus
.get(connectorId
)!.authorizeRequests
474 if (await isIdTagAuthorized(this.chargingStation
, connectorId
, idTag
)) {
475 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
476 ++this.connectorsStatus
.get(connectorId
)!.acceptedAuthorizeRequests
477 logger
.info(startTransactionLogMsg
)
479 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
480 Partial
<StartTransactionRequest
>,
481 StartTransactionResponse
482 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, {
486 this.handleStartTransactionResponse(connectorId
, startResponse
)
487 PerformanceStatistics
.endMeasure(measureId
, beginId
)
490 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
491 ++this.connectorsStatus
.get(connectorId
)!.rejectedAuthorizeRequests
492 PerformanceStatistics
.endMeasure(measureId
, beginId
)
495 logger
.info(startTransactionLogMsg
)
497 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
498 Partial
<StartTransactionRequest
>,
499 StartTransactionResponse
500 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, {
504 this.handleStartTransactionResponse(connectorId
, startResponse
)
505 PerformanceStatistics
.endMeasure(measureId
, beginId
)
508 logger
.info(`${this.logPrefix(connectorId)} start transaction without an idTag`)
509 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
510 Partial
<StartTransactionRequest
>,
511 StartTransactionResponse
512 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, {
515 this.handleStartTransactionResponse(connectorId
, startResponse
)
516 PerformanceStatistics
.endMeasure(measureId
, beginId
)
520 private async stopTransaction (
522 reason
= StopTransactionReason
.LOCAL
523 ): Promise
<StopTransactionResponse
| undefined> {
524 const measureId
= 'StopTransaction with ATG'
525 const beginId
= PerformanceStatistics
.beginMeasure(measureId
)
526 let stopResponse
: StopTransactionResponse
| undefined
527 if (this.chargingStation
.getConnectorStatus(connectorId
)?.transactionStarted
=== true) {
529 `${this.logPrefix(connectorId)} stop transaction with id ${
530 this.chargingStation.getConnectorStatus(connectorId)?.transactionId
533 stopResponse
= await this.chargingStation
.stopTransactionOnConnector(connectorId
, reason
)
534 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
535 ++this.connectorsStatus
.get(connectorId
)!.stopTransactionRequests
536 if (stopResponse
.idTagInfo
?.status === AuthorizationStatus
.ACCEPTED
) {
537 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
538 ++this.connectorsStatus
.get(connectorId
)!.acceptedStopTransactionRequests
540 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
541 ++this.connectorsStatus
.get(connectorId
)!.rejectedStopTransactionRequests
544 const transactionId
= this.chargingStation
.getConnectorStatus(connectorId
)?.transactionId
546 `${this.logPrefix(connectorId)} stopping a not started transaction${
547 transactionId != null ? ` with id ${transactionId}
` : ''
551 PerformanceStatistics
.endMeasure(measureId
, beginId
)
555 private getRequireAuthorize (): boolean {
557 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()?.requireAuthorize
?? true
561 private readonly logPrefix
= (connectorId
?: number): string => {
563 ` ${this.chargingStation.stationInfo?.chargingStationId} | ATG${
564 connectorId != null ? ` on connector #${connectorId}
` : ''
569 private handleStartTransactionResponse (
571 startResponse
: StartTransactionResponse
573 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
574 ++this.connectorsStatus
.get(connectorId
)!.startTransactionRequests
575 if (startResponse
.idTagInfo
.status === AuthorizationStatus
.ACCEPTED
) {
576 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
577 ++this.connectorsStatus
.get(connectorId
)!.acceptedStartTransactionRequests
579 logger
.warn(`${this.logPrefix(connectorId)} start transaction rejected`)
580 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
581 ++this.connectorsStatus
.get(connectorId
)!.rejectedStartTransactionRequests