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