1 // Partial Copyright Jerome Benoit. 2021. All Rights Reserved.
7 StartTransactionRequest
,
8 StartTransactionResponse
,
10 StopTransactionRequest
,
11 StopTransactionResponse
,
12 } from
'../types/ocpp/Transaction';
14 AutomaticTransactionGeneratorConfiguration
,
16 } from
'../types/AutomaticTransactionGenerator';
17 import { MeterValuesRequest
, RequestCommand
} from
'../types/ocpp/Requests';
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';
27 export default class AutomaticTransactionGenerator
{
28 private static readonly instances
: Map
<string, AutomaticTransactionGenerator
> = new Map
<
30 AutomaticTransactionGenerator
33 public readonly configuration
: AutomaticTransactionGeneratorConfiguration
;
34 public started
: boolean;
35 private readonly chargingStation
: ChargingStation
;
36 private readonly connectorsStatus
: Map
<number, Status
>;
39 automaticTransactionGeneratorConfiguration
: AutomaticTransactionGeneratorConfiguration
,
40 chargingStation
: ChargingStation
42 this.configuration
= automaticTransactionGeneratorConfiguration
;
43 this.chargingStation
= chargingStation
;
44 this.connectorsStatus
= new Map
<number, Status
>();
45 this.stopConnectors();
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
,
62 return AutomaticTransactionGenerator
.instances
.get(chargingStation
.hashId
);
65 public start(): void {
67 logger
.error(`${this.logPrefix()} trying to start while already started`);
70 this.startConnectors();
76 logger
.error(`${this.logPrefix()} trying to stop while not started`);
79 this.stopConnectors();
83 private startConnectors(): void {
85 this.connectorsStatus
?.size
> 0 &&
86 this.connectorsStatus
.size
!== this.chargingStation
.getNumberOfConnectors()
88 this.connectorsStatus
.clear();
90 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
91 if (connectorId
> 0) {
92 this.startConnector(connectorId
);
97 private stopConnectors(): void {
98 for (const connectorId
of this.chargingStation
.connectors
.keys()) {
99 if (connectorId
> 0) {
100 this.stopConnector(connectorId
);
105 private async internalStartConnector(connectorId
: number): Promise
<void> {
106 this.initStartConnectorStatus(connectorId
);
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()
115 while (this.connectorsStatus
.get(connectorId
).start
) {
116 if (new Date() > this.connectorsStatus
.get(connectorId
).stopDate
) {
117 this.stopConnector(connectorId
);
120 if (!this.chargingStation
.isInAcceptedState()) {
122 this.logPrefix(connectorId
) +
123 ' entered in transaction loop while the charging station is not in accepted state'
125 this.stopConnector(connectorId
);
128 if (!this.chargingStation
.isChargingStationAvailable()) {
130 this.logPrefix(connectorId
) +
131 ' entered in transaction loop while the charging station is unavailable'
133 this.stopConnector(connectorId
);
136 if (!this.chargingStation
.isConnectorAvailable(connectorId
)) {
140 )} entered in transaction loop while the connector ${connectorId} is unavailable`
142 this.stopConnector(connectorId
);
145 if (!this.chargingStation
?.ocppRequestService
) {
149 )} transaction loop waiting for charging station service to be initialized`
152 await Utils
.sleep(Constants
.CHARGING_STATION_ATG_INITIALIZATION_TIME
);
153 } while (!this.chargingStation
?.ocppRequestService
);
156 Utils
.getRandomInteger(
157 this.configuration
.maxDelayBetweenTwoTransactions
,
158 this.configuration
.minDelayBetweenTwoTransactions
161 this.logPrefix(connectorId
) + ' waiting for ' + Utils
.formatDurationMilliSeconds(wait
)
163 await Utils
.sleep(wait
);
164 const start
= Utils
.secureRandom();
165 if (start
< this.configuration
.probabilityOfStart
) {
166 this.connectorsStatus
.get(connectorId
).skippedConsecutiveTransactions
= 0;
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
++;
174 // Wait until end of transaction
176 Utils
.getRandomInteger(this.configuration
.maxDuration
, this.configuration
.minDuration
) *
179 this.logPrefix(connectorId
) +
181 this.chargingStation
.getConnectorStatus(connectorId
).transactionId
.toString() +
182 ' started and will stop in ' +
183 Utils
.formatDurationMilliSeconds(waitTrxEnd
)
185 this.connectorsStatus
.get(connectorId
).acceptedStartTransactionRequests
++;
186 await Utils
.sleep(waitTrxEnd
);
189 this.logPrefix(connectorId
) +
190 ' stop transaction ' +
191 this.chargingStation
.getConnectorStatus(connectorId
).transactionId
.toString()
193 await this.stopTransaction(connectorId
);
196 this.connectorsStatus
.get(connectorId
).skippedConsecutiveTransactions
++;
197 this.connectorsStatus
.get(connectorId
).skippedTransactions
++;
199 this.logPrefix(connectorId
) +
200 ' skipped consecutively ' +
201 this.connectorsStatus
.get(connectorId
).skippedConsecutiveTransactions
.toString() +
203 this.connectorsStatus
.get(connectorId
).skippedTransactions
.toString() +
207 this.connectorsStatus
.get(connectorId
).lastRunDate
= new Date();
209 await this.stopTransaction(connectorId
);
210 this.connectorsStatus
.get(connectorId
).stoppedDate
= new Date();
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()
220 `${this.logPrefix(connectorId)} connector status %j`,
221 this.connectorsStatus
.get(connectorId
)
225 private startConnector(connectorId
: number): void {
226 // Avoid hogging the event loop with a busy loop
228 this.internalStartConnector(connectorId
).catch(() => {
229 /* This is intentional */
234 private stopConnector(connectorId
: number): void {
235 this.connectorsStatus
.set(connectorId
, {
236 ...this.connectorsStatus
.get(connectorId
),
241 private initStartConnectorStatus(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()
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
) *
274 this.connectorsStatus
.get(connectorId
).start
= true;
277 private async startTransaction(
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
;
288 const authorizeResponse
: AuthorizeResponse
=
289 await this.chargingStation
.ocppRequestService
.requestHandler
<
292 >(this.chargingStation
, RequestCommand
.AUTHORIZE
, {
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
);
300 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
301 StartTransactionRequest
,
302 StartTransactionResponse
303 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, {
307 PerformanceStatistics
.endMeasure(measureId
, beginId
);
308 return startResponse
;
310 this.connectorsStatus
.get(connectorId
).rejectedAuthorizeRequests
++;
311 PerformanceStatistics
.endMeasure(measureId
, beginId
);
312 return authorizeResponse
;
314 logger
.info(this.logPrefix(connectorId
) + ' start transaction for idTag ' + idTag
);
316 startResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
317 StartTransactionRequest
,
318 StartTransactionResponse
319 >(this.chargingStation
, RequestCommand
.START_TRANSACTION
, {
323 PerformanceStatistics
.endMeasure(measureId
, beginId
);
324 return startResponse
;
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
;
335 private async stopTransaction(
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
;
346 this.chargingStation
.getBeginEndMeterValues() &&
347 this.chargingStation
.getOcppStrictCompliance() &&
348 !this.chargingStation
.getOutOfOrderEndMeterValues()
350 // FIXME: Implement OCPP version agnostic helpers
351 const transactionEndMeterValue
= OCPP16ServiceUtils
.buildTransactionEndMeterValue(
352 this.chargingStation
,
354 this.chargingStation
.getEnergyActiveImportRegisterByTransactionId(transactionId
)
356 await this.chargingStation
.ocppRequestService
.requestHandler
<
359 >(this.chargingStation
, RequestCommand
.METER_VALUES
, {
362 meterValue
: transactionEndMeterValue
,
365 stopResponse
= await this.chargingStation
.ocppRequestService
.requestHandler
<
366 StopTransactionRequest
,
367 StopTransactionResponse
368 >(this.chargingStation
, RequestCommand
.STOP_TRANSACTION
, {
370 meterStop
: this.chargingStation
.getEnergyActiveImportRegisterByTransactionId(transactionId
),
371 idTag
: this.chargingStation
.getTransactionIdTag(transactionId
),
374 this.connectorsStatus
.get(connectorId
).stopTransactionRequests
++;
377 `${this.logPrefix(connectorId)} trying to stop a not started transaction${
378 transactionId ? ' ' + transactionId.toString() : ''
382 PerformanceStatistics
.endMeasure(measureId
, beginId
);
386 private getRequireAuthorize(): boolean {
387 return this.configuration
?.requireAuthorize
?? true;
390 private logPrefix(connectorId
?: number): string {
392 return Utils
.logPrefix(
394 this.chargingStation
.stationInfo
.chargingStationId
+
395 ' | ATG on connector #' +
396 connectorId
.toString() +
400 return Utils
.logPrefix(' ' + this.chargingStation
.stationInfo
.chargingStationId
+ ' | ATG:');