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