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 if (!AutomaticTransactionGenerator
.instances
.has(chargingStation
.stationInfo
.hashId
)) {
57 AutomaticTransactionGenerator
.instances
.set(
58 chargingStation
.stationInfo
.hashId
,
59 new AutomaticTransactionGenerator(chargingStation
)
62 return AutomaticTransactionGenerator
.instances
.get(chargingStation
.stationInfo
.hashId
)
65 public start (): void {
66 if (!checkChargingStation(this.chargingStation
, this.logPrefix())) {
70 logger
.warn(`${this.logPrefix()} is already started`)
74 logger
.warn(`${this.logPrefix()} is already starting`)
78 this.startConnectors()
83 public stop (): void {
85 logger
.warn(`${this.logPrefix()} is already stopped`)
89 logger
.warn(`${this.logPrefix()} is already stopping`)
98 public startConnector (connectorId
: number): void {
99 if (!checkChargingStation(this.chargingStation
, this.logPrefix(connectorId
))) {
102 if (!this.connectorsStatus
.has(connectorId
)) {
103 logger
.error(`${this.logPrefix(connectorId)} starting on non existing connector`)
104 throw new BaseError(`Connector ${connectorId} does not exist`)
106 if (this.connectorsStatus
.get(connectorId
)?.start
=== false) {
107 this.internalStartConnector(connectorId
).catch(Constants
.EMPTY_FUNCTION
)
108 } else if (this.connectorsStatus
.get(connectorId
)?.start
=== true) {
109 logger
.warn(`${this.logPrefix(connectorId)} is already started on connector`)
113 public stopConnector (connectorId
: number): void {
114 if (!this.connectorsStatus
.has(connectorId
)) {
115 logger
.error(`${this.logPrefix(connectorId)} stopping on non existing connector`)
116 throw new BaseError(`Connector ${connectorId} does not exist`)
118 if (this.connectorsStatus
.get(connectorId
)?.start
=== true) {
119 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
120 this.connectorsStatus
.get(connectorId
)!.start
= false
121 } else if (this.connectorsStatus
.get(connectorId
)?.start
=== false) {
122 logger
.warn(`${this.logPrefix(connectorId)} is already stopped on connector`)
126 private startConnectors (): void {
128 this.connectorsStatus
?.size
> 0 &&
129 this.connectorsStatus
.size
!== this.chargingStation
.getNumberOfConnectors()
131 this.connectorsStatus
.clear()
132 this.initializeConnectorsStatus()
134 if (this.chargingStation
.hasEvses
) {
135 for (const [evseId
, evseStatus
] of this.chargingStation
.evses
) {
137 for (const connectorId
of evseStatus
.connectors
.keys()) {
138 this.startConnector(connectorId
)
143 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
144 if (connectorId
> 0) {
145 this.startConnector(connectorId
)
151 private stopConnectors (): void {
152 if (this.chargingStation
.hasEvses
) {
153 for (const [evseId
, evseStatus
] of this.chargingStation
.evses
) {
155 for (const connectorId
of evseStatus
.connectors
.keys()) {
156 this.stopConnector(connectorId
)
161 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
162 if (connectorId
> 0) {
163 this.stopConnector(connectorId
)
169 private async internalStartConnector (connectorId
: number): Promise
<void> {
170 this.setStartConnectorStatus(connectorId
)
174 )} started on connector and will run for ${formatDurationMilliSeconds(
175 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
176 this.connectorsStatus.get(connectorId)!.stopDate!.getTime() -
177 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
178 this.connectorsStatus.get(connectorId)!.startDate!.getTime()
181 while (this.connectorsStatus
.get(connectorId
)?.start
=== true) {
182 await this.waitChargingStationServiceInitialization(connectorId
)
183 await this.waitChargingStationAvailable(connectorId
)
184 await this.waitConnectorAvailable(connectorId
)
185 if (!this.canStartConnector(connectorId
)) {
186 this.stopConnector(connectorId
)
189 const wait
= secondsToMilliseconds(
191 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()
192 .maxDelayBetweenTwoTransactions
,
193 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()
194 .minDelayBetweenTwoTransactions
197 logger
.info(`${this.logPrefix(connectorId)} waiting for ${formatDurationMilliSeconds(wait)}`)
199 const start
= secureRandom()
202 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration().probabilityOfStart
204 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
205 this.connectorsStatus
.get(connectorId
)!.skippedConsecutiveTransactions
= 0
207 const startResponse
= await this.startTransaction(connectorId
)
208 if (startResponse
?.idTagInfo
?.status === AuthorizationStatus
.ACCEPTED
) {
209 // Wait until end of transaction
210 const waitTrxEnd
= secondsToMilliseconds(
212 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration().maxDuration
,
213 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration().minDuration
219 )} transaction started with id ${this.chargingStation.getConnectorStatus(connectorId)
220 ?.transactionId} and will stop in ${formatDurationMilliSeconds(waitTrxEnd)}`
222 await sleep(waitTrxEnd
)
223 await this.stopTransaction(connectorId
)
226 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
227 ++this.connectorsStatus
.get(connectorId
)!.skippedConsecutiveTransactions
!
228 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
229 ++this.connectorsStatus
.get(connectorId
)!.skippedTransactions
!
231 `${this.logPrefix(connectorId)} skipped consecutively ${this.connectorsStatus.get(
233 )?.skippedConsecutiveTransactions}/${this.connectorsStatus.get(connectorId)
234 ?.skippedTransactions} transaction(s)`
237 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
238 this.connectorsStatus
.get(connectorId
)!.lastRunDate
= new Date()
240 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
241 this.connectorsStatus
.get(connectorId
)!.stoppedDate
= new Date()
245 )} stopped on connector and lasted for ${formatDurationMilliSeconds(
246 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
247 this.connectorsStatus.get(connectorId)!.stoppedDate!.getTime() -
248 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
249 this.connectorsStatus.get(connectorId)!.startDate!.getTime()
253 `${this.logPrefix(connectorId)} connector status: %j`,
254 this.connectorsStatus
.get(connectorId
)
258 private setStartConnectorStatus (connectorId
: number): void {
259 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
260 this.connectorsStatus
.get(connectorId
)!.skippedConsecutiveTransactions
= 0
261 const previousRunDuration
=
262 this.connectorsStatus
.get(connectorId
)?.startDate
!= null &&
263 this.connectorsStatus
.get(connectorId
)?.lastRunDate
!= null
264 ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
265 this.connectorsStatus
.get(connectorId
)!.lastRunDate
!.getTime() -
266 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
267 this.connectorsStatus
.get(connectorId
)!.startDate
!.getTime()
269 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
270 this.connectorsStatus
.get(connectorId
)!.startDate
= new Date()
271 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
272 this.connectorsStatus
.get(connectorId
)!.stopDate
= new Date(
273 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
274 this.connectorsStatus
.get(connectorId
)!.startDate
!.getTime() +
276 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration().stopAfterHours
280 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
281 this.connectorsStatus
.get(connectorId
)!.start
= true
284 private canStartConnector (connectorId
: number): boolean {
285 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
286 if (new Date() > this.connectorsStatus
.get(connectorId
)!.stopDate
!) {
289 if (!this.chargingStation
.inAcceptedState()) {
293 )} entered in transaction loop while the charging station is not in accepted state`
297 if (!this.chargingStation
.isChargingStationAvailable()) {
301 )} entered in transaction loop while the charging station is unavailable`
305 if (!this.chargingStation
.isConnectorAvailable(connectorId
)) {
309 )} entered in transaction loop while the connector ${connectorId} is unavailable`
316 private async waitChargingStationServiceInitialization (connectorId
: number): Promise
<void> {
318 while (this.chargingStation
?.ocppRequestService
== null) {
323 )} transaction loop waiting for charging station service to be initialized`
327 await sleep(Constants
.CHARGING_STATION_ATG_INITIALIZATION_TIME
)
331 private async waitChargingStationAvailable (connectorId
: number): Promise
<void> {
333 while (!this.chargingStation
.isChargingStationAvailable()) {
338 )} transaction loop waiting for charging station to be available`
342 await sleep(Constants
.CHARGING_STATION_ATG_AVAILABILITY_TIME
)
346 private async waitConnectorAvailable (connectorId
: number): Promise
<void> {
348 while (!this.chargingStation
.isConnectorAvailable(connectorId
)) {
353 )} transaction loop waiting for connector ${connectorId} to be available`
357 await sleep(Constants
.CHARGING_STATION_ATG_AVAILABILITY_TIME
)
361 private initializeConnectorsStatus (): void {
362 if (this.chargingStation
.hasEvses
) {
363 for (const [evseId
, evseStatus
] of this.chargingStation
.evses
) {
365 for (const connectorId
of evseStatus
.connectors
.keys()) {
366 this.connectorsStatus
.set(connectorId
, this.getConnectorStatus(connectorId
))
371 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
372 if (connectorId
> 0) {
373 this.connectorsStatus
.set(connectorId
, this.getConnectorStatus(connectorId
))
379 private getConnectorStatus (connectorId
: number): Status
{
380 const connectorStatus
=
381 this.chargingStation
.getAutomaticTransactionGeneratorStatuses()?.[connectorId
] != null
382 ? cloneObject
<Status
>(
383 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
384 this.chargingStation
.getAutomaticTransactionGeneratorStatuses()![connectorId
]
387 this.resetConnectorStatus(connectorStatus
)
391 authorizeRequests
: 0,
392 acceptedAuthorizeRequests
: 0,
393 rejectedAuthorizeRequests
: 0,
394 startTransactionRequests
: 0,
395 acceptedStartTransactionRequests
: 0,
396 rejectedStartTransactionRequests
: 0,
397 stopTransactionRequests
: 0,
398 acceptedStopTransactionRequests
: 0,
399 rejectedStopTransactionRequests
: 0,
400 skippedConsecutiveTransactions
: 0,
401 skippedTransactions
: 0
406 private resetConnectorStatus (connectorStatus
: Status
| undefined): void {
407 if (connectorStatus
=== undefined) {
410 delete connectorStatus
?.startDate
411 delete connectorStatus
?.lastRunDate
412 delete connectorStatus
?.stopDate
413 delete connectorStatus
?.stoppedDate
416 (connectorStatus
.start
||
417 !this.chargingStation
.getAutomaticTransactionGeneratorConfiguration().enable
)
419 connectorStatus
.start
= false
423 private async startTransaction (
425 ): Promise
<StartTransactionResponse
| undefined> {
426 const measureId
= 'StartTransaction with ATG'
427 const beginId
= PerformanceStatistics
.beginMeasure(measureId
)
428 let startResponse
: StartTransactionResponse
| undefined
429 if (this.chargingStation
.hasIdTags()) {
430 const idTag
= IdTagsCache
.getInstance().getIdTag(
431 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
432 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration().idTagDistribution
!,
433 this.chargingStation
,
436 const startTransactionLogMsg
= `${this.logPrefix(
438 )} start transaction with an idTag '${idTag}'`
439 if (this.getRequireAuthorize()) {
440 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
441 ++this.connectorsStatus
.get(connectorId
)!.authorizeRequests
!
442 if (await isIdTagAuthorized(this.chargingStation
, connectorId
, idTag
)) {
443 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
444 ++this.connectorsStatus
.get(connectorId
)!.acceptedAuthorizeRequests
!
445 logger
.info(startTransactionLogMsg
)
447 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
448 StartTransactionRequest
,
449 StartTransactionResponse
450 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, {
454 this.handleStartTransactionResponse(connectorId
, startResponse
)
455 PerformanceStatistics
.endMeasure(measureId
, beginId
)
458 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
459 ++this.connectorsStatus
.get(connectorId
)!.rejectedAuthorizeRequests
!
460 PerformanceStatistics
.endMeasure(measureId
, beginId
)
463 logger
.info(startTransactionLogMsg
)
465 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
466 StartTransactionRequest
,
467 StartTransactionResponse
468 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, {
472 this.handleStartTransactionResponse(connectorId
, startResponse
)
473 PerformanceStatistics
.endMeasure(measureId
, beginId
)
476 logger
.info(`${this.logPrefix(connectorId)} start transaction without an idTag`)
477 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
478 StartTransactionRequest
,
479 StartTransactionResponse
480 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, { connectorId
})
481 this.handleStartTransactionResponse(connectorId
, startResponse
)
482 PerformanceStatistics
.endMeasure(measureId
, beginId
)
486 private async stopTransaction (
488 reason
= StopTransactionReason
.LOCAL
489 ): Promise
<StopTransactionResponse
| undefined> {
490 const measureId
= 'StopTransaction with ATG'
491 const beginId
= PerformanceStatistics
.beginMeasure(measureId
)
492 let stopResponse
: StopTransactionResponse
| undefined
493 if (this.chargingStation
.getConnectorStatus(connectorId
)?.transactionStarted
=== true) {
497 )} stop transaction with id ${this.chargingStation.getConnectorStatus(connectorId)
500 stopResponse
= await this.chargingStation
.stopTransactionOnConnector(connectorId
, reason
)
501 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
502 ++this.connectorsStatus
.get(connectorId
)!.stopTransactionRequests
!
503 if (stopResponse
?.idTagInfo
?.status === AuthorizationStatus
.ACCEPTED
) {
504 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
505 ++this.connectorsStatus
.get(connectorId
)!.acceptedStopTransactionRequests
!
507 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
508 ++this.connectorsStatus
.get(connectorId
)!.rejectedStopTransactionRequests
!
511 const transactionId
= this.chargingStation
.getConnectorStatus(connectorId
)?.transactionId
513 `${this.logPrefix(connectorId)} stopping a not started transaction${
514 !isNullOrUndefined(transactionId) ? ` with id ${transactionId}
` : ''
518 PerformanceStatistics
.endMeasure(measureId
, beginId
)
522 private getRequireAuthorize (): boolean {
524 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()?.requireAuthorize
?? true
528 private readonly logPrefix
= (connectorId
?: number): string => {
530 ` ${this.chargingStation.stationInfo.chargingStationId} | ATG${
531 !isNullOrUndefined(connectorId) ? ` on connector #${connectorId}
` : ''
536 private handleStartTransactionResponse (
538 startResponse
: StartTransactionResponse
540 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
541 ++this.connectorsStatus
.get(connectorId
)!.startTransactionRequests
!
542 if (startResponse
?.idTagInfo
?.status === AuthorizationStatus
.ACCEPTED
) {
543 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
544 ++this.connectorsStatus
.get(connectorId
)!.acceptedStartTransactionRequests
!
546 logger
.warn(`${this.logPrefix(connectorId)} start transaction rejected`)
547 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
548 ++this.connectorsStatus
.get(connectorId
)!.rejectedStartTransactionRequests
!