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