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