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 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
56 if (!AutomaticTransactionGenerator
.instances
.has(chargingStation
.stationInfo
!.hashId
)) {
57 AutomaticTransactionGenerator
.instances
.set(
58 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
59 chargingStation
.stationInfo
!.hashId
,
60 new AutomaticTransactionGenerator(chargingStation
)
63 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
64 return AutomaticTransactionGenerator
.instances
.get(chargingStation
.stationInfo
!.hashId
)
67 public start (): void {
68 if (!checkChargingStation(this.chargingStation
, this.logPrefix())) {
72 logger
.warn(`${this.logPrefix()} is already started`)
76 logger
.warn(`${this.logPrefix()} is already starting`)
80 this.startConnectors()
85 public stop (): void {
87 logger
.warn(`${this.logPrefix()} is already stopped`)
91 logger
.warn(`${this.logPrefix()} is already stopping`)
100 public startConnector (connectorId
: number): void {
101 if (!checkChargingStation(this.chargingStation
, this.logPrefix(connectorId
))) {
104 if (!this.connectorsStatus
.has(connectorId
)) {
105 logger
.error(`${this.logPrefix(connectorId)} starting on non existing connector`)
106 throw new BaseError(`Connector ${connectorId} does not exist`)
108 if (this.connectorsStatus
.get(connectorId
)?.start
=== false) {
109 this.internalStartConnector(connectorId
).catch(Constants
.EMPTY_FUNCTION
)
110 } else if (this.connectorsStatus
.get(connectorId
)?.start
=== true) {
111 logger
.warn(`${this.logPrefix(connectorId)} is already started on connector`)
115 public stopConnector (connectorId
: number): void {
116 if (!this.connectorsStatus
.has(connectorId
)) {
117 logger
.error(`${this.logPrefix(connectorId)} stopping on non existing connector`)
118 throw new BaseError(`Connector ${connectorId} does not exist`)
120 if (this.connectorsStatus
.get(connectorId
)?.start
=== true) {
121 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
122 this.connectorsStatus
.get(connectorId
)!.start
= false
123 } else if (this.connectorsStatus
.get(connectorId
)?.start
=== false) {
124 logger
.warn(`${this.logPrefix(connectorId)} is already stopped on connector`)
128 private startConnectors (): void {
130 this.connectorsStatus
.size
> 0 &&
131 this.connectorsStatus
.size
!== this.chargingStation
.getNumberOfConnectors()
133 this.connectorsStatus
.clear()
134 this.initializeConnectorsStatus()
136 if (this.chargingStation
.hasEvses
) {
137 for (const [evseId
, evseStatus
] of this.chargingStation
.evses
) {
139 for (const connectorId
of evseStatus
.connectors
.keys()) {
140 this.startConnector(connectorId
)
145 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
146 if (connectorId
> 0) {
147 this.startConnector(connectorId
)
153 private stopConnectors (): void {
154 if (this.chargingStation
.hasEvses
) {
155 for (const [evseId
, evseStatus
] of this.chargingStation
.evses
) {
157 for (const connectorId
of evseStatus
.connectors
.keys()) {
158 this.stopConnector(connectorId
)
163 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
164 if (connectorId
> 0) {
165 this.stopConnector(connectorId
)
171 private async internalStartConnector (connectorId
: number): Promise
<void> {
172 this.setStartConnectorStatus(connectorId
)
176 )} started on connector and will run for ${formatDurationMilliSeconds(
177 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
178 this.connectorsStatus.get(connectorId)!.stopDate!.getTime() -
179 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
180 this.connectorsStatus.get(connectorId)!.startDate!.getTime()
183 while (this.connectorsStatus
.get(connectorId
)?.start
=== true) {
184 await this.waitChargingStationAvailable(connectorId
)
185 await this.waitConnectorAvailable(connectorId
)
186 if (!this.canStartConnector(connectorId
)) {
187 this.stopConnector(connectorId
)
190 const wait
= secondsToMilliseconds(
192 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()
193 ?.maxDelayBetweenTwoTransactions
,
194 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()
195 ?.minDelayBetweenTwoTransactions
198 logger
.info(`${this.logPrefix(connectorId)} waiting for ${formatDurationMilliSeconds(wait)}`)
200 const start
= secureRandom()
203 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
204 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()!.probabilityOfStart
206 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
207 this.connectorsStatus
.get(connectorId
)!.skippedConsecutiveTransactions
= 0
209 const startResponse
= await this.startTransaction(connectorId
)
210 if (startResponse
?.idTagInfo
.status === AuthorizationStatus
.ACCEPTED
) {
211 // Wait until end of transaction
212 const waitTrxEnd
= secondsToMilliseconds(
214 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()?.maxDuration
,
215 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()?.minDuration
221 )} transaction started with id ${this.chargingStation.getConnectorStatus(connectorId)
222 ?.transactionId} and will stop in ${formatDurationMilliSeconds(waitTrxEnd)}`
224 await sleep(waitTrxEnd
)
225 await this.stopTransaction(connectorId
)
228 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
229 ++this.connectorsStatus
.get(connectorId
)!.skippedConsecutiveTransactions
!
230 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
231 ++this.connectorsStatus
.get(connectorId
)!.skippedTransactions
!
233 `${this.logPrefix(connectorId)} skipped consecutively ${this.connectorsStatus.get(
235 )?.skippedConsecutiveTransactions}/${this.connectorsStatus.get(connectorId)
236 ?.skippedTransactions} transaction(s)`
239 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
240 this.connectorsStatus
.get(connectorId
)!.lastRunDate
= new Date()
242 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
243 this.connectorsStatus
.get(connectorId
)!.stoppedDate
= new Date()
247 )} stopped on connector and lasted for ${formatDurationMilliSeconds(
248 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
249 this.connectorsStatus.get(connectorId)!.stoppedDate!.getTime() -
250 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
251 this.connectorsStatus.get(connectorId)!.startDate!.getTime()
255 `${this.logPrefix(connectorId)} connector status: %j`,
256 this.connectorsStatus
.get(connectorId
)
260 private setStartConnectorStatus (connectorId
: number): void {
261 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
262 this.connectorsStatus
.get(connectorId
)!.skippedConsecutiveTransactions
= 0
263 const previousRunDuration
=
264 this.connectorsStatus
.get(connectorId
)?.startDate
!= null &&
265 this.connectorsStatus
.get(connectorId
)?.lastRunDate
!= null
266 ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
267 this.connectorsStatus
.get(connectorId
)!.lastRunDate
!.getTime() -
268 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
269 this.connectorsStatus
.get(connectorId
)!.startDate
!.getTime()
271 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
272 this.connectorsStatus
.get(connectorId
)!.startDate
= new Date()
273 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
274 this.connectorsStatus
.get(connectorId
)!.stopDate
= new Date(
275 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
276 this.connectorsStatus
.get(connectorId
)!.startDate
!.getTime() +
278 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
279 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()!.stopAfterHours
283 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
284 this.connectorsStatus
.get(connectorId
)!.start
= true
287 private canStartConnector (connectorId
: number): boolean {
288 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
289 if (new Date() > this.connectorsStatus
.get(connectorId
)!.stopDate
!) {
292 if (!this.chargingStation
.inAcceptedState()) {
296 )} entered in transaction loop while the charging station is not in accepted state`
300 if (!this.chargingStation
.isChargingStationAvailable()) {
304 )} entered in transaction loop while the charging station is unavailable`
308 if (!this.chargingStation
.isConnectorAvailable(connectorId
)) {
312 )} entered in transaction loop while the connector ${connectorId} is unavailable`
319 private async waitChargingStationAvailable (connectorId
: number): Promise
<void> {
321 while (!this.chargingStation
.isChargingStationAvailable()) {
326 )} transaction loop waiting for charging station to be available`
330 await sleep(Constants
.CHARGING_STATION_ATG_AVAILABILITY_TIME
)
334 private async waitConnectorAvailable (connectorId
: number): Promise
<void> {
336 while (!this.chargingStation
.isConnectorAvailable(connectorId
)) {
341 )} transaction loop waiting for connector ${connectorId} to be available`
345 await sleep(Constants
.CHARGING_STATION_ATG_AVAILABILITY_TIME
)
349 private initializeConnectorsStatus (): void {
350 if (this.chargingStation
.hasEvses
) {
351 for (const [evseId
, evseStatus
] of this.chargingStation
.evses
) {
353 for (const connectorId
of evseStatus
.connectors
.keys()) {
354 this.connectorsStatus
.set(connectorId
, this.getConnectorStatus(connectorId
))
359 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
360 if (connectorId
> 0) {
361 this.connectorsStatus
.set(connectorId
, this.getConnectorStatus(connectorId
))
367 private getConnectorStatus (connectorId
: number): Status
{
368 const connectorStatus
=
369 this.chargingStation
.getAutomaticTransactionGeneratorStatuses()?.[connectorId
] != null
370 ? cloneObject
<Status
>(
371 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
372 this.chargingStation
.getAutomaticTransactionGeneratorStatuses()![connectorId
]
375 this.resetConnectorStatus(connectorStatus
)
379 authorizeRequests
: 0,
380 acceptedAuthorizeRequests
: 0,
381 rejectedAuthorizeRequests
: 0,
382 startTransactionRequests
: 0,
383 acceptedStartTransactionRequests
: 0,
384 rejectedStartTransactionRequests
: 0,
385 stopTransactionRequests
: 0,
386 acceptedStopTransactionRequests
: 0,
387 rejectedStopTransactionRequests
: 0,
388 skippedConsecutiveTransactions
: 0,
389 skippedTransactions
: 0
394 private resetConnectorStatus (connectorStatus
: Status
| undefined): void {
395 if (connectorStatus
== null) {
398 delete connectorStatus
.startDate
399 delete connectorStatus
.lastRunDate
400 delete connectorStatus
.stopDate
401 delete connectorStatus
.stoppedDate
404 (connectorStatus
.start
||
405 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()?.enable
!== true)
407 connectorStatus
.start
= false
411 private async startTransaction (
413 ): Promise
<StartTransactionResponse
| undefined> {
414 const measureId
= 'StartTransaction with ATG'
415 const beginId
= PerformanceStatistics
.beginMeasure(measureId
)
416 let startResponse
: StartTransactionResponse
| undefined
417 if (this.chargingStation
.hasIdTags()) {
418 const idTag
= IdTagsCache
.getInstance().getIdTag(
419 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
420 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()!.idTagDistribution
!,
421 this.chargingStation
,
424 const startTransactionLogMsg
= `${this.logPrefix(
426 )} start transaction with an idTag '${idTag}'`
427 if (this.getRequireAuthorize()) {
428 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
429 ++this.connectorsStatus
.get(connectorId
)!.authorizeRequests
!
430 if (await isIdTagAuthorized(this.chargingStation
, connectorId
, idTag
)) {
431 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
432 ++this.connectorsStatus
.get(connectorId
)!.acceptedAuthorizeRequests
!
433 logger
.info(startTransactionLogMsg
)
435 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
436 StartTransactionRequest
,
437 StartTransactionResponse
438 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, {
442 this.handleStartTransactionResponse(connectorId
, startResponse
)
443 PerformanceStatistics
.endMeasure(measureId
, beginId
)
446 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
447 ++this.connectorsStatus
.get(connectorId
)!.rejectedAuthorizeRequests
!
448 PerformanceStatistics
.endMeasure(measureId
, beginId
)
451 logger
.info(startTransactionLogMsg
)
453 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
454 StartTransactionRequest
,
455 StartTransactionResponse
456 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, {
460 this.handleStartTransactionResponse(connectorId
, startResponse
)
461 PerformanceStatistics
.endMeasure(measureId
, beginId
)
464 logger
.info(`${this.logPrefix(connectorId)} start transaction without an idTag`)
465 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
466 StartTransactionRequest
,
467 StartTransactionResponse
468 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, { connectorId
})
469 this.handleStartTransactionResponse(connectorId
, startResponse
)
470 PerformanceStatistics
.endMeasure(measureId
, beginId
)
474 private async stopTransaction (
476 reason
= StopTransactionReason
.LOCAL
477 ): Promise
<StopTransactionResponse
| undefined> {
478 const measureId
= 'StopTransaction with ATG'
479 const beginId
= PerformanceStatistics
.beginMeasure(measureId
)
480 let stopResponse
: StopTransactionResponse
| undefined
481 if (this.chargingStation
.getConnectorStatus(connectorId
)?.transactionStarted
=== true) {
485 )} stop transaction with id ${this.chargingStation.getConnectorStatus(connectorId)
488 stopResponse
= await this.chargingStation
.stopTransactionOnConnector(connectorId
, reason
)
489 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
490 ++this.connectorsStatus
.get(connectorId
)!.stopTransactionRequests
!
491 if (stopResponse
.idTagInfo
?.status === AuthorizationStatus
.ACCEPTED
) {
492 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
493 ++this.connectorsStatus
.get(connectorId
)!.acceptedStopTransactionRequests
!
495 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
496 ++this.connectorsStatus
.get(connectorId
)!.rejectedStopTransactionRequests
!
499 const transactionId
= this.chargingStation
.getConnectorStatus(connectorId
)?.transactionId
501 `${this.logPrefix(connectorId)} stopping a not started transaction${
502 transactionId != null ? ` with id ${transactionId}
` : ''
506 PerformanceStatistics
.endMeasure(measureId
, beginId
)
510 private getRequireAuthorize (): boolean {
512 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()?.requireAuthorize
?? true
516 private readonly logPrefix
= (connectorId
?: number): string => {
518 ` ${this.chargingStation.stationInfo?.chargingStationId} | ATG${
519 connectorId != null ? ` on connector #${connectorId}
` : ''
524 private handleStartTransactionResponse (
526 startResponse
: StartTransactionResponse
528 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
529 ++this.connectorsStatus
.get(connectorId
)!.startTransactionRequests
!
530 if (startResponse
.idTagInfo
.status === AuthorizationStatus
.ACCEPTED
) {
531 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
532 ++this.connectorsStatus
.get(connectorId
)!.acceptedStartTransactionRequests
!
534 logger
.warn(`${this.logPrefix(connectorId)} start transaction rejected`)
535 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
536 ++this.connectorsStatus
.get(connectorId
)!.rejectedStartTransactionRequests
!