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