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