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