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