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