1 // Partial Copyright Jerome Benoit. 2021. All Rights Reserved.
3 import { AsyncResource
} from
'async_hooks';
5 import BaseError from
'../exception/BaseError';
6 import PerformanceStatistics from
'../performance/PerformanceStatistics';
8 type AutomaticTransactionGeneratorConfiguration
,
11 } from
'../types/AutomaticTransactionGenerator';
12 import { RequestCommand
} from
'../types/ocpp/Requests';
15 type AuthorizeRequest
,
16 type AuthorizeResponse
,
17 type StartTransactionRequest
,
18 type StartTransactionResponse
,
19 StopTransactionReason
,
20 type StopTransactionResponse
,
21 } from
'../types/ocpp/Transaction';
22 import Constants from
'../utils/Constants';
23 import logger from
'../utils/Logger';
24 import Utils from
'../utils/Utils';
25 import type ChargingStation from
'./ChargingStation';
26 import { ChargingStationUtils
} from
'./ChargingStationUtils';
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.started
=== true) {
73 logger
.warn(`${this.logPrefix()} is already started`);
76 this.startConnectors();
81 if (this.started
=== false) {
82 logger
.warn(`${this.logPrefix()} is already stopped`);
85 this.stopConnectors();
89 public startConnector(connectorId
: number): void {
90 if (this.connectorsStatus
.has(connectorId
) === false) {
91 logger
.error(`${this.logPrefix(connectorId)} starting on non existing connector`);
92 throw new BaseError(`Connector ${connectorId} does not exist`);
94 if (this.connectorsStatus
.get(connectorId
)?.start
=== false) {
96 this.internalStartConnector
.bind(this) as (this: this, ...args
: any[]) => unknown
,
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 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
126 if (connectorId
> 0) {
127 this.startConnector(connectorId
);
132 private stopConnectors(): void {
133 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
134 if (connectorId
> 0) {
135 this.stopConnector(connectorId
);
140 private async internalStartConnector(connectorId
: number): Promise
<void> {
141 this.initializeConnectorStatus(connectorId
);
142 this.connectorsStatus
.get(connectorId
).start
= true;
144 this.logPrefix(connectorId
) +
145 ' started on connector and will run for ' +
146 Utils
.formatDurationMilliSeconds(
147 this.connectorsStatus
.get(connectorId
).stopDate
.getTime() -
148 this.connectorsStatus
.get(connectorId
).startDate
.getTime()
151 while (this.connectorsStatus
.get(connectorId
).start
=== true) {
152 if (new Date() > this.connectorsStatus
.get(connectorId
).stopDate
) {
153 this.stopConnector(connectorId
);
156 if (!this.chargingStation
.isInAcceptedState()) {
158 this.logPrefix(connectorId
) +
159 ' entered in transaction loop while the charging station is not in accepted state'
161 this.stopConnector(connectorId
);
164 if (!this.chargingStation
.isChargingStationAvailable()) {
166 this.logPrefix(connectorId
) +
167 ' entered in transaction loop while the charging station is unavailable'
169 this.stopConnector(connectorId
);
172 if (!this.chargingStation
.isConnectorAvailable(connectorId
)) {
176 )} entered in transaction loop while the connector ${connectorId} is unavailable`
178 this.stopConnector(connectorId
);
181 if (!this.chargingStation
?.ocppRequestService
) {
185 )} transaction loop waiting for charging station service to be initialized`
188 await Utils
.sleep(Constants
.CHARGING_STATION_ATG_INITIALIZATION_TIME
);
189 } while (!this.chargingStation
?.ocppRequestService
);
192 Utils
.getRandomInteger(
193 this.configuration
.maxDelayBetweenTwoTransactions
,
194 this.configuration
.minDelayBetweenTwoTransactions
197 this.logPrefix(connectorId
) + ' waiting for ' + Utils
.formatDurationMilliSeconds(wait
)
199 await Utils
.sleep(wait
);
200 const start
= Utils
.secureRandom();
201 if (start
< this.configuration
.probabilityOfStart
) {
202 this.connectorsStatus
.get(connectorId
).skippedConsecutiveTransactions
= 0;
204 const startResponse
= await this.startTransaction(connectorId
);
205 if (startResponse
?.idTagInfo
?.status === AuthorizationStatus
.ACCEPTED
) {
206 // Wait until end of transaction
208 Utils
.getRandomInteger(this.configuration
.maxDuration
, this.configuration
.minDuration
) *
211 this.logPrefix(connectorId
) +
213 this.chargingStation
.getConnectorStatus(connectorId
).transactionId
.toString() +
214 ' started and will stop in ' +
215 Utils
.formatDurationMilliSeconds(waitTrxEnd
)
217 await Utils
.sleep(waitTrxEnd
);
220 this.logPrefix(connectorId
) +
221 ' stop transaction ' +
222 this.chargingStation
.getConnectorStatus(connectorId
).transactionId
.toString()
224 await this.stopTransaction(connectorId
);
227 this.connectorsStatus
.get(connectorId
).skippedConsecutiveTransactions
++;
228 this.connectorsStatus
.get(connectorId
).skippedTransactions
++;
230 this.logPrefix(connectorId
) +
231 ' skipped consecutively ' +
232 this.connectorsStatus
.get(connectorId
).skippedConsecutiveTransactions
.toString() +
234 this.connectorsStatus
.get(connectorId
).skippedTransactions
.toString() +
238 this.connectorsStatus
.get(connectorId
).lastRunDate
= new Date();
240 this.connectorsStatus
.get(connectorId
).stoppedDate
= new Date();
242 this.logPrefix(connectorId
) +
243 ' stopped on connector and lasted for ' +
244 Utils
.formatDurationMilliSeconds(
245 this.connectorsStatus
.get(connectorId
).stoppedDate
.getTime() -
246 this.connectorsStatus
.get(connectorId
).startDate
.getTime()
250 `${this.logPrefix(connectorId)} connector status: %j`,
251 this.connectorsStatus
.get(connectorId
)
255 private initializeConnectorStatus(connectorId
: number): void {
256 this.connectorsStatus
.get(connectorId
).authorizeRequests
=
257 this?.connectorsStatus
.get(connectorId
)?.authorizeRequests
?? 0;
258 this.connectorsStatus
.get(connectorId
).acceptedAuthorizeRequests
=
259 this?.connectorsStatus
.get(connectorId
)?.acceptedAuthorizeRequests
?? 0;
260 this.connectorsStatus
.get(connectorId
).rejectedAuthorizeRequests
=
261 this?.connectorsStatus
.get(connectorId
)?.rejectedAuthorizeRequests
?? 0;
262 this.connectorsStatus
.get(connectorId
).startTransactionRequests
=
263 this?.connectorsStatus
.get(connectorId
)?.startTransactionRequests
?? 0;
264 this.connectorsStatus
.get(connectorId
).acceptedStartTransactionRequests
=
265 this?.connectorsStatus
.get(connectorId
)?.acceptedStartTransactionRequests
?? 0;
266 this.connectorsStatus
.get(connectorId
).rejectedStartTransactionRequests
=
267 this?.connectorsStatus
.get(connectorId
)?.rejectedStartTransactionRequests
?? 0;
268 this.connectorsStatus
.get(connectorId
).stopTransactionRequests
=
269 this?.connectorsStatus
.get(connectorId
)?.stopTransactionRequests
?? 0;
270 this.connectorsStatus
.get(connectorId
).acceptedStopTransactionRequests
=
271 this?.connectorsStatus
.get(connectorId
)?.acceptedStopTransactionRequests
?? 0;
272 this.connectorsStatus
.get(connectorId
).rejectedStopTransactionRequests
=
273 this?.connectorsStatus
.get(connectorId
)?.rejectedStopTransactionRequests
?? 0;
274 this.connectorsStatus
.get(connectorId
).skippedConsecutiveTransactions
= 0;
275 this.connectorsStatus
.get(connectorId
).skippedTransactions
=
276 this?.connectorsStatus
.get(connectorId
)?.skippedTransactions
?? 0;
277 const previousRunDuration
=
278 this?.connectorsStatus
.get(connectorId
)?.startDate
&&
279 this?.connectorsStatus
.get(connectorId
)?.lastRunDate
280 ? this.connectorsStatus
.get(connectorId
).lastRunDate
.getTime() -
281 this.connectorsStatus
.get(connectorId
).startDate
.getTime()
283 this.connectorsStatus
.get(connectorId
).startDate
= new Date();
284 this.connectorsStatus
.get(connectorId
).stopDate
= new Date(
285 this.connectorsStatus
.get(connectorId
).startDate
.getTime() +
286 (this.configuration
.stopAfterHours
??
287 Constants
.CHARGING_STATION_ATG_DEFAULT_STOP_AFTER_HOURS
) *
292 this.connectorsStatus
.get(connectorId
).start
=
293 this?.connectorsStatus
.get(connectorId
)?.start
?? false;
296 private initializeConnectorsStatus(): void {
297 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
298 if (connectorId
> 0) {
299 this.connectorsStatus
.set(connectorId
, {
301 authorizeRequests
: 0,
302 acceptedAuthorizeRequests
: 0,
303 rejectedAuthorizeRequests
: 0,
304 startTransactionRequests
: 0,
305 acceptedStartTransactionRequests
: 0,
306 rejectedStartTransactionRequests
: 0,
307 stopTransactionRequests
: 0,
308 acceptedStopTransactionRequests
: 0,
309 rejectedStopTransactionRequests
: 0,
310 skippedConsecutiveTransactions
: 0,
311 skippedTransactions
: 0,
317 private async startTransaction(
319 ): Promise
<StartTransactionResponse
| undefined> {
320 const measureId
= 'StartTransaction with ATG';
321 const beginId
= PerformanceStatistics
.beginMeasure(measureId
);
322 let startResponse
: StartTransactionResponse
;
323 if (this.chargingStation
.hasAuthorizedTags()) {
324 const idTag
= this.getIdTag(connectorId
);
325 const startTransactionLogMsg
= `${this.logPrefix(
327 )} start transaction with an idTag '${idTag}'`;
328 if (this.getRequireAuthorize()) {
329 this.chargingStation
.getConnectorStatus(connectorId
).authorizeIdTag
= idTag
;
331 const authorizeResponse
: AuthorizeResponse
=
332 await this.chargingStation
.ocppRequestService
.requestHandler
<
335 >(this.chargingStation
, RequestCommand
.AUTHORIZE
, {
338 this.connectorsStatus
.get(connectorId
).authorizeRequests
++;
339 if (authorizeResponse
?.idTagInfo
?.status === AuthorizationStatus
.ACCEPTED
) {
340 this.connectorsStatus
.get(connectorId
).acceptedAuthorizeRequests
++;
341 logger
.info(startTransactionLogMsg
);
343 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
344 StartTransactionRequest
,
345 StartTransactionResponse
346 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, {
350 this.handleStartTransactionResponse(connectorId
, startResponse
);
351 PerformanceStatistics
.endMeasure(measureId
, beginId
);
352 return startResponse
;
354 this.connectorsStatus
.get(connectorId
).rejectedAuthorizeRequests
++;
355 PerformanceStatistics
.endMeasure(measureId
, beginId
);
356 return startResponse
;
358 logger
.info(startTransactionLogMsg
);
360 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
361 StartTransactionRequest
,
362 StartTransactionResponse
363 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, {
367 this.handleStartTransactionResponse(connectorId
, startResponse
);
368 PerformanceStatistics
.endMeasure(measureId
, beginId
);
369 return startResponse
;
371 logger
.info(`${this.logPrefix(connectorId)} start transaction without an idTag`);
372 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
373 StartTransactionRequest
,
374 StartTransactionResponse
375 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, { connectorId
});
376 this.handleStartTransactionResponse(connectorId
, startResponse
);
377 PerformanceStatistics
.endMeasure(measureId
, beginId
);
378 return startResponse
;
381 private async stopTransaction(
383 reason
: StopTransactionReason
= StopTransactionReason
.LOCAL
384 ): Promise
<StopTransactionResponse
> {
385 const measureId
= 'StopTransaction with ATG';
386 const beginId
= PerformanceStatistics
.beginMeasure(measureId
);
387 let stopResponse
: StopTransactionResponse
;
388 if (this.chargingStation
.getConnectorStatus(connectorId
)?.transactionStarted
=== true) {
389 stopResponse
= await this.chargingStation
.stopTransactionOnConnector(connectorId
, reason
);
390 this.connectorsStatus
.get(connectorId
).stopTransactionRequests
++;
391 if (stopResponse
?.idTagInfo
?.status === AuthorizationStatus
.ACCEPTED
) {
392 this.connectorsStatus
.get(connectorId
).acceptedStopTransactionRequests
++;
394 this.connectorsStatus
.get(connectorId
).rejectedStopTransactionRequests
++;
397 const transactionId
= this.chargingStation
.getConnectorStatus(connectorId
).transactionId
;
399 `${this.logPrefix(connectorId)} stopping a not started transaction${
400 transactionId ? ' ' + transactionId.toString() : ''
404 PerformanceStatistics
.endMeasure(measureId
, beginId
);
408 private getRequireAuthorize(): boolean {
409 return this.configuration
?.requireAuthorize
?? true;
412 private getRandomIdTag(authorizationFile
: string): string {
413 const tags
= this.chargingStation
.authorizedTagsCache
.getAuthorizedTags(authorizationFile
);
414 this.idTagIndex
= Math.floor(Utils
.secureRandom() * tags
.length
);
415 return tags
[this.idTagIndex
];
418 private getRoundRobinIdTag(authorizationFile
: string): string {
419 const tags
= this.chargingStation
.authorizedTagsCache
.getAuthorizedTags(authorizationFile
);
420 const idTag
= tags
[this.idTagIndex
];
421 this.idTagIndex
= this.idTagIndex
=== tags
.length
- 1 ? 0 : this.idTagIndex
+ 1;
425 private getConnectorAffinityIdTag(authorizationFile
: string, connectorId
: number): string {
426 const tags
= this.chargingStation
.authorizedTagsCache
.getAuthorizedTags(authorizationFile
);
427 this.idTagIndex
= (this.chargingStation
.index
- 1 + (connectorId
- 1)) % tags
.length
;
428 return tags
[this.idTagIndex
];
431 private getIdTag(connectorId
: number): string {
432 const authorizationFile
= ChargingStationUtils
.getAuthorizationFile(
433 this.chargingStation
.stationInfo
435 switch (this.configuration
?.idTagDistribution
) {
436 case IdTagDistribution
.RANDOM
:
437 return this.getRandomIdTag(authorizationFile
);
438 case IdTagDistribution
.ROUND_ROBIN
:
439 return this.getRoundRobinIdTag(authorizationFile
);
440 case IdTagDistribution
.CONNECTOR_AFFINITY
:
441 return this.getConnectorAffinityIdTag(authorizationFile
, connectorId
);
443 return this.getRoundRobinIdTag(authorizationFile
);
447 private logPrefix(connectorId
?: number): string {
448 return Utils
.logPrefix(
449 ` ${this.chargingStation.stationInfo.chargingStationId} | ATG${
450 connectorId !== undefined ? ` on connector #${connectorId.toString()}
` : ''
455 private handleStartTransactionResponse(
457 startResponse
: StartTransactionResponse
459 this.connectorsStatus
.get(connectorId
).startTransactionRequests
++;
460 if (startResponse
?.idTagInfo
?.status === AuthorizationStatus
.ACCEPTED
) {
461 this.connectorsStatus
.get(connectorId
).acceptedStartTransactionRequests
++;
463 logger
.warn(this.logPrefix(connectorId
) + ' start transaction rejected');
464 this.connectorsStatus
.get(connectorId
).rejectedStartTransactionRequests
++;