refactor(simulator): switch utils to internal module export/import
[e-mobility-charging-stations-simulator.git] / src / charging-station / AutomaticTransactionGenerator.ts
CommitLineData
edd13439 1// Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
c8eeb62b 2
d4b944ae
JB
3import { AsyncResource } from 'async_hooks';
4
2896e06d 5import { type ChargingStation, ChargingStationUtils } from './internal';
268a74bb 6import { BaseError } from '../exception';
2896e06d
JB
7// import { PerformanceStatistics } from '../performance';
8import { PerformanceStatistics } from '../performance/PerformanceStatistics';
e7aeea18
JB
9import {
10 AuthorizationStatus,
976d11ec
JB
11 type AuthorizeRequest,
12 type AuthorizeResponse,
268a74bb
JB
13 type AutomaticTransactionGeneratorConfiguration,
14 IdTagDistribution,
15 RequestCommand,
976d11ec
JB
16 type StartTransactionRequest,
17 type StartTransactionResponse,
268a74bb 18 type Status,
e7aeea18 19 StopTransactionReason,
976d11ec 20 type StopTransactionResponse,
268a74bb 21} from '../types';
60a74391 22import { Constants, Utils, logger } from '../utils';
6af9012e 23
d4b944ae
JB
24const moduleName = 'AutomaticTransactionGenerator';
25
268a74bb 26export class AutomaticTransactionGenerator extends AsyncResource {
e7aeea18
JB
27 private static readonly instances: Map<string, AutomaticTransactionGenerator> = new Map<
28 string,
29 AutomaticTransactionGenerator
30 >();
10068088 31
5e3cb728 32 public readonly connectorsStatus: Map<number, Status>;
fa7bccf4 33 public readonly configuration: AutomaticTransactionGeneratorConfiguration;
265e4266 34 public started: boolean;
9e23580d 35 private readonly chargingStation: ChargingStation;
c72f6634 36 private idTagIndex: number;
6af9012e 37
fa7bccf4
JB
38 private constructor(
39 automaticTransactionGeneratorConfiguration: AutomaticTransactionGeneratorConfiguration,
40 chargingStation: ChargingStation
41 ) {
d4b944ae 42 super(moduleName);
aa428a31 43 this.started = false;
fa7bccf4 44 this.configuration = automaticTransactionGeneratorConfiguration;
ad2f27c3 45 this.chargingStation = chargingStation;
c72f6634 46 this.idTagIndex = 0;
7807ccf2
JB
47 this.connectorsStatus = new Map<number, Status>();
48 this.initializeConnectorsStatus();
6af9012e
JB
49 }
50
fa7bccf4
JB
51 public static getInstance(
52 automaticTransactionGeneratorConfiguration: AutomaticTransactionGeneratorConfiguration,
53 chargingStation: ChargingStation
1895299d 54 ): AutomaticTransactionGenerator | undefined {
4dff3039 55 if (AutomaticTransactionGenerator.instances.has(chargingStation.stationInfo.hashId) === false) {
e7aeea18 56 AutomaticTransactionGenerator.instances.set(
51c83d6f 57 chargingStation.stationInfo.hashId,
fa7bccf4
JB
58 new AutomaticTransactionGenerator(
59 automaticTransactionGeneratorConfiguration,
60 chargingStation
61 )
e7aeea18 62 );
73b9adec 63 }
51c83d6f 64 return AutomaticTransactionGenerator.instances.get(chargingStation.stationInfo.hashId);
73b9adec
JB
65 }
66
7d75bee1 67 public start(): void {
d1c6c833
JB
68 if (this.checkChargingStation() === false) {
69 return;
70 }
a5e9befc 71 if (this.started === true) {
ba7965c4 72 logger.warn(`${this.logPrefix()} is already started`);
b809adf1
JB
73 return;
74 }
72740232 75 this.startConnectors();
265e4266 76 this.started = true;
6af9012e
JB
77 }
78
0045cef5 79 public stop(): void {
a5e9befc 80 if (this.started === false) {
ba7965c4 81 logger.warn(`${this.logPrefix()} is already stopped`);
265e4266
JB
82 return;
83 }
72740232 84 this.stopConnectors();
265e4266 85 this.started = false;
6af9012e
JB
86 }
87
a5e9befc 88 public startConnector(connectorId: number): void {
d1c6c833
JB
89 if (this.checkChargingStation(connectorId) === false) {
90 return;
91 }
7807ccf2 92 if (this.connectorsStatus.has(connectorId) === false) {
a03a128d 93 logger.error(`${this.logPrefix(connectorId)} starting on non existing connector`);
7807ccf2 94 throw new BaseError(`Connector ${connectorId} does not exist`);
a5e9befc
JB
95 }
96 if (this.connectorsStatus.get(connectorId)?.start === false) {
d4b944ae 97 this.runInAsyncScope(
e6159ce8
JB
98 this.internalStartConnector.bind(this) as (
99 this: AutomaticTransactionGenerator,
100 ...args: any[]
64818750 101 ) => Promise<void>,
d4b944ae
JB
102 this,
103 connectorId
64818750
JB
104 ).catch(() => {
105 /* This is intentional */
106 });
ecb3869d 107 } else if (this.connectorsStatus.get(connectorId)?.start === true) {
ba7965c4 108 logger.warn(`${this.logPrefix(connectorId)} is already started on connector`);
a5e9befc
JB
109 }
110 }
111
112 public stopConnector(connectorId: number): void {
7807ccf2 113 if (this.connectorsStatus.has(connectorId) === false) {
a03a128d 114 logger.error(`${this.logPrefix(connectorId)} stopping on non existing connector`);
7807ccf2 115 throw new BaseError(`Connector ${connectorId} does not exist`);
ba7965c4
JB
116 }
117 if (this.connectorsStatus.get(connectorId)?.start === true) {
7807ccf2 118 this.connectorsStatus.get(connectorId).start = false;
ba7965c4
JB
119 } else if (this.connectorsStatus.get(connectorId)?.start === false) {
120 logger.warn(`${this.logPrefix(connectorId)} is already stopped on connector`);
121 }
a5e9befc
JB
122 }
123
72740232 124 private startConnectors(): void {
e7aeea18
JB
125 if (
126 this.connectorsStatus?.size > 0 &&
127 this.connectorsStatus.size !== this.chargingStation.getNumberOfConnectors()
128 ) {
54544ef1 129 this.connectorsStatus.clear();
7807ccf2 130 this.initializeConnectorsStatus();
54544ef1 131 }
734d790d 132 for (const connectorId of this.chargingStation.connectors.keys()) {
72740232 133 if (connectorId > 0) {
83a3286a 134 this.startConnector(connectorId);
72740232
JB
135 }
136 }
137 }
138
139 private stopConnectors(): void {
734d790d 140 for (const connectorId of this.chargingStation.connectors.keys()) {
72740232
JB
141 if (connectorId > 0) {
142 this.stopConnector(connectorId);
143 }
144 }
145 }
146
83a3286a 147 private async internalStartConnector(connectorId: number): Promise<void> {
083fb002 148 this.setStartConnectorStatus(connectorId);
e7aeea18 149 logger.info(
44eb6026
JB
150 `${this.logPrefix(
151 connectorId
152 )} started on connector and will run for ${Utils.formatDurationMilliSeconds(
153 this.connectorsStatus.get(connectorId).stopDate.getTime() -
154 this.connectorsStatus.get(connectorId).startDate.getTime()
155 )}`
e7aeea18 156 );
1895299d 157 while (this.connectorsStatus.get(connectorId)?.start === true) {
e7aeea18 158 if (new Date() > this.connectorsStatus.get(connectorId).stopDate) {
9664ec50 159 this.stopConnector(connectorId);
17991e8c
JB
160 break;
161 }
1789ba2c 162 if (this.chargingStation.isInAcceptedState() === false) {
e7aeea18 163 logger.error(
44eb6026
JB
164 `${this.logPrefix(
165 connectorId
166 )} entered in transaction loop while the charging station is not in accepted state`
e7aeea18 167 );
9664ec50 168 this.stopConnector(connectorId);
17991e8c
JB
169 break;
170 }
1789ba2c 171 if (this.chargingStation.isChargingStationAvailable() === false) {
e7aeea18 172 logger.info(
44eb6026
JB
173 `${this.logPrefix(
174 connectorId
175 )} entered in transaction loop while the charging station is unavailable`
e7aeea18 176 );
9664ec50 177 this.stopConnector(connectorId);
ab5f4b03
JB
178 break;
179 }
1789ba2c 180 if (this.chargingStation.isConnectorAvailable(connectorId) === false) {
e7aeea18
JB
181 logger.info(
182 `${this.logPrefix(
183 connectorId
184 )} entered in transaction loop while the connector ${connectorId} is unavailable`
185 );
9c7195b2 186 this.stopConnector(connectorId);
17991e8c
JB
187 break;
188 }
c0560973 189 if (!this.chargingStation?.ocppRequestService) {
e7aeea18
JB
190 logger.info(
191 `${this.logPrefix(
192 connectorId
193 )} transaction loop waiting for charging station service to be initialized`
194 );
c0560973 195 do {
a4cc42ea 196 await Utils.sleep(Constants.CHARGING_STATION_ATG_INITIALIZATION_TIME);
c0560973
JB
197 } while (!this.chargingStation?.ocppRequestService);
198 }
e7aeea18
JB
199 const wait =
200 Utils.getRandomInteger(
fa7bccf4
JB
201 this.configuration.maxDelayBetweenTwoTransactions,
202 this.configuration.minDelayBetweenTwoTransactions
e7aeea18
JB
203 ) * 1000;
204 logger.info(
44eb6026 205 `${this.logPrefix(connectorId)} waiting for ${Utils.formatDurationMilliSeconds(wait)}`
e7aeea18 206 );
6af9012e 207 await Utils.sleep(wait);
c37528f1 208 const start = Utils.secureRandom();
fa7bccf4 209 if (start < this.configuration.probabilityOfStart) {
9664ec50 210 this.connectorsStatus.get(connectorId).skippedConsecutiveTransactions = 0;
6af9012e 211 // Start transaction
aef1b33a 212 const startResponse = await this.startTransaction(connectorId);
0afed85f 213 if (startResponse?.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
6af9012e 214 // Wait until end of transaction
e7aeea18 215 const waitTrxEnd =
fa7bccf4
JB
216 Utils.getRandomInteger(this.configuration.maxDuration, this.configuration.minDuration) *
217 1000;
e7aeea18 218 logger.info(
44eb6026
JB
219 `${this.logPrefix(connectorId)} transaction ${this.chargingStation
220 .getConnectorStatus(connectorId)
72092cfc 221 ?.transactionId?.toString()} started and will stop in ${Utils.formatDurationMilliSeconds(
44eb6026
JB
222 waitTrxEnd
223 )}`
e7aeea18 224 );
6af9012e
JB
225 await Utils.sleep(waitTrxEnd);
226 // Stop transaction
e7aeea18 227 logger.info(
44eb6026
JB
228 `${this.logPrefix(connectorId)} stop transaction ${this.chargingStation
229 .getConnectorStatus(connectorId)
1895299d 230 ?.transactionId?.toString()}`
e7aeea18 231 );
85d20667 232 await this.stopTransaction(connectorId);
6af9012e
JB
233 }
234 } else {
9664ec50
JB
235 this.connectorsStatus.get(connectorId).skippedConsecutiveTransactions++;
236 this.connectorsStatus.get(connectorId).skippedTransactions++;
e7aeea18 237 logger.info(
44eb6026
JB
238 `${this.logPrefix(connectorId)} skipped consecutively ${this.connectorsStatus
239 .get(connectorId)
1895299d 240 ?.skippedConsecutiveTransactions?.toString()}/${this.connectorsStatus
44eb6026 241 .get(connectorId)
1895299d 242 ?.skippedTransactions?.toString()} transaction(s)`
e7aeea18 243 );
6af9012e 244 }
9664ec50 245 this.connectorsStatus.get(connectorId).lastRunDate = new Date();
7d75bee1 246 }
9664ec50 247 this.connectorsStatus.get(connectorId).stoppedDate = new Date();
e7aeea18 248 logger.info(
44eb6026
JB
249 `${this.logPrefix(
250 connectorId
251 )} stopped on connector and lasted for ${Utils.formatDurationMilliSeconds(
252 this.connectorsStatus.get(connectorId).stoppedDate.getTime() -
253 this.connectorsStatus.get(connectorId).startDate.getTime()
254 )}`
e7aeea18
JB
255 );
256 logger.debug(
be9ee554 257 `${this.logPrefix(connectorId)} connector status: %j`,
e7aeea18
JB
258 this.connectorsStatus.get(connectorId)
259 );
6af9012e
JB
260 }
261
083fb002 262 private setStartConnectorStatus(connectorId: number): void {
9664ec50 263 this.connectorsStatus.get(connectorId).skippedConsecutiveTransactions = 0;
e7aeea18 264 const previousRunDuration =
72092cfc
JB
265 this.connectorsStatus.get(connectorId)?.startDate &&
266 this.connectorsStatus.get(connectorId)?.lastRunDate
e7aeea18
JB
267 ? this.connectorsStatus.get(connectorId).lastRunDate.getTime() -
268 this.connectorsStatus.get(connectorId).startDate.getTime()
269 : 0;
9664ec50 270 this.connectorsStatus.get(connectorId).startDate = new Date();
e7aeea18
JB
271 this.connectorsStatus.get(connectorId).stopDate = new Date(
272 this.connectorsStatus.get(connectorId).startDate.getTime() +
fa7bccf4 273 (this.configuration.stopAfterHours ??
e7aeea18
JB
274 Constants.CHARGING_STATION_ATG_DEFAULT_STOP_AFTER_HOURS) *
275 3600 *
276 1000 -
277 previousRunDuration
278 );
083fb002 279 this.connectorsStatus.get(connectorId).start = true;
4dff3039
JB
280 }
281
7807ccf2 282 private initializeConnectorsStatus(): void {
4dff3039
JB
283 for (const connectorId of this.chargingStation.connectors.keys()) {
284 if (connectorId > 0) {
7807ccf2
JB
285 this.connectorsStatus.set(connectorId, {
286 start: false,
287 authorizeRequests: 0,
288 acceptedAuthorizeRequests: 0,
289 rejectedAuthorizeRequests: 0,
290 startTransactionRequests: 0,
291 acceptedStartTransactionRequests: 0,
292 rejectedStartTransactionRequests: 0,
293 stopTransactionRequests: 0,
294 acceptedStopTransactionRequests: 0,
295 rejectedStopTransactionRequests: 0,
296 skippedConsecutiveTransactions: 0,
297 skippedTransactions: 0,
298 });
4dff3039
JB
299 }
300 }
72740232
JB
301 }
302
e7aeea18
JB
303 private async startTransaction(
304 connectorId: number
0afed85f 305 ): Promise<StartTransactionResponse | undefined> {
aef1b33a
JB
306 const measureId = 'StartTransaction with ATG';
307 const beginId = PerformanceStatistics.beginMeasure(measureId);
308 let startResponse: StartTransactionResponse;
309 if (this.chargingStation.hasAuthorizedTags()) {
c72f6634 310 const idTag = this.getIdTag(connectorId);
5cf9050d
JB
311 const startTransactionLogMsg = `${this.logPrefix(
312 connectorId
ba7965c4 313 )} start transaction with an idTag '${idTag}'`;
ccb1d6e9 314 if (this.getRequireAuthorize()) {
2e3d65ae 315 this.chargingStation.getConnectorStatus(connectorId).authorizeIdTag = idTag;
f4bf2abd 316 // Authorize idTag
2e3d65ae 317 const authorizeResponse: AuthorizeResponse =
f7f98c68 318 await this.chargingStation.ocppRequestService.requestHandler<
ef6fa3fb
JB
319 AuthorizeRequest,
320 AuthorizeResponse
08f130a0 321 >(this.chargingStation, RequestCommand.AUTHORIZE, {
ef6fa3fb
JB
322 idTag,
323 });
071a9315 324 this.connectorsStatus.get(connectorId).authorizeRequests++;
5fdab605 325 if (authorizeResponse?.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
071a9315 326 this.connectorsStatus.get(connectorId).acceptedAuthorizeRequests++;
5cf9050d 327 logger.info(startTransactionLogMsg);
5fdab605 328 // Start transaction
f7f98c68 329 startResponse = await this.chargingStation.ocppRequestService.requestHandler<
ef6fa3fb
JB
330 StartTransactionRequest,
331 StartTransactionResponse
08f130a0 332 >(this.chargingStation, RequestCommand.START_TRANSACTION, {
ef6fa3fb
JB
333 connectorId,
334 idTag,
335 });
d9ac47ef 336 this.handleStartTransactionResponse(connectorId, startResponse);
aef1b33a
JB
337 PerformanceStatistics.endMeasure(measureId, beginId);
338 return startResponse;
5fdab605 339 }
071a9315 340 this.connectorsStatus.get(connectorId).rejectedAuthorizeRequests++;
aef1b33a 341 PerformanceStatistics.endMeasure(measureId, beginId);
0afed85f 342 return startResponse;
ef6076c1 343 }
5cf9050d 344 logger.info(startTransactionLogMsg);
5fdab605 345 // Start transaction
f7f98c68 346 startResponse = await this.chargingStation.ocppRequestService.requestHandler<
ef6fa3fb
JB
347 StartTransactionRequest,
348 StartTransactionResponse
08f130a0 349 >(this.chargingStation, RequestCommand.START_TRANSACTION, {
ef6fa3fb
JB
350 connectorId,
351 idTag,
352 });
d9ac47ef 353 this.handleStartTransactionResponse(connectorId, startResponse);
aef1b33a
JB
354 PerformanceStatistics.endMeasure(measureId, beginId);
355 return startResponse;
6af9012e 356 }
5cf9050d 357 logger.info(`${this.logPrefix(connectorId)} start transaction without an idTag`);
f7f98c68 358 startResponse = await this.chargingStation.ocppRequestService.requestHandler<
ef6fa3fb
JB
359 StartTransactionRequest,
360 StartTransactionResponse
08f130a0 361 >(this.chargingStation, RequestCommand.START_TRANSACTION, { connectorId });
431b6bd5 362 this.handleStartTransactionResponse(connectorId, startResponse);
aef1b33a
JB
363 PerformanceStatistics.endMeasure(measureId, beginId);
364 return startResponse;
6af9012e
JB
365 }
366
e7aeea18
JB
367 private async stopTransaction(
368 connectorId: number,
5e3cb728 369 reason: StopTransactionReason = StopTransactionReason.LOCAL
e7aeea18 370 ): Promise<StopTransactionResponse> {
aef1b33a
JB
371 const measureId = 'StopTransaction with ATG';
372 const beginId = PerformanceStatistics.beginMeasure(measureId);
0045cef5 373 let stopResponse: StopTransactionResponse;
6d9876e7 374 if (this.chargingStation.getConnectorStatus(connectorId)?.transactionStarted === true) {
5e3cb728 375 stopResponse = await this.chargingStation.stopTransactionOnConnector(connectorId, reason);
071a9315 376 this.connectorsStatus.get(connectorId).stopTransactionRequests++;
0afed85f 377 if (stopResponse?.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
6d9876e7
JB
378 this.connectorsStatus.get(connectorId).acceptedStopTransactionRequests++;
379 } else {
380 this.connectorsStatus.get(connectorId).rejectedStopTransactionRequests++;
381 }
0045cef5 382 } else {
1895299d 383 const transactionId = this.chargingStation.getConnectorStatus(connectorId)?.transactionId;
e7aeea18 384 logger.warn(
ba7965c4 385 `${this.logPrefix(connectorId)} stopping a not started transaction${
d812bdcb 386 !Utils.isNullOrUndefined(transactionId) ? ` ${transactionId?.toString()}` : ''
e7aeea18
JB
387 }`
388 );
0045cef5 389 }
aef1b33a
JB
390 PerformanceStatistics.endMeasure(measureId, beginId);
391 return stopResponse;
c0560973
JB
392 }
393
ccb1d6e9 394 private getRequireAuthorize(): boolean {
fa7bccf4 395 return this.configuration?.requireAuthorize ?? true;
ccb1d6e9
JB
396 }
397
c72f6634
JB
398 private getRandomIdTag(authorizationFile: string): string {
399 const tags = this.chargingStation.authorizedTagsCache.getAuthorizedTags(authorizationFile);
400 this.idTagIndex = Math.floor(Utils.secureRandom() * tags.length);
401 return tags[this.idTagIndex];
402 }
403
404 private getRoundRobinIdTag(authorizationFile: string): string {
405 const tags = this.chargingStation.authorizedTagsCache.getAuthorizedTags(authorizationFile);
406 const idTag = tags[this.idTagIndex];
407 this.idTagIndex = this.idTagIndex === tags.length - 1 ? 0 : this.idTagIndex + 1;
408 return idTag;
409 }
410
411 private getConnectorAffinityIdTag(authorizationFile: string, connectorId: number): string {
412 const tags = this.chargingStation.authorizedTagsCache.getAuthorizedTags(authorizationFile);
413 this.idTagIndex = (this.chargingStation.index - 1 + (connectorId - 1)) % tags.length;
414 return tags[this.idTagIndex];
415 }
416
417 private getIdTag(connectorId: number): string {
418 const authorizationFile = ChargingStationUtils.getAuthorizationFile(
419 this.chargingStation.stationInfo
420 );
421 switch (this.configuration?.idTagDistribution) {
422 case IdTagDistribution.RANDOM:
423 return this.getRandomIdTag(authorizationFile);
424 case IdTagDistribution.ROUND_ROBIN:
425 return this.getRoundRobinIdTag(authorizationFile);
426 case IdTagDistribution.CONNECTOR_AFFINITY:
427 return this.getConnectorAffinityIdTag(authorizationFile, connectorId);
428 default:
429 return this.getRoundRobinIdTag(authorizationFile);
430 }
431 }
432
8b7072dc 433 private logPrefix = (connectorId?: number): string => {
6cd85def
JB
434 return Utils.logPrefix(
435 ` ${this.chargingStation.stationInfo.chargingStationId} | ATG${
a5e9befc 436 connectorId !== undefined ? ` on connector #${connectorId.toString()}` : ''
6cd85def
JB
437 }:`
438 );
8b7072dc 439 };
d9ac47ef
JB
440
441 private handleStartTransactionResponse(
442 connectorId: number,
443 startResponse: StartTransactionResponse
444 ): void {
445 this.connectorsStatus.get(connectorId).startTransactionRequests++;
446 if (startResponse?.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
447 this.connectorsStatus.get(connectorId).acceptedStartTransactionRequests++;
448 } else {
44eb6026 449 logger.warn(`${this.logPrefix(connectorId)} start transaction rejected`);
d9ac47ef
JB
450 this.connectorsStatus.get(connectorId).rejectedStartTransactionRequests++;
451 }
452 }
d1c6c833
JB
453
454 private checkChargingStation(connectorId?: number): boolean {
cbf9b878 455 if (this.chargingStation.started === false && this.chargingStation.starting === false) {
d1c6c833 456 logger.warn(`${this.logPrefix(connectorId)} charging station is stopped, cannot proceed`);
cbf9b878 457 return false;
d1c6c833 458 }
cbf9b878 459 return true;
d1c6c833 460 }
6af9012e 461}