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