Convert sendTransactionBeginMeterValues to OCPP message sending handler
[e-mobility-charging-stations-simulator.git] / src / charging-station / AutomaticTransactionGenerator.ts
CommitLineData
c8eeb62b
JB
1// Partial Copyright Jerome Benoit. 2021. All Rights Reserved.
2
e7aeea18
JB
3import {
4 AuthorizationStatus,
5 AuthorizeResponse,
6 StartTransactionResponse,
7 StopTransactionReason,
8 StopTransactionResponse,
9} from '../types/ocpp/Transaction';
6af9012e 10
73b9adec 11import type ChargingStation from './ChargingStation';
6af9012e 12import Constants from '../utils/Constants';
68c993d5 13import { OCPP16ServiceUtils } from './ocpp/1.6/OCPP16ServiceUtils';
a6b3c6c3 14import PerformanceStatistics from '../performance/PerformanceStatistics';
2e3d65ae 15import { RequestCommand } from '../types/ocpp/Requests';
9664ec50 16import { Status } from '../types/AutomaticTransactionGenerator';
6af9012e 17import Utils from '../utils/Utils';
9f2e3130 18import logger from '../utils/Logger';
6af9012e
JB
19
20export default class AutomaticTransactionGenerator {
e7aeea18
JB
21 private static readonly instances: Map<string, AutomaticTransactionGenerator> = new Map<
22 string,
23 AutomaticTransactionGenerator
24 >();
10068088 25
265e4266 26 public started: boolean;
9e23580d
JB
27 private readonly chargingStation: ChargingStation;
28 private readonly connectorsStatus: Map<number, Status>;
6af9012e 29
73b9adec 30 private constructor(chargingStation: ChargingStation) {
ad2f27c3 31 this.chargingStation = chargingStation;
9664ec50 32 this.connectorsStatus = new Map<number, Status>();
72740232 33 this.stopConnectors();
265e4266 34 this.started = false;
6af9012e
JB
35 }
36
73b9adec
JB
37 public static getInstance(chargingStation: ChargingStation): AutomaticTransactionGenerator {
38 if (!AutomaticTransactionGenerator.instances.has(chargingStation.id)) {
e7aeea18
JB
39 AutomaticTransactionGenerator.instances.set(
40 chargingStation.id,
41 new AutomaticTransactionGenerator(chargingStation)
42 );
73b9adec
JB
43 }
44 return AutomaticTransactionGenerator.instances.get(chargingStation.id);
45 }
46
7d75bee1 47 public start(): void {
b809adf1 48 if (this.started) {
9f2e3130 49 logger.error(`${this.logPrefix()} trying to start while already started`);
b809adf1
JB
50 return;
51 }
72740232 52 this.startConnectors();
265e4266 53 this.started = true;
6af9012e
JB
54 }
55
0045cef5 56 public stop(): void {
265e4266 57 if (!this.started) {
9f2e3130 58 logger.error(`${this.logPrefix()} trying to stop while not started`);
265e4266
JB
59 return;
60 }
72740232 61 this.stopConnectors();
265e4266 62 this.started = false;
6af9012e
JB
63 }
64
72740232 65 private startConnectors(): void {
e7aeea18
JB
66 if (
67 this.connectorsStatus?.size > 0 &&
68 this.connectorsStatus.size !== this.chargingStation.getNumberOfConnectors()
69 ) {
54544ef1
JB
70 this.connectorsStatus.clear();
71 }
734d790d 72 for (const connectorId of this.chargingStation.connectors.keys()) {
72740232 73 if (connectorId > 0) {
83a3286a 74 this.startConnector(connectorId);
72740232
JB
75 }
76 }
77 }
78
79 private stopConnectors(): void {
734d790d 80 for (const connectorId of this.chargingStation.connectors.keys()) {
72740232
JB
81 if (connectorId > 0) {
82 this.stopConnector(connectorId);
83 }
84 }
85 }
86
83a3286a 87 private async internalStartConnector(connectorId: number): Promise<void> {
9664ec50 88 this.initStartConnectorStatus(connectorId);
e7aeea18
JB
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 );
9664ec50 97 while (this.connectorsStatus.get(connectorId).start) {
e7aeea18 98 if (new Date() > this.connectorsStatus.get(connectorId).stopDate) {
9664ec50 99 this.stopConnector(connectorId);
17991e8c
JB
100 break;
101 }
16cd35ad 102 if (!this.chargingStation.isInAcceptedState()) {
e7aeea18
JB
103 logger.error(
104 this.logPrefix(connectorId) +
105 ' entered in transaction loop while the charging station is not in accepted state'
106 );
9664ec50 107 this.stopConnector(connectorId);
17991e8c
JB
108 break;
109 }
c0560973 110 if (!this.chargingStation.isChargingStationAvailable()) {
e7aeea18
JB
111 logger.info(
112 this.logPrefix(connectorId) +
113 ' entered in transaction loop while the charging station is unavailable'
114 );
9664ec50 115 this.stopConnector(connectorId);
ab5f4b03
JB
116 break;
117 }
c0560973 118 if (!this.chargingStation.isConnectorAvailable(connectorId)) {
e7aeea18
JB
119 logger.info(
120 `${this.logPrefix(
121 connectorId
122 )} entered in transaction loop while the connector ${connectorId} is unavailable`
123 );
9c7195b2 124 this.stopConnector(connectorId);
17991e8c
JB
125 break;
126 }
c0560973 127 if (!this.chargingStation?.ocppRequestService) {
e7aeea18
JB
128 logger.info(
129 `${this.logPrefix(
130 connectorId
131 )} transaction loop waiting for charging station service to be initialized`
132 );
c0560973 133 do {
a4cc42ea 134 await Utils.sleep(Constants.CHARGING_STATION_ATG_INITIALIZATION_TIME);
c0560973
JB
135 } while (!this.chargingStation?.ocppRequestService);
136 }
e7aeea18
JB
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 );
6af9012e 147 await Utils.sleep(wait);
c37528f1 148 const start = Utils.secureRandom();
e7aeea18
JB
149 if (
150 start < this.chargingStation.stationInfo.AutomaticTransactionGenerator.probabilityOfStart
151 ) {
9664ec50 152 this.connectorsStatus.get(connectorId).skippedConsecutiveTransactions = 0;
6af9012e 153 // Start transaction
aef1b33a 154 const startResponse = await this.startTransaction(connectorId);
071a9315 155 this.connectorsStatus.get(connectorId).startTransactionRequests++;
ef6076c1 156 if (startResponse?.idTagInfo?.status !== AuthorizationStatus.ACCEPTED) {
9f2e3130 157 logger.warn(this.logPrefix(connectorId) + ' start transaction rejected');
071a9315 158 this.connectorsStatus.get(connectorId).rejectedStartTransactionRequests++;
6af9012e
JB
159 } else {
160 // Wait until end of transaction
e7aeea18
JB
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 );
071a9315 173 this.connectorsStatus.get(connectorId).acceptedStartTransactionRequests++;
6af9012e
JB
174 await Utils.sleep(waitTrxEnd);
175 // Stop transaction
e7aeea18
JB
176 logger.info(
177 this.logPrefix(connectorId) +
178 ' stop transaction ' +
179 this.chargingStation.getConnectorStatus(connectorId).transactionId.toString()
180 );
85d20667 181 await this.stopTransaction(connectorId);
6af9012e
JB
182 }
183 } else {
9664ec50
JB
184 this.connectorsStatus.get(connectorId).skippedConsecutiveTransactions++;
185 this.connectorsStatus.get(connectorId).skippedTransactions++;
e7aeea18
JB
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 );
6af9012e 194 }
9664ec50 195 this.connectorsStatus.get(connectorId).lastRunDate = new Date();
7d75bee1 196 }
0045cef5 197 await this.stopTransaction(connectorId);
9664ec50 198 this.connectorsStatus.get(connectorId).stoppedDate = new Date();
e7aeea18
JB
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 );
6af9012e
JB
211 }
212
83a3286a
JB
213 private startConnector(connectorId: number): void {
214 // Avoid hogging the event loop with a busy loop
215 setImmediate(() => {
e7aeea18
JB
216 this.internalStartConnector(connectorId).catch(() => {
217 /* This is intentional */
218 });
83a3286a
JB
219 });
220 }
221
72740232 222 private stopConnector(connectorId: number): void {
e7aeea18
JB
223 this.connectorsStatus.set(connectorId, {
224 ...this.connectorsStatus.get(connectorId),
225 start: false,
226 });
9664ec50
JB
227 }
228
229 private initStartConnectorStatus(connectorId: number): void {
e7aeea18
JB
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;
9664ec50 244 this.connectorsStatus.get(connectorId).skippedConsecutiveTransactions = 0;
e7aeea18
JB
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;
9664ec50 253 this.connectorsStatus.get(connectorId).startDate = new Date();
e7aeea18
JB
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 );
9664ec50 262 this.connectorsStatus.get(connectorId).start = true;
72740232
JB
263 }
264
e7aeea18
JB
265 private async startTransaction(
266 connectorId: number
267 ): Promise<StartTransactionResponse | AuthorizeResponse> {
aef1b33a
JB
268 const measureId = 'StartTransaction with ATG';
269 const beginId = PerformanceStatistics.beginMeasure(measureId);
270 let startResponse: StartTransactionResponse;
271 if (this.chargingStation.hasAuthorizedTags()) {
f4bf2abd 272 const idTag = this.chargingStation.getRandomIdTag();
aef1b33a 273 if (this.chargingStation.getAutomaticTransactionGeneratorRequireAuthorize()) {
2e3d65ae 274 this.chargingStation.getConnectorStatus(connectorId).authorizeIdTag = idTag;
f4bf2abd 275 // Authorize idTag
2e3d65ae
JB
276 const authorizeResponse: AuthorizeResponse =
277 (await this.chargingStation.ocppRequestService.sendMessageHandler(
278 RequestCommand.AUTHORIZE,
279 {
280 idTag,
281 }
282 )) as AuthorizeResponse;
071a9315 283 this.connectorsStatus.get(connectorId).authorizeRequests++;
5fdab605 284 if (authorizeResponse?.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
071a9315 285 this.connectorsStatus.get(connectorId).acceptedAuthorizeRequests++;
9f2e3130 286 logger.info(this.logPrefix(connectorId) + ' start transaction for idTag ' + idTag);
5fdab605 287 // Start transaction
e7454a1f
JB
288 startResponse = (await this.chargingStation.ocppRequestService.sendMessageHandler(
289 RequestCommand.START_TRANSACTION,
290 {
291 connectorId,
292 idTag,
293 }
294 )) as StartTransactionResponse;
aef1b33a
JB
295 PerformanceStatistics.endMeasure(measureId, beginId);
296 return startResponse;
5fdab605 297 }
071a9315 298 this.connectorsStatus.get(connectorId).rejectedAuthorizeRequests++;
aef1b33a 299 PerformanceStatistics.endMeasure(measureId, beginId);
4faad557 300 return authorizeResponse;
ef6076c1 301 }
9f2e3130 302 logger.info(this.logPrefix(connectorId) + ' start transaction for idTag ' + idTag);
5fdab605 303 // Start transaction
e7454a1f
JB
304 startResponse = (await this.chargingStation.ocppRequestService.sendMessageHandler(
305 RequestCommand.START_TRANSACTION,
306 {
307 connectorId,
308 idTag,
309 }
310 )) as StartTransactionResponse;
aef1b33a
JB
311 PerformanceStatistics.endMeasure(measureId, beginId);
312 return startResponse;
6af9012e 313 }
9f2e3130 314 logger.info(this.logPrefix(connectorId) + ' start transaction without an idTag');
e7454a1f
JB
315 startResponse = (await this.chargingStation.ocppRequestService.sendMessageHandler(
316 RequestCommand.START_TRANSACTION,
317 { connectorId }
318 )) as StartTransactionResponse;
aef1b33a
JB
319 PerformanceStatistics.endMeasure(measureId, beginId);
320 return startResponse;
6af9012e
JB
321 }
322
e7aeea18
JB
323 private async stopTransaction(
324 connectorId: number,
325 reason: StopTransactionReason = StopTransactionReason.NONE
326 ): Promise<StopTransactionResponse> {
aef1b33a
JB
327 const measureId = 'StopTransaction with ATG';
328 const beginId = PerformanceStatistics.beginMeasure(measureId);
8eb02b62 329 let transactionId = 0;
0045cef5 330 let stopResponse: StopTransactionResponse;
734d790d
JB
331 if (this.chargingStation.getConnectorStatus(connectorId)?.transactionStarted) {
332 transactionId = this.chargingStation.getConnectorStatus(connectorId).transactionId;
68c993d5
JB
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.sendTransactionEndMeterValues(
345 connectorId,
346 transactionId,
347 transactionEndMeterValue
348 );
349 }
350 stopResponse = (await this.chargingStation.ocppRequestService.sendMessageHandler(
351 RequestCommand.STOP_TRANSACTION,
352 {
353 transactionId,
354 meterStop:
355 this.chargingStation.getEnergyActiveImportRegisterByTransactionId(transactionId),
356 idTag: this.chargingStation.getTransactionIdTag(transactionId),
357 reason,
358 }
359 )) as StopTransactionResponse;
071a9315 360 this.connectorsStatus.get(connectorId).stopTransactionRequests++;
0045cef5 361 } else {
e7aeea18
JB
362 logger.warn(
363 `${this.logPrefix(connectorId)} trying to stop a not started transaction${
364 transactionId ? ' ' + transactionId.toString() : ''
365 }`
366 );
0045cef5 367 }
aef1b33a
JB
368 PerformanceStatistics.endMeasure(measureId, beginId);
369 return stopResponse;
c0560973
JB
370 }
371
6e0964c8 372 private logPrefix(connectorId?: number): string {
c0560973 373 if (connectorId) {
e7aeea18
JB
374 return Utils.logPrefix(
375 ' ' +
376 this.chargingStation.stationInfo.chargingStationId +
377 ' | ATG on connector #' +
378 connectorId.toString() +
379 ':'
380 );
c0560973 381 }
54b1efe0 382 return Utils.logPrefix(' ' + this.chargingStation.stationInfo.chargingStationId + ' | ATG:');
6af9012e
JB
383 }
384}