1 // Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
3 import { AsyncResource
} from
'node:async_hooks';
5 import { type ChargingStation
, ChargingStationUtils
} from
'./internal';
6 import { BaseError
} from
'../exception';
7 // import { PerformanceStatistics } from '../performance';
8 import { PerformanceStatistics
} from
'../performance/PerformanceStatistics';
11 type AuthorizeRequest
,
12 type AuthorizeResponse
,
13 type AutomaticTransactionGeneratorConfiguration
,
17 type StartTransactionRequest
,
18 type StartTransactionResponse
,
20 StopTransactionReason
,
21 type StopTransactionResponse
,
23 import { Constants
, Utils
, logger
} from
'../utils';
25 const moduleName
= 'AutomaticTransactionGenerator';
27 export class AutomaticTransactionGenerator
extends AsyncResource
{
28 private static readonly instances
: Map
<string, AutomaticTransactionGenerator
> = new Map
<
30 AutomaticTransactionGenerator
33 public readonly connectorsStatus
: Map
<number, Status
>;
34 public readonly configuration
: AutomaticTransactionGeneratorConfiguration
;
35 public started
: boolean;
36 private readonly chargingStation
: ChargingStation
;
37 private idTagIndex
: number;
40 automaticTransactionGeneratorConfiguration
: AutomaticTransactionGeneratorConfiguration
,
41 chargingStation
: ChargingStation
45 this.configuration
= automaticTransactionGeneratorConfiguration
;
46 this.chargingStation
= chargingStation
;
48 this.connectorsStatus
= new Map
<number, Status
>();
49 this.initializeConnectorsStatus();
52 public static getInstance(
53 automaticTransactionGeneratorConfiguration
: AutomaticTransactionGeneratorConfiguration
,
54 chargingStation
: ChargingStation
55 ): AutomaticTransactionGenerator
| undefined {
56 if (AutomaticTransactionGenerator
.instances
.has(chargingStation
.stationInfo
.hashId
) === false) {
57 AutomaticTransactionGenerator
.instances
.set(
58 chargingStation
.stationInfo
.hashId
,
59 new AutomaticTransactionGenerator(
60 automaticTransactionGeneratorConfiguration
,
65 return AutomaticTransactionGenerator
.instances
.get(chargingStation
.stationInfo
.hashId
);
68 public start(): void {
70 ChargingStationUtils
.checkChargingStation(this.chargingStation
, this.logPrefix()) === false
74 if (this.started
=== true) {
75 logger
.warn(`${this.logPrefix()} is already started`);
78 this.startConnectors();
83 if (this.started
=== false) {
84 logger
.warn(`${this.logPrefix()} is already stopped`);
87 this.stopConnectors();
91 public startConnector(connectorId
: number): void {
93 ChargingStationUtils
.checkChargingStation(
95 this.logPrefix(connectorId
)
100 if (this.connectorsStatus
.has(connectorId
) === false) {
101 logger
.error(`${this.logPrefix(connectorId)} starting on non existing connector`);
102 throw new BaseError(`Connector ${connectorId} does not exist`);
104 if (this.connectorsStatus
.get(connectorId
)?.start
=== false) {
105 this.runInAsyncScope(
106 this.internalStartConnector
.bind(this) as (
107 this: AutomaticTransactionGenerator
,
112 ).catch(Constants
.EMPTY_FUNCTION
);
113 } else if (this.connectorsStatus
.get(connectorId
)?.start
=== true) {
114 logger
.warn(`${this.logPrefix(connectorId)} is already started on connector`);
118 public stopConnector(connectorId
: number): void {
119 if (this.connectorsStatus
.has(connectorId
) === false) {
120 logger
.error(`${this.logPrefix(connectorId)} stopping on non existing connector`);
121 throw new BaseError(`Connector ${connectorId} does not exist`);
123 if (this.connectorsStatus
.get(connectorId
)?.start
=== true) {
124 this.connectorsStatus
.get(connectorId
).start
= false;
125 } else if (this.connectorsStatus
.get(connectorId
)?.start
=== false) {
126 logger
.warn(`${this.logPrefix(connectorId)} is already stopped on connector`);
130 private startConnectors(): void {
132 this.connectorsStatus
?.size
> 0 &&
133 this.connectorsStatus
.size
!== this.chargingStation
.getNumberOfConnectors()
135 this.connectorsStatus
.clear();
136 this.initializeConnectorsStatus();
138 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
139 if (connectorId
> 0) {
140 this.startConnector(connectorId
);
145 private stopConnectors(): void {
146 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
147 if (connectorId
> 0) {
148 this.stopConnector(connectorId
);
153 private async internalStartConnector(connectorId
: number): Promise
<void> {
154 this.setStartConnectorStatus(connectorId
);
158 )} started on connector and will run for ${Utils.formatDurationMilliSeconds(
159 this.connectorsStatus.get(connectorId).stopDate.getTime() -
160 this.connectorsStatus.get(connectorId).startDate.getTime()
163 while (this.connectorsStatus
.get(connectorId
)?.start
=== true) {
164 if (new Date() > this.connectorsStatus
.get(connectorId
).stopDate
) {
165 this.stopConnector(connectorId
);
168 if (this.chargingStation
.isInAcceptedState() === false) {
172 )} entered in transaction loop while the charging station is not in accepted state`
174 this.stopConnector(connectorId
);
177 if (this.chargingStation
.isChargingStationAvailable() === false) {
181 )} entered in transaction loop while the charging station is unavailable`
183 this.stopConnector(connectorId
);
186 if (this.chargingStation
.isConnectorAvailable(connectorId
) === false) {
190 )} entered in transaction loop while the connector ${connectorId} is unavailable`
192 this.stopConnector(connectorId
);
196 this.chargingStation
.getConnectorStatus(connectorId
)?.status ===
197 ConnectorStatusEnum
.Unavailable
202 )} entered in transaction loop while the connector ${connectorId} status is unavailable`
204 this.stopConnector(connectorId
);
207 if (!this.chargingStation
?.ocppRequestService
) {
211 )} transaction loop waiting for charging station service to be initialized`
214 await Utils
.sleep(Constants
.CHARGING_STATION_ATG_INITIALIZATION_TIME
);
215 } while (!this.chargingStation
?.ocppRequestService
);
218 Utils
.getRandomInteger(
219 this.configuration
.maxDelayBetweenTwoTransactions
,
220 this.configuration
.minDelayBetweenTwoTransactions
223 `${this.logPrefix(connectorId)} waiting for ${Utils.formatDurationMilliSeconds(wait)}`
225 await Utils
.sleep(wait
);
226 const start
= Utils
.secureRandom();
227 if (start
< this.configuration
.probabilityOfStart
) {
228 this.connectorsStatus
.get(connectorId
).skippedConsecutiveTransactions
= 0;
230 const startResponse
= await this.startTransaction(connectorId
);
231 if (startResponse
?.idTagInfo
?.status === AuthorizationStatus
.ACCEPTED
) {
232 // Wait until end of transaction
234 Utils
.getRandomInteger(this.configuration
.maxDuration
, this.configuration
.minDuration
) *
237 `${this.logPrefix(connectorId)} transaction ${this.chargingStation
238 .getConnectorStatus(connectorId)
239 ?.transactionId?.toString()} started and will stop in ${Utils.formatDurationMilliSeconds(
243 await Utils
.sleep(waitTrxEnd
);
246 `${this.logPrefix(connectorId)} stop transaction ${this.chargingStation
247 .getConnectorStatus(connectorId)
248 ?.transactionId?.toString()}`
250 await this.stopTransaction(connectorId
);
253 this.connectorsStatus
.get(connectorId
).skippedConsecutiveTransactions
++;
254 this.connectorsStatus
.get(connectorId
).skippedTransactions
++;
256 `${this.logPrefix(connectorId)} skipped consecutively ${this.connectorsStatus
258 ?.skippedConsecutiveTransactions?.toString()}/${this.connectorsStatus
260 ?.skippedTransactions?.toString()} transaction(s)`
263 this.connectorsStatus
.get(connectorId
).lastRunDate
= new Date();
265 this.connectorsStatus
.get(connectorId
).stoppedDate
= new Date();
269 )} stopped on connector and lasted for ${Utils.formatDurationMilliSeconds(
270 this.connectorsStatus.get(connectorId).stoppedDate.getTime() -
271 this.connectorsStatus.get(connectorId).startDate.getTime()
275 `${this.logPrefix(connectorId)} connector status: %j`,
276 this.connectorsStatus
.get(connectorId
)
280 private setStartConnectorStatus(connectorId
: number): void {
281 this.connectorsStatus
.get(connectorId
).skippedConsecutiveTransactions
= 0;
282 const previousRunDuration
=
283 this.connectorsStatus
.get(connectorId
)?.startDate
&&
284 this.connectorsStatus
.get(connectorId
)?.lastRunDate
285 ? this.connectorsStatus
.get(connectorId
).lastRunDate
.getTime() -
286 this.connectorsStatus
.get(connectorId
).startDate
.getTime()
288 this.connectorsStatus
.get(connectorId
).startDate
= new Date();
289 this.connectorsStatus
.get(connectorId
).stopDate
= new Date(
290 this.connectorsStatus
.get(connectorId
).startDate
.getTime() +
291 (this.configuration
.stopAfterHours
??
292 Constants
.CHARGING_STATION_ATG_DEFAULT_STOP_AFTER_HOURS
) *
297 this.connectorsStatus
.get(connectorId
).start
= true;
300 private initializeConnectorsStatus(): void {
301 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
302 if (connectorId
> 0) {
303 this.connectorsStatus
.set(connectorId
, {
305 authorizeRequests
: 0,
306 acceptedAuthorizeRequests
: 0,
307 rejectedAuthorizeRequests
: 0,
308 startTransactionRequests
: 0,
309 acceptedStartTransactionRequests
: 0,
310 rejectedStartTransactionRequests
: 0,
311 stopTransactionRequests
: 0,
312 acceptedStopTransactionRequests
: 0,
313 rejectedStopTransactionRequests
: 0,
314 skippedConsecutiveTransactions
: 0,
315 skippedTransactions
: 0,
321 private async startTransaction(
323 ): Promise
<StartTransactionResponse
| undefined> {
324 const measureId
= 'StartTransaction with ATG';
325 const beginId
= PerformanceStatistics
.beginMeasure(measureId
);
326 let startResponse
: StartTransactionResponse
;
327 if (this.chargingStation
.hasAuthorizedTags()) {
328 const idTag
= this.getIdTag(connectorId
);
329 const startTransactionLogMsg
= `${this.logPrefix(
331 )} start transaction with an idTag '${idTag}'`;
332 if (this.getRequireAuthorize()) {
333 this.chargingStation
.getConnectorStatus(connectorId
).authorizeIdTag
= idTag
;
335 const authorizeResponse
: AuthorizeResponse
=
336 await this.chargingStation
.ocppRequestService
.requestHandler
<
339 >(this.chargingStation
, RequestCommand
.AUTHORIZE
, {
342 this.connectorsStatus
.get(connectorId
).authorizeRequests
++;
343 if (authorizeResponse
?.idTagInfo
?.status === AuthorizationStatus
.ACCEPTED
) {
344 this.connectorsStatus
.get(connectorId
).acceptedAuthorizeRequests
++;
345 logger
.info(startTransactionLogMsg
);
347 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
348 StartTransactionRequest
,
349 StartTransactionResponse
350 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, {
354 this.handleStartTransactionResponse(connectorId
, startResponse
);
355 PerformanceStatistics
.endMeasure(measureId
, beginId
);
356 return startResponse
;
358 this.connectorsStatus
.get(connectorId
).rejectedAuthorizeRequests
++;
359 PerformanceStatistics
.endMeasure(measureId
, beginId
);
360 return startResponse
;
362 logger
.info(startTransactionLogMsg
);
364 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
365 StartTransactionRequest
,
366 StartTransactionResponse
367 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, {
371 this.handleStartTransactionResponse(connectorId
, startResponse
);
372 PerformanceStatistics
.endMeasure(measureId
, beginId
);
373 return startResponse
;
375 logger
.info(`${this.logPrefix(connectorId)} start transaction without an idTag`);
376 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
377 StartTransactionRequest
,
378 StartTransactionResponse
379 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, { connectorId
});
380 this.handleStartTransactionResponse(connectorId
, startResponse
);
381 PerformanceStatistics
.endMeasure(measureId
, beginId
);
382 return startResponse
;
385 private async stopTransaction(
387 reason
: StopTransactionReason
= StopTransactionReason
.LOCAL
388 ): Promise
<StopTransactionResponse
> {
389 const measureId
= 'StopTransaction with ATG';
390 const beginId
= PerformanceStatistics
.beginMeasure(measureId
);
391 let stopResponse
: StopTransactionResponse
;
392 if (this.chargingStation
.getConnectorStatus(connectorId
)?.transactionStarted
=== true) {
393 stopResponse
= await this.chargingStation
.stopTransactionOnConnector(connectorId
, reason
);
394 this.connectorsStatus
.get(connectorId
).stopTransactionRequests
++;
395 if (stopResponse
?.idTagInfo
?.status === AuthorizationStatus
.ACCEPTED
) {
396 this.connectorsStatus
.get(connectorId
).acceptedStopTransactionRequests
++;
398 this.connectorsStatus
.get(connectorId
).rejectedStopTransactionRequests
++;
401 const transactionId
= this.chargingStation
.getConnectorStatus(connectorId
)?.transactionId
;
403 `${this.logPrefix(connectorId)} stopping a not started transaction${
404 !Utils.isNullOrUndefined(transactionId) ? ` ${transactionId?.toString()}
` : ''
408 PerformanceStatistics
.endMeasure(measureId
, beginId
);
412 private getRequireAuthorize(): boolean {
413 return this.configuration
?.requireAuthorize
?? true;
416 private getRandomIdTag(authorizationFile
: string): string {
417 const tags
= this.chargingStation
.authorizedTagsCache
.getAuthorizedTags(authorizationFile
);
418 this.idTagIndex
= Math.floor(Utils
.secureRandom() * tags
.length
);
419 return tags
[this.idTagIndex
];
422 private getRoundRobinIdTag(authorizationFile
: string): string {
423 const tags
= this.chargingStation
.authorizedTagsCache
.getAuthorizedTags(authorizationFile
);
424 const idTag
= tags
[this.idTagIndex
];
425 this.idTagIndex
= this.idTagIndex
=== tags
.length
- 1 ? 0 : this.idTagIndex
+ 1;
429 private getConnectorAffinityIdTag(authorizationFile
: string, connectorId
: number): string {
430 const tags
= this.chargingStation
.authorizedTagsCache
.getAuthorizedTags(authorizationFile
);
431 this.idTagIndex
= (this.chargingStation
.index
- 1 + (connectorId
- 1)) % tags
.length
;
432 return tags
[this.idTagIndex
];
435 private getIdTag(connectorId
: number): string {
436 const authorizationFile
= ChargingStationUtils
.getAuthorizationFile(
437 this.chargingStation
.stationInfo
439 switch (this.configuration
?.idTagDistribution
) {
440 case IdTagDistribution
.RANDOM
:
441 return this.getRandomIdTag(authorizationFile
);
442 case IdTagDistribution
.ROUND_ROBIN
:
443 return this.getRoundRobinIdTag(authorizationFile
);
444 case IdTagDistribution
.CONNECTOR_AFFINITY
:
445 return this.getConnectorAffinityIdTag(authorizationFile
, connectorId
);
447 return this.getRoundRobinIdTag(authorizationFile
);
451 private logPrefix
= (connectorId
?: number): string => {
452 return Utils
.logPrefix(
453 ` ${this.chargingStation.stationInfo.chargingStationId} | ATG${
454 connectorId !== undefined ? ` on connector #${connectorId.toString()}
` : ''
459 private handleStartTransactionResponse(
461 startResponse
: StartTransactionResponse
463 this.connectorsStatus
.get(connectorId
).startTransactionRequests
++;
464 if (startResponse
?.idTagInfo
?.status === AuthorizationStatus
.ACCEPTED
) {
465 this.connectorsStatus
.get(connectorId
).acceptedStartTransactionRequests
++;
467 logger
.warn(`${this.logPrefix(connectorId)} start transaction rejected`);
468 this.connectorsStatus
.get(connectorId
).rejectedStartTransactionRequests
++;