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