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