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