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