1 // Partial Copyright Jerome Benoit. 2021. All Rights Reserved.
3 import BaseError from
'../exception/BaseError';
4 import PerformanceStatistics from
'../performance/PerformanceStatistics';
6 type AutomaticTransactionGeneratorConfiguration
,
9 } from
'../types/AutomaticTransactionGenerator';
10 import { RequestCommand
} from
'../types/ocpp/Requests';
13 type AuthorizeRequest
,
14 type AuthorizeResponse
,
15 type StartTransactionRequest
,
16 type StartTransactionResponse
,
17 StopTransactionReason
,
18 type StopTransactionResponse
,
19 } from
'../types/ocpp/Transaction';
20 import Constants from
'../utils/Constants';
21 import logger from
'../utils/Logger';
22 import Utils from
'../utils/Utils';
23 import type ChargingStation from
'./ChargingStation';
24 import { ChargingStationUtils
} from
'./ChargingStationUtils';
26 export default class AutomaticTransactionGenerator
{
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
43 this.configuration
= automaticTransactionGeneratorConfiguration
;
44 this.chargingStation
= chargingStation
;
46 this.connectorsStatus
= new Map
<number, Status
>();
47 this.initializeConnectorsStatus();
50 public static getInstance(
51 automaticTransactionGeneratorConfiguration
: AutomaticTransactionGeneratorConfiguration
,
52 chargingStation
: ChargingStation
53 ): AutomaticTransactionGenerator
{
54 if (AutomaticTransactionGenerator
.instances
.has(chargingStation
.stationInfo
.hashId
) === false) {
55 AutomaticTransactionGenerator
.instances
.set(
56 chargingStation
.stationInfo
.hashId
,
57 new AutomaticTransactionGenerator(
58 automaticTransactionGeneratorConfiguration
,
63 return AutomaticTransactionGenerator
.instances
.get(chargingStation
.stationInfo
.hashId
);
66 public start(): void {
67 if (this.started
=== true) {
68 logger
.warn(`${this.logPrefix()} is already started`);
71 this.startConnectors();
76 if (this.started
=== false) {
77 logger
.warn(`${this.logPrefix()} is already stopped`);
80 this.stopConnectors();
84 public startConnector(connectorId
: number): void {
85 if (this.connectorsStatus
.has(connectorId
) === false) {
86 logger
.error(`${this.logPrefix(connectorId)} starting on non existing connector`);
87 throw new BaseError(`Connector ${connectorId} does not exist`);
89 if (this.connectorsStatus
.get(connectorId
)?.start
=== false) {
90 // Avoid hogging the event loop with a busy loop
92 this.internalStartConnector(connectorId
).catch(() => {
93 /* This is intentional */
96 } else if (this.connectorsStatus
.get(connectorId
)?.start
=== true) {
97 logger
.warn(`${this.logPrefix(connectorId)} is already started on connector`);
101 public stopConnector(connectorId
: number): void {
102 if (this.connectorsStatus
.has(connectorId
) === false) {
103 logger
.error(`${this.logPrefix(connectorId)} stopping on non existing connector`);
104 throw new BaseError(`Connector ${connectorId} does not exist`);
106 if (this.connectorsStatus
.get(connectorId
)?.start
=== true) {
107 this.connectorsStatus
.get(connectorId
).start
= false;
108 } else if (this.connectorsStatus
.get(connectorId
)?.start
=== false) {
109 logger
.warn(`${this.logPrefix(connectorId)} is already stopped on connector`);
113 private startConnectors(): void {
115 this.connectorsStatus
?.size
> 0 &&
116 this.connectorsStatus
.size
!== this.chargingStation
.getNumberOfConnectors()
118 this.connectorsStatus
.clear();
119 this.initializeConnectorsStatus();
121 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
122 if (connectorId
> 0) {
123 this.startConnector(connectorId
);
128 private stopConnectors(): void {
129 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
130 if (connectorId
> 0) {
131 this.stopConnector(connectorId
);
136 private async internalStartConnector(connectorId
: number): Promise
<void> {
137 this.initializeConnectorStatus(connectorId
);
138 this.connectorsStatus
.get(connectorId
).start
= true;
140 this.logPrefix(connectorId
) +
141 ' started on connector and will run for ' +
142 Utils
.formatDurationMilliSeconds(
143 this.connectorsStatus
.get(connectorId
).stopDate
.getTime() -
144 this.connectorsStatus
.get(connectorId
).startDate
.getTime()
147 while (this.connectorsStatus
.get(connectorId
).start
=== true) {
148 if (new Date() > this.connectorsStatus
.get(connectorId
).stopDate
) {
149 this.stopConnector(connectorId
);
152 if (!this.chargingStation
.isInAcceptedState()) {
154 this.logPrefix(connectorId
) +
155 ' entered in transaction loop while the charging station is not in accepted state'
157 this.stopConnector(connectorId
);
160 if (!this.chargingStation
.isChargingStationAvailable()) {
162 this.logPrefix(connectorId
) +
163 ' entered in transaction loop while the charging station is unavailable'
165 this.stopConnector(connectorId
);
168 if (!this.chargingStation
.isConnectorAvailable(connectorId
)) {
172 )} entered in transaction loop while the connector ${connectorId} is unavailable`
174 this.stopConnector(connectorId
);
177 if (!this.chargingStation
?.ocppRequestService
) {
181 )} transaction loop waiting for charging station service to be initialized`
184 await Utils
.sleep(Constants
.CHARGING_STATION_ATG_INITIALIZATION_TIME
);
185 } while (!this.chargingStation
?.ocppRequestService
);
188 Utils
.getRandomInteger(
189 this.configuration
.maxDelayBetweenTwoTransactions
,
190 this.configuration
.minDelayBetweenTwoTransactions
193 this.logPrefix(connectorId
) + ' waiting for ' + Utils
.formatDurationMilliSeconds(wait
)
195 await Utils
.sleep(wait
);
196 const start
= Utils
.secureRandom();
197 if (start
< this.configuration
.probabilityOfStart
) {
198 this.connectorsStatus
.get(connectorId
).skippedConsecutiveTransactions
= 0;
200 const startResponse
= await this.startTransaction(connectorId
);
201 if (startResponse
?.idTagInfo
?.status === AuthorizationStatus
.ACCEPTED
) {
202 // Wait until end of transaction
204 Utils
.getRandomInteger(this.configuration
.maxDuration
, this.configuration
.minDuration
) *
207 this.logPrefix(connectorId
) +
209 this.chargingStation
.getConnectorStatus(connectorId
).transactionId
.toString() +
210 ' started and will stop in ' +
211 Utils
.formatDurationMilliSeconds(waitTrxEnd
)
213 await Utils
.sleep(waitTrxEnd
);
216 this.logPrefix(connectorId
) +
217 ' stop transaction ' +
218 this.chargingStation
.getConnectorStatus(connectorId
).transactionId
.toString()
220 await this.stopTransaction(connectorId
);
223 this.connectorsStatus
.get(connectorId
).skippedConsecutiveTransactions
++;
224 this.connectorsStatus
.get(connectorId
).skippedTransactions
++;
226 this.logPrefix(connectorId
) +
227 ' skipped consecutively ' +
228 this.connectorsStatus
.get(connectorId
).skippedConsecutiveTransactions
.toString() +
230 this.connectorsStatus
.get(connectorId
).skippedTransactions
.toString() +
234 this.connectorsStatus
.get(connectorId
).lastRunDate
= new Date();
236 this.connectorsStatus
.get(connectorId
).stoppedDate
= new Date();
238 this.logPrefix(connectorId
) +
239 ' stopped on connector and lasted for ' +
240 Utils
.formatDurationMilliSeconds(
241 this.connectorsStatus
.get(connectorId
).stoppedDate
.getTime() -
242 this.connectorsStatus
.get(connectorId
).startDate
.getTime()
246 `${this.logPrefix(connectorId)} connector status: %j`,
247 this.connectorsStatus
.get(connectorId
)
251 private initializeConnectorStatus(connectorId
: number): void {
252 this.connectorsStatus
.get(connectorId
).authorizeRequests
=
253 this?.connectorsStatus
.get(connectorId
)?.authorizeRequests
?? 0;
254 this.connectorsStatus
.get(connectorId
).acceptedAuthorizeRequests
=
255 this?.connectorsStatus
.get(connectorId
)?.acceptedAuthorizeRequests
?? 0;
256 this.connectorsStatus
.get(connectorId
).rejectedAuthorizeRequests
=
257 this?.connectorsStatus
.get(connectorId
)?.rejectedAuthorizeRequests
?? 0;
258 this.connectorsStatus
.get(connectorId
).startTransactionRequests
=
259 this?.connectorsStatus
.get(connectorId
)?.startTransactionRequests
?? 0;
260 this.connectorsStatus
.get(connectorId
).acceptedStartTransactionRequests
=
261 this?.connectorsStatus
.get(connectorId
)?.acceptedStartTransactionRequests
?? 0;
262 this.connectorsStatus
.get(connectorId
).rejectedStartTransactionRequests
=
263 this?.connectorsStatus
.get(connectorId
)?.rejectedStartTransactionRequests
?? 0;
264 this.connectorsStatus
.get(connectorId
).stopTransactionRequests
=
265 this?.connectorsStatus
.get(connectorId
)?.stopTransactionRequests
?? 0;
266 this.connectorsStatus
.get(connectorId
).acceptedStopTransactionRequests
=
267 this?.connectorsStatus
.get(connectorId
)?.acceptedStopTransactionRequests
?? 0;
268 this.connectorsStatus
.get(connectorId
).rejectedStopTransactionRequests
=
269 this?.connectorsStatus
.get(connectorId
)?.rejectedStopTransactionRequests
?? 0;
270 this.connectorsStatus
.get(connectorId
).skippedConsecutiveTransactions
= 0;
271 this.connectorsStatus
.get(connectorId
).skippedTransactions
=
272 this?.connectorsStatus
.get(connectorId
)?.skippedTransactions
?? 0;
273 const previousRunDuration
=
274 this?.connectorsStatus
.get(connectorId
)?.startDate
&&
275 this?.connectorsStatus
.get(connectorId
)?.lastRunDate
276 ? this.connectorsStatus
.get(connectorId
).lastRunDate
.getTime() -
277 this.connectorsStatus
.get(connectorId
).startDate
.getTime()
279 this.connectorsStatus
.get(connectorId
).startDate
= new Date();
280 this.connectorsStatus
.get(connectorId
).stopDate
= new Date(
281 this.connectorsStatus
.get(connectorId
).startDate
.getTime() +
282 (this.configuration
.stopAfterHours
??
283 Constants
.CHARGING_STATION_ATG_DEFAULT_STOP_AFTER_HOURS
) *
288 this.connectorsStatus
.get(connectorId
).start
=
289 this?.connectorsStatus
.get(connectorId
)?.start
?? false;
292 private initializeConnectorsStatus(): void {
293 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
294 if (connectorId
> 0) {
295 this.connectorsStatus
.set(connectorId
, {
297 authorizeRequests
: 0,
298 acceptedAuthorizeRequests
: 0,
299 rejectedAuthorizeRequests
: 0,
300 startTransactionRequests
: 0,
301 acceptedStartTransactionRequests
: 0,
302 rejectedStartTransactionRequests
: 0,
303 stopTransactionRequests
: 0,
304 acceptedStopTransactionRequests
: 0,
305 rejectedStopTransactionRequests
: 0,
306 skippedConsecutiveTransactions
: 0,
307 skippedTransactions
: 0,
313 private async startTransaction(
315 ): Promise
<StartTransactionResponse
| undefined> {
316 const measureId
= 'StartTransaction with ATG';
317 const beginId
= PerformanceStatistics
.beginMeasure(measureId
);
318 let startResponse
: StartTransactionResponse
;
319 if (this.chargingStation
.hasAuthorizedTags()) {
320 const idTag
= this.getIdTag(connectorId
);
321 const startTransactionLogMsg
= `${this.logPrefix(
323 )} start transaction with an idTag '${idTag}'`;
324 if (this.getRequireAuthorize()) {
325 this.chargingStation
.getConnectorStatus(connectorId
).authorizeIdTag
= idTag
;
327 const authorizeResponse
: AuthorizeResponse
=
328 await this.chargingStation
.ocppRequestService
.requestHandler
<
331 >(this.chargingStation
, RequestCommand
.AUTHORIZE
, {
334 this.connectorsStatus
.get(connectorId
).authorizeRequests
++;
335 if (authorizeResponse
?.idTagInfo
?.status === AuthorizationStatus
.ACCEPTED
) {
336 this.connectorsStatus
.get(connectorId
).acceptedAuthorizeRequests
++;
337 logger
.info(startTransactionLogMsg
);
339 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
340 StartTransactionRequest
,
341 StartTransactionResponse
342 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, {
346 this.handleStartTransactionResponse(connectorId
, startResponse
);
347 PerformanceStatistics
.endMeasure(measureId
, beginId
);
348 return startResponse
;
350 this.connectorsStatus
.get(connectorId
).rejectedAuthorizeRequests
++;
351 PerformanceStatistics
.endMeasure(measureId
, beginId
);
352 return startResponse
;
354 logger
.info(startTransactionLogMsg
);
356 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
357 StartTransactionRequest
,
358 StartTransactionResponse
359 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, {
363 this.handleStartTransactionResponse(connectorId
, startResponse
);
364 PerformanceStatistics
.endMeasure(measureId
, beginId
);
365 return startResponse
;
367 logger
.info(`${this.logPrefix(connectorId)} start transaction without an idTag`);
368 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
369 StartTransactionRequest
,
370 StartTransactionResponse
371 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, { connectorId
});
372 this.handleStartTransactionResponse(connectorId
, startResponse
);
373 PerformanceStatistics
.endMeasure(measureId
, beginId
);
374 return startResponse
;
377 private async stopTransaction(
379 reason
: StopTransactionReason
= StopTransactionReason
.LOCAL
380 ): Promise
<StopTransactionResponse
> {
381 const measureId
= 'StopTransaction with ATG';
382 const beginId
= PerformanceStatistics
.beginMeasure(measureId
);
383 let stopResponse
: StopTransactionResponse
;
384 if (this.chargingStation
.getConnectorStatus(connectorId
)?.transactionStarted
=== true) {
385 stopResponse
= await this.chargingStation
.stopTransactionOnConnector(connectorId
, reason
);
386 this.connectorsStatus
.get(connectorId
).stopTransactionRequests
++;
387 if (stopResponse
?.idTagInfo
?.status === AuthorizationStatus
.ACCEPTED
) {
388 this.connectorsStatus
.get(connectorId
).acceptedStopTransactionRequests
++;
390 this.connectorsStatus
.get(connectorId
).rejectedStopTransactionRequests
++;
393 const transactionId
= this.chargingStation
.getConnectorStatus(connectorId
).transactionId
;
395 `${this.logPrefix(connectorId)} stopping a not started transaction${
396 transactionId ? ' ' + transactionId.toString() : ''
400 PerformanceStatistics
.endMeasure(measureId
, beginId
);
404 private getRequireAuthorize(): boolean {
405 return this.configuration
?.requireAuthorize
?? true;
408 private getRandomIdTag(authorizationFile
: string): string {
409 const tags
= this.chargingStation
.authorizedTagsCache
.getAuthorizedTags(authorizationFile
);
410 this.idTagIndex
= Math.floor(Utils
.secureRandom() * tags
.length
);
411 return tags
[this.idTagIndex
];
414 private getRoundRobinIdTag(authorizationFile
: string): string {
415 const tags
= this.chargingStation
.authorizedTagsCache
.getAuthorizedTags(authorizationFile
);
416 const idTag
= tags
[this.idTagIndex
];
417 this.idTagIndex
= this.idTagIndex
=== tags
.length
- 1 ? 0 : this.idTagIndex
+ 1;
421 private getConnectorAffinityIdTag(authorizationFile
: string, connectorId
: number): string {
422 const tags
= this.chargingStation
.authorizedTagsCache
.getAuthorizedTags(authorizationFile
);
423 this.idTagIndex
= (this.chargingStation
.index
- 1 + (connectorId
- 1)) % tags
.length
;
424 return tags
[this.idTagIndex
];
427 private getIdTag(connectorId
: number): string {
428 const authorizationFile
= ChargingStationUtils
.getAuthorizationFile(
429 this.chargingStation
.stationInfo
431 switch (this.configuration
?.idTagDistribution
) {
432 case IdTagDistribution
.RANDOM
:
433 return this.getRandomIdTag(authorizationFile
);
434 case IdTagDistribution
.ROUND_ROBIN
:
435 return this.getRoundRobinIdTag(authorizationFile
);
436 case IdTagDistribution
.CONNECTOR_AFFINITY
:
437 return this.getConnectorAffinityIdTag(authorizationFile
, connectorId
);
439 return this.getRoundRobinIdTag(authorizationFile
);
443 private logPrefix(connectorId
?: number): string {
444 return Utils
.logPrefix(
445 ` ${this.chargingStation.stationInfo.chargingStationId} | ATG${
446 connectorId !== undefined ? ` on connector #${connectorId.toString()}
` : ''
451 private handleStartTransactionResponse(
453 startResponse
: StartTransactionResponse
455 this.connectorsStatus
.get(connectorId
).startTransactionRequests
++;
456 if (startResponse
?.idTagInfo
?.status === AuthorizationStatus
.ACCEPTED
) {
457 this.connectorsStatus
.get(connectorId
).acceptedStartTransactionRequests
++;
459 logger
.warn(this.logPrefix(connectorId
) + ' start transaction rejected');
460 this.connectorsStatus
.get(connectorId
).rejectedStartTransactionRequests
++;