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