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