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