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