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