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