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