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