1 // Partial Copyright Jerome Benoit. 2021-2024. All Rights Reserved.
3 import { hoursToMilliseconds
, secondsToMilliseconds
} from
'date-fns'
5 import { BaseError
} from
'../exception/index.js'
6 import { PerformanceStatistics
} from
'../performance/index.js'
10 type StartTransactionRequest
,
11 type StartTransactionResponse
,
13 StopTransactionReason
,
14 type StopTransactionResponse
15 } from
'../types/index.js'
20 formatDurationMilliSeconds
,
27 } from
'../utils/index.js'
28 import type { ChargingStation
} from
'./ChargingStation.js'
29 import { checkChargingStation
} from
'./Helpers.js'
30 import { IdTagsCache
} from
'./IdTagsCache.js'
31 import { isIdTagAuthorized
} from
'./ocpp/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 static deleteInstance (chargingStation
: ChargingStation
): boolean {
70 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
71 return AutomaticTransactionGenerator
.instances
.delete(chargingStation
.stationInfo
!.hashId
)
74 public start (stopAbsoluteDuration
?: boolean): void {
75 if (!checkChargingStation(this.chargingStation
, this.logPrefix())) {
79 logger
.warn(`${this.logPrefix()} is already started`)
83 logger
.warn(`${this.logPrefix()} is already starting`)
87 this.startConnectors(stopAbsoluteDuration
)
92 public stop (): void {
94 logger
.warn(`${this.logPrefix()} is already stopped`)
98 logger
.warn(`${this.logPrefix()} is already stopping`)
102 this.stopConnectors()
104 this.stopping
= false
107 public startConnector (connectorId
: number, stopAbsoluteDuration
?: boolean): void {
108 if (!checkChargingStation(this.chargingStation
, this.logPrefix(connectorId
))) {
111 if (!this.connectorsStatus
.has(connectorId
)) {
112 logger
.error(`${this.logPrefix(connectorId)} starting on non existing connector`)
113 throw new BaseError(`Connector ${connectorId} does not exist`)
115 if (this.connectorsStatus
.get(connectorId
)?.start
=== false) {
116 this.internalStartConnector(connectorId
, stopAbsoluteDuration
).catch(Constants
.EMPTY_FUNCTION
)
117 } else if (this.connectorsStatus
.get(connectorId
)?.start
=== true) {
118 logger
.warn(`${this.logPrefix(connectorId)} is already started on connector`)
122 public stopConnector (connectorId
: number): void {
123 if (!this.connectorsStatus
.has(connectorId
)) {
124 logger
.error(`${this.logPrefix(connectorId)} stopping on non existing connector`)
125 throw new BaseError(`Connector ${connectorId} does not exist`)
127 if (this.connectorsStatus
.get(connectorId
)?.start
=== true) {
128 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
129 this.connectorsStatus
.get(connectorId
)!.start
= false
130 } else if (this.connectorsStatus
.get(connectorId
)?.start
=== false) {
131 logger
.warn(`${this.logPrefix(connectorId)} is already stopped on connector`)
135 private startConnectors (stopAbsoluteDuration
?: boolean): void {
137 this.connectorsStatus
.size
> 0 &&
138 this.connectorsStatus
.size
!== this.chargingStation
.getNumberOfConnectors()
140 this.connectorsStatus
.clear()
141 this.initializeConnectorsStatus()
143 if (this.chargingStation
.hasEvses
) {
144 for (const [evseId
, evseStatus
] of this.chargingStation
.evses
) {
146 for (const connectorId
of evseStatus
.connectors
.keys()) {
147 this.startConnector(connectorId
, stopAbsoluteDuration
)
152 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
153 if (connectorId
> 0) {
154 this.startConnector(connectorId
, stopAbsoluteDuration
)
160 private stopConnectors (): void {
161 if (this.chargingStation
.hasEvses
) {
162 for (const [evseId
, evseStatus
] of this.chargingStation
.evses
) {
164 for (const connectorId
of evseStatus
.connectors
.keys()) {
165 this.stopConnector(connectorId
)
170 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
171 if (connectorId
> 0) {
172 this.stopConnector(connectorId
)
178 private async internalStartConnector (
180 stopAbsoluteDuration
?: boolean
182 this.setStartConnectorStatus(connectorId
, stopAbsoluteDuration
)
186 )} started on connector and will run for ${formatDurationMilliSeconds(
187 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
188 this.connectorsStatus.get(connectorId)!.stopDate!.getTime() -
189 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
190 this.connectorsStatus.get(connectorId)!.startDate!.getTime()
193 while (this.connectorsStatus
.get(connectorId
)?.start
=== true) {
194 await this.waitChargingStationAvailable(connectorId
)
195 await this.waitConnectorAvailable(connectorId
)
196 await this.waitRunningTransactionStopped(connectorId
)
197 if (!this.canStartConnector(connectorId
)) {
198 this.stopConnector(connectorId
)
201 const wait
= secondsToMilliseconds(
203 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()
204 ?.maxDelayBetweenTwoTransactions
,
205 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()
206 ?.minDelayBetweenTwoTransactions
209 logger
.info(`${this.logPrefix(connectorId)} waiting for ${formatDurationMilliSeconds(wait)}`)
211 const start
= secureRandom()
214 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
215 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()!.probabilityOfStart
217 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
218 this.connectorsStatus
.get(connectorId
)!.skippedConsecutiveTransactions
= 0
220 const startResponse
= await this.startTransaction(connectorId
)
221 if (startResponse
?.idTagInfo
.status === AuthorizationStatus
.ACCEPTED
) {
222 // Wait until end of transaction
223 const waitTrxEnd
= secondsToMilliseconds(
225 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()?.maxDuration
,
226 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()?.minDuration
230 `${this.logPrefix(connectorId)} transaction started with id ${
231 this.chargingStation.getConnectorStatus(connectorId)?.transactionId
232 } and will stop in ${formatDurationMilliSeconds(waitTrxEnd)}`
234 await sleep(waitTrxEnd
)
235 await this.stopTransaction(connectorId
)
238 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
239 ++this.connectorsStatus
.get(connectorId
)!.skippedConsecutiveTransactions
240 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
241 ++this.connectorsStatus
.get(connectorId
)!.skippedTransactions
243 `${this.logPrefix(connectorId)} skipped consecutively ${
244 this.connectorsStatus.get(connectorId)?.skippedConsecutiveTransactions
245 }/${this.connectorsStatus.get(connectorId)?.skippedTransactions} transaction(s)`
248 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
249 this.connectorsStatus
.get(connectorId
)!.lastRunDate
= new Date()
251 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
252 this.connectorsStatus
.get(connectorId
)!.stoppedDate
= new Date()
256 )} stopped on connector and lasted for ${formatDurationMilliSeconds(
257 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
258 this.connectorsStatus.get(connectorId)!.stoppedDate!.getTime() -
259 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
260 this.connectorsStatus.get(connectorId)!.startDate!.getTime()
264 `${this.logPrefix(connectorId)} connector status: %j`,
265 this.connectorsStatus
.get(connectorId
)
269 private setStartConnectorStatus (
271 stopAbsoluteDuration
= this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()
272 ?.stopAbsoluteDuration
274 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
275 this.connectorsStatus
.get(connectorId
)!.startDate
= new Date()
277 stopAbsoluteDuration
=== false ||
278 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
279 !isValidDate(this.connectorsStatus
.get(connectorId
)!.stopDate
)
281 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
282 this.connectorsStatus
.get(connectorId
)!.stopDate
= new Date(
283 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
284 this.connectorsStatus
.get(connectorId
)!.startDate
!.getTime() +
286 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
287 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()!.stopAfterHours
291 delete this.connectorsStatus
.get(connectorId
)?.stoppedDate
292 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
293 this.connectorsStatus
.get(connectorId
)!.skippedConsecutiveTransactions
= 0
294 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
295 this.connectorsStatus
.get(connectorId
)!.start
= true
298 private canStartConnector (connectorId
: number): boolean {
299 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
300 if (new Date() > this.connectorsStatus
.get(connectorId
)!.stopDate
!) {
304 )} entered in transaction loop while the ATG stop date has been reached`
308 if (!this.chargingStation
.inAcceptedState()) {
312 )} entered in transaction loop while the charging station is not in accepted state`
316 if (!this.chargingStation
.isChargingStationAvailable()) {
320 )} entered in transaction loop while the charging station is unavailable`
324 if (!this.chargingStation
.isConnectorAvailable(connectorId
)) {
328 )} entered in transaction loop while the connector ${connectorId} is unavailable`
332 const connectorStatus
= this.chargingStation
.getConnectorStatus(connectorId
)
333 if (connectorStatus
?.transactionStarted
=== true) {
337 )} entered in transaction loop while a transaction ${connectorStatus.transactionId} is already started on connector ${connectorId}`
344 private async waitChargingStationAvailable (connectorId
: number): Promise
<void> {
346 while (!this.chargingStation
.isChargingStationAvailable()) {
351 )} transaction loop waiting for charging station to be available`
355 await sleep(Constants
.DEFAULT_ATG_WAIT_TIME
)
359 private async waitConnectorAvailable (connectorId
: number): Promise
<void> {
361 while (!this.chargingStation
.isConnectorAvailable(connectorId
)) {
366 )} transaction loop waiting for connector ${connectorId} to be available`
370 await sleep(Constants
.DEFAULT_ATG_WAIT_TIME
)
374 private async waitRunningTransactionStopped (connectorId
: number): Promise
<void> {
375 const connectorStatus
= this.chargingStation
.getConnectorStatus(connectorId
)
377 while (connectorStatus
?.transactionStarted
=== true) {
382 )} transaction loop waiting for started transaction ${connectorStatus.transactionId} on connector ${connectorId} to be stopped`
386 await sleep(Constants
.DEFAULT_ATG_WAIT_TIME
)
390 private initializeConnectorsStatus (): void {
391 if (this.chargingStation
.hasEvses
) {
392 for (const [evseId
, evseStatus
] of this.chargingStation
.evses
) {
394 for (const connectorId
of evseStatus
.connectors
.keys()) {
395 this.connectorsStatus
.set(connectorId
, this.getConnectorStatus(connectorId
))
400 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
401 if (connectorId
> 0) {
402 this.connectorsStatus
.set(connectorId
, this.getConnectorStatus(connectorId
))
408 private getConnectorStatus (connectorId
: number): Status
{
409 const statusIndex
= connectorId
- 1
410 let connectorStatus
: Status
| undefined
411 if (this.chargingStation
.getAutomaticTransactionGeneratorStatuses()?.[statusIndex
] != null) {
412 connectorStatus
= clone
<Status
>(
413 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
414 this.chargingStation
.getAutomaticTransactionGeneratorStatuses()![statusIndex
]
416 } else if (this.chargingStation
.getAutomaticTransactionGeneratorStatuses() != null) {
418 `${this.logPrefix(connectorId)} no status found for connector #${connectorId} in charging station configuration file. New status will be created`
421 if (connectorStatus
!= null) {
422 connectorStatus
.startDate
= convertToDate(connectorStatus
.startDate
)
423 connectorStatus
.lastRunDate
= convertToDate(connectorStatus
.lastRunDate
)
424 connectorStatus
.stopDate
= convertToDate(connectorStatus
.stopDate
)
425 connectorStatus
.stoppedDate
= convertToDate(connectorStatus
.stoppedDate
)
428 (connectorStatus
.start
||
429 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()?.enable
!== true)
431 connectorStatus
.start
= false
437 authorizeRequests
: 0,
438 acceptedAuthorizeRequests
: 0,
439 rejectedAuthorizeRequests
: 0,
440 startTransactionRequests
: 0,
441 acceptedStartTransactionRequests
: 0,
442 rejectedStartTransactionRequests
: 0,
443 stopTransactionRequests
: 0,
444 acceptedStopTransactionRequests
: 0,
445 rejectedStopTransactionRequests
: 0,
446 skippedConsecutiveTransactions
: 0,
447 skippedTransactions
: 0
452 private async startTransaction (
454 ): Promise
<StartTransactionResponse
| undefined> {
455 const measureId
= 'StartTransaction with ATG'
456 const beginId
= PerformanceStatistics
.beginMeasure(measureId
)
457 let startResponse
: StartTransactionResponse
| undefined
458 if (this.chargingStation
.hasIdTags()) {
459 const idTag
= IdTagsCache
.getInstance().getIdTag(
460 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
461 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()!.idTagDistribution
!,
462 this.chargingStation
,
465 const startTransactionLogMsg
= `${this.logPrefix(
467 )} start transaction with an idTag '${idTag}'`
468 if (this.getRequireAuthorize()) {
469 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
470 ++this.connectorsStatus
.get(connectorId
)!.authorizeRequests
471 if (await isIdTagAuthorized(this.chargingStation
, connectorId
, idTag
)) {
472 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
473 ++this.connectorsStatus
.get(connectorId
)!.acceptedAuthorizeRequests
474 logger
.info(startTransactionLogMsg
)
476 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
477 StartTransactionRequest
,
478 StartTransactionResponse
479 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, {
483 this.handleStartTransactionResponse(connectorId
, startResponse
)
484 PerformanceStatistics
.endMeasure(measureId
, beginId
)
487 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
488 ++this.connectorsStatus
.get(connectorId
)!.rejectedAuthorizeRequests
489 PerformanceStatistics
.endMeasure(measureId
, beginId
)
492 logger
.info(startTransactionLogMsg
)
494 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
495 StartTransactionRequest
,
496 StartTransactionResponse
497 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, {
501 this.handleStartTransactionResponse(connectorId
, startResponse
)
502 PerformanceStatistics
.endMeasure(measureId
, beginId
)
505 logger
.info(`${this.logPrefix(connectorId)} start transaction without an idTag`)
506 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
507 StartTransactionRequest
,
508 StartTransactionResponse
509 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, { connectorId
})
510 this.handleStartTransactionResponse(connectorId
, startResponse
)
511 PerformanceStatistics
.endMeasure(measureId
, beginId
)
515 private async stopTransaction (
517 reason
= StopTransactionReason
.LOCAL
518 ): Promise
<StopTransactionResponse
| undefined> {
519 const measureId
= 'StopTransaction with ATG'
520 const beginId
= PerformanceStatistics
.beginMeasure(measureId
)
521 let stopResponse
: StopTransactionResponse
| undefined
522 if (this.chargingStation
.getConnectorStatus(connectorId
)?.transactionStarted
=== true) {
524 `${this.logPrefix(connectorId)} stop transaction with id ${
525 this.chargingStation.getConnectorStatus(connectorId)?.transactionId
528 stopResponse
= await this.chargingStation
.stopTransactionOnConnector(connectorId
, reason
)
529 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
530 ++this.connectorsStatus
.get(connectorId
)!.stopTransactionRequests
531 if (stopResponse
.idTagInfo
?.status === AuthorizationStatus
.ACCEPTED
) {
532 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
533 ++this.connectorsStatus
.get(connectorId
)!.acceptedStopTransactionRequests
535 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
536 ++this.connectorsStatus
.get(connectorId
)!.rejectedStopTransactionRequests
539 const transactionId
= this.chargingStation
.getConnectorStatus(connectorId
)?.transactionId
541 `${this.logPrefix(connectorId)} stopping a not started transaction${
542 transactionId != null ? ` with id ${transactionId}
` : ''
546 PerformanceStatistics
.endMeasure(measureId
, beginId
)
550 private getRequireAuthorize (): boolean {
552 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()?.requireAuthorize
?? true
556 private readonly logPrefix
= (connectorId
?: number): string => {
558 ` ${this.chargingStation.stationInfo?.chargingStationId} | ATG${
559 connectorId != null ? ` on connector #${connectorId}
` : ''
564 private handleStartTransactionResponse (
566 startResponse
: StartTransactionResponse
568 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
569 ++this.connectorsStatus
.get(connectorId
)!.startTransactionRequests
570 if (startResponse
.idTagInfo
.status === AuthorizationStatus
.ACCEPTED
) {
571 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
572 ++this.connectorsStatus
.get(connectorId
)!.acceptedStartTransactionRequests
574 logger
.warn(`${this.logPrefix(connectorId)} start transaction rejected`)
575 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
576 ++this.connectorsStatus
.get(connectorId
)!.rejectedStartTransactionRequests