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