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