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