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