Permit to run code in async scope in the OCPP stack
[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[]
99 ) => void,
d4b944ae
JB
100 this,
101 connectorId
102 );
ecb3869d 103 } else if (this.connectorsStatus.get(connectorId)?.start === true) {
ba7965c4 104 logger.warn(`${this.logPrefix(connectorId)} is already started on connector`);
a5e9befc
JB
105 }
106 }
107
108 public stopConnector(connectorId: number): void {
7807ccf2 109 if (this.connectorsStatus.has(connectorId) === false) {
a03a128d 110 logger.error(`${this.logPrefix(connectorId)} stopping on non existing connector`);
7807ccf2 111 throw new BaseError(`Connector ${connectorId} does not exist`);
ba7965c4
JB
112 }
113 if (this.connectorsStatus.get(connectorId)?.start === true) {
7807ccf2 114 this.connectorsStatus.get(connectorId).start = false;
ba7965c4
JB
115 } else if (this.connectorsStatus.get(connectorId)?.start === false) {
116 logger.warn(`${this.logPrefix(connectorId)} is already stopped on connector`);
117 }
a5e9befc
JB
118 }
119
72740232 120 private startConnectors(): void {
e7aeea18
JB
121 if (
122 this.connectorsStatus?.size > 0 &&
123 this.connectorsStatus.size !== this.chargingStation.getNumberOfConnectors()
124 ) {
54544ef1 125 this.connectorsStatus.clear();
7807ccf2 126 this.initializeConnectorsStatus();
54544ef1 127 }
734d790d 128 for (const connectorId of this.chargingStation.connectors.keys()) {
72740232 129 if (connectorId > 0) {
83a3286a 130 this.startConnector(connectorId);
72740232
JB
131 }
132 }
133 }
134
135 private stopConnectors(): void {
734d790d 136 for (const connectorId of this.chargingStation.connectors.keys()) {
72740232
JB
137 if (connectorId > 0) {
138 this.stopConnector(connectorId);
139 }
140 }
141 }
142
83a3286a 143 private async internalStartConnector(connectorId: number): Promise<void> {
083fb002 144 this.setStartConnectorStatus(connectorId);
e7aeea18
JB
145 logger.info(
146 this.logPrefix(connectorId) +
147 ' started on connector and will run for ' +
148 Utils.formatDurationMilliSeconds(
149 this.connectorsStatus.get(connectorId).stopDate.getTime() -
150 this.connectorsStatus.get(connectorId).startDate.getTime()
151 )
152 );
a5e9befc 153 while (this.connectorsStatus.get(connectorId).start === true) {
e7aeea18 154 if (new Date() > this.connectorsStatus.get(connectorId).stopDate) {
9664ec50 155 this.stopConnector(connectorId);
17991e8c
JB
156 break;
157 }
1789ba2c 158 if (this.chargingStation.isInAcceptedState() === false) {
e7aeea18
JB
159 logger.error(
160 this.logPrefix(connectorId) +
161 ' entered in transaction loop while the charging station is not in accepted state'
162 );
9664ec50 163 this.stopConnector(connectorId);
17991e8c
JB
164 break;
165 }
1789ba2c 166 if (this.chargingStation.isChargingStationAvailable() === false) {
e7aeea18
JB
167 logger.info(
168 this.logPrefix(connectorId) +
169 ' entered in transaction loop while the charging station is unavailable'
170 );
9664ec50 171 this.stopConnector(connectorId);
ab5f4b03
JB
172 break;
173 }
1789ba2c 174 if (this.chargingStation.isConnectorAvailable(connectorId) === false) {
e7aeea18
JB
175 logger.info(
176 `${this.logPrefix(
177 connectorId
178 )} entered in transaction loop while the connector ${connectorId} is unavailable`
179 );
9c7195b2 180 this.stopConnector(connectorId);
17991e8c
JB
181 break;
182 }
c0560973 183 if (!this.chargingStation?.ocppRequestService) {
e7aeea18
JB
184 logger.info(
185 `${this.logPrefix(
186 connectorId
187 )} transaction loop waiting for charging station service to be initialized`
188 );
c0560973 189 do {
a4cc42ea 190 await Utils.sleep(Constants.CHARGING_STATION_ATG_INITIALIZATION_TIME);
c0560973
JB
191 } while (!this.chargingStation?.ocppRequestService);
192 }
e7aeea18
JB
193 const wait =
194 Utils.getRandomInteger(
fa7bccf4
JB
195 this.configuration.maxDelayBetweenTwoTransactions,
196 this.configuration.minDelayBetweenTwoTransactions
e7aeea18
JB
197 ) * 1000;
198 logger.info(
199 this.logPrefix(connectorId) + ' waiting for ' + Utils.formatDurationMilliSeconds(wait)
200 );
6af9012e 201 await Utils.sleep(wait);
c37528f1 202 const start = Utils.secureRandom();
fa7bccf4 203 if (start < this.configuration.probabilityOfStart) {
9664ec50 204 this.connectorsStatus.get(connectorId).skippedConsecutiveTransactions = 0;
6af9012e 205 // Start transaction
aef1b33a 206 const startResponse = await this.startTransaction(connectorId);
0afed85f 207 if (startResponse?.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
6af9012e 208 // Wait until end of transaction
e7aeea18 209 const waitTrxEnd =
fa7bccf4
JB
210 Utils.getRandomInteger(this.configuration.maxDuration, this.configuration.minDuration) *
211 1000;
e7aeea18
JB
212 logger.info(
213 this.logPrefix(connectorId) +
214 ' transaction ' +
215 this.chargingStation.getConnectorStatus(connectorId).transactionId.toString() +
216 ' started and will stop in ' +
217 Utils.formatDurationMilliSeconds(waitTrxEnd)
218 );
6af9012e
JB
219 await Utils.sleep(waitTrxEnd);
220 // Stop transaction
e7aeea18
JB
221 logger.info(
222 this.logPrefix(connectorId) +
223 ' stop transaction ' +
224 this.chargingStation.getConnectorStatus(connectorId).transactionId.toString()
225 );
85d20667 226 await this.stopTransaction(connectorId);
6af9012e
JB
227 }
228 } else {
9664ec50
JB
229 this.connectorsStatus.get(connectorId).skippedConsecutiveTransactions++;
230 this.connectorsStatus.get(connectorId).skippedTransactions++;
e7aeea18
JB
231 logger.info(
232 this.logPrefix(connectorId) +
233 ' skipped consecutively ' +
234 this.connectorsStatus.get(connectorId).skippedConsecutiveTransactions.toString() +
235 '/' +
236 this.connectorsStatus.get(connectorId).skippedTransactions.toString() +
237 ' transaction(s)'
238 );
6af9012e 239 }
9664ec50 240 this.connectorsStatus.get(connectorId).lastRunDate = new Date();
7d75bee1 241 }
9664ec50 242 this.connectorsStatus.get(connectorId).stoppedDate = new Date();
e7aeea18
JB
243 logger.info(
244 this.logPrefix(connectorId) +
245 ' stopped on connector and lasted for ' +
246 Utils.formatDurationMilliSeconds(
247 this.connectorsStatus.get(connectorId).stoppedDate.getTime() -
248 this.connectorsStatus.get(connectorId).startDate.getTime()
249 )
250 );
251 logger.debug(
be9ee554 252 `${this.logPrefix(connectorId)} connector status: %j`,
e7aeea18
JB
253 this.connectorsStatus.get(connectorId)
254 );
6af9012e
JB
255 }
256
083fb002 257 private setStartConnectorStatus(connectorId: number): void {
9664ec50 258 this.connectorsStatus.get(connectorId).skippedConsecutiveTransactions = 0;
e7aeea18
JB
259 const previousRunDuration =
260 this?.connectorsStatus.get(connectorId)?.startDate &&
261 this?.connectorsStatus.get(connectorId)?.lastRunDate
262 ? this.connectorsStatus.get(connectorId).lastRunDate.getTime() -
263 this.connectorsStatus.get(connectorId).startDate.getTime()
264 : 0;
9664ec50 265 this.connectorsStatus.get(connectorId).startDate = new Date();
e7aeea18
JB
266 this.connectorsStatus.get(connectorId).stopDate = new Date(
267 this.connectorsStatus.get(connectorId).startDate.getTime() +
fa7bccf4 268 (this.configuration.stopAfterHours ??
e7aeea18
JB
269 Constants.CHARGING_STATION_ATG_DEFAULT_STOP_AFTER_HOURS) *
270 3600 *
271 1000 -
272 previousRunDuration
273 );
083fb002 274 this.connectorsStatus.get(connectorId).start = true;
4dff3039
JB
275 }
276
7807ccf2 277 private initializeConnectorsStatus(): void {
4dff3039
JB
278 for (const connectorId of this.chargingStation.connectors.keys()) {
279 if (connectorId > 0) {
7807ccf2
JB
280 this.connectorsStatus.set(connectorId, {
281 start: false,
282 authorizeRequests: 0,
283 acceptedAuthorizeRequests: 0,
284 rejectedAuthorizeRequests: 0,
285 startTransactionRequests: 0,
286 acceptedStartTransactionRequests: 0,
287 rejectedStartTransactionRequests: 0,
288 stopTransactionRequests: 0,
289 acceptedStopTransactionRequests: 0,
290 rejectedStopTransactionRequests: 0,
291 skippedConsecutiveTransactions: 0,
292 skippedTransactions: 0,
293 });
4dff3039
JB
294 }
295 }
72740232
JB
296 }
297
e7aeea18
JB
298 private async startTransaction(
299 connectorId: number
0afed85f 300 ): Promise<StartTransactionResponse | undefined> {
aef1b33a
JB
301 const measureId = 'StartTransaction with ATG';
302 const beginId = PerformanceStatistics.beginMeasure(measureId);
303 let startResponse: StartTransactionResponse;
304 if (this.chargingStation.hasAuthorizedTags()) {
c72f6634 305 const idTag = this.getIdTag(connectorId);
5cf9050d
JB
306 const startTransactionLogMsg = `${this.logPrefix(
307 connectorId
ba7965c4 308 )} start transaction with an idTag '${idTag}'`;
ccb1d6e9 309 if (this.getRequireAuthorize()) {
2e3d65ae 310 this.chargingStation.getConnectorStatus(connectorId).authorizeIdTag = idTag;
f4bf2abd 311 // Authorize idTag
2e3d65ae 312 const authorizeResponse: AuthorizeResponse =
f7f98c68 313 await this.chargingStation.ocppRequestService.requestHandler<
ef6fa3fb
JB
314 AuthorizeRequest,
315 AuthorizeResponse
08f130a0 316 >(this.chargingStation, RequestCommand.AUTHORIZE, {
ef6fa3fb
JB
317 idTag,
318 });
071a9315 319 this.connectorsStatus.get(connectorId).authorizeRequests++;
5fdab605 320 if (authorizeResponse?.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
071a9315 321 this.connectorsStatus.get(connectorId).acceptedAuthorizeRequests++;
5cf9050d 322 logger.info(startTransactionLogMsg);
5fdab605 323 // Start transaction
f7f98c68 324 startResponse = await this.chargingStation.ocppRequestService.requestHandler<
ef6fa3fb
JB
325 StartTransactionRequest,
326 StartTransactionResponse
08f130a0 327 >(this.chargingStation, RequestCommand.START_TRANSACTION, {
ef6fa3fb
JB
328 connectorId,
329 idTag,
330 });
d9ac47ef 331 this.handleStartTransactionResponse(connectorId, startResponse);
aef1b33a
JB
332 PerformanceStatistics.endMeasure(measureId, beginId);
333 return startResponse;
5fdab605 334 }
071a9315 335 this.connectorsStatus.get(connectorId).rejectedAuthorizeRequests++;
aef1b33a 336 PerformanceStatistics.endMeasure(measureId, beginId);
0afed85f 337 return startResponse;
ef6076c1 338 }
5cf9050d 339 logger.info(startTransactionLogMsg);
5fdab605 340 // Start transaction
f7f98c68 341 startResponse = await this.chargingStation.ocppRequestService.requestHandler<
ef6fa3fb
JB
342 StartTransactionRequest,
343 StartTransactionResponse
08f130a0 344 >(this.chargingStation, RequestCommand.START_TRANSACTION, {
ef6fa3fb
JB
345 connectorId,
346 idTag,
347 });
d9ac47ef 348 this.handleStartTransactionResponse(connectorId, startResponse);
aef1b33a
JB
349 PerformanceStatistics.endMeasure(measureId, beginId);
350 return startResponse;
6af9012e 351 }
5cf9050d 352 logger.info(`${this.logPrefix(connectorId)} start transaction without an idTag`);
f7f98c68 353 startResponse = await this.chargingStation.ocppRequestService.requestHandler<
ef6fa3fb
JB
354 StartTransactionRequest,
355 StartTransactionResponse
08f130a0 356 >(this.chargingStation, RequestCommand.START_TRANSACTION, { connectorId });
431b6bd5 357 this.handleStartTransactionResponse(connectorId, startResponse);
aef1b33a
JB
358 PerformanceStatistics.endMeasure(measureId, beginId);
359 return startResponse;
6af9012e
JB
360 }
361
e7aeea18
JB
362 private async stopTransaction(
363 connectorId: number,
5e3cb728 364 reason: StopTransactionReason = StopTransactionReason.LOCAL
e7aeea18 365 ): Promise<StopTransactionResponse> {
aef1b33a
JB
366 const measureId = 'StopTransaction with ATG';
367 const beginId = PerformanceStatistics.beginMeasure(measureId);
0045cef5 368 let stopResponse: StopTransactionResponse;
6d9876e7 369 if (this.chargingStation.getConnectorStatus(connectorId)?.transactionStarted === true) {
5e3cb728 370 stopResponse = await this.chargingStation.stopTransactionOnConnector(connectorId, reason);
071a9315 371 this.connectorsStatus.get(connectorId).stopTransactionRequests++;
0afed85f 372 if (stopResponse?.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
6d9876e7
JB
373 this.connectorsStatus.get(connectorId).acceptedStopTransactionRequests++;
374 } else {
375 this.connectorsStatus.get(connectorId).rejectedStopTransactionRequests++;
376 }
0045cef5 377 } else {
5e3cb728 378 const transactionId = this.chargingStation.getConnectorStatus(connectorId).transactionId;
e7aeea18 379 logger.warn(
ba7965c4 380 `${this.logPrefix(connectorId)} stopping a not started transaction${
e7aeea18
JB
381 transactionId ? ' ' + transactionId.toString() : ''
382 }`
383 );
0045cef5 384 }
aef1b33a
JB
385 PerformanceStatistics.endMeasure(measureId, beginId);
386 return stopResponse;
c0560973
JB
387 }
388
ccb1d6e9 389 private getRequireAuthorize(): boolean {
fa7bccf4 390 return this.configuration?.requireAuthorize ?? true;
ccb1d6e9
JB
391 }
392
c72f6634
JB
393 private getRandomIdTag(authorizationFile: string): string {
394 const tags = this.chargingStation.authorizedTagsCache.getAuthorizedTags(authorizationFile);
395 this.idTagIndex = Math.floor(Utils.secureRandom() * tags.length);
396 return tags[this.idTagIndex];
397 }
398
399 private getRoundRobinIdTag(authorizationFile: string): string {
400 const tags = this.chargingStation.authorizedTagsCache.getAuthorizedTags(authorizationFile);
401 const idTag = tags[this.idTagIndex];
402 this.idTagIndex = this.idTagIndex === tags.length - 1 ? 0 : this.idTagIndex + 1;
403 return idTag;
404 }
405
406 private getConnectorAffinityIdTag(authorizationFile: string, connectorId: number): string {
407 const tags = this.chargingStation.authorizedTagsCache.getAuthorizedTags(authorizationFile);
408 this.idTagIndex = (this.chargingStation.index - 1 + (connectorId - 1)) % tags.length;
409 return tags[this.idTagIndex];
410 }
411
412 private getIdTag(connectorId: number): string {
413 const authorizationFile = ChargingStationUtils.getAuthorizationFile(
414 this.chargingStation.stationInfo
415 );
416 switch (this.configuration?.idTagDistribution) {
417 case IdTagDistribution.RANDOM:
418 return this.getRandomIdTag(authorizationFile);
419 case IdTagDistribution.ROUND_ROBIN:
420 return this.getRoundRobinIdTag(authorizationFile);
421 case IdTagDistribution.CONNECTOR_AFFINITY:
422 return this.getConnectorAffinityIdTag(authorizationFile, connectorId);
423 default:
424 return this.getRoundRobinIdTag(authorizationFile);
425 }
426 }
427
6e0964c8 428 private logPrefix(connectorId?: number): string {
6cd85def
JB
429 return Utils.logPrefix(
430 ` ${this.chargingStation.stationInfo.chargingStationId} | ATG${
a5e9befc 431 connectorId !== undefined ? ` on connector #${connectorId.toString()}` : ''
6cd85def
JB
432 }:`
433 );
6af9012e 434 }
d9ac47ef
JB
435
436 private handleStartTransactionResponse(
437 connectorId: number,
438 startResponse: StartTransactionResponse
439 ): void {
440 this.connectorsStatus.get(connectorId).startTransactionRequests++;
441 if (startResponse?.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
442 this.connectorsStatus.get(connectorId).acceptedStartTransactionRequests++;
443 } else {
444 logger.warn(this.logPrefix(connectorId) + ' start transaction rejected');
445 this.connectorsStatus.get(connectorId).rejectedStartTransactionRequests++;
446 }
447 }
6af9012e 448}