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
,
16 type StartTransactionRequest
,
17 type StartTransactionResponse
,
19 StopTransactionReason
,
20 type StopTransactionResponse
,
22 import { Constants
, Utils
, logger
} from
'../utils';
24 const moduleName
= 'AutomaticTransactionGenerator';
26 export class AutomaticTransactionGenerator
extends AsyncResource
{
27 private static readonly instances
: Map
<string, AutomaticTransactionGenerator
> = new Map
<
29 AutomaticTransactionGenerator
32 public readonly connectorsStatus
: Map
<number, Status
>;
33 public readonly configuration
: AutomaticTransactionGeneratorConfiguration
;
34 public started
: boolean;
35 private readonly chargingStation
: ChargingStation
;
36 private idTagIndex
: number;
39 automaticTransactionGeneratorConfiguration
: AutomaticTransactionGeneratorConfiguration
,
40 chargingStation
: ChargingStation
44 this.configuration
= automaticTransactionGeneratorConfiguration
;
45 this.chargingStation
= chargingStation
;
47 this.connectorsStatus
= new Map
<number, Status
>();
48 this.initializeConnectorsStatus();
51 public static getInstance(
52 automaticTransactionGeneratorConfiguration
: AutomaticTransactionGeneratorConfiguration
,
53 chargingStation
: ChargingStation
54 ): AutomaticTransactionGenerator
| undefined {
55 if (AutomaticTransactionGenerator
.instances
.has(chargingStation
.stationInfo
.hashId
) === false) {
56 AutomaticTransactionGenerator
.instances
.set(
57 chargingStation
.stationInfo
.hashId
,
58 new AutomaticTransactionGenerator(
59 automaticTransactionGeneratorConfiguration
,
64 return AutomaticTransactionGenerator
.instances
.get(chargingStation
.stationInfo
.hashId
);
67 public start(): void {
68 if (this.checkChargingStation() === false) {
71 if (this.started
=== true) {
72 logger
.warn(`${this.logPrefix()} is already started`);
75 this.startConnectors();
80 if (this.started
=== false) {
81 logger
.warn(`${this.logPrefix()} is already stopped`);
84 this.stopConnectors();
88 public startConnector(connectorId
: number): void {
89 if (this.checkChargingStation(connectorId
) === false) {
92 if (this.connectorsStatus
.has(connectorId
) === false) {
93 logger
.error(`${this.logPrefix(connectorId)} starting on non existing connector`);
94 throw new BaseError(`Connector ${connectorId} does not exist`);
96 if (this.connectorsStatus
.get(connectorId
)?.start
=== false) {
98 this.internalStartConnector
.bind(this) as (
99 this: AutomaticTransactionGenerator
,
105 /* This is intentional */
107 } else if (this.connectorsStatus
.get(connectorId
)?.start
=== true) {
108 logger
.warn(`${this.logPrefix(connectorId)} is already started on connector`);
112 public stopConnector(connectorId
: number): void {
113 if (this.connectorsStatus
.has(connectorId
) === false) {
114 logger
.error(`${this.logPrefix(connectorId)} stopping on non existing connector`);
115 throw new BaseError(`Connector ${connectorId} does not exist`);
117 if (this.connectorsStatus
.get(connectorId
)?.start
=== true) {
118 this.connectorsStatus
.get(connectorId
).start
= false;
119 } else if (this.connectorsStatus
.get(connectorId
)?.start
=== false) {
120 logger
.warn(`${this.logPrefix(connectorId)} is already stopped on connector`);
124 private startConnectors(): void {
126 this.connectorsStatus
?.size
> 0 &&
127 this.connectorsStatus
.size
!== this.chargingStation
.getNumberOfConnectors()
129 this.connectorsStatus
.clear();
130 this.initializeConnectorsStatus();
132 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
133 if (connectorId
> 0) {
134 this.startConnector(connectorId
);
139 private stopConnectors(): void {
140 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
141 if (connectorId
> 0) {
142 this.stopConnector(connectorId
);
147 private async internalStartConnector(connectorId
: number): Promise
<void> {
148 this.setStartConnectorStatus(connectorId
);
152 )} started on connector and will run for ${Utils.formatDurationMilliSeconds(
153 this.connectorsStatus.get(connectorId).stopDate.getTime() -
154 this.connectorsStatus.get(connectorId).startDate.getTime()
157 while (this.connectorsStatus
.get(connectorId
)?.start
=== true) {
158 if (new Date() > this.connectorsStatus
.get(connectorId
).stopDate
) {
159 this.stopConnector(connectorId
);
162 if (this.chargingStation
.isInAcceptedState() === false) {
166 )} entered in transaction loop while the charging station is not in accepted state`
168 this.stopConnector(connectorId
);
171 if (this.chargingStation
.isChargingStationAvailable() === false) {
175 )} entered in transaction loop while the charging station is unavailable`
177 this.stopConnector(connectorId
);
180 if (this.chargingStation
.isConnectorAvailable(connectorId
) === false) {
184 )} entered in transaction loop while the connector ${connectorId} is unavailable`
186 this.stopConnector(connectorId
);
189 if (!this.chargingStation
?.ocppRequestService
) {
193 )} transaction loop waiting for charging station service to be initialized`
196 await Utils
.sleep(Constants
.CHARGING_STATION_ATG_INITIALIZATION_TIME
);
197 } while (!this.chargingStation
?.ocppRequestService
);
200 Utils
.getRandomInteger(
201 this.configuration
.maxDelayBetweenTwoTransactions
,
202 this.configuration
.minDelayBetweenTwoTransactions
205 `${this.logPrefix(connectorId)} waiting for ${Utils.formatDurationMilliSeconds(wait)}`
207 await Utils
.sleep(wait
);
208 const start
= Utils
.secureRandom();
209 if (start
< this.configuration
.probabilityOfStart
) {
210 this.connectorsStatus
.get(connectorId
).skippedConsecutiveTransactions
= 0;
212 const startResponse
= await this.startTransaction(connectorId
);
213 if (startResponse
?.idTagInfo
?.status === AuthorizationStatus
.ACCEPTED
) {
214 // Wait until end of transaction
216 Utils
.getRandomInteger(this.configuration
.maxDuration
, this.configuration
.minDuration
) *
219 `${this.logPrefix(connectorId)} transaction ${this.chargingStation
220 .getConnectorStatus(connectorId)
221 ?.transactionId?.toString()} started and will stop in ${Utils.formatDurationMilliSeconds(
225 await Utils
.sleep(waitTrxEnd
);
228 `${this.logPrefix(connectorId)} stop transaction ${this.chargingStation
229 .getConnectorStatus(connectorId)
230 ?.transactionId?.toString()}`
232 await this.stopTransaction(connectorId
);
235 this.connectorsStatus
.get(connectorId
).skippedConsecutiveTransactions
++;
236 this.connectorsStatus
.get(connectorId
).skippedTransactions
++;
238 `${this.logPrefix(connectorId)} skipped consecutively ${this.connectorsStatus
240 ?.skippedConsecutiveTransactions?.toString()}/${this.connectorsStatus
242 ?.skippedTransactions?.toString()} transaction(s)`
245 this.connectorsStatus
.get(connectorId
).lastRunDate
= new Date();
247 this.connectorsStatus
.get(connectorId
).stoppedDate
= new Date();
251 )} stopped on connector and lasted for ${Utils.formatDurationMilliSeconds(
252 this.connectorsStatus.get(connectorId).stoppedDate.getTime() -
253 this.connectorsStatus.get(connectorId).startDate.getTime()
257 `${this.logPrefix(connectorId)} connector status: %j`,
258 this.connectorsStatus
.get(connectorId
)
262 private setStartConnectorStatus(connectorId
: number): void {
263 this.connectorsStatus
.get(connectorId
).skippedConsecutiveTransactions
= 0;
264 const previousRunDuration
=
265 this.connectorsStatus
.get(connectorId
)?.startDate
&&
266 this.connectorsStatus
.get(connectorId
)?.lastRunDate
267 ? this.connectorsStatus
.get(connectorId
).lastRunDate
.getTime() -
268 this.connectorsStatus
.get(connectorId
).startDate
.getTime()
270 this.connectorsStatus
.get(connectorId
).startDate
= new Date();
271 this.connectorsStatus
.get(connectorId
).stopDate
= new Date(
272 this.connectorsStatus
.get(connectorId
).startDate
.getTime() +
273 (this.configuration
.stopAfterHours
??
274 Constants
.CHARGING_STATION_ATG_DEFAULT_STOP_AFTER_HOURS
) *
279 this.connectorsStatus
.get(connectorId
).start
= true;
282 private initializeConnectorsStatus(): void {
283 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
284 if (connectorId
> 0) {
285 this.connectorsStatus
.set(connectorId
, {
287 authorizeRequests
: 0,
288 acceptedAuthorizeRequests
: 0,
289 rejectedAuthorizeRequests
: 0,
290 startTransactionRequests
: 0,
291 acceptedStartTransactionRequests
: 0,
292 rejectedStartTransactionRequests
: 0,
293 stopTransactionRequests
: 0,
294 acceptedStopTransactionRequests
: 0,
295 rejectedStopTransactionRequests
: 0,
296 skippedConsecutiveTransactions
: 0,
297 skippedTransactions
: 0,
303 private async startTransaction(
305 ): Promise
<StartTransactionResponse
| undefined> {
306 const measureId
= 'StartTransaction with ATG';
307 const beginId
= PerformanceStatistics
.beginMeasure(measureId
);
308 let startResponse
: StartTransactionResponse
;
309 if (this.chargingStation
.hasAuthorizedTags()) {
310 const idTag
= this.getIdTag(connectorId
);
311 const startTransactionLogMsg
= `${this.logPrefix(
313 )} start transaction with an idTag '${idTag}'`;
314 if (this.getRequireAuthorize()) {
315 this.chargingStation
.getConnectorStatus(connectorId
).authorizeIdTag
= idTag
;
317 const authorizeResponse
: AuthorizeResponse
=
318 await this.chargingStation
.ocppRequestService
.requestHandler
<
321 >(this.chargingStation
, RequestCommand
.AUTHORIZE
, {
324 this.connectorsStatus
.get(connectorId
).authorizeRequests
++;
325 if (authorizeResponse
?.idTagInfo
?.status === AuthorizationStatus
.ACCEPTED
) {
326 this.connectorsStatus
.get(connectorId
).acceptedAuthorizeRequests
++;
327 logger
.info(startTransactionLogMsg
);
329 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
330 StartTransactionRequest
,
331 StartTransactionResponse
332 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, {
336 this.handleStartTransactionResponse(connectorId
, startResponse
);
337 PerformanceStatistics
.endMeasure(measureId
, beginId
);
338 return startResponse
;
340 this.connectorsStatus
.get(connectorId
).rejectedAuthorizeRequests
++;
341 PerformanceStatistics
.endMeasure(measureId
, beginId
);
342 return startResponse
;
344 logger
.info(startTransactionLogMsg
);
346 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
347 StartTransactionRequest
,
348 StartTransactionResponse
349 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, {
353 this.handleStartTransactionResponse(connectorId
, startResponse
);
354 PerformanceStatistics
.endMeasure(measureId
, beginId
);
355 return startResponse
;
357 logger
.info(`${this.logPrefix(connectorId)} start transaction without an idTag`);
358 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
359 StartTransactionRequest
,
360 StartTransactionResponse
361 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, { connectorId
});
362 this.handleStartTransactionResponse(connectorId
, startResponse
);
363 PerformanceStatistics
.endMeasure(measureId
, beginId
);
364 return startResponse
;
367 private async stopTransaction(
369 reason
: StopTransactionReason
= StopTransactionReason
.LOCAL
370 ): Promise
<StopTransactionResponse
> {
371 const measureId
= 'StopTransaction with ATG';
372 const beginId
= PerformanceStatistics
.beginMeasure(measureId
);
373 let stopResponse
: StopTransactionResponse
;
374 if (this.chargingStation
.getConnectorStatus(connectorId
)?.transactionStarted
=== true) {
375 stopResponse
= await this.chargingStation
.stopTransactionOnConnector(connectorId
, reason
);
376 this.connectorsStatus
.get(connectorId
).stopTransactionRequests
++;
377 if (stopResponse
?.idTagInfo
?.status === AuthorizationStatus
.ACCEPTED
) {
378 this.connectorsStatus
.get(connectorId
).acceptedStopTransactionRequests
++;
380 this.connectorsStatus
.get(connectorId
).rejectedStopTransactionRequests
++;
383 const transactionId
= this.chargingStation
.getConnectorStatus(connectorId
)?.transactionId
;
385 `${this.logPrefix(connectorId)} stopping a not started transaction${
386 !Utils.isNullOrUndefined(transactionId) ? ` ${transactionId?.toString()}
` : ''
390 PerformanceStatistics
.endMeasure(measureId
, beginId
);
394 private getRequireAuthorize(): boolean {
395 return this.configuration
?.requireAuthorize
?? true;
398 private getRandomIdTag(authorizationFile
: string): string {
399 const tags
= this.chargingStation
.authorizedTagsCache
.getAuthorizedTags(authorizationFile
);
400 this.idTagIndex
= Math.floor(Utils
.secureRandom() * tags
.length
);
401 return tags
[this.idTagIndex
];
404 private getRoundRobinIdTag(authorizationFile
: string): string {
405 const tags
= this.chargingStation
.authorizedTagsCache
.getAuthorizedTags(authorizationFile
);
406 const idTag
= tags
[this.idTagIndex
];
407 this.idTagIndex
= this.idTagIndex
=== tags
.length
- 1 ? 0 : this.idTagIndex
+ 1;
411 private getConnectorAffinityIdTag(authorizationFile
: string, connectorId
: number): string {
412 const tags
= this.chargingStation
.authorizedTagsCache
.getAuthorizedTags(authorizationFile
);
413 this.idTagIndex
= (this.chargingStation
.index
- 1 + (connectorId
- 1)) % tags
.length
;
414 return tags
[this.idTagIndex
];
417 private getIdTag(connectorId
: number): string {
418 const authorizationFile
= ChargingStationUtils
.getAuthorizationFile(
419 this.chargingStation
.stationInfo
421 switch (this.configuration
?.idTagDistribution
) {
422 case IdTagDistribution
.RANDOM
:
423 return this.getRandomIdTag(authorizationFile
);
424 case IdTagDistribution
.ROUND_ROBIN
:
425 return this.getRoundRobinIdTag(authorizationFile
);
426 case IdTagDistribution
.CONNECTOR_AFFINITY
:
427 return this.getConnectorAffinityIdTag(authorizationFile
, connectorId
);
429 return this.getRoundRobinIdTag(authorizationFile
);
433 private logPrefix
= (connectorId
?: number): string => {
434 return Utils
.logPrefix(
435 ` ${this.chargingStation.stationInfo.chargingStationId} | ATG${
436 connectorId !== undefined ? ` on connector #${connectorId.toString()}
` : ''
441 private handleStartTransactionResponse(
443 startResponse
: StartTransactionResponse
445 this.connectorsStatus
.get(connectorId
).startTransactionRequests
++;
446 if (startResponse
?.idTagInfo
?.status === AuthorizationStatus
.ACCEPTED
) {
447 this.connectorsStatus
.get(connectorId
).acceptedStartTransactionRequests
++;
449 logger
.warn(`${this.logPrefix(connectorId)} start transaction rejected`);
450 this.connectorsStatus
.get(connectorId
).rejectedStartTransactionRequests
++;
454 private checkChargingStation(connectorId
?: number): boolean {
455 if (this.chargingStation
.started
=== false && this.chargingStation
.starting
=== false) {
456 logger
.warn(`${this.logPrefix(connectorId)} charging station is stopped, cannot proceed`);