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
,
30 } from
'../utils/index.js'
32 export class AutomaticTransactionGenerator
{
33 private static readonly instances
: Map
<string, AutomaticTransactionGenerator
> = new Map
<
35 AutomaticTransactionGenerator
38 public readonly connectorsStatus
: Map
<number, Status
>
39 public started
: boolean
40 private starting
: boolean
41 private stopping
: boolean
42 private readonly chargingStation
: ChargingStation
44 private constructor (chargingStation
: ChargingStation
) {
48 this.chargingStation
= chargingStation
49 this.connectorsStatus
= new Map
<number, Status
>()
50 this.initializeConnectorsStatus()
53 public static getInstance (
54 chargingStation
: ChargingStation
55 ): AutomaticTransactionGenerator
| undefined {
56 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
57 if (!AutomaticTransactionGenerator
.instances
.has(chargingStation
.stationInfo
!.hashId
)) {
58 AutomaticTransactionGenerator
.instances
.set(
59 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
60 chargingStation
.stationInfo
!.hashId
,
61 new AutomaticTransactionGenerator(chargingStation
)
64 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
65 return AutomaticTransactionGenerator
.instances
.get(chargingStation
.stationInfo
!.hashId
)
68 public start (): void {
69 if (!checkChargingStation(this.chargingStation
, this.logPrefix())) {
73 logger
.warn(`${this.logPrefix()} is already started`)
77 logger
.warn(`${this.logPrefix()} is already starting`)
81 this.startConnectors()
86 public stop (): void {
88 logger
.warn(`${this.logPrefix()} is already stopped`)
92 logger
.warn(`${this.logPrefix()} is already stopping`)
101 public startConnector (connectorId
: number): void {
102 if (!checkChargingStation(this.chargingStation
, this.logPrefix(connectorId
))) {
105 if (!this.connectorsStatus
.has(connectorId
)) {
106 logger
.error(`${this.logPrefix(connectorId)} starting on non existing connector`)
107 throw new BaseError(`Connector ${connectorId} does not exist`)
109 if (this.connectorsStatus
.get(connectorId
)?.start
=== false) {
110 this.internalStartConnector(connectorId
).catch(Constants
.EMPTY_FUNCTION
)
111 } else if (this.connectorsStatus
.get(connectorId
)?.start
=== true) {
112 logger
.warn(`${this.logPrefix(connectorId)} is already started on connector`)
116 public stopConnector (connectorId
: number): void {
117 if (!this.connectorsStatus
.has(connectorId
)) {
118 logger
.error(`${this.logPrefix(connectorId)} stopping on non existing connector`)
119 throw new BaseError(`Connector ${connectorId} does not exist`)
121 if (this.connectorsStatus
.get(connectorId
)?.start
=== true) {
122 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
123 this.connectorsStatus
.get(connectorId
)!.start
= false
124 } else if (this.connectorsStatus
.get(connectorId
)?.start
=== false) {
125 logger
.warn(`${this.logPrefix(connectorId)} is already stopped on connector`)
129 private startConnectors (): void {
131 this.connectorsStatus
.size
> 0 &&
132 this.connectorsStatus
.size
!== this.chargingStation
.getNumberOfConnectors()
134 this.connectorsStatus
.clear()
135 this.initializeConnectorsStatus()
137 if (this.chargingStation
.hasEvses
) {
138 for (const [evseId
, evseStatus
] of this.chargingStation
.evses
) {
140 for (const connectorId
of evseStatus
.connectors
.keys()) {
141 this.startConnector(connectorId
)
146 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
147 if (connectorId
> 0) {
148 this.startConnector(connectorId
)
154 private stopConnectors (): void {
155 if (this.chargingStation
.hasEvses
) {
156 for (const [evseId
, evseStatus
] of this.chargingStation
.evses
) {
158 for (const connectorId
of evseStatus
.connectors
.keys()) {
159 this.stopConnector(connectorId
)
164 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
165 if (connectorId
> 0) {
166 this.stopConnector(connectorId
)
172 private async internalStartConnector (connectorId
: number): Promise
<void> {
173 this.setStartConnectorStatus(connectorId
)
177 )} started on connector and will run for ${formatDurationMilliSeconds(
178 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
179 this.connectorsStatus.get(connectorId)!.stopDate!.getTime() -
180 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
181 this.connectorsStatus.get(connectorId)!.startDate!.getTime()
184 while (this.connectorsStatus
.get(connectorId
)?.start
=== true) {
185 await this.waitChargingStationAvailable(connectorId
)
186 await this.waitConnectorAvailable(connectorId
)
187 if (!this.canStartConnector(connectorId
)) {
188 this.stopConnector(connectorId
)
191 const wait
= secondsToMilliseconds(
193 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()
194 ?.maxDelayBetweenTwoTransactions
,
195 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()
196 ?.minDelayBetweenTwoTransactions
199 logger
.info(`${this.logPrefix(connectorId)} waiting for ${formatDurationMilliSeconds(wait)}`)
201 const start
= secureRandom()
204 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
205 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()!.probabilityOfStart
207 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
208 this.connectorsStatus
.get(connectorId
)!.skippedConsecutiveTransactions
= 0
210 const startResponse
= await this.startTransaction(connectorId
)
211 if (startResponse
?.idTagInfo
.status === AuthorizationStatus
.ACCEPTED
) {
212 // Wait until end of transaction
213 const waitTrxEnd
= secondsToMilliseconds(
215 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()?.maxDuration
,
216 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()?.minDuration
222 )} transaction started with id ${this.chargingStation.getConnectorStatus(connectorId)
223 ?.transactionId} and will stop in ${formatDurationMilliSeconds(waitTrxEnd)}`
225 await sleep(waitTrxEnd
)
226 await this.stopTransaction(connectorId
)
229 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
230 ++this.connectorsStatus
.get(connectorId
)!.skippedConsecutiveTransactions
231 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
232 ++this.connectorsStatus
.get(connectorId
)!.skippedTransactions
234 `${this.logPrefix(connectorId)} skipped consecutively ${this.connectorsStatus.get(
236 )?.skippedConsecutiveTransactions}/${this.connectorsStatus.get(connectorId)
237 ?.skippedTransactions} transaction(s)`
240 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
241 this.connectorsStatus
.get(connectorId
)!.lastRunDate
= new Date()
243 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
244 this.connectorsStatus
.get(connectorId
)!.stoppedDate
= new Date()
248 )} stopped on connector and lasted for ${formatDurationMilliSeconds(
249 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
250 this.connectorsStatus.get(connectorId)!.stoppedDate!.getTime() -
251 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
252 this.connectorsStatus.get(connectorId)!.startDate!.getTime()
256 `${this.logPrefix(connectorId)} connector status: %j`,
257 this.connectorsStatus
.get(connectorId
)
261 private setStartConnectorStatus (connectorId
: number): void {
262 const previousRunDuration
=
263 isValidTime(this.connectorsStatus
.get(connectorId
)?.startDate
) &&
264 isValidTime(this.connectorsStatus
.get(connectorId
)?.lastRunDate
)
265 ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
266 this.connectorsStatus
.get(connectorId
)!.lastRunDate
!.getTime() -
267 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
268 this.connectorsStatus
.get(connectorId
)!.startDate
!.getTime()
270 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
271 this.connectorsStatus
.get(connectorId
)!.startDate
= new Date()
272 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
273 this.connectorsStatus
.get(connectorId
)!.stopDate
= new Date(
274 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
275 this.connectorsStatus
.get(connectorId
)!.startDate
!.getTime() +
277 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
278 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()!.stopAfterHours
282 delete this.connectorsStatus
.get(connectorId
)?.stoppedDate
283 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
284 this.connectorsStatus
.get(connectorId
)!.skippedConsecutiveTransactions
= 0
285 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
286 this.connectorsStatus
.get(connectorId
)!.start
= true
289 private canStartConnector (connectorId
: number): boolean {
290 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
291 if (new Date() > this.connectorsStatus
.get(connectorId
)!.stopDate
!) {
295 )} entered in transaction loop while the ATG stop date has been reached`
299 if (!this.chargingStation
.inAcceptedState()) {
303 )} entered in transaction loop while the charging station is not in accepted state`
307 if (!this.chargingStation
.isChargingStationAvailable()) {
311 )} entered in transaction loop while the charging station is unavailable`
315 if (!this.chargingStation
.isConnectorAvailable(connectorId
)) {
319 )} entered in transaction loop while the connector ${connectorId} is unavailable`
326 private async waitChargingStationAvailable (connectorId
: number): Promise
<void> {
328 while (!this.chargingStation
.isChargingStationAvailable()) {
333 )} transaction loop waiting for charging station to be available`
337 await sleep(Constants
.CHARGING_STATION_ATG_AVAILABILITY_TIME
)
341 private async waitConnectorAvailable (connectorId
: number): Promise
<void> {
343 while (!this.chargingStation
.isConnectorAvailable(connectorId
)) {
348 )} transaction loop waiting for connector ${connectorId} to be available`
352 await sleep(Constants
.CHARGING_STATION_ATG_AVAILABILITY_TIME
)
356 private initializeConnectorsStatus (): void {
357 if (this.chargingStation
.hasEvses
) {
358 for (const [evseId
, evseStatus
] of this.chargingStation
.evses
) {
360 for (const connectorId
of evseStatus
.connectors
.keys()) {
361 this.connectorsStatus
.set(connectorId
, this.getConnectorStatus(connectorId
))
366 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
367 if (connectorId
> 0) {
368 this.connectorsStatus
.set(connectorId
, this.getConnectorStatus(connectorId
))
374 private getConnectorStatus (connectorId
: number): Status
{
375 const connectorStatus
=
376 this.chargingStation
.getAutomaticTransactionGeneratorStatuses()?.[connectorId
- 1] != null
377 ? cloneObject
<Status
>(
378 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
379 this.chargingStation
.getAutomaticTransactionGeneratorStatuses()![connectorId
- 1]
382 this.resetConnectorStatus(connectorStatus
)
386 authorizeRequests
: 0,
387 acceptedAuthorizeRequests
: 0,
388 rejectedAuthorizeRequests
: 0,
389 startTransactionRequests
: 0,
390 acceptedStartTransactionRequests
: 0,
391 rejectedStartTransactionRequests
: 0,
392 stopTransactionRequests
: 0,
393 acceptedStopTransactionRequests
: 0,
394 rejectedStopTransactionRequests
: 0,
395 skippedConsecutiveTransactions
: 0,
396 skippedTransactions
: 0
401 private resetConnectorStatus (connectorStatus
: Status
| undefined): void {
402 if (connectorStatus
== null) {
407 (connectorStatus
.start
||
408 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()?.enable
!== true)
410 connectorStatus
.start
= false
414 private async startTransaction (
416 ): Promise
<StartTransactionResponse
| undefined> {
417 const measureId
= 'StartTransaction with ATG'
418 const beginId
= PerformanceStatistics
.beginMeasure(measureId
)
419 let startResponse
: StartTransactionResponse
| undefined
420 if (this.chargingStation
.hasIdTags()) {
421 const idTag
= IdTagsCache
.getInstance().getIdTag(
422 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
423 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()!.idTagDistribution
!,
424 this.chargingStation
,
427 const startTransactionLogMsg
= `${this.logPrefix(
429 )} start transaction with an idTag '${idTag}'`
430 if (this.getRequireAuthorize()) {
431 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
432 ++this.connectorsStatus
.get(connectorId
)!.authorizeRequests
433 if (await isIdTagAuthorized(this.chargingStation
, connectorId
, idTag
)) {
434 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
435 ++this.connectorsStatus
.get(connectorId
)!.acceptedAuthorizeRequests
436 logger
.info(startTransactionLogMsg
)
438 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
439 StartTransactionRequest
,
440 StartTransactionResponse
441 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, {
445 this.handleStartTransactionResponse(connectorId
, startResponse
)
446 PerformanceStatistics
.endMeasure(measureId
, beginId
)
449 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
450 ++this.connectorsStatus
.get(connectorId
)!.rejectedAuthorizeRequests
451 PerformanceStatistics
.endMeasure(measureId
, beginId
)
454 logger
.info(startTransactionLogMsg
)
456 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
457 StartTransactionRequest
,
458 StartTransactionResponse
459 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, {
463 this.handleStartTransactionResponse(connectorId
, startResponse
)
464 PerformanceStatistics
.endMeasure(measureId
, beginId
)
467 logger
.info(`${this.logPrefix(connectorId)} start transaction without an idTag`)
468 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
469 StartTransactionRequest
,
470 StartTransactionResponse
471 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, { connectorId
})
472 this.handleStartTransactionResponse(connectorId
, startResponse
)
473 PerformanceStatistics
.endMeasure(measureId
, beginId
)
477 private async stopTransaction (
479 reason
= StopTransactionReason
.LOCAL
480 ): Promise
<StopTransactionResponse
| undefined> {
481 const measureId
= 'StopTransaction with ATG'
482 const beginId
= PerformanceStatistics
.beginMeasure(measureId
)
483 let stopResponse
: StopTransactionResponse
| undefined
484 if (this.chargingStation
.getConnectorStatus(connectorId
)?.transactionStarted
=== true) {
488 )} stop transaction with id ${this.chargingStation.getConnectorStatus(connectorId)
491 stopResponse
= await this.chargingStation
.stopTransactionOnConnector(connectorId
, reason
)
492 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
493 ++this.connectorsStatus
.get(connectorId
)!.stopTransactionRequests
494 if (stopResponse
.idTagInfo
?.status === AuthorizationStatus
.ACCEPTED
) {
495 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
496 ++this.connectorsStatus
.get(connectorId
)!.acceptedStopTransactionRequests
498 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
499 ++this.connectorsStatus
.get(connectorId
)!.rejectedStopTransactionRequests
502 const transactionId
= this.chargingStation
.getConnectorStatus(connectorId
)?.transactionId
504 `${this.logPrefix(connectorId)} stopping a not started transaction${
505 transactionId != null ? ` with id ${transactionId}
` : ''
509 PerformanceStatistics
.endMeasure(measureId
, beginId
)
513 private getRequireAuthorize (): boolean {
515 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()?.requireAuthorize
?? true
519 private readonly logPrefix
= (connectorId
?: number): string => {
521 ` ${this.chargingStation.stationInfo?.chargingStationId} | ATG${
522 connectorId != null ? ` on connector #${connectorId}
` : ''
527 private handleStartTransactionResponse (
529 startResponse
: StartTransactionResponse
531 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
532 ++this.connectorsStatus
.get(connectorId
)!.startTransactionRequests
533 if (startResponse
.idTagInfo
.status === AuthorizationStatus
.ACCEPTED
) {
534 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
535 ++this.connectorsStatus
.get(connectorId
)!.acceptedStartTransactionRequests
537 logger
.warn(`${this.logPrefix(connectorId)} start transaction rejected`)
538 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
539 ++this.connectorsStatus
.get(connectorId
)!.rejectedStartTransactionRequests