1 // Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
3 import { AsyncResource
} from
'node:async_hooks';
5 import type { ChargingStation
} from
'./ChargingStation';
6 import { ChargingStationUtils
} from
'./ChargingStationUtils';
7 import { IdTagsCache
} from
'./IdTagsCache';
8 import { BaseError
} from
'../exception';
9 import { PerformanceStatistics
} from
'../performance';
12 type AuthorizeRequest
,
13 type AuthorizeResponse
,
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 started
: boolean;
34 private starting
: boolean;
35 private stopping
: boolean;
36 private readonly chargingStation
: ChargingStation
;
38 private constructor(chargingStation
: ChargingStation
) {
41 this.starting
= false;
42 this.stopping
= false;
43 this.chargingStation
= chargingStation
;
44 this.connectorsStatus
= new Map
<number, Status
>();
45 this.initializeConnectorsStatus();
48 public static getInstance(
49 chargingStation
: ChargingStation
50 ): AutomaticTransactionGenerator
| undefined {
51 if (AutomaticTransactionGenerator
.instances
.has(chargingStation
.stationInfo
.hashId
) === false) {
52 AutomaticTransactionGenerator
.instances
.set(
53 chargingStation
.stationInfo
.hashId
,
54 new AutomaticTransactionGenerator(chargingStation
)
57 return AutomaticTransactionGenerator
.instances
.get(chargingStation
.stationInfo
.hashId
);
60 public start(): void {
62 ChargingStationUtils
.checkChargingStation(this.chargingStation
, this.logPrefix()) === false
66 if (this.started
=== true) {
67 logger
.warn(`${this.logPrefix()} is already started`);
70 if (this.starting
=== true) {
71 logger
.warn(`${this.logPrefix()} is already starting`);
75 this.startConnectors();
77 this.starting
= false;
81 if (this.started
=== false) {
82 logger
.warn(`${this.logPrefix()} is already stopped`);
85 if (this.stopping
=== true) {
86 logger
.warn(`${this.logPrefix()} is already stopping`);
90 this.stopConnectors();
92 this.stopping
= false;
95 public startConnector(connectorId
: number): void {
97 ChargingStationUtils
.checkChargingStation(
99 this.logPrefix(connectorId
)
104 if (this.connectorsStatus
.has(connectorId
) === false) {
105 logger
.error(`${this.logPrefix(connectorId)} starting on non existing connector`);
106 throw new BaseError(`Connector ${connectorId} does not exist`);
108 if (this.connectorsStatus
.get(connectorId
)?.start
=== false) {
109 this.runInAsyncScope(
110 this.internalStartConnector
.bind(this) as (
111 this: AutomaticTransactionGenerator
,
116 ).catch(Constants
.EMPTY_FUNCTION
);
117 } else if (this.connectorsStatus
.get(connectorId
)?.start
=== true) {
118 logger
.warn(`${this.logPrefix(connectorId)} is already started on connector`);
122 public stopConnector(connectorId
: number): void {
123 if (this.connectorsStatus
.has(connectorId
) === false) {
124 logger
.error(`${this.logPrefix(connectorId)} stopping on non existing connector`);
125 throw new BaseError(`Connector ${connectorId} does not exist`);
127 if (this.connectorsStatus
.get(connectorId
)?.start
=== true) {
128 this.connectorsStatus
.get(connectorId
).start
= false;
129 } else if (this.connectorsStatus
.get(connectorId
)?.start
=== false) {
130 logger
.warn(`${this.logPrefix(connectorId)} is already stopped on connector`);
134 private startConnectors(): void {
136 this.connectorsStatus
?.size
> 0 &&
137 this.connectorsStatus
.size
!== this.chargingStation
.getNumberOfConnectors()
139 this.connectorsStatus
.clear();
140 this.initializeConnectorsStatus();
142 if (this.chargingStation
.hasEvses
) {
143 for (const [evseId
, evseStatus
] of this.chargingStation
.evses
) {
145 for (const connectorId
of evseStatus
.connectors
.keys()) {
146 this.startConnector(connectorId
);
151 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
152 if (connectorId
> 0) {
153 this.startConnector(connectorId
);
159 private stopConnectors(): void {
160 if (this.chargingStation
.hasEvses
) {
161 for (const [evseId
, evseStatus
] of this.chargingStation
.evses
) {
163 for (const connectorId
of evseStatus
.connectors
.keys()) {
164 this.stopConnector(connectorId
);
169 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
170 if (connectorId
> 0) {
171 this.stopConnector(connectorId
);
177 private async internalStartConnector(connectorId
: number): Promise
<void> {
178 this.setStartConnectorStatus(connectorId
);
182 )} started on connector and will run for ${Utils.formatDurationMilliSeconds(
183 this.connectorsStatus.get(connectorId).stopDate.getTime() -
184 this.connectorsStatus.get(connectorId).startDate.getTime()
187 while (this.connectorsStatus
.get(connectorId
)?.start
=== true) {
188 if (new Date() > this.connectorsStatus
.get(connectorId
).stopDate
) {
189 this.stopConnector(connectorId
);
192 if (this.chargingStation
.inAcceptedState() === false) {
196 )} entered in transaction loop while the charging station is not in accepted state`
198 this.stopConnector(connectorId
);
201 if (this.chargingStation
.isChargingStationAvailable() === false) {
205 )} entered in transaction loop while the charging station is unavailable`
207 this.stopConnector(connectorId
);
210 if (this.chargingStation
.isConnectorAvailable(connectorId
) === false) {
214 )} entered in transaction loop while the connector ${connectorId} is unavailable`
216 this.stopConnector(connectorId
);
220 this.chargingStation
.getConnectorStatus(connectorId
)?.status ===
221 ConnectorStatusEnum
.Unavailable
226 )} entered in transaction loop while the connector ${connectorId} status is unavailable`
228 this.stopConnector(connectorId
);
231 if (!this.chargingStation
?.ocppRequestService
) {
235 )} transaction loop waiting for charging station service to be initialized`
238 await Utils
.sleep(Constants
.CHARGING_STATION_ATG_INITIALIZATION_TIME
);
239 } while (!this.chargingStation
?.ocppRequestService
);
242 Utils
.getRandomInteger(
243 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()
244 .maxDelayBetweenTwoTransactions
,
245 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()
246 .minDelayBetweenTwoTransactions
249 `${this.logPrefix(connectorId)} waiting for ${Utils.formatDurationMilliSeconds(wait)}`
251 await Utils
.sleep(wait
);
252 const start
= Utils
.secureRandom();
255 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration().probabilityOfStart
257 this.connectorsStatus
.get(connectorId
).skippedConsecutiveTransactions
= 0;
259 const startResponse
= await this.startTransaction(connectorId
);
260 if (startResponse
?.idTagInfo
?.status === AuthorizationStatus
.ACCEPTED
) {
261 // Wait until end of transaction
263 Utils
.getRandomInteger(
264 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration().maxDuration
,
265 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration().minDuration
268 `${this.logPrefix(connectorId)} transaction started with id ${this.chargingStation
269 .getConnectorStatus(connectorId)
270 ?.transactionId?.toString()} and will stop in ${Utils.formatDurationMilliSeconds(
274 await Utils
.sleep(waitTrxEnd
);
277 `${this.logPrefix(connectorId)} stop transaction with id ${this.chargingStation
278 .getConnectorStatus(connectorId)
279 ?.transactionId?.toString()}`
281 await this.stopTransaction(connectorId
);
284 ++this.connectorsStatus
.get(connectorId
).skippedConsecutiveTransactions
;
285 ++this.connectorsStatus
.get(connectorId
).skippedTransactions
;
287 `${this.logPrefix(connectorId)} skipped consecutively ${this.connectorsStatus
289 ?.skippedConsecutiveTransactions?.toString()}/${this.connectorsStatus
291 ?.skippedTransactions?.toString()} transaction(s)`
294 this.connectorsStatus
.get(connectorId
).lastRunDate
= new Date();
296 this.connectorsStatus
.get(connectorId
).stoppedDate
= new Date();
300 )} stopped on connector and lasted for ${Utils.formatDurationMilliSeconds(
301 this.connectorsStatus.get(connectorId).stoppedDate.getTime() -
302 this.connectorsStatus.get(connectorId).startDate.getTime()
306 `${this.logPrefix(connectorId)} connector status: %j`,
307 this.connectorsStatus
.get(connectorId
)
311 private setStartConnectorStatus(connectorId
: number): void {
312 this.connectorsStatus
.get(connectorId
).skippedConsecutiveTransactions
= 0;
313 const previousRunDuration
=
314 this.connectorsStatus
.get(connectorId
)?.startDate
&&
315 this.connectorsStatus
.get(connectorId
)?.lastRunDate
316 ? this.connectorsStatus
.get(connectorId
).lastRunDate
.getTime() -
317 this.connectorsStatus
.get(connectorId
).startDate
.getTime()
319 this.connectorsStatus
.get(connectorId
).startDate
= new Date();
320 this.connectorsStatus
.get(connectorId
).stopDate
= new Date(
321 this.connectorsStatus
.get(connectorId
).startDate
.getTime() +
322 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration().stopAfterHours
*
327 this.connectorsStatus
.get(connectorId
).start
= true;
330 private initializeConnectorsStatus(): void {
331 if (this.chargingStation
.hasEvses
) {
332 for (const [evseId
, evseStatus
] of this.chargingStation
.evses
) {
334 for (const connectorId
of evseStatus
.connectors
.keys()) {
335 this.connectorsStatus
.set(connectorId
, this.getConnectorStatus(connectorId
));
340 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
341 if (connectorId
> 0) {
342 this.connectorsStatus
.set(connectorId
, this.getConnectorStatus(connectorId
));
348 private getConnectorStatus(connectorId
: number): Status
{
349 const connectorStatus
= Utils
.cloneObject(
350 this.chargingStation
.getAutomaticTransactionGeneratorStatuses()
352 delete connectorStatus
?.startDate
;
353 delete connectorStatus
?.lastRunDate
;
354 delete connectorStatus
?.stopDate
;
355 delete connectorStatus
?.stoppedDate
;
359 authorizeRequests
: 0,
360 acceptedAuthorizeRequests
: 0,
361 rejectedAuthorizeRequests
: 0,
362 startTransactionRequests
: 0,
363 acceptedStartTransactionRequests
: 0,
364 rejectedStartTransactionRequests
: 0,
365 stopTransactionRequests
: 0,
366 acceptedStopTransactionRequests
: 0,
367 rejectedStopTransactionRequests
: 0,
368 skippedConsecutiveTransactions
: 0,
369 skippedTransactions
: 0,
374 private async startTransaction(
376 ): Promise
<StartTransactionResponse
| undefined> {
377 const measureId
= 'StartTransaction with ATG';
378 const beginId
= PerformanceStatistics
.beginMeasure(measureId
);
379 let startResponse
: StartTransactionResponse
;
380 if (this.chargingStation
.hasIdTags()) {
381 const idTag
= IdTagsCache
.getInstance().getIdTag(
382 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()?.idTagDistribution
,
383 this.chargingStation
,
386 const startTransactionLogMsg
= `${this.logPrefix(
388 )} start transaction with an idTag '${idTag}'`;
389 if (this.getRequireAuthorize()) {
390 this.chargingStation
.getConnectorStatus(connectorId
).authorizeIdTag
= idTag
;
392 const authorizeResponse
: AuthorizeResponse
=
393 await this.chargingStation
.ocppRequestService
.requestHandler
<
396 >(this.chargingStation
, RequestCommand
.AUTHORIZE
, {
399 ++this.connectorsStatus
.get(connectorId
).authorizeRequests
;
400 if (authorizeResponse
?.idTagInfo
?.status === AuthorizationStatus
.ACCEPTED
) {
401 ++this.connectorsStatus
.get(connectorId
).acceptedAuthorizeRequests
;
402 logger
.info(startTransactionLogMsg
);
404 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
405 StartTransactionRequest
,
406 StartTransactionResponse
407 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, {
411 this.handleStartTransactionResponse(connectorId
, startResponse
);
412 PerformanceStatistics
.endMeasure(measureId
, beginId
);
413 return startResponse
;
415 ++this.connectorsStatus
.get(connectorId
).rejectedAuthorizeRequests
;
416 PerformanceStatistics
.endMeasure(measureId
, beginId
);
417 return startResponse
;
419 logger
.info(startTransactionLogMsg
);
421 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
422 StartTransactionRequest
,
423 StartTransactionResponse
424 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, {
428 this.handleStartTransactionResponse(connectorId
, startResponse
);
429 PerformanceStatistics
.endMeasure(measureId
, beginId
);
430 return startResponse
;
432 logger
.info(`${this.logPrefix(connectorId)} start transaction without an idTag`);
433 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
434 StartTransactionRequest
,
435 StartTransactionResponse
436 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, { connectorId
});
437 this.handleStartTransactionResponse(connectorId
, startResponse
);
438 PerformanceStatistics
.endMeasure(measureId
, beginId
);
439 return startResponse
;
442 private async stopTransaction(
444 reason
: StopTransactionReason
= StopTransactionReason
.LOCAL
445 ): Promise
<StopTransactionResponse
> {
446 const measureId
= 'StopTransaction with ATG';
447 const beginId
= PerformanceStatistics
.beginMeasure(measureId
);
448 let stopResponse
: StopTransactionResponse
;
449 if (this.chargingStation
.getConnectorStatus(connectorId
)?.transactionStarted
=== true) {
450 stopResponse
= await this.chargingStation
.stopTransactionOnConnector(connectorId
, reason
);
451 ++this.connectorsStatus
.get(connectorId
).stopTransactionRequests
;
452 if (stopResponse
?.idTagInfo
?.status === AuthorizationStatus
.ACCEPTED
) {
453 ++this.connectorsStatus
.get(connectorId
).acceptedStopTransactionRequests
;
455 ++this.connectorsStatus
.get(connectorId
).rejectedStopTransactionRequests
;
458 const transactionId
= this.chargingStation
.getConnectorStatus(connectorId
)?.transactionId
;
460 `${this.logPrefix(connectorId)} stopping a not started transaction${
461 !Utils.isNullOrUndefined(transactionId) ? ` with id ${transactionId?.toString()}
` : ''
465 PerformanceStatistics
.endMeasure(measureId
, beginId
);
469 private getRequireAuthorize(): boolean {
471 this.chargingStation
.getAutomaticTransactionGeneratorConfiguration()?.requireAuthorize
?? true
475 private logPrefix
= (connectorId
?: number): string => {
476 return Utils
.logPrefix(
477 ` ${this.chargingStation.stationInfo.chargingStationId} | ATG${
478 !Utils.isNullOrUndefined(connectorId) ? ` on connector #${connectorId.toString()}
` : ''
483 private handleStartTransactionResponse(
485 startResponse
: StartTransactionResponse
487 ++this.connectorsStatus
.get(connectorId
).startTransactionRequests
;
488 if (startResponse
?.idTagInfo
?.status === AuthorizationStatus
.ACCEPTED
) {
489 ++this.connectorsStatus
.get(connectorId
).acceptedStartTransactionRequests
;
491 logger
.warn(`${this.logPrefix(connectorId)} start transaction rejected`);
492 ++this.connectorsStatus
.get(connectorId
).rejectedStartTransactionRequests
;