fix: fix ATG refresh at template changes
[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,
9b4d0c70 13 ConnectorStatusEnum,
268a74bb 14 RequestCommand,
976d11ec
JB
15 type StartTransactionRequest,
16 type StartTransactionResponse,
268a74bb 17 type Status,
e7aeea18 18 StopTransactionReason,
976d11ec 19 type StopTransactionResponse,
268a74bb 20} from '../types';
60a74391 21import { Constants, Utils, logger } from '../utils';
6af9012e 22
d4b944ae
JB
23const moduleName = 'AutomaticTransactionGenerator';
24
268a74bb 25export class AutomaticTransactionGenerator extends AsyncResource {
e7aeea18
JB
26 private static readonly instances: Map<string, AutomaticTransactionGenerator> = new Map<
27 string,
28 AutomaticTransactionGenerator
29 >();
10068088 30
5e3cb728 31 public readonly connectorsStatus: Map<number, Status>;
265e4266 32 public started: boolean;
9e23580d 33 private readonly chargingStation: ChargingStation;
6af9012e 34
ac7f79af 35 private constructor(chargingStation: ChargingStation) {
d4b944ae 36 super(moduleName);
aa428a31 37 this.started = false;
ad2f27c3 38 this.chargingStation = chargingStation;
7807ccf2
JB
39 this.connectorsStatus = new Map<number, Status>();
40 this.initializeConnectorsStatus();
6af9012e
JB
41 }
42
fa7bccf4 43 public static getInstance(
fa7bccf4 44 chargingStation: ChargingStation
1895299d 45 ): AutomaticTransactionGenerator | undefined {
4dff3039 46 if (AutomaticTransactionGenerator.instances.has(chargingStation.stationInfo.hashId) === false) {
e7aeea18 47 AutomaticTransactionGenerator.instances.set(
51c83d6f 48 chargingStation.stationInfo.hashId,
ac7f79af 49 new AutomaticTransactionGenerator(chargingStation)
e7aeea18 50 );
73b9adec 51 }
51c83d6f 52 return AutomaticTransactionGenerator.instances.get(chargingStation.stationInfo.hashId);
73b9adec
JB
53 }
54
7d75bee1 55 public start(): void {
1bf29f5b
JB
56 if (
57 ChargingStationUtils.checkChargingStation(this.chargingStation, this.logPrefix()) === false
58 ) {
d1c6c833
JB
59 return;
60 }
a5e9befc 61 if (this.started === true) {
ba7965c4 62 logger.warn(`${this.logPrefix()} is already started`);
b809adf1
JB
63 return;
64 }
72740232 65 this.startConnectors();
265e4266 66 this.started = true;
6af9012e
JB
67 }
68
0045cef5 69 public stop(): void {
a5e9befc 70 if (this.started === false) {
ba7965c4 71 logger.warn(`${this.logPrefix()} is already stopped`);
265e4266
JB
72 return;
73 }
72740232 74 this.stopConnectors();
265e4266 75 this.started = false;
6af9012e
JB
76 }
77
a5e9befc 78 public startConnector(connectorId: number): void {
1bf29f5b
JB
79 if (
80 ChargingStationUtils.checkChargingStation(
81 this.chargingStation,
82 this.logPrefix(connectorId)
83 ) === false
84 ) {
d1c6c833
JB
85 return;
86 }
7807ccf2 87 if (this.connectorsStatus.has(connectorId) === false) {
a03a128d 88 logger.error(`${this.logPrefix(connectorId)} starting on non existing connector`);
7807ccf2 89 throw new BaseError(`Connector ${connectorId} does not exist`);
a5e9befc
JB
90 }
91 if (this.connectorsStatus.get(connectorId)?.start === false) {
d4b944ae 92 this.runInAsyncScope(
e6159ce8
JB
93 this.internalStartConnector.bind(this) as (
94 this: AutomaticTransactionGenerator,
95 ...args: any[]
64818750 96 ) => Promise<void>,
d4b944ae
JB
97 this,
98 connectorId
59b6ed8d 99 ).catch(Constants.EMPTY_FUNCTION);
ecb3869d 100 } else if (this.connectorsStatus.get(connectorId)?.start === true) {
ba7965c4 101 logger.warn(`${this.logPrefix(connectorId)} is already started on connector`);
a5e9befc
JB
102 }
103 }
104
105 public stopConnector(connectorId: number): void {
7807ccf2 106 if (this.connectorsStatus.has(connectorId) === false) {
a03a128d 107 logger.error(`${this.logPrefix(connectorId)} stopping on non existing connector`);
7807ccf2 108 throw new BaseError(`Connector ${connectorId} does not exist`);
ba7965c4
JB
109 }
110 if (this.connectorsStatus.get(connectorId)?.start === true) {
7807ccf2 111 this.connectorsStatus.get(connectorId).start = false;
ba7965c4
JB
112 } else if (this.connectorsStatus.get(connectorId)?.start === false) {
113 logger.warn(`${this.logPrefix(connectorId)} is already stopped on connector`);
114 }
a5e9befc
JB
115 }
116
72740232 117 private startConnectors(): void {
e7aeea18
JB
118 if (
119 this.connectorsStatus?.size > 0 &&
120 this.connectorsStatus.size !== this.chargingStation.getNumberOfConnectors()
121 ) {
54544ef1 122 this.connectorsStatus.clear();
7807ccf2 123 this.initializeConnectorsStatus();
54544ef1 124 }
4334db72
JB
125 if (this.chargingStation.hasEvses) {
126 for (const [evseId, evseStatus] of this.chargingStation.evses) {
127 if (evseId > 0) {
128 for (const connectorId of evseStatus.connectors.keys()) {
129 this.startConnector(connectorId);
130 }
131 }
132 }
133 } else {
134 for (const connectorId of this.chargingStation.connectors.keys()) {
135 if (connectorId > 0) {
136 this.startConnector(connectorId);
137 }
72740232
JB
138 }
139 }
140 }
141
142 private stopConnectors(): void {
4334db72
JB
143 if (this.chargingStation.hasEvses) {
144 for (const [evseId, evseStatus] of this.chargingStation.evses) {
145 if (evseId > 0) {
146 for (const connectorId of evseStatus.connectors.keys()) {
147 this.stopConnector(connectorId);
148 }
149 }
150 }
151 } else {
152 for (const connectorId of this.chargingStation.connectors.keys()) {
153 if (connectorId > 0) {
154 this.stopConnector(connectorId);
155 }
72740232
JB
156 }
157 }
158 }
159
83a3286a 160 private async internalStartConnector(connectorId: number): Promise<void> {
083fb002 161 this.setStartConnectorStatus(connectorId);
e7aeea18 162 logger.info(
44eb6026
JB
163 `${this.logPrefix(
164 connectorId
165 )} started on connector and will run for ${Utils.formatDurationMilliSeconds(
166 this.connectorsStatus.get(connectorId).stopDate.getTime() -
167 this.connectorsStatus.get(connectorId).startDate.getTime()
168 )}`
e7aeea18 169 );
1895299d 170 while (this.connectorsStatus.get(connectorId)?.start === true) {
e7aeea18 171 if (new Date() > this.connectorsStatus.get(connectorId).stopDate) {
9664ec50 172 this.stopConnector(connectorId);
17991e8c
JB
173 break;
174 }
1789ba2c 175 if (this.chargingStation.isInAcceptedState() === false) {
e7aeea18 176 logger.error(
44eb6026
JB
177 `${this.logPrefix(
178 connectorId
179 )} entered in transaction loop while the charging station is not in accepted state`
e7aeea18 180 );
9664ec50 181 this.stopConnector(connectorId);
17991e8c
JB
182 break;
183 }
1789ba2c 184 if (this.chargingStation.isChargingStationAvailable() === false) {
e7aeea18 185 logger.info(
44eb6026
JB
186 `${this.logPrefix(
187 connectorId
188 )} entered in transaction loop while the charging station is unavailable`
e7aeea18 189 );
9664ec50 190 this.stopConnector(connectorId);
ab5f4b03
JB
191 break;
192 }
1789ba2c 193 if (this.chargingStation.isConnectorAvailable(connectorId) === false) {
e7aeea18
JB
194 logger.info(
195 `${this.logPrefix(
196 connectorId
197 )} entered in transaction loop while the connector ${connectorId} is unavailable`
198 );
9c7195b2 199 this.stopConnector(connectorId);
17991e8c
JB
200 break;
201 }
9b4d0c70
JB
202 if (
203 this.chargingStation.getConnectorStatus(connectorId)?.status ===
204 ConnectorStatusEnum.Unavailable
205 ) {
206 logger.info(
207 `${this.logPrefix(
208 connectorId
209 )} entered in transaction loop while the connector ${connectorId} status is unavailable`
210 );
211 this.stopConnector(connectorId);
212 break;
213 }
c0560973 214 if (!this.chargingStation?.ocppRequestService) {
e7aeea18
JB
215 logger.info(
216 `${this.logPrefix(
217 connectorId
218 )} transaction loop waiting for charging station service to be initialized`
219 );
c0560973 220 do {
a4cc42ea 221 await Utils.sleep(Constants.CHARGING_STATION_ATG_INITIALIZATION_TIME);
c0560973
JB
222 } while (!this.chargingStation?.ocppRequestService);
223 }
e7aeea18
JB
224 const wait =
225 Utils.getRandomInteger(
ac7f79af
JB
226 this.chargingStation.getAutomaticTransactionGeneratorConfiguration()
227 .maxDelayBetweenTwoTransactions,
228 this.chargingStation.getAutomaticTransactionGeneratorConfiguration()
229 .minDelayBetweenTwoTransactions
e7aeea18
JB
230 ) * 1000;
231 logger.info(
44eb6026 232 `${this.logPrefix(connectorId)} waiting for ${Utils.formatDurationMilliSeconds(wait)}`
e7aeea18 233 );
6af9012e 234 await Utils.sleep(wait);
c37528f1 235 const start = Utils.secureRandom();
ac7f79af
JB
236 if (
237 start <
238 this.chargingStation.getAutomaticTransactionGeneratorConfiguration().probabilityOfStart
239 ) {
9664ec50 240 this.connectorsStatus.get(connectorId).skippedConsecutiveTransactions = 0;
6af9012e 241 // Start transaction
aef1b33a 242 const startResponse = await this.startTransaction(connectorId);
0afed85f 243 if (startResponse?.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
6af9012e 244 // Wait until end of transaction
e7aeea18 245 const waitTrxEnd =
ac7f79af
JB
246 Utils.getRandomInteger(
247 this.chargingStation.getAutomaticTransactionGeneratorConfiguration().maxDuration,
248 this.chargingStation.getAutomaticTransactionGeneratorConfiguration().minDuration
249 ) * 1000;
e7aeea18 250 logger.info(
54ebb82c 251 `${this.logPrefix(connectorId)} transaction started with id ${this.chargingStation
44eb6026 252 .getConnectorStatus(connectorId)
54ebb82c 253 ?.transactionId?.toString()} and will stop in ${Utils.formatDurationMilliSeconds(
44eb6026
JB
254 waitTrxEnd
255 )}`
e7aeea18 256 );
6af9012e
JB
257 await Utils.sleep(waitTrxEnd);
258 // Stop transaction
e7aeea18 259 logger.info(
54ebb82c 260 `${this.logPrefix(connectorId)} stop transaction with id ${this.chargingStation
44eb6026 261 .getConnectorStatus(connectorId)
1895299d 262 ?.transactionId?.toString()}`
e7aeea18 263 );
85d20667 264 await this.stopTransaction(connectorId);
6af9012e
JB
265 }
266 } else {
9664ec50
JB
267 this.connectorsStatus.get(connectorId).skippedConsecutiveTransactions++;
268 this.connectorsStatus.get(connectorId).skippedTransactions++;
e7aeea18 269 logger.info(
44eb6026
JB
270 `${this.logPrefix(connectorId)} skipped consecutively ${this.connectorsStatus
271 .get(connectorId)
1895299d 272 ?.skippedConsecutiveTransactions?.toString()}/${this.connectorsStatus
44eb6026 273 .get(connectorId)
1895299d 274 ?.skippedTransactions?.toString()} transaction(s)`
e7aeea18 275 );
6af9012e 276 }
9664ec50 277 this.connectorsStatus.get(connectorId).lastRunDate = new Date();
7d75bee1 278 }
9664ec50 279 this.connectorsStatus.get(connectorId).stoppedDate = new Date();
e7aeea18 280 logger.info(
44eb6026
JB
281 `${this.logPrefix(
282 connectorId
283 )} stopped on connector and lasted for ${Utils.formatDurationMilliSeconds(
284 this.connectorsStatus.get(connectorId).stoppedDate.getTime() -
285 this.connectorsStatus.get(connectorId).startDate.getTime()
286 )}`
e7aeea18
JB
287 );
288 logger.debug(
be9ee554 289 `${this.logPrefix(connectorId)} connector status: %j`,
e7aeea18
JB
290 this.connectorsStatus.get(connectorId)
291 );
6af9012e
JB
292 }
293
083fb002 294 private setStartConnectorStatus(connectorId: number): void {
9664ec50 295 this.connectorsStatus.get(connectorId).skippedConsecutiveTransactions = 0;
e7aeea18 296 const previousRunDuration =
72092cfc
JB
297 this.connectorsStatus.get(connectorId)?.startDate &&
298 this.connectorsStatus.get(connectorId)?.lastRunDate
e7aeea18
JB
299 ? this.connectorsStatus.get(connectorId).lastRunDate.getTime() -
300 this.connectorsStatus.get(connectorId).startDate.getTime()
301 : 0;
9664ec50 302 this.connectorsStatus.get(connectorId).startDate = new Date();
e7aeea18
JB
303 this.connectorsStatus.get(connectorId).stopDate = new Date(
304 this.connectorsStatus.get(connectorId).startDate.getTime() +
ac7f79af 305 (this.chargingStation.getAutomaticTransactionGeneratorConfiguration().stopAfterHours ??
e7aeea18
JB
306 Constants.CHARGING_STATION_ATG_DEFAULT_STOP_AFTER_HOURS) *
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 });
071a9315 383 this.connectorsStatus.get(connectorId).authorizeRequests++;
5fdab605 384 if (authorizeResponse?.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
071a9315 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 }
071a9315 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);
071a9315 435 this.connectorsStatus.get(connectorId).stopTransactionRequests++;
0afed85f 436 if (stopResponse?.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
6d9876e7
JB
437 this.connectorsStatus.get(connectorId).acceptedStopTransactionRequests++;
438 } else {
439 this.connectorsStatus.get(connectorId).rejectedStopTransactionRequests++;
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 {
471 this.connectorsStatus.get(connectorId).startTransactionRequests++;
472 if (startResponse?.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
473 this.connectorsStatus.get(connectorId).acceptedStartTransactionRequests++;
474 } else {
44eb6026 475 logger.warn(`${this.logPrefix(connectorId)} start transaction rejected`);
d9ac47ef
JB
476 this.connectorsStatus.get(connectorId).rejectedStartTransactionRequests++;
477 }
478 }
6af9012e 479}