1 // Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
3 import { AsyncResource
} from
'node:async_hooks';
5 import { type ChargingStation
, ChargingStationUtils
, IdTagsCache
} from
'./internal';
6 import { BaseError
} from
'../exception';
7 // import { PerformanceStatistics } from '../performance';
8 import { PerformanceStatistics
} from
'../performance/PerformanceStatistics';
11 type AuthorizeRequest
,
12 type AuthorizeResponse
,
15 type StartTransactionRequest
,
16 type StartTransactionResponse
,
18 StopTransactionReason
,
19 type StopTransactionResponse
,
21 import { Constants
, Utils
, logger
} from
'../utils';
23 const moduleName
= 'AutomaticTransactionGenerator';
25 export class AutomaticTransactionGenerator
extends AsyncResource
{
26 private static readonly instances
: Map
<string, AutomaticTransactionGenerator
> = new Map
<
28 AutomaticTransactionGenerator
31 public readonly connectorsStatus
: Map
<number, Status
>;
32 public started
: boolean;
33 private readonly chargingStation
: ChargingStation
;
35 private constructor(chargingStation
: ChargingStation
) {
38 this.chargingStation
= chargingStation
;
39 this.connectorsStatus
= new Map
<number, Status
>();
40 this.initializeConnectorsStatus();
43 public static getInstance(
44 chargingStation
: ChargingStation
45 ): AutomaticTransactionGenerator
| undefined {
46 if (AutomaticTransactionGenerator
.instances
.has(chargingStation
.stationInfo
.hashId
) === false) {
47 AutomaticTransactionGenerator
.instances
.set(
48 chargingStation
.stationInfo
.hashId
,
49 new AutomaticTransactionGenerator(chargingStation
)
52 return AutomaticTransactionGenerator
.instances
.get(chargingStation
.stationInfo
.hashId
);
55 public start(): void {
57 ChargingStationUtils
.checkChargingStation(this.chargingStation
, this.logPrefix()) === false
61 if (this.started
=== true) {
62 logger
.warn(`${this.logPrefix()} is already started`);
65 this.startConnectors();
70 if (this.started
=== false) {
71 logger
.warn(`${this.logPrefix()} is already stopped`);
74 this.stopConnectors();
78 public startConnector(connectorId
: number): void {
80 ChargingStationUtils
.checkChargingStation(
82 this.logPrefix(connectorId
)
87 if (this.connectorsStatus
.has(connectorId
) === false) {
88 logger
.error(`${this.logPrefix(connectorId)} starting on non existing connector`);
89 throw new BaseError(`Connector ${connectorId} does not exist`);
91 if (this.connectorsStatus
.get(connectorId
)?.start
=== false) {
93 this.internalStartConnector
.bind(this) as (
94 this: AutomaticTransactionGenerator
,
99 ).catch(Constants
.EMPTY_FUNCTION
);
100 } else if (this.connectorsStatus
.get(connectorId
)?.start
=== true) {
101 logger
.warn(`${this.logPrefix(connectorId)} is already started on connector`);
105 public stopConnector(connectorId
: number): void {
106 if (this.connectorsStatus
.has(connectorId
) === false) {
107 logger
.error(`${this.logPrefix(connectorId)} stopping on non existing connector`);
108 throw new BaseError(`Connector ${connectorId} does not exist`);
110 if (this.connectorsStatus
.get(connectorId
)?.start
=== true) {
111 this.connectorsStatus
.get(connectorId
).start
= false;
112 } else if (this.connectorsStatus
.get(connectorId
)?.start
=== false) {
113 logger
.warn(`${this.logPrefix(connectorId)} is already stopped on connector`);
117 private startConnectors(): void {
119 this.connectorsStatus
?.size
> 0 &&
120 this.connectorsStatus
.size
!== this.chargingStation
.getNumberOfConnectors()
122 this.connectorsStatus
.clear();
123 this.initializeConnectorsStatus();
125 if (this.chargingStation
.hasEvses
) {
126 for (const [evseId
, evseStatus
] of this.chargingStation
.evses
) {
128 for (const connectorId
of evseStatus
.connectors
.keys()) {
129 this.startConnector(connectorId
);
134 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
135 if (connectorId
> 0) {
136 this.startConnector(connectorId
);
142 private stopConnectors(): void {
143 if (this.chargingStation
.hasEvses
) {
144 for (const [evseId
, evseStatus
] of this.chargingStation
.evses
) {
146 for (const connectorId
of evseStatus
.connectors
.keys()) {
147 this.stopConnector(connectorId
);
152 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
153 if (connectorId
> 0) {
154 this.stopConnector(connectorId
);
160 private async internalStartConnector(connectorId
: number): Promise
<void> {
161 this.setStartConnectorStatus(connectorId
);
165 )} started on connector and will run for ${Utils.formatDurationMilliSeconds(
166 this.connectorsStatus.get(connectorId).stopDate.getTime() -
167 this.connectorsStatus.get(connectorId).startDate.getTime()
170 while (this.connectorsStatus
.get(connectorId
)?.start
=== true) {
171 if (new Date() > this.connectorsStatus
.get(connectorId
).stopDate
) {
172 this.stopConnector(connectorId
);
175 if (this.chargingStation
.inAcceptedState() === false) {
179 )} entered in transaction loop while the charging station is not in accepted state`
181 this.stopConnector(connectorId
);
184 if (this.chargingStation
.isChargingStationAvailable() === false) {
188 )} entered in transaction loop while the charging station is unavailable`
190 this.stopConnector(connectorId
);
193 if (this.chargingStation
.isConnectorAvailable(connectorId
) === false) {
197 )} entered in transaction loop while the connector ${connectorId} is unavailable`
199 this.stopConnector(connectorId
);
203 this.chargingStation
.getConnectorStatus(connectorId
)?.status ===
204 ConnectorStatusEnum
.Unavailable
209 )} entered in transaction loop while the connector ${connectorId} status is unavailable`
211 this.stopConnector(connectorId
);
214 if (!this.chargingStation
?.ocppRequestService
) {
218 )} transaction loop waiting for charging station service to be initialized`
221 await Utils
.sleep(Constants
.CHARGING_STATION_ATG_INITIALIZATION_TIME
);
222 } while (!this.chargingStation
?.ocppRequestService
);
225 Utils
.getRandomInteger(
226 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()
227 .maxDelayBetweenTwoTransactions
,
228 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()
229 .minDelayBetweenTwoTransactions
232 `${this.logPrefix(connectorId)} waiting for ${Utils.formatDurationMilliSeconds(wait)}`
234 await Utils
.sleep(wait
);
235 const start
= Utils
.secureRandom();
238 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration().probabilityOfStart
240 this.connectorsStatus
.get(connectorId
).skippedConsecutiveTransactions
= 0;
242 const startResponse
= await this.startTransaction(connectorId
);
243 if (startResponse
?.idTagInfo
?.status === AuthorizationStatus
.ACCEPTED
) {
244 // Wait until end of transaction
246 Utils
.getRandomInteger(
247 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration().maxDuration
,
248 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration().minDuration
251 `${this.logPrefix(connectorId)} transaction started with id ${this.chargingStation
252 .getConnectorStatus(connectorId)
253 ?.transactionId?.toString()} and will stop in ${Utils.formatDurationMilliSeconds(
257 await Utils
.sleep(waitTrxEnd
);
260 `${this.logPrefix(connectorId)} stop transaction with id ${this.chargingStation
261 .getConnectorStatus(connectorId)
262 ?.transactionId?.toString()}`
264 await this.stopTransaction(connectorId
);
267 this.connectorsStatus
.get(connectorId
).skippedConsecutiveTransactions
++;
268 this.connectorsStatus
.get(connectorId
).skippedTransactions
++;
270 `${this.logPrefix(connectorId)} skipped consecutively ${this.connectorsStatus
272 ?.skippedConsecutiveTransactions?.toString()}/${this.connectorsStatus
274 ?.skippedTransactions?.toString()} transaction(s)`
277 this.connectorsStatus
.get(connectorId
).lastRunDate
= new Date();
279 this.connectorsStatus
.get(connectorId
).stoppedDate
= new Date();
283 )} stopped on connector and lasted for ${Utils.formatDurationMilliSeconds(
284 this.connectorsStatus.get(connectorId).stoppedDate.getTime() -
285 this.connectorsStatus.get(connectorId).startDate.getTime()
289 `${this.logPrefix(connectorId)} connector status: %j`,
290 this.connectorsStatus
.get(connectorId
)
294 private setStartConnectorStatus(connectorId
: number): void {
295 this.connectorsStatus
.get(connectorId
).skippedConsecutiveTransactions
= 0;
296 const previousRunDuration
=
297 this.connectorsStatus
.get(connectorId
)?.startDate
&&
298 this.connectorsStatus
.get(connectorId
)?.lastRunDate
299 ? this.connectorsStatus
.get(connectorId
).lastRunDate
.getTime() -
300 this.connectorsStatus
.get(connectorId
).startDate
.getTime()
302 this.connectorsStatus
.get(connectorId
).startDate
= new Date();
303 this.connectorsStatus
.get(connectorId
).stopDate
= new Date(
304 this.connectorsStatus
.get(connectorId
).startDate
.getTime() +
305 (this.chargingStation
.getAutomaticTransactionGeneratorConfiguration().stopAfterHours
??
306 Constants
.CHARGING_STATION_ATG_DEFAULT_STOP_AFTER_HOURS
) *
311 this.connectorsStatus
.get(connectorId
).start
= true;
314 private initializeConnectorsStatus(): void {
315 if (this.chargingStation
.hasEvses
) {
316 for (const [evseId
, evseStatus
] of this.chargingStation
.evses
) {
318 for (const connectorId
of evseStatus
.connectors
.keys()) {
319 this.connectorsStatus
.set(connectorId
, {
321 authorizeRequests
: 0,
322 acceptedAuthorizeRequests
: 0,
323 rejectedAuthorizeRequests
: 0,
324 startTransactionRequests
: 0,
325 acceptedStartTransactionRequests
: 0,
326 rejectedStartTransactionRequests
: 0,
327 stopTransactionRequests
: 0,
328 acceptedStopTransactionRequests
: 0,
329 rejectedStopTransactionRequests
: 0,
330 skippedConsecutiveTransactions
: 0,
331 skippedTransactions
: 0,
337 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
338 if (connectorId
> 0) {
339 this.connectorsStatus
.set(connectorId
, {
341 authorizeRequests
: 0,
342 acceptedAuthorizeRequests
: 0,
343 rejectedAuthorizeRequests
: 0,
344 startTransactionRequests
: 0,
345 acceptedStartTransactionRequests
: 0,
346 rejectedStartTransactionRequests
: 0,
347 stopTransactionRequests
: 0,
348 acceptedStopTransactionRequests
: 0,
349 rejectedStopTransactionRequests
: 0,
350 skippedConsecutiveTransactions
: 0,
351 skippedTransactions
: 0,
358 private async startTransaction(
360 ): Promise
<StartTransactionResponse
| undefined> {
361 const measureId
= 'StartTransaction with ATG';
362 const beginId
= PerformanceStatistics
.beginMeasure(measureId
);
363 let startResponse
: StartTransactionResponse
;
364 if (this.chargingStation
.hasIdTags()) {
365 const idTag
= IdTagsCache
.getInstance().getIdTag(
366 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()?.idTagDistribution
,
367 this.chargingStation
,
370 const startTransactionLogMsg
= `${this.logPrefix(
372 )} start transaction with an idTag '${idTag}'`;
373 if (this.getRequireAuthorize()) {
374 this.chargingStation
.getConnectorStatus(connectorId
).authorizeIdTag
= idTag
;
376 const authorizeResponse
: AuthorizeResponse
=
377 await this.chargingStation
.ocppRequestService
.requestHandler
<
380 >(this.chargingStation
, RequestCommand
.AUTHORIZE
, {
383 this.connectorsStatus
.get(connectorId
).authorizeRequests
++;
384 if (authorizeResponse
?.idTagInfo
?.status === AuthorizationStatus
.ACCEPTED
) {
385 this.connectorsStatus
.get(connectorId
).acceptedAuthorizeRequests
++;
386 logger
.info(startTransactionLogMsg
);
388 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
389 StartTransactionRequest
,
390 StartTransactionResponse
391 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, {
395 this.handleStartTransactionResponse(connectorId
, startResponse
);
396 PerformanceStatistics
.endMeasure(measureId
, beginId
);
397 return startResponse
;
399 this.connectorsStatus
.get(connectorId
).rejectedAuthorizeRequests
++;
400 PerformanceStatistics
.endMeasure(measureId
, beginId
);
401 return startResponse
;
403 logger
.info(startTransactionLogMsg
);
405 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
406 StartTransactionRequest
,
407 StartTransactionResponse
408 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, {
412 this.handleStartTransactionResponse(connectorId
, startResponse
);
413 PerformanceStatistics
.endMeasure(measureId
, beginId
);
414 return startResponse
;
416 logger
.info(`${this.logPrefix(connectorId)} start transaction without an idTag`);
417 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
418 StartTransactionRequest
,
419 StartTransactionResponse
420 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, { connectorId
});
421 this.handleStartTransactionResponse(connectorId
, startResponse
);
422 PerformanceStatistics
.endMeasure(measureId
, beginId
);
423 return startResponse
;
426 private async stopTransaction(
428 reason
: StopTransactionReason
= StopTransactionReason
.LOCAL
429 ): Promise
<StopTransactionResponse
> {
430 const measureId
= 'StopTransaction with ATG';
431 const beginId
= PerformanceStatistics
.beginMeasure(measureId
);
432 let stopResponse
: StopTransactionResponse
;
433 if (this.chargingStation
.getConnectorStatus(connectorId
)?.transactionStarted
=== true) {
434 stopResponse
= await this.chargingStation
.stopTransactionOnConnector(connectorId
, reason
);
435 this.connectorsStatus
.get(connectorId
).stopTransactionRequests
++;
436 if (stopResponse
?.idTagInfo
?.status === AuthorizationStatus
.ACCEPTED
) {
437 this.connectorsStatus
.get(connectorId
).acceptedStopTransactionRequests
++;
439 this.connectorsStatus
.get(connectorId
).rejectedStopTransactionRequests
++;
442 const transactionId
= this.chargingStation
.getConnectorStatus(connectorId
)?.transactionId
;
444 `${this.logPrefix(connectorId)} stopping a not started transaction${
445 !Utils.isNullOrUndefined(transactionId) ? ` with id ${transactionId?.toString()}
` : ''
449 PerformanceStatistics
.endMeasure(measureId
, beginId
);
453 private getRequireAuthorize(): boolean {
455 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()?.requireAuthorize
?? true
459 private logPrefix
= (connectorId
?: number): string => {
460 return Utils
.logPrefix(
461 ` ${this.chargingStation.stationInfo.chargingStationId} | ATG${
462 !Utils.isNullOrUndefined(connectorId) ? ` on connector #${connectorId.toString()}
` : ''
467 private handleStartTransactionResponse(
469 startResponse
: StartTransactionResponse
471 this.connectorsStatus
.get(connectorId
).startTransactionRequests
++;
472 if (startResponse
?.idTagInfo
?.status === AuthorizationStatus
.ACCEPTED
) {
473 this.connectorsStatus
.get(connectorId
).acceptedStartTransactionRequests
++;
475 logger
.warn(`${this.logPrefix(connectorId)} start transaction rejected`);
476 this.connectorsStatus
.get(connectorId
).rejectedStartTransactionRequests
++;