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