8bb33b81b65ec0336722d546e66f7f25f2f185c3
[e-mobility-charging-stations-simulator.git] / src / charging-station / AutomaticTransactionGenerator.ts
1 // Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
2
3 import { AsyncResource } from 'async_hooks';
4
5 import { type ChargingStation, ChargingStationUtils } from './internal';
6 import { BaseError } from '../exception';
7 // import { PerformanceStatistics } from '../performance';
8 import { PerformanceStatistics } from '../performance/PerformanceStatistics';
9 import {
10 AuthorizationStatus,
11 type AuthorizeRequest,
12 type AuthorizeResponse,
13 type AutomaticTransactionGeneratorConfiguration,
14 IdTagDistribution,
15 RequestCommand,
16 type StartTransactionRequest,
17 type StartTransactionResponse,
18 type Status,
19 StopTransactionReason,
20 type StopTransactionResponse,
21 } from '../types';
22 import { Constants } from '../utils/Constants';
23 import { logger } from '../utils/Logger';
24 import { Utils } from '../utils/Utils';
25
26 const moduleName = 'AutomaticTransactionGenerator';
27
28 export class AutomaticTransactionGenerator extends AsyncResource {
29 private static readonly instances: Map<string, AutomaticTransactionGenerator> = new Map<
30 string,
31 AutomaticTransactionGenerator
32 >();
33
34 public readonly connectorsStatus: Map<number, Status>;
35 public readonly configuration: AutomaticTransactionGeneratorConfiguration;
36 public started: boolean;
37 private readonly chargingStation: ChargingStation;
38 private idTagIndex: number;
39
40 private constructor(
41 automaticTransactionGeneratorConfiguration: AutomaticTransactionGeneratorConfiguration,
42 chargingStation: ChargingStation
43 ) {
44 super(moduleName);
45 this.started = false;
46 this.configuration = automaticTransactionGeneratorConfiguration;
47 this.chargingStation = chargingStation;
48 this.idTagIndex = 0;
49 this.connectorsStatus = new Map<number, Status>();
50 this.initializeConnectorsStatus();
51 }
52
53 public static getInstance(
54 automaticTransactionGeneratorConfiguration: AutomaticTransactionGeneratorConfiguration,
55 chargingStation: ChargingStation
56 ): AutomaticTransactionGenerator | undefined {
57 if (AutomaticTransactionGenerator.instances.has(chargingStation.stationInfo.hashId) === false) {
58 AutomaticTransactionGenerator.instances.set(
59 chargingStation.stationInfo.hashId,
60 new AutomaticTransactionGenerator(
61 automaticTransactionGeneratorConfiguration,
62 chargingStation
63 )
64 );
65 }
66 return AutomaticTransactionGenerator.instances.get(chargingStation.stationInfo.hashId);
67 }
68
69 public start(): void {
70 if (this.checkChargingStation() === false) {
71 return;
72 }
73 if (this.started === true) {
74 logger.warn(`${this.logPrefix()} is already started`);
75 return;
76 }
77 this.startConnectors();
78 this.started = true;
79 }
80
81 public stop(): void {
82 if (this.started === false) {
83 logger.warn(`${this.logPrefix()} is already stopped`);
84 return;
85 }
86 this.stopConnectors();
87 this.started = false;
88 }
89
90 public startConnector(connectorId: number): void {
91 if (this.checkChargingStation(connectorId) === false) {
92 return;
93 }
94 if (this.connectorsStatus.has(connectorId) === false) {
95 logger.error(`${this.logPrefix(connectorId)} starting on non existing connector`);
96 throw new BaseError(`Connector ${connectorId} does not exist`);
97 }
98 if (this.connectorsStatus.get(connectorId)?.start === false) {
99 this.runInAsyncScope(
100 this.internalStartConnector.bind(this) as (
101 this: AutomaticTransactionGenerator,
102 ...args: any[]
103 ) => Promise<void>,
104 this,
105 connectorId
106 ).catch(() => {
107 /* This is intentional */
108 });
109 } else if (this.connectorsStatus.get(connectorId)?.start === true) {
110 logger.warn(`${this.logPrefix(connectorId)} is already started on connector`);
111 }
112 }
113
114 public stopConnector(connectorId: number): void {
115 if (this.connectorsStatus.has(connectorId) === false) {
116 logger.error(`${this.logPrefix(connectorId)} stopping on non existing connector`);
117 throw new BaseError(`Connector ${connectorId} does not exist`);
118 }
119 if (this.connectorsStatus.get(connectorId)?.start === true) {
120 this.connectorsStatus.get(connectorId).start = false;
121 } else if (this.connectorsStatus.get(connectorId)?.start === false) {
122 logger.warn(`${this.logPrefix(connectorId)} is already stopped on connector`);
123 }
124 }
125
126 private startConnectors(): void {
127 if (
128 this.connectorsStatus?.size > 0 &&
129 this.connectorsStatus.size !== this.chargingStation.getNumberOfConnectors()
130 ) {
131 this.connectorsStatus.clear();
132 this.initializeConnectorsStatus();
133 }
134 for (const connectorId of this.chargingStation.connectors.keys()) {
135 if (connectorId > 0) {
136 this.startConnector(connectorId);
137 }
138 }
139 }
140
141 private stopConnectors(): void {
142 for (const connectorId of this.chargingStation.connectors.keys()) {
143 if (connectorId > 0) {
144 this.stopConnector(connectorId);
145 }
146 }
147 }
148
149 private async internalStartConnector(connectorId: number): Promise<void> {
150 this.setStartConnectorStatus(connectorId);
151 logger.info(
152 `${this.logPrefix(
153 connectorId
154 )} started on connector and will run for ${Utils.formatDurationMilliSeconds(
155 this.connectorsStatus.get(connectorId).stopDate.getTime() -
156 this.connectorsStatus.get(connectorId).startDate.getTime()
157 )}`
158 );
159 while (this.connectorsStatus.get(connectorId)?.start === true) {
160 if (new Date() > this.connectorsStatus.get(connectorId).stopDate) {
161 this.stopConnector(connectorId);
162 break;
163 }
164 if (this.chargingStation.isInAcceptedState() === false) {
165 logger.error(
166 `${this.logPrefix(
167 connectorId
168 )} entered in transaction loop while the charging station is not in accepted state`
169 );
170 this.stopConnector(connectorId);
171 break;
172 }
173 if (this.chargingStation.isChargingStationAvailable() === false) {
174 logger.info(
175 `${this.logPrefix(
176 connectorId
177 )} entered in transaction loop while the charging station is unavailable`
178 );
179 this.stopConnector(connectorId);
180 break;
181 }
182 if (this.chargingStation.isConnectorAvailable(connectorId) === false) {
183 logger.info(
184 `${this.logPrefix(
185 connectorId
186 )} entered in transaction loop while the connector ${connectorId} is unavailable`
187 );
188 this.stopConnector(connectorId);
189 break;
190 }
191 if (!this.chargingStation?.ocppRequestService) {
192 logger.info(
193 `${this.logPrefix(
194 connectorId
195 )} transaction loop waiting for charging station service to be initialized`
196 );
197 do {
198 await Utils.sleep(Constants.CHARGING_STATION_ATG_INITIALIZATION_TIME);
199 } while (!this.chargingStation?.ocppRequestService);
200 }
201 const wait =
202 Utils.getRandomInteger(
203 this.configuration.maxDelayBetweenTwoTransactions,
204 this.configuration.minDelayBetweenTwoTransactions
205 ) * 1000;
206 logger.info(
207 `${this.logPrefix(connectorId)} waiting for ${Utils.formatDurationMilliSeconds(wait)}`
208 );
209 await Utils.sleep(wait);
210 const start = Utils.secureRandom();
211 if (start < this.configuration.probabilityOfStart) {
212 this.connectorsStatus.get(connectorId).skippedConsecutiveTransactions = 0;
213 // Start transaction
214 const startResponse = await this.startTransaction(connectorId);
215 if (startResponse?.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
216 // Wait until end of transaction
217 const waitTrxEnd =
218 Utils.getRandomInteger(this.configuration.maxDuration, this.configuration.minDuration) *
219 1000;
220 logger.info(
221 `${this.logPrefix(connectorId)} transaction ${this.chargingStation
222 .getConnectorStatus(connectorId)
223 ?.transactionId?.toString()} started and will stop in ${Utils.formatDurationMilliSeconds(
224 waitTrxEnd
225 )}`
226 );
227 await Utils.sleep(waitTrxEnd);
228 // Stop transaction
229 logger.info(
230 `${this.logPrefix(connectorId)} stop transaction ${this.chargingStation
231 .getConnectorStatus(connectorId)
232 ?.transactionId?.toString()}`
233 );
234 await this.stopTransaction(connectorId);
235 }
236 } else {
237 this.connectorsStatus.get(connectorId).skippedConsecutiveTransactions++;
238 this.connectorsStatus.get(connectorId).skippedTransactions++;
239 logger.info(
240 `${this.logPrefix(connectorId)} skipped consecutively ${this.connectorsStatus
241 .get(connectorId)
242 ?.skippedConsecutiveTransactions?.toString()}/${this.connectorsStatus
243 .get(connectorId)
244 ?.skippedTransactions?.toString()} transaction(s)`
245 );
246 }
247 this.connectorsStatus.get(connectorId).lastRunDate = new Date();
248 }
249 this.connectorsStatus.get(connectorId).stoppedDate = new Date();
250 logger.info(
251 `${this.logPrefix(
252 connectorId
253 )} stopped on connector and lasted for ${Utils.formatDurationMilliSeconds(
254 this.connectorsStatus.get(connectorId).stoppedDate.getTime() -
255 this.connectorsStatus.get(connectorId).startDate.getTime()
256 )}`
257 );
258 logger.debug(
259 `${this.logPrefix(connectorId)} connector status: %j`,
260 this.connectorsStatus.get(connectorId)
261 );
262 }
263
264 private setStartConnectorStatus(connectorId: number): void {
265 this.connectorsStatus.get(connectorId).skippedConsecutiveTransactions = 0;
266 const previousRunDuration =
267 this.connectorsStatus.get(connectorId)?.startDate &&
268 this.connectorsStatus.get(connectorId)?.lastRunDate
269 ? this.connectorsStatus.get(connectorId).lastRunDate.getTime() -
270 this.connectorsStatus.get(connectorId).startDate.getTime()
271 : 0;
272 this.connectorsStatus.get(connectorId).startDate = new Date();
273 this.connectorsStatus.get(connectorId).stopDate = new Date(
274 this.connectorsStatus.get(connectorId).startDate.getTime() +
275 (this.configuration.stopAfterHours ??
276 Constants.CHARGING_STATION_ATG_DEFAULT_STOP_AFTER_HOURS) *
277 3600 *
278 1000 -
279 previousRunDuration
280 );
281 this.connectorsStatus.get(connectorId).start = true;
282 }
283
284 private initializeConnectorsStatus(): void {
285 for (const connectorId of this.chargingStation.connectors.keys()) {
286 if (connectorId > 0) {
287 this.connectorsStatus.set(connectorId, {
288 start: false,
289 authorizeRequests: 0,
290 acceptedAuthorizeRequests: 0,
291 rejectedAuthorizeRequests: 0,
292 startTransactionRequests: 0,
293 acceptedStartTransactionRequests: 0,
294 rejectedStartTransactionRequests: 0,
295 stopTransactionRequests: 0,
296 acceptedStopTransactionRequests: 0,
297 rejectedStopTransactionRequests: 0,
298 skippedConsecutiveTransactions: 0,
299 skippedTransactions: 0,
300 });
301 }
302 }
303 }
304
305 private async startTransaction(
306 connectorId: number
307 ): Promise<StartTransactionResponse | undefined> {
308 const measureId = 'StartTransaction with ATG';
309 const beginId = PerformanceStatistics.beginMeasure(measureId);
310 let startResponse: StartTransactionResponse;
311 if (this.chargingStation.hasAuthorizedTags()) {
312 const idTag = this.getIdTag(connectorId);
313 const startTransactionLogMsg = `${this.logPrefix(
314 connectorId
315 )} start transaction with an idTag '${idTag}'`;
316 if (this.getRequireAuthorize()) {
317 this.chargingStation.getConnectorStatus(connectorId).authorizeIdTag = idTag;
318 // Authorize idTag
319 const authorizeResponse: AuthorizeResponse =
320 await this.chargingStation.ocppRequestService.requestHandler<
321 AuthorizeRequest,
322 AuthorizeResponse
323 >(this.chargingStation, RequestCommand.AUTHORIZE, {
324 idTag,
325 });
326 this.connectorsStatus.get(connectorId).authorizeRequests++;
327 if (authorizeResponse?.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
328 this.connectorsStatus.get(connectorId).acceptedAuthorizeRequests++;
329 logger.info(startTransactionLogMsg);
330 // Start transaction
331 startResponse = await this.chargingStation.ocppRequestService.requestHandler<
332 StartTransactionRequest,
333 StartTransactionResponse
334 >(this.chargingStation, RequestCommand.START_TRANSACTION, {
335 connectorId,
336 idTag,
337 });
338 this.handleStartTransactionResponse(connectorId, startResponse);
339 PerformanceStatistics.endMeasure(measureId, beginId);
340 return startResponse;
341 }
342 this.connectorsStatus.get(connectorId).rejectedAuthorizeRequests++;
343 PerformanceStatistics.endMeasure(measureId, beginId);
344 return startResponse;
345 }
346 logger.info(startTransactionLogMsg);
347 // Start transaction
348 startResponse = await this.chargingStation.ocppRequestService.requestHandler<
349 StartTransactionRequest,
350 StartTransactionResponse
351 >(this.chargingStation, RequestCommand.START_TRANSACTION, {
352 connectorId,
353 idTag,
354 });
355 this.handleStartTransactionResponse(connectorId, startResponse);
356 PerformanceStatistics.endMeasure(measureId, beginId);
357 return startResponse;
358 }
359 logger.info(`${this.logPrefix(connectorId)} start transaction without an idTag`);
360 startResponse = await this.chargingStation.ocppRequestService.requestHandler<
361 StartTransactionRequest,
362 StartTransactionResponse
363 >(this.chargingStation, RequestCommand.START_TRANSACTION, { connectorId });
364 this.handleStartTransactionResponse(connectorId, startResponse);
365 PerformanceStatistics.endMeasure(measureId, beginId);
366 return startResponse;
367 }
368
369 private async stopTransaction(
370 connectorId: number,
371 reason: StopTransactionReason = StopTransactionReason.LOCAL
372 ): Promise<StopTransactionResponse> {
373 const measureId = 'StopTransaction with ATG';
374 const beginId = PerformanceStatistics.beginMeasure(measureId);
375 let stopResponse: StopTransactionResponse;
376 if (this.chargingStation.getConnectorStatus(connectorId)?.transactionStarted === true) {
377 stopResponse = await this.chargingStation.stopTransactionOnConnector(connectorId, reason);
378 this.connectorsStatus.get(connectorId).stopTransactionRequests++;
379 if (stopResponse?.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
380 this.connectorsStatus.get(connectorId).acceptedStopTransactionRequests++;
381 } else {
382 this.connectorsStatus.get(connectorId).rejectedStopTransactionRequests++;
383 }
384 } else {
385 const transactionId = this.chargingStation.getConnectorStatus(connectorId)?.transactionId;
386 logger.warn(
387 `${this.logPrefix(connectorId)} stopping a not started transaction${
388 !Utils.isNullOrUndefined(transactionId) ? ` ${transactionId?.toString()}` : ''
389 }`
390 );
391 }
392 PerformanceStatistics.endMeasure(measureId, beginId);
393 return stopResponse;
394 }
395
396 private getRequireAuthorize(): boolean {
397 return this.configuration?.requireAuthorize ?? true;
398 }
399
400 private getRandomIdTag(authorizationFile: string): string {
401 const tags = this.chargingStation.authorizedTagsCache.getAuthorizedTags(authorizationFile);
402 this.idTagIndex = Math.floor(Utils.secureRandom() * tags.length);
403 return tags[this.idTagIndex];
404 }
405
406 private getRoundRobinIdTag(authorizationFile: string): string {
407 const tags = this.chargingStation.authorizedTagsCache.getAuthorizedTags(authorizationFile);
408 const idTag = tags[this.idTagIndex];
409 this.idTagIndex = this.idTagIndex === tags.length - 1 ? 0 : this.idTagIndex + 1;
410 return idTag;
411 }
412
413 private getConnectorAffinityIdTag(authorizationFile: string, connectorId: number): string {
414 const tags = this.chargingStation.authorizedTagsCache.getAuthorizedTags(authorizationFile);
415 this.idTagIndex = (this.chargingStation.index - 1 + (connectorId - 1)) % tags.length;
416 return tags[this.idTagIndex];
417 }
418
419 private getIdTag(connectorId: number): string {
420 const authorizationFile = ChargingStationUtils.getAuthorizationFile(
421 this.chargingStation.stationInfo
422 );
423 switch (this.configuration?.idTagDistribution) {
424 case IdTagDistribution.RANDOM:
425 return this.getRandomIdTag(authorizationFile);
426 case IdTagDistribution.ROUND_ROBIN:
427 return this.getRoundRobinIdTag(authorizationFile);
428 case IdTagDistribution.CONNECTOR_AFFINITY:
429 return this.getConnectorAffinityIdTag(authorizationFile, connectorId);
430 default:
431 return this.getRoundRobinIdTag(authorizationFile);
432 }
433 }
434
435 private logPrefix = (connectorId?: number): string => {
436 return Utils.logPrefix(
437 ` ${this.chargingStation.stationInfo.chargingStationId} | ATG${
438 connectorId !== undefined ? ` on connector #${connectorId.toString()}` : ''
439 }:`
440 );
441 };
442
443 private handleStartTransactionResponse(
444 connectorId: number,
445 startResponse: StartTransactionResponse
446 ): void {
447 this.connectorsStatus.get(connectorId).startTransactionRequests++;
448 if (startResponse?.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
449 this.connectorsStatus.get(connectorId).acceptedStartTransactionRequests++;
450 } else {
451 logger.warn(`${this.logPrefix(connectorId)} start transaction rejected`);
452 this.connectorsStatus.get(connectorId).rejectedStartTransactionRequests++;
453 }
454 }
455
456 private checkChargingStation(connectorId?: number): boolean {
457 if (this.chargingStation.started === false && this.chargingStation.starting === false) {
458 logger.warn(`${this.logPrefix(connectorId)} charging station is stopped, cannot proceed`);
459 return false;
460 }
461 return true;
462 }
463 }