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