Fix ATG connectors status initialization
[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';
5e3cb728 8import { RequestCommand } from '../types/ocpp/Requests';
e7aeea18
JB
9import {
10 AuthorizationStatus,
976d11ec
JB
11 type AuthorizeRequest,
12 type AuthorizeResponse,
13 type StartTransactionRequest,
14 type StartTransactionResponse,
e7aeea18 15 StopTransactionReason,
976d11ec 16 type StopTransactionResponse,
e7aeea18 17} from '../types/ocpp/Transaction';
6af9012e 18import Constants from '../utils/Constants';
9f2e3130 19import logger from '../utils/Logger';
8114d10e
JB
20import Utils from '../utils/Utils';
21import type ChargingStation from './ChargingStation';
6af9012e
JB
22
23export default class AutomaticTransactionGenerator {
e7aeea18
JB
24 private static readonly instances: Map<string, AutomaticTransactionGenerator> = new Map<
25 string,
26 AutomaticTransactionGenerator
27 >();
10068088 28
5e3cb728 29 public readonly connectorsStatus: Map<number, Status>;
fa7bccf4 30 public readonly configuration: AutomaticTransactionGeneratorConfiguration;
265e4266 31 public started: boolean;
9e23580d 32 private readonly chargingStation: ChargingStation;
6af9012e 33
fa7bccf4
JB
34 private constructor(
35 automaticTransactionGeneratorConfiguration: AutomaticTransactionGeneratorConfiguration,
36 chargingStation: ChargingStation
37 ) {
aa428a31 38 this.started = false;
fa7bccf4 39 this.configuration = automaticTransactionGeneratorConfiguration;
ad2f27c3 40 this.chargingStation = chargingStation;
4dff3039 41 this.connectorsStatus = new Map<number, Status>(this.buildInitialConnectorsStatus());
6af9012e
JB
42 }
43
fa7bccf4
JB
44 public static getInstance(
45 automaticTransactionGeneratorConfiguration: AutomaticTransactionGeneratorConfiguration,
46 chargingStation: ChargingStation
47 ): AutomaticTransactionGenerator {
4dff3039 48 if (AutomaticTransactionGenerator.instances.has(chargingStation.stationInfo.hashId) === false) {
e7aeea18 49 AutomaticTransactionGenerator.instances.set(
51c83d6f 50 chargingStation.stationInfo.hashId,
fa7bccf4
JB
51 new AutomaticTransactionGenerator(
52 automaticTransactionGeneratorConfiguration,
53 chargingStation
54 )
e7aeea18 55 );
73b9adec 56 }
51c83d6f 57 return AutomaticTransactionGenerator.instances.get(chargingStation.stationInfo.hashId);
73b9adec
JB
58 }
59
7d75bee1 60 public start(): void {
a5e9befc 61 if (this.started === true) {
ba7965c4 62 logger.warn(`${this.logPrefix()} is already started`);
b809adf1
JB
63 return;
64 }
72740232 65 this.startConnectors();
265e4266 66 this.started = true;
6af9012e
JB
67 }
68
0045cef5 69 public stop(): void {
a5e9befc 70 if (this.started === false) {
ba7965c4 71 logger.warn(`${this.logPrefix()} is already stopped`);
265e4266
JB
72 return;
73 }
72740232 74 this.stopConnectors();
265e4266 75 this.started = false;
6af9012e
JB
76 }
77
a5e9befc 78 public startConnector(connectorId: number): void {
ecb3869d 79 if (this.chargingStation.connectors.has(connectorId) === false) {
ba7965c4 80 logger.warn(`${this.logPrefix(connectorId)} starting on non existing connector`);
a5e9befc
JB
81 return;
82 }
83 if (this.connectorsStatus.get(connectorId)?.start === false) {
84 // Avoid hogging the event loop with a busy loop
85 setImmediate(() => {
86 this.internalStartConnector(connectorId).catch(() => {
87 /* This is intentional */
88 });
89 });
ecb3869d 90 } else if (this.connectorsStatus.get(connectorId)?.start === true) {
ba7965c4 91 logger.warn(`${this.logPrefix(connectorId)} is already started on connector`);
a5e9befc
JB
92 }
93 }
94
95 public stopConnector(connectorId: number): void {
ba7965c4
JB
96 if (this.chargingStation.connectors.has(connectorId) === false) {
97 logger.warn(`${this.logPrefix(connectorId)} stopping on non existing connector`);
98 return;
99 }
100 if (this.connectorsStatus.get(connectorId)?.start === true) {
101 this.connectorsStatus.set(connectorId, {
102 ...this.connectorsStatus.get(connectorId),
103 start: false,
104 });
105 } else if (this.connectorsStatus.get(connectorId)?.start === false) {
106 logger.warn(`${this.logPrefix(connectorId)} is already stopped on connector`);
107 }
a5e9befc
JB
108 }
109
72740232 110 private startConnectors(): void {
e7aeea18
JB
111 if (
112 this.connectorsStatus?.size > 0 &&
113 this.connectorsStatus.size !== this.chargingStation.getNumberOfConnectors()
114 ) {
54544ef1
JB
115 this.connectorsStatus.clear();
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
288 private buildInitialConnectorsStatus(): [number, Status][] {
289 const connectorsStatus: [number, Status][] = [];
290 for (const connectorId of this.chargingStation.connectors.keys()) {
291 if (connectorId > 0) {
292 connectorsStatus.push([
293 connectorId,
294 {
295 start: false,
296 authorizeRequests: 0,
297 acceptedAuthorizeRequests: 0,
298 rejectedAuthorizeRequests: 0,
299 startTransactionRequests: 0,
300 acceptedStartTransactionRequests: 0,
301 rejectedStartTransactionRequests: 0,
302 stopTransactionRequests: 0,
303 acceptedStopTransactionRequests: 0,
304 rejectedStopTransactionRequests: 0,
305 skippedConsecutiveTransactions: 0,
306 skippedTransactions: 0,
307 },
308 ]);
309 }
310 }
311 return connectorsStatus;
72740232
JB
312 }
313
e7aeea18
JB
314 private async startTransaction(
315 connectorId: number
0afed85f 316 ): Promise<StartTransactionResponse | undefined> {
aef1b33a
JB
317 const measureId = 'StartTransaction with ATG';
318 const beginId = PerformanceStatistics.beginMeasure(measureId);
319 let startResponse: StartTransactionResponse;
320 if (this.chargingStation.hasAuthorizedTags()) {
f4bf2abd 321 const idTag = this.chargingStation.getRandomIdTag();
5cf9050d
JB
322 const startTransactionLogMsg = `${this.logPrefix(
323 connectorId
ba7965c4 324 )} start transaction with an idTag '${idTag}'`;
ccb1d6e9 325 if (this.getRequireAuthorize()) {
2e3d65ae 326 this.chargingStation.getConnectorStatus(connectorId).authorizeIdTag = idTag;
f4bf2abd 327 // Authorize idTag
2e3d65ae 328 const authorizeResponse: AuthorizeResponse =
f7f98c68 329 await this.chargingStation.ocppRequestService.requestHandler<
ef6fa3fb
JB
330 AuthorizeRequest,
331 AuthorizeResponse
08f130a0 332 >(this.chargingStation, RequestCommand.AUTHORIZE, {
ef6fa3fb
JB
333 idTag,
334 });
071a9315 335 this.connectorsStatus.get(connectorId).authorizeRequests++;
5fdab605 336 if (authorizeResponse?.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
071a9315 337 this.connectorsStatus.get(connectorId).acceptedAuthorizeRequests++;
5cf9050d 338 logger.info(startTransactionLogMsg);
5fdab605 339 // Start transaction
f7f98c68 340 startResponse = await this.chargingStation.ocppRequestService.requestHandler<
ef6fa3fb
JB
341 StartTransactionRequest,
342 StartTransactionResponse
08f130a0 343 >(this.chargingStation, RequestCommand.START_TRANSACTION, {
ef6fa3fb
JB
344 connectorId,
345 idTag,
346 });
d9ac47ef 347 this.handleStartTransactionResponse(connectorId, startResponse);
aef1b33a
JB
348 PerformanceStatistics.endMeasure(measureId, beginId);
349 return startResponse;
5fdab605 350 }
071a9315 351 this.connectorsStatus.get(connectorId).rejectedAuthorizeRequests++;
aef1b33a 352 PerformanceStatistics.endMeasure(measureId, beginId);
0afed85f 353 return startResponse;
ef6076c1 354 }
5cf9050d 355 logger.info(startTransactionLogMsg);
5fdab605 356 // Start transaction
f7f98c68 357 startResponse = await this.chargingStation.ocppRequestService.requestHandler<
ef6fa3fb
JB
358 StartTransactionRequest,
359 StartTransactionResponse
08f130a0 360 >(this.chargingStation, RequestCommand.START_TRANSACTION, {
ef6fa3fb
JB
361 connectorId,
362 idTag,
363 });
d9ac47ef 364 this.handleStartTransactionResponse(connectorId, startResponse);
aef1b33a
JB
365 PerformanceStatistics.endMeasure(measureId, beginId);
366 return startResponse;
6af9012e 367 }
5cf9050d 368 logger.info(`${this.logPrefix(connectorId)} start transaction without an idTag`);
f7f98c68 369 startResponse = await this.chargingStation.ocppRequestService.requestHandler<
ef6fa3fb
JB
370 StartTransactionRequest,
371 StartTransactionResponse
08f130a0 372 >(this.chargingStation, RequestCommand.START_TRANSACTION, { connectorId });
431b6bd5 373 this.handleStartTransactionResponse(connectorId, startResponse);
aef1b33a
JB
374 PerformanceStatistics.endMeasure(measureId, beginId);
375 return startResponse;
6af9012e
JB
376 }
377
e7aeea18
JB
378 private async stopTransaction(
379 connectorId: number,
5e3cb728 380 reason: StopTransactionReason = StopTransactionReason.LOCAL
e7aeea18 381 ): Promise<StopTransactionResponse> {
aef1b33a
JB
382 const measureId = 'StopTransaction with ATG';
383 const beginId = PerformanceStatistics.beginMeasure(measureId);
0045cef5 384 let stopResponse: StopTransactionResponse;
6d9876e7 385 if (this.chargingStation.getConnectorStatus(connectorId)?.transactionStarted === true) {
5e3cb728 386 stopResponse = await this.chargingStation.stopTransactionOnConnector(connectorId, reason);
071a9315 387 this.connectorsStatus.get(connectorId).stopTransactionRequests++;
0afed85f 388 if (stopResponse?.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
6d9876e7
JB
389 this.connectorsStatus.get(connectorId).acceptedStopTransactionRequests++;
390 } else {
391 this.connectorsStatus.get(connectorId).rejectedStopTransactionRequests++;
392 }
0045cef5 393 } else {
5e3cb728 394 const transactionId = this.chargingStation.getConnectorStatus(connectorId).transactionId;
e7aeea18 395 logger.warn(
ba7965c4 396 `${this.logPrefix(connectorId)} stopping a not started transaction${
e7aeea18
JB
397 transactionId ? ' ' + transactionId.toString() : ''
398 }`
399 );
0045cef5 400 }
aef1b33a
JB
401 PerformanceStatistics.endMeasure(measureId, beginId);
402 return stopResponse;
c0560973
JB
403 }
404
ccb1d6e9 405 private getRequireAuthorize(): boolean {
fa7bccf4 406 return this.configuration?.requireAuthorize ?? true;
ccb1d6e9
JB
407 }
408
6e0964c8 409 private logPrefix(connectorId?: number): string {
6cd85def
JB
410 return Utils.logPrefix(
411 ` ${this.chargingStation.stationInfo.chargingStationId} | ATG${
a5e9befc 412 connectorId !== undefined ? ` on connector #${connectorId.toString()}` : ''
6cd85def
JB
413 }:`
414 );
6af9012e 415 }
d9ac47ef
JB
416
417 private handleStartTransactionResponse(
418 connectorId: number,
419 startResponse: StartTransactionResponse
420 ): void {
421 this.connectorsStatus.get(connectorId).startTransactionRequests++;
422 if (startResponse?.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
423 this.connectorsStatus.get(connectorId).acceptedStartTransactionRequests++;
424 } else {
425 logger.warn(this.logPrefix(connectorId) + ' start transaction rejected');
426 this.connectorsStatus.get(connectorId).rejectedStartTransactionRequests++;
427 }
428 }
6af9012e 429}