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