fix: fix CS configuration hash calculation
[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()) {
5ced7e80 319 this.connectorsStatus.set(connectorId, this.getConnectorStatus(connectorId));
4334db72
JB
320 }
321 }
322 }
323 } else {
324 for (const connectorId of this.chargingStation.connectors.keys()) {
325 if (connectorId > 0) {
5ced7e80 326 this.connectorsStatus.set(connectorId, this.getConnectorStatus(connectorId));
4334db72 327 }
4dff3039
JB
328 }
329 }
72740232
JB
330 }
331
5ced7e80
JB
332 private getConnectorStatus(connectorId: number): Status {
333 const connectorStatus =
334 this.chargingStation.getAutomaticTransactionGeneratorStatuses()[connectorId];
335 delete connectorStatus?.startDate;
336 delete connectorStatus?.lastRunDate;
337 delete connectorStatus?.stopDate;
338 delete connectorStatus?.stoppedDate;
339 return (
340 connectorStatus ?? {
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 );
355 }
356
e7aeea18
JB
357 private async startTransaction(
358 connectorId: number
0afed85f 359 ): Promise<StartTransactionResponse | undefined> {
aef1b33a
JB
360 const measureId = 'StartTransaction with ATG';
361 const beginId = PerformanceStatistics.beginMeasure(measureId);
362 let startResponse: StartTransactionResponse;
f911a4af
JB
363 if (this.chargingStation.hasIdTags()) {
364 const idTag = IdTagsCache.getInstance().getIdTag(
ac7f79af 365 this.chargingStation.getAutomaticTransactionGeneratorConfiguration()?.idTagDistribution,
aaf2bf9c
JB
366 this.chargingStation,
367 connectorId
368 );
5cf9050d
JB
369 const startTransactionLogMsg = `${this.logPrefix(
370 connectorId
ba7965c4 371 )} start transaction with an idTag '${idTag}'`;
ccb1d6e9 372 if (this.getRequireAuthorize()) {
2e3d65ae 373 this.chargingStation.getConnectorStatus(connectorId).authorizeIdTag = idTag;
f4bf2abd 374 // Authorize idTag
2e3d65ae 375 const authorizeResponse: AuthorizeResponse =
f7f98c68 376 await this.chargingStation.ocppRequestService.requestHandler<
ef6fa3fb
JB
377 AuthorizeRequest,
378 AuthorizeResponse
08f130a0 379 >(this.chargingStation, RequestCommand.AUTHORIZE, {
ef6fa3fb
JB
380 idTag,
381 });
1fe0632a 382 ++this.connectorsStatus.get(connectorId).authorizeRequests;
5fdab605 383 if (authorizeResponse?.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
1fe0632a 384 ++this.connectorsStatus.get(connectorId).acceptedAuthorizeRequests;
5cf9050d 385 logger.info(startTransactionLogMsg);
5fdab605 386 // Start transaction
f7f98c68 387 startResponse = await this.chargingStation.ocppRequestService.requestHandler<
ef6fa3fb
JB
388 StartTransactionRequest,
389 StartTransactionResponse
08f130a0 390 >(this.chargingStation, RequestCommand.START_TRANSACTION, {
ef6fa3fb
JB
391 connectorId,
392 idTag,
393 });
d9ac47ef 394 this.handleStartTransactionResponse(connectorId, startResponse);
aef1b33a
JB
395 PerformanceStatistics.endMeasure(measureId, beginId);
396 return startResponse;
5fdab605 397 }
1fe0632a 398 ++this.connectorsStatus.get(connectorId).rejectedAuthorizeRequests;
aef1b33a 399 PerformanceStatistics.endMeasure(measureId, beginId);
0afed85f 400 return startResponse;
ef6076c1 401 }
5cf9050d 402 logger.info(startTransactionLogMsg);
5fdab605 403 // Start transaction
f7f98c68 404 startResponse = await this.chargingStation.ocppRequestService.requestHandler<
ef6fa3fb
JB
405 StartTransactionRequest,
406 StartTransactionResponse
08f130a0 407 >(this.chargingStation, RequestCommand.START_TRANSACTION, {
ef6fa3fb
JB
408 connectorId,
409 idTag,
410 });
d9ac47ef 411 this.handleStartTransactionResponse(connectorId, startResponse);
aef1b33a
JB
412 PerformanceStatistics.endMeasure(measureId, beginId);
413 return startResponse;
6af9012e 414 }
5cf9050d 415 logger.info(`${this.logPrefix(connectorId)} start transaction without an idTag`);
f7f98c68 416 startResponse = await this.chargingStation.ocppRequestService.requestHandler<
ef6fa3fb
JB
417 StartTransactionRequest,
418 StartTransactionResponse
08f130a0 419 >(this.chargingStation, RequestCommand.START_TRANSACTION, { connectorId });
431b6bd5 420 this.handleStartTransactionResponse(connectorId, startResponse);
aef1b33a
JB
421 PerformanceStatistics.endMeasure(measureId, beginId);
422 return startResponse;
6af9012e
JB
423 }
424
e7aeea18
JB
425 private async stopTransaction(
426 connectorId: number,
5e3cb728 427 reason: StopTransactionReason = StopTransactionReason.LOCAL
e7aeea18 428 ): Promise<StopTransactionResponse> {
aef1b33a
JB
429 const measureId = 'StopTransaction with ATG';
430 const beginId = PerformanceStatistics.beginMeasure(measureId);
0045cef5 431 let stopResponse: StopTransactionResponse;
6d9876e7 432 if (this.chargingStation.getConnectorStatus(connectorId)?.transactionStarted === true) {
5e3cb728 433 stopResponse = await this.chargingStation.stopTransactionOnConnector(connectorId, reason);
1fe0632a 434 ++this.connectorsStatus.get(connectorId).stopTransactionRequests;
0afed85f 435 if (stopResponse?.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
1fe0632a 436 ++this.connectorsStatus.get(connectorId).acceptedStopTransactionRequests;
6d9876e7 437 } else {
1fe0632a 438 ++this.connectorsStatus.get(connectorId).rejectedStopTransactionRequests;
6d9876e7 439 }
0045cef5 440 } else {
1895299d 441 const transactionId = this.chargingStation.getConnectorStatus(connectorId)?.transactionId;
e7aeea18 442 logger.warn(
ba7965c4 443 `${this.logPrefix(connectorId)} stopping a not started transaction${
54ebb82c 444 !Utils.isNullOrUndefined(transactionId) ? ` with id ${transactionId?.toString()}` : ''
e7aeea18
JB
445 }`
446 );
0045cef5 447 }
aef1b33a
JB
448 PerformanceStatistics.endMeasure(measureId, beginId);
449 return stopResponse;
c0560973
JB
450 }
451
ccb1d6e9 452 private getRequireAuthorize(): boolean {
ac7f79af
JB
453 return (
454 this.chargingStation.getAutomaticTransactionGeneratorConfiguration()?.requireAuthorize ?? true
455 );
ccb1d6e9
JB
456 }
457
8b7072dc 458 private logPrefix = (connectorId?: number): string => {
6cd85def
JB
459 return Utils.logPrefix(
460 ` ${this.chargingStation.stationInfo.chargingStationId} | ATG${
9bb1159e 461 !Utils.isNullOrUndefined(connectorId) ? ` on connector #${connectorId.toString()}` : ''
6cd85def
JB
462 }:`
463 );
8b7072dc 464 };
d9ac47ef
JB
465
466 private handleStartTransactionResponse(
467 connectorId: number,
468 startResponse: StartTransactionResponse
469 ): void {
1fe0632a 470 ++this.connectorsStatus.get(connectorId).startTransactionRequests;
d9ac47ef 471 if (startResponse?.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
1fe0632a 472 ++this.connectorsStatus.get(connectorId).acceptedStartTransactionRequests;
d9ac47ef 473 } else {
44eb6026 474 logger.warn(`${this.logPrefix(connectorId)} start transaction rejected`);
1fe0632a 475 ++this.connectorsStatus.get(connectorId).rejectedStartTransactionRequests;
d9ac47ef
JB
476 }
477 }
6af9012e 478}