1 // Partial Copyright Jerome Benoit. 2021-2023. 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'
23 formatDurationMilliSeconds
,
29 } from
'../utils/index.js'
31 export class AutomaticTransactionGenerator
{
32 private static readonly instances
: Map
<string, AutomaticTransactionGenerator
> = new Map
<
34 AutomaticTransactionGenerator
37 public readonly connectorsStatus
: Map
<number, Status
>
38 public started
: boolean
39 private starting
: boolean
40 private stopping
: boolean
41 private readonly chargingStation
: ChargingStation
43 private constructor (chargingStation
: ChargingStation
) {
47 this.chargingStation
= chargingStation
48 this.connectorsStatus
= new Map
<number, Status
>()
49 this.initializeConnectorsStatus()
52 public static getInstance (
53 chargingStation
: ChargingStation
54 ): AutomaticTransactionGenerator
| undefined {
55 if (!AutomaticTransactionGenerator
.instances
.has(chargingStation
.stationInfo
.hashId
)) {
56 AutomaticTransactionGenerator
.instances
.set(
57 chargingStation
.stationInfo
.hashId
,
58 new AutomaticTransactionGenerator(chargingStation
)
61 return AutomaticTransactionGenerator
.instances
.get(chargingStation
.stationInfo
.hashId
)
64 public start (): void {
65 if (!checkChargingStation(this.chargingStation
, this.logPrefix())) {
69 logger
.warn(`${this.logPrefix()} is already started`)
73 logger
.warn(`${this.logPrefix()} is already starting`)
77 this.startConnectors()
82 public stop (): void {
84 logger
.warn(`${this.logPrefix()} is already stopped`)
88 logger
.warn(`${this.logPrefix()} is already stopping`)
97 public startConnector (connectorId
: number): void {
98 if (!checkChargingStation(this.chargingStation
, this.logPrefix(connectorId
))) {
101 if (!this.connectorsStatus
.has(connectorId
)) {
102 logger
.error(`${this.logPrefix(connectorId)} starting on non existing connector`)
103 throw new BaseError(`Connector ${connectorId} does not exist`)
105 if (this.connectorsStatus
.get(connectorId
)?.start
=== false) {
106 this.internalStartConnector(connectorId
).catch(Constants
.EMPTY_FUNCTION
)
107 } else if (this.connectorsStatus
.get(connectorId
)?.start
=== true) {
108 logger
.warn(`${this.logPrefix(connectorId)} is already started on connector`)
112 public stopConnector (connectorId
: number): void {
113 if (!this.connectorsStatus
.has(connectorId
)) {
114 logger
.error(`${this.logPrefix(connectorId)} stopping on non existing connector`)
115 throw new BaseError(`Connector ${connectorId} does not exist`)
117 if (this.connectorsStatus
.get(connectorId
)?.start
=== true) {
118 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
119 this.connectorsStatus
.get(connectorId
)!.start
= false
120 } else if (this.connectorsStatus
.get(connectorId
)?.start
=== false) {
121 logger
.warn(`${this.logPrefix(connectorId)} is already stopped on connector`)
125 private startConnectors (): void {
127 this.connectorsStatus
?.size
> 0 &&
128 this.connectorsStatus
.size
!== this.chargingStation
.getNumberOfConnectors()
130 this.connectorsStatus
.clear()
131 this.initializeConnectorsStatus()
133 if (this.chargingStation
.hasEvses
) {
134 for (const [evseId
, evseStatus
] of this.chargingStation
.evses
) {
136 for (const connectorId
of evseStatus
.connectors
.keys()) {
137 this.startConnector(connectorId
)
142 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
143 if (connectorId
> 0) {
144 this.startConnector(connectorId
)
150 private stopConnectors (): void {
151 if (this.chargingStation
.hasEvses
) {
152 for (const [evseId
, evseStatus
] of this.chargingStation
.evses
) {
154 for (const connectorId
of evseStatus
.connectors
.keys()) {
155 this.stopConnector(connectorId
)
160 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
161 if (connectorId
> 0) {
162 this.stopConnector(connectorId
)
168 private async internalStartConnector (connectorId
: number): Promise
<void> {
169 this.setStartConnectorStatus(connectorId
)
173 )} started on connector and will run for ${formatDurationMilliSeconds(
174 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
175 this.connectorsStatus.get(connectorId)!.stopDate!.getTime() -
176 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
177 this.connectorsStatus.get(connectorId)!.startDate!.getTime()
180 while (this.connectorsStatus
.get(connectorId
)?.start
=== true) {
181 await this.waitChargingStationServiceInitialization(connectorId
)
182 await this.waitChargingStationAvailable(connectorId
)
183 await this.waitConnectorAvailable(connectorId
)
184 if (!this.canStartConnector(connectorId
)) {
185 this.stopConnector(connectorId
)
188 const wait
= secondsToMilliseconds(
190 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()
191 .maxDelayBetweenTwoTransactions
,
192 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()
193 .minDelayBetweenTwoTransactions
196 logger
.info(`${this.logPrefix(connectorId)} waiting for ${formatDurationMilliSeconds(wait)}`)
198 const start
= secureRandom()
201 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration().probabilityOfStart
203 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
204 this.connectorsStatus
.get(connectorId
)!.skippedConsecutiveTransactions
= 0
206 const startResponse
= await this.startTransaction(connectorId
)
207 if (startResponse
?.idTagInfo
?.status === AuthorizationStatus
.ACCEPTED
) {
208 // Wait until end of transaction
209 const waitTrxEnd
= secondsToMilliseconds(
211 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration().maxDuration
,
212 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration().minDuration
218 )} transaction started with id ${this.chargingStation.getConnectorStatus(connectorId)
219 ?.transactionId} and will stop in ${formatDurationMilliSeconds(waitTrxEnd)}`
221 await sleep(waitTrxEnd
)
222 await this.stopTransaction(connectorId
)
225 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
226 ++this.connectorsStatus
.get(connectorId
)!.skippedConsecutiveTransactions
!
227 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
228 ++this.connectorsStatus
.get(connectorId
)!.skippedTransactions
!
230 `${this.logPrefix(connectorId)} skipped consecutively ${this.connectorsStatus.get(
232 )?.skippedConsecutiveTransactions}/${this.connectorsStatus.get(connectorId)
233 ?.skippedTransactions} transaction(s)`
236 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
237 this.connectorsStatus
.get(connectorId
)!.lastRunDate
= new Date()
239 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
240 this.connectorsStatus
.get(connectorId
)!.stoppedDate
= new Date()
244 )} stopped on connector and lasted for ${formatDurationMilliSeconds(
245 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
246 this.connectorsStatus.get(connectorId)!.stoppedDate!.getTime() -
247 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
248 this.connectorsStatus.get(connectorId)!.startDate!.getTime()
252 `${this.logPrefix(connectorId)} connector status: %j`,
253 this.connectorsStatus
.get(connectorId
)
257 private setStartConnectorStatus (connectorId
: number): void {
258 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
259 this.connectorsStatus
.get(connectorId
)!.skippedConsecutiveTransactions
= 0
260 const previousRunDuration
=
261 this.connectorsStatus
.get(connectorId
)?.startDate
!= null &&
262 this.connectorsStatus
.get(connectorId
)?.lastRunDate
!= null
263 ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
264 this.connectorsStatus
.get(connectorId
)!.lastRunDate
!.getTime() -
265 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
266 this.connectorsStatus
.get(connectorId
)!.startDate
!.getTime()
268 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
269 this.connectorsStatus
.get(connectorId
)!.startDate
= new Date()
270 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
271 this.connectorsStatus
.get(connectorId
)!.stopDate
= new Date(
272 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
273 this.connectorsStatus
.get(connectorId
)!.startDate
!.getTime() +
275 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration().stopAfterHours
279 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
280 this.connectorsStatus
.get(connectorId
)!.start
= true
283 private canStartConnector (connectorId
: number): boolean {
284 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
285 if (new Date() > this.connectorsStatus
.get(connectorId
)!.stopDate
!) {
288 if (!this.chargingStation
.inAcceptedState()) {
292 )} entered in transaction loop while the charging station is not in accepted state`
296 if (!this.chargingStation
.isChargingStationAvailable()) {
300 )} entered in transaction loop while the charging station is unavailable`
304 if (!this.chargingStation
.isConnectorAvailable(connectorId
)) {
308 )} entered in transaction loop while the connector ${connectorId} is unavailable`
315 private async waitChargingStationServiceInitialization (connectorId
: number): Promise
<void> {
317 while (this.chargingStation
?.ocppRequestService
== null) {
322 )} transaction loop waiting for charging station service to be initialized`
326 await sleep(Constants
.CHARGING_STATION_ATG_INITIALIZATION_TIME
)
330 private async waitChargingStationAvailable (connectorId
: number): Promise
<void> {
332 while (!this.chargingStation
.isChargingStationAvailable()) {
337 )} transaction loop waiting for charging station to be available`
341 await sleep(Constants
.CHARGING_STATION_ATG_AVAILABILITY_TIME
)
345 private async waitConnectorAvailable (connectorId
: number): Promise
<void> {
347 while (!this.chargingStation
.isConnectorAvailable(connectorId
)) {
352 )} transaction loop waiting for connector ${connectorId} to be available`
356 await sleep(Constants
.CHARGING_STATION_ATG_AVAILABILITY_TIME
)
360 private initializeConnectorsStatus (): void {
361 if (this.chargingStation
.hasEvses
) {
362 for (const [evseId
, evseStatus
] of this.chargingStation
.evses
) {
364 for (const connectorId
of evseStatus
.connectors
.keys()) {
365 this.connectorsStatus
.set(connectorId
, this.getConnectorStatus(connectorId
))
370 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
371 if (connectorId
> 0) {
372 this.connectorsStatus
.set(connectorId
, this.getConnectorStatus(connectorId
))
378 private getConnectorStatus (connectorId
: number): Status
{
379 const connectorStatus
=
380 this.chargingStation
.getAutomaticTransactionGeneratorStatuses()?.[connectorId
] != null
381 ? cloneObject
<Status
>(
382 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
383 this.chargingStation
.getAutomaticTransactionGeneratorStatuses()![connectorId
]
386 this.resetConnectorStatus(connectorStatus
)
390 authorizeRequests
: 0,
391 acceptedAuthorizeRequests
: 0,
392 rejectedAuthorizeRequests
: 0,
393 startTransactionRequests
: 0,
394 acceptedStartTransactionRequests
: 0,
395 rejectedStartTransactionRequests
: 0,
396 stopTransactionRequests
: 0,
397 acceptedStopTransactionRequests
: 0,
398 rejectedStopTransactionRequests
: 0,
399 skippedConsecutiveTransactions
: 0,
400 skippedTransactions
: 0
405 private resetConnectorStatus (connectorStatus
: Status
| undefined): void {
406 if (connectorStatus
== null) {
409 delete connectorStatus
?.startDate
410 delete connectorStatus
?.lastRunDate
411 delete connectorStatus
?.stopDate
412 delete connectorStatus
?.stoppedDate
415 (connectorStatus
.start
||
416 !this.chargingStation
.getAutomaticTransactionGeneratorConfiguration().enable
)
418 connectorStatus
.start
= false
422 private async startTransaction (
424 ): Promise
<StartTransactionResponse
| undefined> {
425 const measureId
= 'StartTransaction with ATG'
426 const beginId
= PerformanceStatistics
.beginMeasure(measureId
)
427 let startResponse
: StartTransactionResponse
| undefined
428 if (this.chargingStation
.hasIdTags()) {
429 const idTag
= IdTagsCache
.getInstance().getIdTag(
430 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
431 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration().idTagDistribution
!,
432 this.chargingStation
,
435 const startTransactionLogMsg
= `${this.logPrefix(
437 )} start transaction with an idTag '${idTag}'`
438 if (this.getRequireAuthorize()) {
439 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
440 ++this.connectorsStatus
.get(connectorId
)!.authorizeRequests
!
441 if (await isIdTagAuthorized(this.chargingStation
, connectorId
, idTag
)) {
442 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
443 ++this.connectorsStatus
.get(connectorId
)!.acceptedAuthorizeRequests
!
444 logger
.info(startTransactionLogMsg
)
446 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
447 StartTransactionRequest
,
448 StartTransactionResponse
449 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, {
453 this.handleStartTransactionResponse(connectorId
, startResponse
)
454 PerformanceStatistics
.endMeasure(measureId
, beginId
)
457 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
458 ++this.connectorsStatus
.get(connectorId
)!.rejectedAuthorizeRequests
!
459 PerformanceStatistics
.endMeasure(measureId
, beginId
)
462 logger
.info(startTransactionLogMsg
)
464 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
465 StartTransactionRequest
,
466 StartTransactionResponse
467 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, {
471 this.handleStartTransactionResponse(connectorId
, startResponse
)
472 PerformanceStatistics
.endMeasure(measureId
, beginId
)
475 logger
.info(`${this.logPrefix(connectorId)} start transaction without an idTag`)
476 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
477 StartTransactionRequest
,
478 StartTransactionResponse
479 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, { connectorId
})
480 this.handleStartTransactionResponse(connectorId
, startResponse
)
481 PerformanceStatistics
.endMeasure(measureId
, beginId
)
485 private async stopTransaction (
487 reason
= StopTransactionReason
.LOCAL
488 ): Promise
<StopTransactionResponse
| undefined> {
489 const measureId
= 'StopTransaction with ATG'
490 const beginId
= PerformanceStatistics
.beginMeasure(measureId
)
491 let stopResponse
: StopTransactionResponse
| undefined
492 if (this.chargingStation
.getConnectorStatus(connectorId
)?.transactionStarted
=== true) {
496 )} stop transaction with id ${this.chargingStation.getConnectorStatus(connectorId)
499 stopResponse
= await this.chargingStation
.stopTransactionOnConnector(connectorId
, reason
)
500 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
501 ++this.connectorsStatus
.get(connectorId
)!.stopTransactionRequests
!
502 if (stopResponse
?.idTagInfo
?.status === AuthorizationStatus
.ACCEPTED
) {
503 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
504 ++this.connectorsStatus
.get(connectorId
)!.acceptedStopTransactionRequests
!
506 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
507 ++this.connectorsStatus
.get(connectorId
)!.rejectedStopTransactionRequests
!
510 const transactionId
= this.chargingStation
.getConnectorStatus(connectorId
)?.transactionId
512 `${this.logPrefix(connectorId)} stopping a not started transaction${
513 transactionId != null ? ` with id ${transactionId}
` : ''
517 PerformanceStatistics
.endMeasure(measureId
, beginId
)
521 private getRequireAuthorize (): boolean {
523 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()?.requireAuthorize
?? true
527 private readonly logPrefix
= (connectorId
?: number): string => {
529 ` ${this.chargingStation.stationInfo.chargingStationId} | ATG${
530 connectorId != null ? ` on connector #${connectorId}
` : ''
535 private handleStartTransactionResponse (
537 startResponse
: StartTransactionResponse
539 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
540 ++this.connectorsStatus
.get(connectorId
)!.startTransactionRequests
!
541 if (startResponse
?.idTagInfo
?.status === AuthorizationStatus
.ACCEPTED
) {
542 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
543 ++this.connectorsStatus
.get(connectorId
)!.acceptedStartTransactionRequests
!
545 logger
.warn(`${this.logPrefix(connectorId)} start transaction rejected`)
546 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
547 ++this.connectorsStatus
.get(connectorId
)!.rejectedStartTransactionRequests
!