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