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