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