UI Server: fix connection upgrade error handling
[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.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(connectorId) +
155 ' started on connector and will run for ' +
156 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(connectorId) +
169 ' entered in transaction loop while the charging station is not in accepted state'
170 );
171 this.stopConnector(connectorId);
172 break;
173 }
174 if (this.chargingStation.isChargingStationAvailable() === false) {
175 logger.info(
176 this.logPrefix(connectorId) +
177 ' entered in transaction loop while the charging station is unavailable'
178 );
179 this.stopConnector(connectorId);
180 break;
181 }
182 if (this.chargingStation.isConnectorAvailable(connectorId) === false) {
183 logger.info(
184 `${this.logPrefix(
185 connectorId
186 )} entered in transaction loop while the connector ${connectorId} is unavailable`
187 );
188 this.stopConnector(connectorId);
189 break;
190 }
191 if (!this.chargingStation?.ocppRequestService) {
192 logger.info(
193 `${this.logPrefix(
194 connectorId
195 )} transaction loop waiting for charging station service to be initialized`
196 );
197 do {
198 await Utils.sleep(Constants.CHARGING_STATION_ATG_INITIALIZATION_TIME);
199 } while (!this.chargingStation?.ocppRequestService);
200 }
201 const wait =
202 Utils.getRandomInteger(
203 this.configuration.maxDelayBetweenTwoTransactions,
204 this.configuration.minDelayBetweenTwoTransactions
205 ) * 1000;
206 logger.info(
207 this.logPrefix(connectorId) + ' waiting for ' + Utils.formatDurationMilliSeconds(wait)
208 );
209 await Utils.sleep(wait);
210 const start = Utils.secureRandom();
211 if (start < this.configuration.probabilityOfStart) {
212 this.connectorsStatus.get(connectorId).skippedConsecutiveTransactions = 0;
213 // Start transaction
214 const startResponse = await this.startTransaction(connectorId);
215 if (startResponse?.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
216 // Wait until end of transaction
217 const waitTrxEnd =
218 Utils.getRandomInteger(this.configuration.maxDuration, this.configuration.minDuration) *
219 1000;
220 logger.info(
221 this.logPrefix(connectorId) +
222 ' transaction ' +
223 this.chargingStation.getConnectorStatus(connectorId).transactionId.toString() +
224 ' started and will stop in ' +
225 Utils.formatDurationMilliSeconds(waitTrxEnd)
226 );
227 await Utils.sleep(waitTrxEnd);
228 // Stop transaction
229 logger.info(
230 this.logPrefix(connectorId) +
231 ' stop transaction ' +
232 this.chargingStation.getConnectorStatus(connectorId).transactionId.toString()
233 );
234 await this.stopTransaction(connectorId);
235 }
236 } else {
237 this.connectorsStatus.get(connectorId).skippedConsecutiveTransactions++;
238 this.connectorsStatus.get(connectorId).skippedTransactions++;
239 logger.info(
240 this.logPrefix(connectorId) +
241 ' skipped consecutively ' +
242 this.connectorsStatus.get(connectorId).skippedConsecutiveTransactions.toString() +
243 '/' +
244 this.connectorsStatus.get(connectorId).skippedTransactions.toString() +
245 ' transaction(s)'
246 );
247 }
248 this.connectorsStatus.get(connectorId).lastRunDate = new Date();
249 }
250 this.connectorsStatus.get(connectorId).stoppedDate = new Date();
251 logger.info(
252 this.logPrefix(connectorId) +
253 ' stopped on connector and lasted for ' +
254 Utils.formatDurationMilliSeconds(
255 this.connectorsStatus.get(connectorId).stoppedDate.getTime() -
256 this.connectorsStatus.get(connectorId).startDate.getTime()
257 )
258 );
259 logger.debug(
260 `${this.logPrefix(connectorId)} connector status: %j`,
261 this.connectorsStatus.get(connectorId)
262 );
263 }
264
265 private setStartConnectorStatus(connectorId: number): void {
266 this.connectorsStatus.get(connectorId).skippedConsecutiveTransactions = 0;
267 const previousRunDuration =
268 this?.connectorsStatus.get(connectorId)?.startDate &&
269 this?.connectorsStatus.get(connectorId)?.lastRunDate
270 ? this.connectorsStatus.get(connectorId).lastRunDate.getTime() -
271 this.connectorsStatus.get(connectorId).startDate.getTime()
272 : 0;
273 this.connectorsStatus.get(connectorId).startDate = new Date();
274 this.connectorsStatus.get(connectorId).stopDate = new Date(
275 this.connectorsStatus.get(connectorId).startDate.getTime() +
276 (this.configuration.stopAfterHours ??
277 Constants.CHARGING_STATION_ATG_DEFAULT_STOP_AFTER_HOURS) *
278 3600 *
279 1000 -
280 previousRunDuration
281 );
282 this.connectorsStatus.get(connectorId).start = true;
283 }
284
285 private initializeConnectorsStatus(): void {
286 for (const connectorId of this.chargingStation.connectors.keys()) {
287 if (connectorId > 0) {
288 this.connectorsStatus.set(connectorId, {
289 start: false,
290 authorizeRequests: 0,
291 acceptedAuthorizeRequests: 0,
292 rejectedAuthorizeRequests: 0,
293 startTransactionRequests: 0,
294 acceptedStartTransactionRequests: 0,
295 rejectedStartTransactionRequests: 0,
296 stopTransactionRequests: 0,
297 acceptedStopTransactionRequests: 0,
298 rejectedStopTransactionRequests: 0,
299 skippedConsecutiveTransactions: 0,
300 skippedTransactions: 0,
301 });
302 }
303 }
304 }
305
306 private async startTransaction(
307 connectorId: number
308 ): Promise<StartTransactionResponse | undefined> {
309 const measureId = 'StartTransaction with ATG';
310 const beginId = PerformanceStatistics.beginMeasure(measureId);
311 let startResponse: StartTransactionResponse;
312 if (this.chargingStation.hasAuthorizedTags()) {
313 const idTag = this.getIdTag(connectorId);
314 const startTransactionLogMsg = `${this.logPrefix(
315 connectorId
316 )} start transaction with an idTag '${idTag}'`;
317 if (this.getRequireAuthorize()) {
318 this.chargingStation.getConnectorStatus(connectorId).authorizeIdTag = idTag;
319 // Authorize idTag
320 const authorizeResponse: AuthorizeResponse =
321 await this.chargingStation.ocppRequestService.requestHandler<
322 AuthorizeRequest,
323 AuthorizeResponse
324 >(this.chargingStation, RequestCommand.AUTHORIZE, {
325 idTag,
326 });
327 this.connectorsStatus.get(connectorId).authorizeRequests++;
328 if (authorizeResponse?.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
329 this.connectorsStatus.get(connectorId).acceptedAuthorizeRequests++;
330 logger.info(startTransactionLogMsg);
331 // Start transaction
332 startResponse = await this.chargingStation.ocppRequestService.requestHandler<
333 StartTransactionRequest,
334 StartTransactionResponse
335 >(this.chargingStation, RequestCommand.START_TRANSACTION, {
336 connectorId,
337 idTag,
338 });
339 this.handleStartTransactionResponse(connectorId, startResponse);
340 PerformanceStatistics.endMeasure(measureId, beginId);
341 return startResponse;
342 }
343 this.connectorsStatus.get(connectorId).rejectedAuthorizeRequests++;
344 PerformanceStatistics.endMeasure(measureId, beginId);
345 return startResponse;
346 }
347 logger.info(startTransactionLogMsg);
348 // Start transaction
349 startResponse = await this.chargingStation.ocppRequestService.requestHandler<
350 StartTransactionRequest,
351 StartTransactionResponse
352 >(this.chargingStation, RequestCommand.START_TRANSACTION, {
353 connectorId,
354 idTag,
355 });
356 this.handleStartTransactionResponse(connectorId, startResponse);
357 PerformanceStatistics.endMeasure(measureId, beginId);
358 return startResponse;
359 }
360 logger.info(`${this.logPrefix(connectorId)} start transaction without an idTag`);
361 startResponse = await this.chargingStation.ocppRequestService.requestHandler<
362 StartTransactionRequest,
363 StartTransactionResponse
364 >(this.chargingStation, RequestCommand.START_TRANSACTION, { connectorId });
365 this.handleStartTransactionResponse(connectorId, startResponse);
366 PerformanceStatistics.endMeasure(measureId, beginId);
367 return startResponse;
368 }
369
370 private async stopTransaction(
371 connectorId: number,
372 reason: StopTransactionReason = StopTransactionReason.LOCAL
373 ): Promise<StopTransactionResponse> {
374 const measureId = 'StopTransaction with ATG';
375 const beginId = PerformanceStatistics.beginMeasure(measureId);
376 let stopResponse: StopTransactionResponse;
377 if (this.chargingStation.getConnectorStatus(connectorId)?.transactionStarted === true) {
378 stopResponse = await this.chargingStation.stopTransactionOnConnector(connectorId, reason);
379 this.connectorsStatus.get(connectorId).stopTransactionRequests++;
380 if (stopResponse?.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
381 this.connectorsStatus.get(connectorId).acceptedStopTransactionRequests++;
382 } else {
383 this.connectorsStatus.get(connectorId).rejectedStopTransactionRequests++;
384 }
385 } else {
386 const transactionId = this.chargingStation.getConnectorStatus(connectorId).transactionId;
387 logger.warn(
388 `${this.logPrefix(connectorId)} stopping a not started transaction${
389 transactionId ? ' ' + transactionId.toString() : ''
390 }`
391 );
392 }
393 PerformanceStatistics.endMeasure(measureId, beginId);
394 return stopResponse;
395 }
396
397 private getRequireAuthorize(): boolean {
398 return this.configuration?.requireAuthorize ?? true;
399 }
400
401 private getRandomIdTag(authorizationFile: string): string {
402 const tags = this.chargingStation.authorizedTagsCache.getAuthorizedTags(authorizationFile);
403 this.idTagIndex = Math.floor(Utils.secureRandom() * tags.length);
404 return tags[this.idTagIndex];
405 }
406
407 private getRoundRobinIdTag(authorizationFile: string): string {
408 const tags = this.chargingStation.authorizedTagsCache.getAuthorizedTags(authorizationFile);
409 const idTag = tags[this.idTagIndex];
410 this.idTagIndex = this.idTagIndex === tags.length - 1 ? 0 : this.idTagIndex + 1;
411 return idTag;
412 }
413
414 private getConnectorAffinityIdTag(authorizationFile: string, connectorId: number): string {
415 const tags = this.chargingStation.authorizedTagsCache.getAuthorizedTags(authorizationFile);
416 this.idTagIndex = (this.chargingStation.index - 1 + (connectorId - 1)) % tags.length;
417 return tags[this.idTagIndex];
418 }
419
420 private getIdTag(connectorId: number): string {
421 const authorizationFile = ChargingStationUtils.getAuthorizationFile(
422 this.chargingStation.stationInfo
423 );
424 switch (this.configuration?.idTagDistribution) {
425 case IdTagDistribution.RANDOM:
426 return this.getRandomIdTag(authorizationFile);
427 case IdTagDistribution.ROUND_ROBIN:
428 return this.getRoundRobinIdTag(authorizationFile);
429 case IdTagDistribution.CONNECTOR_AFFINITY:
430 return this.getConnectorAffinityIdTag(authorizationFile, connectorId);
431 default:
432 return this.getRoundRobinIdTag(authorizationFile);
433 }
434 }
435
436 private logPrefix(connectorId?: number): string {
437 return Utils.logPrefix(
438 ` ${this.chargingStation.stationInfo.chargingStationId} | ATG${
439 connectorId !== undefined ? ` on connector #${connectorId.toString()}` : ''
440 }:`
441 );
442 }
443
444 private handleStartTransactionResponse(
445 connectorId: number,
446 startResponse: StartTransactionResponse
447 ): void {
448 this.connectorsStatus.get(connectorId).startTransactionRequests++;
449 if (startResponse?.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
450 this.connectorsStatus.get(connectorId).acceptedStartTransactionRequests++;
451 } else {
452 logger.warn(this.logPrefix(connectorId) + ' start transaction rejected');
453 this.connectorsStatus.get(connectorId).rejectedStartTransactionRequests++;
454 }
455 }
456
457 private checkChargingStation(connectorId?: number): boolean {
458 if (this.chargingStation.started === false) {
459 logger.warn(`${this.logPrefix(connectorId)} charging station is stopped, cannot proceed`);
460 }
461 return this.chargingStation.started;
462 }
463 }