UI Server: Cleanup commands handling initialization
[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
JB
95 this.runInAsyncScope(
96 this.internalStartConnector.bind(this) as (this: this, ...args: any[]) => unknown,
97 this,
98 connectorId
99 );
ecb3869d 100 } else if (this.connectorsStatus.get(connectorId)?.start === true) {
ba7965c4 101 logger.warn(`${this.logPrefix(connectorId)} is already started on connector`);
a5e9befc
JB
102 }
103 }
104
105 public stopConnector(connectorId: number): void {
7807ccf2 106 if (this.connectorsStatus.has(connectorId) === false) {
a03a128d 107 logger.error(`${this.logPrefix(connectorId)} stopping on non existing connector`);
7807ccf2 108 throw new BaseError(`Connector ${connectorId} does not exist`);
ba7965c4
JB
109 }
110 if (this.connectorsStatus.get(connectorId)?.start === true) {
7807ccf2 111 this.connectorsStatus.get(connectorId).start = false;
ba7965c4
JB
112 } else if (this.connectorsStatus.get(connectorId)?.start === false) {
113 logger.warn(`${this.logPrefix(connectorId)} is already stopped on connector`);
114 }
a5e9befc
JB
115 }
116
72740232 117 private startConnectors(): void {
e7aeea18
JB
118 if (
119 this.connectorsStatus?.size > 0 &&
120 this.connectorsStatus.size !== this.chargingStation.getNumberOfConnectors()
121 ) {
54544ef1 122 this.connectorsStatus.clear();
7807ccf2 123 this.initializeConnectorsStatus();
54544ef1 124 }
734d790d 125 for (const connectorId of this.chargingStation.connectors.keys()) {
72740232 126 if (connectorId > 0) {
83a3286a 127 this.startConnector(connectorId);
72740232
JB
128 }
129 }
130 }
131
132 private stopConnectors(): void {
734d790d 133 for (const connectorId of this.chargingStation.connectors.keys()) {
72740232
JB
134 if (connectorId > 0) {
135 this.stopConnector(connectorId);
136 }
137 }
138 }
139
83a3286a 140 private async internalStartConnector(connectorId: number): Promise<void> {
083fb002 141 this.setStartConnectorStatus(connectorId);
e7aeea18
JB
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 );
a5e9befc 150 while (this.connectorsStatus.get(connectorId).start === true) {
e7aeea18 151 if (new Date() > this.connectorsStatus.get(connectorId).stopDate) {
9664ec50 152 this.stopConnector(connectorId);
17991e8c
JB
153 break;
154 }
16cd35ad 155 if (!this.chargingStation.isInAcceptedState()) {
e7aeea18
JB
156 logger.error(
157 this.logPrefix(connectorId) +
158 ' entered in transaction loop while the charging station is not in accepted state'
159 );
9664ec50 160 this.stopConnector(connectorId);
17991e8c
JB
161 break;
162 }
c0560973 163 if (!this.chargingStation.isChargingStationAvailable()) {
e7aeea18
JB
164 logger.info(
165 this.logPrefix(connectorId) +
166 ' entered in transaction loop while the charging station is unavailable'
167 );
9664ec50 168 this.stopConnector(connectorId);
ab5f4b03
JB
169 break;
170 }
c0560973 171 if (!this.chargingStation.isConnectorAvailable(connectorId)) {
e7aeea18
JB
172 logger.info(
173 `${this.logPrefix(
174 connectorId
175 )} entered in transaction loop while the connector ${connectorId} is unavailable`
176 );
9c7195b2 177 this.stopConnector(connectorId);
17991e8c
JB
178 break;
179 }
c0560973 180 if (!this.chargingStation?.ocppRequestService) {
e7aeea18
JB
181 logger.info(
182 `${this.logPrefix(
183 connectorId
184 )} transaction loop waiting for charging station service to be initialized`
185 );
c0560973 186 do {
a4cc42ea 187 await Utils.sleep(Constants.CHARGING_STATION_ATG_INITIALIZATION_TIME);
c0560973
JB
188 } while (!this.chargingStation?.ocppRequestService);
189 }
e7aeea18
JB
190 const wait =
191 Utils.getRandomInteger(
fa7bccf4
JB
192 this.configuration.maxDelayBetweenTwoTransactions,
193 this.configuration.minDelayBetweenTwoTransactions
e7aeea18
JB
194 ) * 1000;
195 logger.info(
196 this.logPrefix(connectorId) + ' waiting for ' + Utils.formatDurationMilliSeconds(wait)
197 );
6af9012e 198 await Utils.sleep(wait);
c37528f1 199 const start = Utils.secureRandom();
fa7bccf4 200 if (start < this.configuration.probabilityOfStart) {
9664ec50 201 this.connectorsStatus.get(connectorId).skippedConsecutiveTransactions = 0;
6af9012e 202 // Start transaction
aef1b33a 203 const startResponse = await this.startTransaction(connectorId);
0afed85f 204 if (startResponse?.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
6af9012e 205 // Wait until end of transaction
e7aeea18 206 const waitTrxEnd =
fa7bccf4
JB
207 Utils.getRandomInteger(this.configuration.maxDuration, this.configuration.minDuration) *
208 1000;
e7aeea18
JB
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 );
6af9012e
JB
216 await Utils.sleep(waitTrxEnd);
217 // Stop transaction
e7aeea18
JB
218 logger.info(
219 this.logPrefix(connectorId) +
220 ' stop transaction ' +
221 this.chargingStation.getConnectorStatus(connectorId).transactionId.toString()
222 );
85d20667 223 await this.stopTransaction(connectorId);
6af9012e
JB
224 }
225 } else {
9664ec50
JB
226 this.connectorsStatus.get(connectorId).skippedConsecutiveTransactions++;
227 this.connectorsStatus.get(connectorId).skippedTransactions++;
e7aeea18
JB
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 );
6af9012e 236 }
9664ec50 237 this.connectorsStatus.get(connectorId).lastRunDate = new Date();
7d75bee1 238 }
9664ec50 239 this.connectorsStatus.get(connectorId).stoppedDate = new Date();
e7aeea18
JB
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(
be9ee554 249 `${this.logPrefix(connectorId)} connector status: %j`,
e7aeea18
JB
250 this.connectorsStatus.get(connectorId)
251 );
6af9012e
JB
252 }
253
083fb002 254 private setStartConnectorStatus(connectorId: number): void {
9664ec50 255 this.connectorsStatus.get(connectorId).skippedConsecutiveTransactions = 0;
e7aeea18
JB
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;
9664ec50 262 this.connectorsStatus.get(connectorId).startDate = new Date();
e7aeea18
JB
263 this.connectorsStatus.get(connectorId).stopDate = new Date(
264 this.connectorsStatus.get(connectorId).startDate.getTime() +
fa7bccf4 265 (this.configuration.stopAfterHours ??
e7aeea18
JB
266 Constants.CHARGING_STATION_ATG_DEFAULT_STOP_AFTER_HOURS) *
267 3600 *
268 1000 -
269 previousRunDuration
270 );
083fb002 271 this.connectorsStatus.get(connectorId).start = true;
4dff3039
JB
272 }
273
7807ccf2 274 private initializeConnectorsStatus(): void {
4dff3039
JB
275 for (const connectorId of this.chargingStation.connectors.keys()) {
276 if (connectorId > 0) {
7807ccf2
JB
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 });
4dff3039
JB
291 }
292 }
72740232
JB
293 }
294
e7aeea18
JB
295 private async startTransaction(
296 connectorId: number
0afed85f 297 ): Promise<StartTransactionResponse | undefined> {
aef1b33a
JB
298 const measureId = 'StartTransaction with ATG';
299 const beginId = PerformanceStatistics.beginMeasure(measureId);
300 let startResponse: StartTransactionResponse;
301 if (this.chargingStation.hasAuthorizedTags()) {
c72f6634 302 const idTag = this.getIdTag(connectorId);
5cf9050d
JB
303 const startTransactionLogMsg = `${this.logPrefix(
304 connectorId
ba7965c4 305 )} start transaction with an idTag '${idTag}'`;
ccb1d6e9 306 if (this.getRequireAuthorize()) {
2e3d65ae 307 this.chargingStation.getConnectorStatus(connectorId).authorizeIdTag = idTag;
f4bf2abd 308 // Authorize idTag
2e3d65ae 309 const authorizeResponse: AuthorizeResponse =
f7f98c68 310 await this.chargingStation.ocppRequestService.requestHandler<
ef6fa3fb
JB
311 AuthorizeRequest,
312 AuthorizeResponse
08f130a0 313 >(this.chargingStation, RequestCommand.AUTHORIZE, {
ef6fa3fb
JB
314 idTag,
315 });
071a9315 316 this.connectorsStatus.get(connectorId).authorizeRequests++;
5fdab605 317 if (authorizeResponse?.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
071a9315 318 this.connectorsStatus.get(connectorId).acceptedAuthorizeRequests++;
5cf9050d 319 logger.info(startTransactionLogMsg);
5fdab605 320 // Start transaction
f7f98c68 321 startResponse = await this.chargingStation.ocppRequestService.requestHandler<
ef6fa3fb
JB
322 StartTransactionRequest,
323 StartTransactionResponse
08f130a0 324 >(this.chargingStation, RequestCommand.START_TRANSACTION, {
ef6fa3fb
JB
325 connectorId,
326 idTag,
327 });
d9ac47ef 328 this.handleStartTransactionResponse(connectorId, startResponse);
aef1b33a
JB
329 PerformanceStatistics.endMeasure(measureId, beginId);
330 return startResponse;
5fdab605 331 }
071a9315 332 this.connectorsStatus.get(connectorId).rejectedAuthorizeRequests++;
aef1b33a 333 PerformanceStatistics.endMeasure(measureId, beginId);
0afed85f 334 return startResponse;
ef6076c1 335 }
5cf9050d 336 logger.info(startTransactionLogMsg);
5fdab605 337 // Start transaction
f7f98c68 338 startResponse = await this.chargingStation.ocppRequestService.requestHandler<
ef6fa3fb
JB
339 StartTransactionRequest,
340 StartTransactionResponse
08f130a0 341 >(this.chargingStation, RequestCommand.START_TRANSACTION, {
ef6fa3fb
JB
342 connectorId,
343 idTag,
344 });
d9ac47ef 345 this.handleStartTransactionResponse(connectorId, startResponse);
aef1b33a
JB
346 PerformanceStatistics.endMeasure(measureId, beginId);
347 return startResponse;
6af9012e 348 }
5cf9050d 349 logger.info(`${this.logPrefix(connectorId)} start transaction without an idTag`);
f7f98c68 350 startResponse = await this.chargingStation.ocppRequestService.requestHandler<
ef6fa3fb
JB
351 StartTransactionRequest,
352 StartTransactionResponse
08f130a0 353 >(this.chargingStation, RequestCommand.START_TRANSACTION, { connectorId });
431b6bd5 354 this.handleStartTransactionResponse(connectorId, startResponse);
aef1b33a
JB
355 PerformanceStatistics.endMeasure(measureId, beginId);
356 return startResponse;
6af9012e
JB
357 }
358
e7aeea18
JB
359 private async stopTransaction(
360 connectorId: number,
5e3cb728 361 reason: StopTransactionReason = StopTransactionReason.LOCAL
e7aeea18 362 ): Promise<StopTransactionResponse> {
aef1b33a
JB
363 const measureId = 'StopTransaction with ATG';
364 const beginId = PerformanceStatistics.beginMeasure(measureId);
0045cef5 365 let stopResponse: StopTransactionResponse;
6d9876e7 366 if (this.chargingStation.getConnectorStatus(connectorId)?.transactionStarted === true) {
5e3cb728 367 stopResponse = await this.chargingStation.stopTransactionOnConnector(connectorId, reason);
071a9315 368 this.connectorsStatus.get(connectorId).stopTransactionRequests++;
0afed85f 369 if (stopResponse?.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
6d9876e7
JB
370 this.connectorsStatus.get(connectorId).acceptedStopTransactionRequests++;
371 } else {
372 this.connectorsStatus.get(connectorId).rejectedStopTransactionRequests++;
373 }
0045cef5 374 } else {
5e3cb728 375 const transactionId = this.chargingStation.getConnectorStatus(connectorId).transactionId;
e7aeea18 376 logger.warn(
ba7965c4 377 `${this.logPrefix(connectorId)} stopping a not started transaction${
e7aeea18
JB
378 transactionId ? ' ' + transactionId.toString() : ''
379 }`
380 );
0045cef5 381 }
aef1b33a
JB
382 PerformanceStatistics.endMeasure(measureId, beginId);
383 return stopResponse;
c0560973
JB
384 }
385
ccb1d6e9 386 private getRequireAuthorize(): boolean {
fa7bccf4 387 return this.configuration?.requireAuthorize ?? true;
ccb1d6e9
JB
388 }
389
c72f6634
JB
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
6e0964c8 425 private logPrefix(connectorId?: number): string {
6cd85def
JB
426 return Utils.logPrefix(
427 ` ${this.chargingStation.stationInfo.chargingStationId} | ATG${
a5e9befc 428 connectorId !== undefined ? ` on connector #${connectorId.toString()}` : ''
6cd85def
JB
429 }:`
430 );
6af9012e 431 }
d9ac47ef
JB
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 }
6af9012e 445}