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