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