refactor(simulator): factor out default ATG 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;
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
JB
227 this.chargingStation.getAutomaticTransactionGeneratorConfiguration()
228 .maxDelayBetweenTwoTransactions,
229 this.chargingStation.getAutomaticTransactionGeneratorConfiguration()
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
JB
247 Utils.getRandomInteger(
248 this.chargingStation.getAutomaticTransactionGeneratorConfiguration().maxDuration,
249 this.chargingStation.getAutomaticTransactionGeneratorConfiguration().minDuration
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() +
ac7f79af 306 (this.chargingStation.getAutomaticTransactionGeneratorConfiguration().stopAfterHours ??
1fe0632a 307 Constants.DEFAULT_ATG_STOP_AFTER_HOURS) *
e7aeea18
JB
308 3600 *
309 1000 -
310 previousRunDuration
311 );
083fb002 312 this.connectorsStatus.get(connectorId).start = true;
4dff3039
JB
313 }
314
7807ccf2 315 private initializeConnectorsStatus(): void {
4334db72
JB
316 if (this.chargingStation.hasEvses) {
317 for (const [evseId, evseStatus] of this.chargingStation.evses) {
318 if (evseId > 0) {
319 for (const connectorId of evseStatus.connectors.keys()) {
320 this.connectorsStatus.set(connectorId, {
321 start: false,
322 authorizeRequests: 0,
323 acceptedAuthorizeRequests: 0,
324 rejectedAuthorizeRequests: 0,
325 startTransactionRequests: 0,
326 acceptedStartTransactionRequests: 0,
327 rejectedStartTransactionRequests: 0,
328 stopTransactionRequests: 0,
329 acceptedStopTransactionRequests: 0,
330 rejectedStopTransactionRequests: 0,
331 skippedConsecutiveTransactions: 0,
332 skippedTransactions: 0,
333 });
334 }
335 }
336 }
337 } else {
338 for (const connectorId of this.chargingStation.connectors.keys()) {
339 if (connectorId > 0) {
340 this.connectorsStatus.set(connectorId, {
341 start: false,
342 authorizeRequests: 0,
343 acceptedAuthorizeRequests: 0,
344 rejectedAuthorizeRequests: 0,
345 startTransactionRequests: 0,
346 acceptedStartTransactionRequests: 0,
347 rejectedStartTransactionRequests: 0,
348 stopTransactionRequests: 0,
349 acceptedStopTransactionRequests: 0,
350 rejectedStopTransactionRequests: 0,
351 skippedConsecutiveTransactions: 0,
352 skippedTransactions: 0,
353 });
354 }
4dff3039
JB
355 }
356 }
72740232
JB
357 }
358
e7aeea18
JB
359 private async startTransaction(
360 connectorId: number
0afed85f 361 ): Promise<StartTransactionResponse | undefined> {
aef1b33a
JB
362 const measureId = 'StartTransaction with ATG';
363 const beginId = PerformanceStatistics.beginMeasure(measureId);
364 let startResponse: StartTransactionResponse;
f911a4af
JB
365 if (this.chargingStation.hasIdTags()) {
366 const idTag = IdTagsCache.getInstance().getIdTag(
ac7f79af 367 this.chargingStation.getAutomaticTransactionGeneratorConfiguration()?.idTagDistribution,
aaf2bf9c
JB
368 this.chargingStation,
369 connectorId
370 );
5cf9050d
JB
371 const startTransactionLogMsg = `${this.logPrefix(
372 connectorId
ba7965c4 373 )} start transaction with an idTag '${idTag}'`;
ccb1d6e9 374 if (this.getRequireAuthorize()) {
2e3d65ae 375 this.chargingStation.getConnectorStatus(connectorId).authorizeIdTag = idTag;
f4bf2abd 376 // Authorize idTag
2e3d65ae 377 const authorizeResponse: AuthorizeResponse =
f7f98c68 378 await this.chargingStation.ocppRequestService.requestHandler<
ef6fa3fb
JB
379 AuthorizeRequest,
380 AuthorizeResponse
08f130a0 381 >(this.chargingStation, RequestCommand.AUTHORIZE, {
ef6fa3fb
JB
382 idTag,
383 });
1fe0632a 384 ++this.connectorsStatus.get(connectorId).authorizeRequests;
5fdab605 385 if (authorizeResponse?.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
1fe0632a 386 ++this.connectorsStatus.get(connectorId).acceptedAuthorizeRequests;
5cf9050d 387 logger.info(startTransactionLogMsg);
5fdab605 388 // Start transaction
f7f98c68 389 startResponse = await this.chargingStation.ocppRequestService.requestHandler<
ef6fa3fb
JB
390 StartTransactionRequest,
391 StartTransactionResponse
08f130a0 392 >(this.chargingStation, RequestCommand.START_TRANSACTION, {
ef6fa3fb
JB
393 connectorId,
394 idTag,
395 });
d9ac47ef 396 this.handleStartTransactionResponse(connectorId, startResponse);
aef1b33a
JB
397 PerformanceStatistics.endMeasure(measureId, beginId);
398 return startResponse;
5fdab605 399 }
1fe0632a 400 ++this.connectorsStatus.get(connectorId).rejectedAuthorizeRequests;
aef1b33a 401 PerformanceStatistics.endMeasure(measureId, beginId);
0afed85f 402 return startResponse;
ef6076c1 403 }
5cf9050d 404 logger.info(startTransactionLogMsg);
5fdab605 405 // Start transaction
f7f98c68 406 startResponse = await this.chargingStation.ocppRequestService.requestHandler<
ef6fa3fb
JB
407 StartTransactionRequest,
408 StartTransactionResponse
08f130a0 409 >(this.chargingStation, RequestCommand.START_TRANSACTION, {
ef6fa3fb
JB
410 connectorId,
411 idTag,
412 });
d9ac47ef 413 this.handleStartTransactionResponse(connectorId, startResponse);
aef1b33a
JB
414 PerformanceStatistics.endMeasure(measureId, beginId);
415 return startResponse;
6af9012e 416 }
5cf9050d 417 logger.info(`${this.logPrefix(connectorId)} start transaction without an idTag`);
f7f98c68 418 startResponse = await this.chargingStation.ocppRequestService.requestHandler<
ef6fa3fb
JB
419 StartTransactionRequest,
420 StartTransactionResponse
08f130a0 421 >(this.chargingStation, RequestCommand.START_TRANSACTION, { connectorId });
431b6bd5 422 this.handleStartTransactionResponse(connectorId, startResponse);
aef1b33a
JB
423 PerformanceStatistics.endMeasure(measureId, beginId);
424 return startResponse;
6af9012e
JB
425 }
426
e7aeea18
JB
427 private async stopTransaction(
428 connectorId: number,
5e3cb728 429 reason: StopTransactionReason = StopTransactionReason.LOCAL
e7aeea18 430 ): Promise<StopTransactionResponse> {
aef1b33a
JB
431 const measureId = 'StopTransaction with ATG';
432 const beginId = PerformanceStatistics.beginMeasure(measureId);
0045cef5 433 let stopResponse: StopTransactionResponse;
6d9876e7 434 if (this.chargingStation.getConnectorStatus(connectorId)?.transactionStarted === true) {
5e3cb728 435 stopResponse = await this.chargingStation.stopTransactionOnConnector(connectorId, reason);
1fe0632a 436 ++this.connectorsStatus.get(connectorId).stopTransactionRequests;
0afed85f 437 if (stopResponse?.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
1fe0632a 438 ++this.connectorsStatus.get(connectorId).acceptedStopTransactionRequests;
6d9876e7 439 } else {
1fe0632a 440 ++this.connectorsStatus.get(connectorId).rejectedStopTransactionRequests;
6d9876e7 441 }
0045cef5 442 } else {
1895299d 443 const transactionId = this.chargingStation.getConnectorStatus(connectorId)?.transactionId;
e7aeea18 444 logger.warn(
ba7965c4 445 `${this.logPrefix(connectorId)} stopping a not started transaction${
54ebb82c 446 !Utils.isNullOrUndefined(transactionId) ? ` with id ${transactionId?.toString()}` : ''
e7aeea18
JB
447 }`
448 );
0045cef5 449 }
aef1b33a
JB
450 PerformanceStatistics.endMeasure(measureId, beginId);
451 return stopResponse;
c0560973
JB
452 }
453
ccb1d6e9 454 private getRequireAuthorize(): boolean {
ac7f79af
JB
455 return (
456 this.chargingStation.getAutomaticTransactionGeneratorConfiguration()?.requireAuthorize ?? true
457 );
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 {
1fe0632a 472 ++this.connectorsStatus.get(connectorId).startTransactionRequests;
d9ac47ef 473 if (startResponse?.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
1fe0632a 474 ++this.connectorsStatus.get(connectorId).acceptedStartTransactionRequests;
d9ac47ef 475 } else {
44eb6026 476 logger.warn(`${this.logPrefix(connectorId)} start transaction rejected`);
1fe0632a 477 ++this.connectorsStatus.get(connectorId).rejectedStartTransactionRequests;
d9ac47ef
JB
478 }
479 }
6af9012e 480}