1 // Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
3 import { AsyncResource
} from
'async_hooks';
5 import type ChargingStation from
'./ChargingStation';
6 import { ChargingStationUtils
} from
'./ChargingStationUtils';
7 import BaseError from
'../exception/BaseError';
8 import PerformanceStatistics from
'../performance/PerformanceStatistics';
10 type AutomaticTransactionGeneratorConfiguration
,
13 } from
'../types/AutomaticTransactionGenerator';
14 import { RequestCommand
} from
'../types/ocpp/Requests';
17 type AuthorizeRequest
,
18 type AuthorizeResponse
,
19 type StartTransactionRequest
,
20 type StartTransactionResponse
,
21 StopTransactionReason
,
22 type StopTransactionResponse
,
23 } from
'../types/ocpp/Transaction';
24 import Constants from
'../utils/Constants';
25 import logger from
'../utils/Logger';
26 import Utils from
'../utils/Utils';
28 const moduleName
= 'AutomaticTransactionGenerator';
30 export default class AutomaticTransactionGenerator
extends AsyncResource
{
31 private static readonly instances
: Map
<string, AutomaticTransactionGenerator
> = new Map
<
33 AutomaticTransactionGenerator
36 public readonly connectorsStatus
: Map
<number, Status
>;
37 public readonly configuration
: AutomaticTransactionGeneratorConfiguration
;
38 public started
: boolean;
39 private readonly chargingStation
: ChargingStation
;
40 private idTagIndex
: number;
43 automaticTransactionGeneratorConfiguration
: AutomaticTransactionGeneratorConfiguration
,
44 chargingStation
: ChargingStation
48 this.configuration
= automaticTransactionGeneratorConfiguration
;
49 this.chargingStation
= chargingStation
;
51 this.connectorsStatus
= new Map
<number, Status
>();
52 this.initializeConnectorsStatus();
55 public static getInstance(
56 automaticTransactionGeneratorConfiguration
: AutomaticTransactionGeneratorConfiguration
,
57 chargingStation
: ChargingStation
58 ): AutomaticTransactionGenerator
{
59 if (AutomaticTransactionGenerator
.instances
.has(chargingStation
.stationInfo
.hashId
) === false) {
60 AutomaticTransactionGenerator
.instances
.set(
61 chargingStation
.stationInfo
.hashId
,
62 new AutomaticTransactionGenerator(
63 automaticTransactionGeneratorConfiguration
,
68 return AutomaticTransactionGenerator
.instances
.get(chargingStation
.stationInfo
.hashId
);
71 public start(): void {
72 if (this.checkChargingStation() === false) {
75 if (this.started
=== true) {
76 logger
.warn(`${this.logPrefix()} is already started`);
79 this.startConnectors();
84 if (this.started
=== false) {
85 logger
.warn(`${this.logPrefix()} is already stopped`);
88 this.stopConnectors();
92 public startConnector(connectorId
: number): void {
93 if (this.checkChargingStation(connectorId
) === false) {
96 if (this.connectorsStatus
.has(connectorId
) === false) {
97 logger
.error(`${this.logPrefix(connectorId)} starting on non existing connector`);
98 throw new BaseError(`Connector ${connectorId} does not exist`);
100 if (this.connectorsStatus
.get(connectorId
)?.start
=== false) {
101 this.runInAsyncScope(
102 this.internalStartConnector
.bind(this) as (
103 this: AutomaticTransactionGenerator
,
109 /* This is intentional */
111 } else if (this.connectorsStatus
.get(connectorId
)?.start
=== true) {
112 logger
.warn(`${this.logPrefix(connectorId)} is already started on connector`);
116 public stopConnector(connectorId
: number): void {
117 if (this.connectorsStatus
.has(connectorId
) === false) {
118 logger
.error(`${this.logPrefix(connectorId)} stopping on non existing connector`);
119 throw new BaseError(`Connector ${connectorId} does not exist`);
121 if (this.connectorsStatus
.get(connectorId
)?.start
=== true) {
122 this.connectorsStatus
.get(connectorId
).start
= false;
123 } else if (this.connectorsStatus
.get(connectorId
)?.start
=== false) {
124 logger
.warn(`${this.logPrefix(connectorId)} is already stopped on connector`);
128 private startConnectors(): void {
130 this.connectorsStatus
?.size
> 0 &&
131 this.connectorsStatus
.size
!== this.chargingStation
.getNumberOfConnectors()
133 this.connectorsStatus
.clear();
134 this.initializeConnectorsStatus();
136 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
137 if (connectorId
> 0) {
138 this.startConnector(connectorId
);
143 private stopConnectors(): void {
144 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
145 if (connectorId
> 0) {
146 this.stopConnector(connectorId
);
151 private async internalStartConnector(connectorId
: number): Promise
<void> {
152 this.setStartConnectorStatus(connectorId
);
156 )} started on connector and will run for ${Utils.formatDurationMilliSeconds(
157 this.connectorsStatus.get(connectorId).stopDate.getTime() -
158 this.connectorsStatus.get(connectorId).startDate.getTime()
161 while (this.connectorsStatus
.get(connectorId
).start
=== true) {
162 if (new Date() > this.connectorsStatus
.get(connectorId
).stopDate
) {
163 this.stopConnector(connectorId
);
166 if (this.chargingStation
.isInAcceptedState() === false) {
170 )} entered in transaction loop while the charging station is not in accepted state`
172 this.stopConnector(connectorId
);
175 if (this.chargingStation
.isChargingStationAvailable() === false) {
179 )} entered in transaction loop while the charging station is unavailable`
181 this.stopConnector(connectorId
);
184 if (this.chargingStation
.isConnectorAvailable(connectorId
) === false) {
188 )} entered in transaction loop while the connector ${connectorId} is unavailable`
190 this.stopConnector(connectorId
);
193 if (!this.chargingStation
?.ocppRequestService
) {
197 )} transaction loop waiting for charging station service to be initialized`
200 await Utils
.sleep(Constants
.CHARGING_STATION_ATG_INITIALIZATION_TIME
);
201 } while (!this.chargingStation
?.ocppRequestService
);
204 Utils
.getRandomInteger(
205 this.configuration
.maxDelayBetweenTwoTransactions
,
206 this.configuration
.minDelayBetweenTwoTransactions
209 `${this.logPrefix(connectorId)} waiting for ${Utils.formatDurationMilliSeconds(wait)}`
211 await Utils
.sleep(wait
);
212 const start
= Utils
.secureRandom();
213 if (start
< this.configuration
.probabilityOfStart
) {
214 this.connectorsStatus
.get(connectorId
).skippedConsecutiveTransactions
= 0;
216 const startResponse
= await this.startTransaction(connectorId
);
217 if (startResponse
?.idTagInfo
?.status === AuthorizationStatus
.ACCEPTED
) {
218 // Wait until end of transaction
220 Utils
.getRandomInteger(this.configuration
.maxDuration
, this.configuration
.minDuration
) *
223 `${this.logPrefix(connectorId)} transaction ${this.chargingStation
224 .getConnectorStatus(connectorId)
225 .transactionId.toString()} started and will stop in ${Utils.formatDurationMilliSeconds(
229 await Utils
.sleep(waitTrxEnd
);
232 `${this.logPrefix(connectorId)} stop transaction ${this.chargingStation
233 .getConnectorStatus(connectorId)
234 .transactionId.toString()}`
236 await this.stopTransaction(connectorId
);
239 this.connectorsStatus
.get(connectorId
).skippedConsecutiveTransactions
++;
240 this.connectorsStatus
.get(connectorId
).skippedTransactions
++;
242 `${this.logPrefix(connectorId)} skipped consecutively ${this.connectorsStatus
244 .skippedConsecutiveTransactions.toString()}/${this.connectorsStatus
246 .skippedTransactions.toString()} transaction(s)`
249 this.connectorsStatus
.get(connectorId
).lastRunDate
= new Date();
251 this.connectorsStatus
.get(connectorId
).stoppedDate
= new Date();
255 )} stopped on connector and lasted for ${Utils.formatDurationMilliSeconds(
256 this.connectorsStatus.get(connectorId).stoppedDate.getTime() -
257 this.connectorsStatus.get(connectorId).startDate.getTime()
261 `${this.logPrefix(connectorId)} connector status: %j`,
262 this.connectorsStatus
.get(connectorId
)
266 private setStartConnectorStatus(connectorId
: number): void {
267 this.connectorsStatus
.get(connectorId
).skippedConsecutiveTransactions
= 0;
268 const previousRunDuration
=
269 this?.connectorsStatus
.get(connectorId
)?.startDate
&&
270 this?.connectorsStatus
.get(connectorId
)?.lastRunDate
271 ? this.connectorsStatus
.get(connectorId
).lastRunDate
.getTime() -
272 this.connectorsStatus
.get(connectorId
).startDate
.getTime()
274 this.connectorsStatus
.get(connectorId
).startDate
= new Date();
275 this.connectorsStatus
.get(connectorId
).stopDate
= new Date(
276 this.connectorsStatus
.get(connectorId
).startDate
.getTime() +
277 (this.configuration
.stopAfterHours
??
278 Constants
.CHARGING_STATION_ATG_DEFAULT_STOP_AFTER_HOURS
) *
283 this.connectorsStatus
.get(connectorId
).start
= true;
286 private initializeConnectorsStatus(): void {
287 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
288 if (connectorId
> 0) {
289 this.connectorsStatus
.set(connectorId
, {
291 authorizeRequests
: 0,
292 acceptedAuthorizeRequests
: 0,
293 rejectedAuthorizeRequests
: 0,
294 startTransactionRequests
: 0,
295 acceptedStartTransactionRequests
: 0,
296 rejectedStartTransactionRequests
: 0,
297 stopTransactionRequests
: 0,
298 acceptedStopTransactionRequests
: 0,
299 rejectedStopTransactionRequests
: 0,
300 skippedConsecutiveTransactions
: 0,
301 skippedTransactions
: 0,
307 private async startTransaction(
309 ): Promise
<StartTransactionResponse
| undefined> {
310 const measureId
= 'StartTransaction with ATG';
311 const beginId
= PerformanceStatistics
.beginMeasure(measureId
);
312 let startResponse
: StartTransactionResponse
;
313 if (this.chargingStation
.hasAuthorizedTags()) {
314 const idTag
= this.getIdTag(connectorId
);
315 const startTransactionLogMsg
= `${this.logPrefix(
317 )} start transaction with an idTag '${idTag}'`;
318 if (this.getRequireAuthorize()) {
319 this.chargingStation
.getConnectorStatus(connectorId
).authorizeIdTag
= idTag
;
321 const authorizeResponse
: AuthorizeResponse
=
322 await this.chargingStation
.ocppRequestService
.requestHandler
<
325 >(this.chargingStation
, RequestCommand
.AUTHORIZE
, {
328 this.connectorsStatus
.get(connectorId
).authorizeRequests
++;
329 if (authorizeResponse
?.idTagInfo
?.status === AuthorizationStatus
.ACCEPTED
) {
330 this.connectorsStatus
.get(connectorId
).acceptedAuthorizeRequests
++;
331 logger
.info(startTransactionLogMsg
);
333 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
334 StartTransactionRequest
,
335 StartTransactionResponse
336 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, {
340 this.handleStartTransactionResponse(connectorId
, startResponse
);
341 PerformanceStatistics
.endMeasure(measureId
, beginId
);
342 return startResponse
;
344 this.connectorsStatus
.get(connectorId
).rejectedAuthorizeRequests
++;
345 PerformanceStatistics
.endMeasure(measureId
, beginId
);
346 return startResponse
;
348 logger
.info(startTransactionLogMsg
);
350 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
351 StartTransactionRequest
,
352 StartTransactionResponse
353 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, {
357 this.handleStartTransactionResponse(connectorId
, startResponse
);
358 PerformanceStatistics
.endMeasure(measureId
, beginId
);
359 return startResponse
;
361 logger
.info(`${this.logPrefix(connectorId)} start transaction without an idTag`);
362 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
363 StartTransactionRequest
,
364 StartTransactionResponse
365 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, { connectorId
});
366 this.handleStartTransactionResponse(connectorId
, startResponse
);
367 PerformanceStatistics
.endMeasure(measureId
, beginId
);
368 return startResponse
;
371 private async stopTransaction(
373 reason
: StopTransactionReason
= StopTransactionReason
.LOCAL
374 ): Promise
<StopTransactionResponse
> {
375 const measureId
= 'StopTransaction with ATG';
376 const beginId
= PerformanceStatistics
.beginMeasure(measureId
);
377 let stopResponse
: StopTransactionResponse
;
378 if (this.chargingStation
.getConnectorStatus(connectorId
)?.transactionStarted
=== true) {
379 stopResponse
= await this.chargingStation
.stopTransactionOnConnector(connectorId
, reason
);
380 this.connectorsStatus
.get(connectorId
).stopTransactionRequests
++;
381 if (stopResponse
?.idTagInfo
?.status === AuthorizationStatus
.ACCEPTED
) {
382 this.connectorsStatus
.get(connectorId
).acceptedStopTransactionRequests
++;
384 this.connectorsStatus
.get(connectorId
).rejectedStopTransactionRequests
++;
387 const transactionId
= this.chargingStation
.getConnectorStatus(connectorId
).transactionId
;
389 `${this.logPrefix(connectorId)} stopping a not started transaction${
390 transactionId ? ` ${transactionId.toString()}
` : ''
394 PerformanceStatistics
.endMeasure(measureId
, beginId
);
398 private getRequireAuthorize(): boolean {
399 return this.configuration
?.requireAuthorize
?? true;
402 private getRandomIdTag(authorizationFile
: string): string {
403 const tags
= this.chargingStation
.authorizedTagsCache
.getAuthorizedTags(authorizationFile
);
404 this.idTagIndex
= Math.floor(Utils
.secureRandom() * tags
.length
);
405 return tags
[this.idTagIndex
];
408 private getRoundRobinIdTag(authorizationFile
: string): string {
409 const tags
= this.chargingStation
.authorizedTagsCache
.getAuthorizedTags(authorizationFile
);
410 const idTag
= tags
[this.idTagIndex
];
411 this.idTagIndex
= this.idTagIndex
=== tags
.length
- 1 ? 0 : this.idTagIndex
+ 1;
415 private getConnectorAffinityIdTag(authorizationFile
: string, connectorId
: number): string {
416 const tags
= this.chargingStation
.authorizedTagsCache
.getAuthorizedTags(authorizationFile
);
417 this.idTagIndex
= (this.chargingStation
.index
- 1 + (connectorId
- 1)) % tags
.length
;
418 return tags
[this.idTagIndex
];
421 private getIdTag(connectorId
: number): string {
422 const authorizationFile
= ChargingStationUtils
.getAuthorizationFile(
423 this.chargingStation
.stationInfo
425 switch (this.configuration
?.idTagDistribution
) {
426 case IdTagDistribution
.RANDOM
:
427 return this.getRandomIdTag(authorizationFile
);
428 case IdTagDistribution
.ROUND_ROBIN
:
429 return this.getRoundRobinIdTag(authorizationFile
);
430 case IdTagDistribution
.CONNECTOR_AFFINITY
:
431 return this.getConnectorAffinityIdTag(authorizationFile
, connectorId
);
433 return this.getRoundRobinIdTag(authorizationFile
);
437 private logPrefix(connectorId
?: number): string {
438 return Utils
.logPrefix(
439 ` ${this.chargingStation.stationInfo.chargingStationId} | ATG${
440 connectorId !== undefined ? ` on connector #${connectorId.toString()}
` : ''
445 private handleStartTransactionResponse(
447 startResponse
: StartTransactionResponse
449 this.connectorsStatus
.get(connectorId
).startTransactionRequests
++;
450 if (startResponse
?.idTagInfo
?.status === AuthorizationStatus
.ACCEPTED
) {
451 this.connectorsStatus
.get(connectorId
).acceptedStartTransactionRequests
++;
453 logger
.warn(`${this.logPrefix(connectorId)} start transaction rejected`);
454 this.connectorsStatus
.get(connectorId
).rejectedStartTransactionRequests
++;
458 private checkChargingStation(connectorId
?: number): boolean {
459 if (this.chargingStation
.started
=== false && this.chargingStation
.starting
=== false) {
460 logger
.warn(`${this.logPrefix(connectorId)} charging station is stopped, cannot proceed`);