1 // Partial Copyright Jerome Benoit. 2021-2024. All Rights Reserved.
3 import { hoursToMilliseconds
, secondsToMilliseconds
} from
'date-fns'
4 import { randomInt
} from
'node:crypto'
6 import type { ChargingStation
} from
'./ChargingStation.js'
8 import { BaseError
} from
'../exception/index.js'
9 import { PerformanceStatistics
} from
'../performance/index.js'
12 ChargingStationEvents
,
14 type StartTransactionRequest
,
15 type StartTransactionResponse
,
17 StopTransactionReason
,
18 type StopTransactionResponse
,
19 } from
'../types/index.js'
24 formatDurationMilliSeconds
,
30 } from
'../utils/index.js'
31 import { checkChargingStationState
} from
'./Helpers.js'
32 import { IdTagsCache
} from
'./IdTagsCache.js'
33 import { isIdTagAuthorized
} from
'./ocpp/index.js'
35 export class AutomaticTransactionGenerator
{
36 private static readonly instances
: Map
<string, AutomaticTransactionGenerator
> = new Map
<
38 AutomaticTransactionGenerator
41 public readonly connectorsStatus
: Map
<number, Status
>
42 public started
: boolean
44 private readonly chargingStation
: ChargingStation
45 private starting
: boolean
46 private stopping
: boolean
48 private constructor (chargingStation
: ChargingStation
) {
52 this.chargingStation
= chargingStation
53 this.connectorsStatus
= new Map
<number, Status
>()
54 this.initializeConnectorsStatus()
57 public static deleteInstance (chargingStation
: ChargingStation
): boolean {
58 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
59 return AutomaticTransactionGenerator
.instances
.delete(chargingStation
.stationInfo
!.hashId
)
62 public static getInstance (
63 chargingStation
: ChargingStation
64 ): AutomaticTransactionGenerator
| undefined {
65 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
66 if (!AutomaticTransactionGenerator
.instances
.has(chargingStation
.stationInfo
!.hashId
)) {
67 AutomaticTransactionGenerator
.instances
.set(
68 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
69 chargingStation
.stationInfo
!.hashId
,
70 new AutomaticTransactionGenerator(chargingStation
)
73 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
74 return AutomaticTransactionGenerator
.instances
.get(chargingStation
.stationInfo
!.hashId
)
77 public start (stopAbsoluteDuration
?: boolean): void {
78 if (!checkChargingStationState(this.chargingStation
, this.logPrefix())) {
82 logger
.warn(`${this.logPrefix()} is already started`)
86 logger
.warn(`${this.logPrefix()} is already starting`)
90 this.startConnectors(stopAbsoluteDuration
)
95 public startConnector (connectorId
: number, stopAbsoluteDuration
?: boolean): void {
96 if (!checkChargingStationState(this.chargingStation
, this.logPrefix(connectorId
))) {
99 if (!this.connectorsStatus
.has(connectorId
)) {
100 logger
.error(`${this.logPrefix(connectorId)} starting on non existing connector`)
101 throw new BaseError(`Connector ${connectorId.toString()} does not exist`)
103 if (this.connectorsStatus
.get(connectorId
)?.start
=== false) {
104 this.internalStartConnector(connectorId
, stopAbsoluteDuration
).catch(Constants
.EMPTY_FUNCTION
)
105 } else if (this.connectorsStatus
.get(connectorId
)?.start
=== true) {
106 logger
.warn(`${this.logPrefix(connectorId)} is already started on connector`)
110 public stop (): void {
112 logger
.warn(`${this.logPrefix()} is already stopped`)
116 logger
.warn(`${this.logPrefix()} is already stopping`)
120 this.stopConnectors()
122 this.stopping
= false
125 public stopConnector (connectorId
: number): void {
126 if (!this.connectorsStatus
.has(connectorId
)) {
127 logger
.error(`${this.logPrefix(connectorId)} stopping on non existing connector`)
128 throw new BaseError(`Connector ${connectorId.toString()} does not exist`)
130 if (this.connectorsStatus
.get(connectorId
)?.start
=== true) {
131 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
132 this.connectorsStatus
.get(connectorId
)!.start
= false
133 } else if (this.connectorsStatus
.get(connectorId
)?.start
=== false) {
134 logger
.warn(`${this.logPrefix(connectorId)} is already stopped on connector`)
138 private canStartConnector (connectorId
: number): boolean {
139 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
140 if (new Date() > this.connectorsStatus
.get(connectorId
)!.stopDate
!) {
144 )} entered in transaction loop while the ATG stop date has been reached`
148 if (!this.chargingStation
.inAcceptedState()) {
152 )} entered in transaction loop while the charging station is not in accepted state`
156 if (!this.chargingStation
.isChargingStationAvailable()) {
160 )} entered in transaction loop while the charging station is unavailable`
164 if (!this.chargingStation
.isConnectorAvailable(connectorId
)) {
168 )} entered in transaction loop while the connector ${connectorId.toString()} is unavailable`
172 const connectorStatus
= this.chargingStation
.getConnectorStatus(connectorId
)
173 if (connectorStatus
?.transactionStarted
=== true) {
175 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
176 `${this.logPrefix(connectorId)} entered in transaction loop while a transaction ${connectorStatus.transactionId?.toString()} is already started on connector ${connectorId.toString()}`
183 private getConnectorStatus (connectorId
: number): Status
{
184 const statusIndex
= connectorId
- 1
185 if (statusIndex
< 0) {
186 logger
.error(`${this.logPrefix(connectorId)} invalid connector id`)
187 throw new BaseError(`Invalid connector id ${connectorId.toString()}`)
189 let connectorStatus
: Status
| undefined
190 if (this.chargingStation
.getAutomaticTransactionGeneratorStatuses()?.[statusIndex
] != null) {
191 connectorStatus
= clone
<Status
>(
192 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
193 this.chargingStation
.getAutomaticTransactionGeneratorStatuses()![statusIndex
]
199 )} no status found for connector #${connectorId.toString()} in charging station configuration file. New status will be created`
202 if (connectorStatus
!= null) {
203 connectorStatus
.startDate
= convertToDate(connectorStatus
.startDate
)
204 connectorStatus
.lastRunDate
= convertToDate(connectorStatus
.lastRunDate
)
205 connectorStatus
.stopDate
= convertToDate(connectorStatus
.stopDate
)
206 connectorStatus
.stoppedDate
= convertToDate(connectorStatus
.stoppedDate
)
209 (connectorStatus
.start
||
210 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()?.enable
!== true)
212 connectorStatus
.start
= false
217 acceptedAuthorizeRequests
: 0,
218 acceptedStartTransactionRequests
: 0,
219 acceptedStopTransactionRequests
: 0,
220 authorizeRequests
: 0,
221 rejectedAuthorizeRequests
: 0,
222 rejectedStartTransactionRequests
: 0,
223 rejectedStopTransactionRequests
: 0,
224 skippedConsecutiveTransactions
: 0,
225 skippedTransactions
: 0,
227 startTransactionRequests
: 0,
228 stopTransactionRequests
: 0,
233 private getRequireAuthorize (): boolean {
235 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()?.requireAuthorize
?? true
239 private handleStartTransactionResponse (
241 startResponse
: StartTransactionResponse
243 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
244 ++this.connectorsStatus
.get(connectorId
)!.startTransactionRequests
245 if (startResponse
.idTagInfo
.status === AuthorizationStatus
.ACCEPTED
) {
246 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
247 ++this.connectorsStatus
.get(connectorId
)!.acceptedStartTransactionRequests
249 logger
.warn(`${this.logPrefix(connectorId)} start transaction rejected`)
250 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
251 ++this.connectorsStatus
.get(connectorId
)!.rejectedStartTransactionRequests
255 private initializeConnectorsStatus (): void {
256 if (this.chargingStation
.hasEvses
) {
257 for (const [evseId
, evseStatus
] of this.chargingStation
.evses
) {
259 for (const connectorId
of evseStatus
.connectors
.keys()) {
260 this.connectorsStatus
.set(connectorId
, this.getConnectorStatus(connectorId
))
265 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
266 if (connectorId
> 0) {
267 this.connectorsStatus
.set(connectorId
, this.getConnectorStatus(connectorId
))
273 private async internalStartConnector (
275 stopAbsoluteDuration
?: boolean
277 this.setStartConnectorStatus(connectorId
, stopAbsoluteDuration
)
281 )} started on connector and will run for ${formatDurationMilliSeconds(
282 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
283 this.connectorsStatus.get(connectorId)!.stopDate!.getTime() -
284 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
285 this.connectorsStatus.get(connectorId)!.startDate!.getTime()
288 while (this.connectorsStatus
.get(connectorId
)?.start
=== true) {
289 await this.waitChargingStationAvailable(connectorId
)
290 await this.waitConnectorAvailable(connectorId
)
291 await this.waitRunningTransactionStopped(connectorId
)
292 if (!this.canStartConnector(connectorId
)) {
293 this.stopConnector(connectorId
)
296 const wait
= secondsToMilliseconds(
298 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
299 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()!
300 .minDelayBetweenTwoTransactions
,
301 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
302 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()!
303 .maxDelayBetweenTwoTransactions
306 logger
.info(`${this.logPrefix(connectorId)} waiting for ${formatDurationMilliSeconds(wait)}`)
308 const start
= secureRandom()
311 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
312 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()!.probabilityOfStart
314 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
315 this.connectorsStatus
.get(connectorId
)!.skippedConsecutiveTransactions
= 0
317 const startResponse
= await this.startTransaction(connectorId
)
318 if (startResponse
?.idTagInfo
.status === AuthorizationStatus
.ACCEPTED
) {
319 // Wait until end of transaction
320 const waitTrxEnd
= secondsToMilliseconds(
322 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
323 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()!.minDuration
,
324 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
325 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()!.maxDuration
329 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
330 `${this.logPrefix(connectorId)} transaction started with id ${this.chargingStation
331 .getConnectorStatus(connectorId)
332 ?.transactionId?.toString()} and will stop in ${formatDurationMilliSeconds(waitTrxEnd)}`
334 await sleep(waitTrxEnd
)
335 await this.stopTransaction(connectorId
)
338 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
339 ++this.connectorsStatus
.get(connectorId
)!.skippedConsecutiveTransactions
340 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
341 ++this.connectorsStatus
.get(connectorId
)!.skippedTransactions
343 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
344 `${this.logPrefix(connectorId)} skipped consecutively ${this.connectorsStatus
346 ?.skippedConsecutiveTransactions.toString()
347 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
348 }/${this.connectorsStatus.get(connectorId)?.skippedTransactions.toString()} transaction(s)`
351 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
352 this.connectorsStatus
.get(connectorId
)!.lastRunDate
= new Date()
354 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
355 this.connectorsStatus
.get(connectorId
)!.stoppedDate
= new Date()
359 )} stopped on connector and lasted for ${formatDurationMilliSeconds(
360 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
361 this.connectorsStatus.get(connectorId)!.stoppedDate!.getTime() -
362 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
363 this.connectorsStatus.get(connectorId)!.startDate!.getTime()
367 `${this.logPrefix(connectorId)} stopped with connector status: %j`,
368 this.connectorsStatus
.get(connectorId
)
370 this.chargingStation
.emit(ChargingStationEvents
.updated
)
373 private readonly logPrefix
= (connectorId
?: number): string => {
375 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
376 ` ${this.chargingStation.stationInfo?.chargingStationId} | ATG${
377 connectorId != null ? ` on connector #${connectorId.toString()}
` : ''
382 private setStartConnectorStatus (
384 stopAbsoluteDuration
= this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()
385 ?.stopAbsoluteDuration
387 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
388 this.connectorsStatus
.get(connectorId
)!.startDate
= new Date()
390 stopAbsoluteDuration
=== false ||
391 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
392 !isValidDate(this.connectorsStatus
.get(connectorId
)!.stopDate
)
394 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
395 this.connectorsStatus
.get(connectorId
)!.stopDate
= new Date(
396 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
397 this.connectorsStatus
.get(connectorId
)!.startDate
!.getTime() +
399 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
400 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()!.stopAfterHours
404 delete this.connectorsStatus
.get(connectorId
)?.stoppedDate
405 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
406 this.connectorsStatus
.get(connectorId
)!.skippedConsecutiveTransactions
= 0
407 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
408 this.connectorsStatus
.get(connectorId
)!.start
= true
409 this.chargingStation
.emit(ChargingStationEvents
.updated
)
412 private startConnectors (stopAbsoluteDuration
?: boolean): void {
414 this.connectorsStatus
.size
> 0 &&
415 this.connectorsStatus
.size
!== this.chargingStation
.getNumberOfConnectors()
417 this.connectorsStatus
.clear()
418 this.initializeConnectorsStatus()
420 if (this.chargingStation
.hasEvses
) {
421 for (const [evseId
, evseStatus
] of this.chargingStation
.evses
) {
423 for (const connectorId
of evseStatus
.connectors
.keys()) {
424 this.startConnector(connectorId
, stopAbsoluteDuration
)
429 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
430 if (connectorId
> 0) {
431 this.startConnector(connectorId
, stopAbsoluteDuration
)
437 private async startTransaction (
439 ): Promise
<StartTransactionResponse
| undefined> {
440 const measureId
= 'StartTransaction with ATG'
441 const beginId
= PerformanceStatistics
.beginMeasure(measureId
)
442 let startResponse
: StartTransactionResponse
| undefined
443 if (this.chargingStation
.hasIdTags()) {
444 const idTag
= IdTagsCache
.getInstance().getIdTag(
445 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
446 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()!.idTagDistribution
!,
447 this.chargingStation
,
450 const startTransactionLogMsg
= `${this.logPrefix(
452 )} start transaction with an idTag '${idTag}'`
453 if (this.getRequireAuthorize()) {
454 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
455 ++this.connectorsStatus
.get(connectorId
)!.authorizeRequests
456 if (await isIdTagAuthorized(this.chargingStation
, connectorId
, idTag
)) {
457 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
458 ++this.connectorsStatus
.get(connectorId
)!.acceptedAuthorizeRequests
459 logger
.info(startTransactionLogMsg
)
461 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
462 Partial
<StartTransactionRequest
>,
463 StartTransactionResponse
464 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, {
468 this.handleStartTransactionResponse(connectorId
, startResponse
)
469 PerformanceStatistics
.endMeasure(measureId
, beginId
)
472 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
473 ++this.connectorsStatus
.get(connectorId
)!.rejectedAuthorizeRequests
474 PerformanceStatistics
.endMeasure(measureId
, beginId
)
477 logger
.info(startTransactionLogMsg
)
479 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
480 Partial
<StartTransactionRequest
>,
481 StartTransactionResponse
482 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, {
486 this.handleStartTransactionResponse(connectorId
, startResponse
)
487 PerformanceStatistics
.endMeasure(measureId
, beginId
)
490 logger
.info(`${this.logPrefix(connectorId)} start transaction without an idTag`)
491 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
492 Partial
<StartTransactionRequest
>,
493 StartTransactionResponse
494 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, {
497 this.handleStartTransactionResponse(connectorId
, startResponse
)
498 PerformanceStatistics
.endMeasure(measureId
, beginId
)
502 private stopConnectors (): void {
503 if (this.chargingStation
.hasEvses
) {
504 for (const [evseId
, evseStatus
] of this.chargingStation
.evses
) {
506 for (const connectorId
of evseStatus
.connectors
.keys()) {
507 this.stopConnector(connectorId
)
512 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
513 if (connectorId
> 0) {
514 this.stopConnector(connectorId
)
520 private async stopTransaction (
522 reason
= StopTransactionReason
.LOCAL
523 ): Promise
<StopTransactionResponse
| undefined> {
524 const measureId
= 'StopTransaction with ATG'
525 const beginId
= PerformanceStatistics
.beginMeasure(measureId
)
526 let stopResponse
: StopTransactionResponse
| undefined
527 if (this.chargingStation
.getConnectorStatus(connectorId
)?.transactionStarted
=== true) {
529 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
530 `${this.logPrefix(connectorId)} stop transaction with id ${this.chargingStation
531 .getConnectorStatus(connectorId)
532 ?.transactionId?.toString()}`
534 stopResponse
= await this.chargingStation
.stopTransactionOnConnector(connectorId
, reason
)
535 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
536 ++this.connectorsStatus
.get(connectorId
)!.stopTransactionRequests
537 if (stopResponse
.idTagInfo
?.status === AuthorizationStatus
.ACCEPTED
) {
538 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
539 ++this.connectorsStatus
.get(connectorId
)!.acceptedStopTransactionRequests
541 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
542 ++this.connectorsStatus
.get(connectorId
)!.rejectedStopTransactionRequests
545 const transactionId
= this.chargingStation
.getConnectorStatus(connectorId
)?.transactionId
547 `${this.logPrefix(connectorId)} stopping a not started transaction${
548 transactionId != null ? ` with id ${transactionId.toString()}
` : ''
552 PerformanceStatistics
.endMeasure(measureId
, beginId
)
556 private async waitChargingStationAvailable (connectorId
: number): Promise
<void> {
558 while (!this.chargingStation
.isChargingStationAvailable()) {
563 )} transaction loop waiting for charging station to be available`
567 await sleep(Constants
.DEFAULT_ATG_WAIT_TIME
)
571 private async waitConnectorAvailable (connectorId
: number): Promise
<void> {
573 while (!this.chargingStation
.isConnectorAvailable(connectorId
)) {
578 )} transaction loop waiting for connector ${connectorId.toString()} to be available`
582 await sleep(Constants
.DEFAULT_ATG_WAIT_TIME
)
586 private async waitRunningTransactionStopped (connectorId
: number): Promise
<void> {
587 const connectorStatus
= this.chargingStation
.getConnectorStatus(connectorId
)
589 while (connectorStatus
?.transactionStarted
=== true) {
592 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
593 `${this.logPrefix(connectorId)} transaction loop waiting for started transaction ${connectorStatus.transactionId?.toString()} on connector ${connectorId.toString()} to be stopped`
597 await sleep(Constants
.DEFAULT_ATG_WAIT_TIME
)