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