38efbf51a22ce5c19390a0219702ddb81c9dfd2c
[e-mobility-charging-stations-simulator.git] / src / charging-station / AutomaticTransactionGenerator.ts
1 // Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
2
3 import { hoursToMilliseconds, secondsToMilliseconds } from 'date-fns'
4
5 import type { ChargingStation } from './ChargingStation.js'
6 import { checkChargingStation } from './Helpers.js'
7 import { IdTagsCache } from './IdTagsCache.js'
8 import { isIdTagAuthorized } from './ocpp/index.js'
9 import { BaseError } from '../exception/index.js'
10 import { PerformanceStatistics } from '../performance/index.js'
11 import {
12 AuthorizationStatus,
13 RequestCommand,
14 type StartTransactionRequest,
15 type StartTransactionResponse,
16 type Status,
17 StopTransactionReason,
18 type StopTransactionResponse
19 } from '../types/index.js'
20 import {
21 Constants,
22 cloneObject,
23 formatDurationMilliSeconds,
24 getRandomInteger,
25 logPrefix,
26 logger,
27 secureRandom,
28 sleep
29 } from '../utils/index.js'
30
31 export class AutomaticTransactionGenerator {
32 private static readonly instances: Map<string, AutomaticTransactionGenerator> = new Map<
33 string,
34 AutomaticTransactionGenerator
35 >()
36
37 public readonly connectorsStatus: Map<number, Status>
38 public started: boolean
39 private starting: boolean
40 private stopping: boolean
41 private readonly chargingStation: ChargingStation
42
43 private constructor (chargingStation: ChargingStation) {
44 this.started = false
45 this.starting = false
46 this.stopping = false
47 this.chargingStation = chargingStation
48 this.connectorsStatus = new Map<number, Status>()
49 this.initializeConnectorsStatus()
50 }
51
52 public static getInstance (
53 chargingStation: ChargingStation
54 ): AutomaticTransactionGenerator | undefined {
55 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
56 if (!AutomaticTransactionGenerator.instances.has(chargingStation.stationInfo!.hashId)) {
57 AutomaticTransactionGenerator.instances.set(
58 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
59 chargingStation.stationInfo!.hashId,
60 new AutomaticTransactionGenerator(chargingStation)
61 )
62 }
63 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
64 return AutomaticTransactionGenerator.instances.get(chargingStation.stationInfo!.hashId)
65 }
66
67 public start (): void {
68 if (!checkChargingStation(this.chargingStation, this.logPrefix())) {
69 return
70 }
71 if (this.started) {
72 logger.warn(`${this.logPrefix()} is already started`)
73 return
74 }
75 if (this.starting) {
76 logger.warn(`${this.logPrefix()} is already starting`)
77 return
78 }
79 this.starting = true
80 this.startConnectors()
81 this.started = true
82 this.starting = false
83 }
84
85 public stop (): void {
86 if (!this.started) {
87 logger.warn(`${this.logPrefix()} is already stopped`)
88 return
89 }
90 if (this.stopping) {
91 logger.warn(`${this.logPrefix()} is already stopping`)
92 return
93 }
94 this.stopping = true
95 this.stopConnectors()
96 this.started = false
97 this.stopping = false
98 }
99
100 public startConnector (connectorId: number): void {
101 if (!checkChargingStation(this.chargingStation, this.logPrefix(connectorId))) {
102 return
103 }
104 if (!this.connectorsStatus.has(connectorId)) {
105 logger.error(`${this.logPrefix(connectorId)} starting on non existing connector`)
106 throw new BaseError(`Connector ${connectorId} does not exist`)
107 }
108 if (this.connectorsStatus.get(connectorId)?.start === false) {
109 this.internalStartConnector(connectorId).catch(Constants.EMPTY_FUNCTION)
110 } else if (this.connectorsStatus.get(connectorId)?.start === true) {
111 logger.warn(`${this.logPrefix(connectorId)} is already started on connector`)
112 }
113 }
114
115 public stopConnector (connectorId: number): void {
116 if (!this.connectorsStatus.has(connectorId)) {
117 logger.error(`${this.logPrefix(connectorId)} stopping on non existing connector`)
118 throw new BaseError(`Connector ${connectorId} does not exist`)
119 }
120 if (this.connectorsStatus.get(connectorId)?.start === true) {
121 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
122 this.connectorsStatus.get(connectorId)!.start = false
123 } else if (this.connectorsStatus.get(connectorId)?.start === false) {
124 logger.warn(`${this.logPrefix(connectorId)} is already stopped on connector`)
125 }
126 }
127
128 private startConnectors (): void {
129 if (
130 this.connectorsStatus.size > 0 &&
131 this.connectorsStatus.size !== this.chargingStation.getNumberOfConnectors()
132 ) {
133 this.connectorsStatus.clear()
134 this.initializeConnectorsStatus()
135 }
136 if (this.chargingStation.hasEvses) {
137 for (const [evseId, evseStatus] of this.chargingStation.evses) {
138 if (evseId > 0) {
139 for (const connectorId of evseStatus.connectors.keys()) {
140 this.startConnector(connectorId)
141 }
142 }
143 }
144 } else {
145 for (const connectorId of this.chargingStation.connectors.keys()) {
146 if (connectorId > 0) {
147 this.startConnector(connectorId)
148 }
149 }
150 }
151 }
152
153 private stopConnectors (): void {
154 if (this.chargingStation.hasEvses) {
155 for (const [evseId, evseStatus] of this.chargingStation.evses) {
156 if (evseId > 0) {
157 for (const connectorId of evseStatus.connectors.keys()) {
158 this.stopConnector(connectorId)
159 }
160 }
161 }
162 } else {
163 for (const connectorId of this.chargingStation.connectors.keys()) {
164 if (connectorId > 0) {
165 this.stopConnector(connectorId)
166 }
167 }
168 }
169 }
170
171 private async internalStartConnector (connectorId: number): Promise<void> {
172 this.setStartConnectorStatus(connectorId)
173 logger.info(
174 `${this.logPrefix(
175 connectorId
176 )} started on connector and will run for ${formatDurationMilliSeconds(
177 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
178 this.connectorsStatus.get(connectorId)!.stopDate!.getTime() -
179 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
180 this.connectorsStatus.get(connectorId)!.startDate!.getTime()
181 )}`
182 )
183 while (this.connectorsStatus.get(connectorId)?.start === true) {
184 await this.waitChargingStationAvailable(connectorId)
185 await this.waitConnectorAvailable(connectorId)
186 if (!this.canStartConnector(connectorId)) {
187 this.stopConnector(connectorId)
188 break
189 }
190 const wait = secondsToMilliseconds(
191 getRandomInteger(
192 this.chargingStation.getAutomaticTransactionGeneratorConfiguration()
193 ?.maxDelayBetweenTwoTransactions,
194 this.chargingStation.getAutomaticTransactionGeneratorConfiguration()
195 ?.minDelayBetweenTwoTransactions
196 )
197 )
198 logger.info(`${this.logPrefix(connectorId)} waiting for ${formatDurationMilliSeconds(wait)}`)
199 await sleep(wait)
200 const start = secureRandom()
201 if (
202 start <
203 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
204 this.chargingStation.getAutomaticTransactionGeneratorConfiguration()!.probabilityOfStart
205 ) {
206 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
207 this.connectorsStatus.get(connectorId)!.skippedConsecutiveTransactions = 0
208 // Start transaction
209 const startResponse = await this.startTransaction(connectorId)
210 if (startResponse?.idTagInfo.status === AuthorizationStatus.ACCEPTED) {
211 // Wait until end of transaction
212 const waitTrxEnd = secondsToMilliseconds(
213 getRandomInteger(
214 this.chargingStation.getAutomaticTransactionGeneratorConfiguration()?.maxDuration,
215 this.chargingStation.getAutomaticTransactionGeneratorConfiguration()?.minDuration
216 )
217 )
218 logger.info(
219 `${this.logPrefix(
220 connectorId
221 )} transaction started with id ${this.chargingStation.getConnectorStatus(connectorId)
222 ?.transactionId} and will stop in ${formatDurationMilliSeconds(waitTrxEnd)}`
223 )
224 await sleep(waitTrxEnd)
225 await this.stopTransaction(connectorId)
226 }
227 } else {
228 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
229 ++this.connectorsStatus.get(connectorId)!.skippedConsecutiveTransactions!
230 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
231 ++this.connectorsStatus.get(connectorId)!.skippedTransactions!
232 logger.info(
233 `${this.logPrefix(connectorId)} skipped consecutively ${this.connectorsStatus.get(
234 connectorId
235 )?.skippedConsecutiveTransactions}/${this.connectorsStatus.get(connectorId)
236 ?.skippedTransactions} transaction(s)`
237 )
238 }
239 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
240 this.connectorsStatus.get(connectorId)!.lastRunDate = new Date()
241 }
242 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
243 this.connectorsStatus.get(connectorId)!.stoppedDate = new Date()
244 logger.info(
245 `${this.logPrefix(
246 connectorId
247 )} stopped on connector and lasted for ${formatDurationMilliSeconds(
248 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
249 this.connectorsStatus.get(connectorId)!.stoppedDate!.getTime() -
250 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
251 this.connectorsStatus.get(connectorId)!.startDate!.getTime()
252 )}`
253 )
254 logger.debug(
255 `${this.logPrefix(connectorId)} connector status: %j`,
256 this.connectorsStatus.get(connectorId)
257 )
258 }
259
260 private setStartConnectorStatus (connectorId: number): void {
261 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
262 this.connectorsStatus.get(connectorId)!.skippedConsecutiveTransactions = 0
263 const previousRunDuration =
264 this.connectorsStatus.get(connectorId)?.startDate != null &&
265 this.connectorsStatus.get(connectorId)?.lastRunDate != null
266 ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
267 this.connectorsStatus.get(connectorId)!.lastRunDate!.getTime() -
268 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
269 this.connectorsStatus.get(connectorId)!.startDate!.getTime()
270 : 0
271 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
272 this.connectorsStatus.get(connectorId)!.startDate = new Date()
273 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
274 this.connectorsStatus.get(connectorId)!.stopDate = new Date(
275 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
276 this.connectorsStatus.get(connectorId)!.startDate!.getTime() +
277 hoursToMilliseconds(
278 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
279 this.chargingStation.getAutomaticTransactionGeneratorConfiguration()!.stopAfterHours
280 ) -
281 previousRunDuration
282 )
283 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
284 this.connectorsStatus.get(connectorId)!.start = true
285 }
286
287 private canStartConnector (connectorId: number): boolean {
288 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
289 if (new Date() > this.connectorsStatus.get(connectorId)!.stopDate!) {
290 return false
291 }
292 if (!this.chargingStation.inAcceptedState()) {
293 logger.error(
294 `${this.logPrefix(
295 connectorId
296 )} entered in transaction loop while the charging station is not in accepted state`
297 )
298 return false
299 }
300 if (!this.chargingStation.isChargingStationAvailable()) {
301 logger.info(
302 `${this.logPrefix(
303 connectorId
304 )} entered in transaction loop while the charging station is unavailable`
305 )
306 return false
307 }
308 if (!this.chargingStation.isConnectorAvailable(connectorId)) {
309 logger.info(
310 `${this.logPrefix(
311 connectorId
312 )} entered in transaction loop while the connector ${connectorId} is unavailable`
313 )
314 return false
315 }
316 return true
317 }
318
319 private async waitChargingStationAvailable (connectorId: number): Promise<void> {
320 let logged = false
321 while (!this.chargingStation.isChargingStationAvailable()) {
322 if (!logged) {
323 logger.info(
324 `${this.logPrefix(
325 connectorId
326 )} transaction loop waiting for charging station to be available`
327 )
328 logged = true
329 }
330 await sleep(Constants.CHARGING_STATION_ATG_AVAILABILITY_TIME)
331 }
332 }
333
334 private async waitConnectorAvailable (connectorId: number): Promise<void> {
335 let logged = false
336 while (!this.chargingStation.isConnectorAvailable(connectorId)) {
337 if (!logged) {
338 logger.info(
339 `${this.logPrefix(
340 connectorId
341 )} transaction loop waiting for connector ${connectorId} to be available`
342 )
343 logged = true
344 }
345 await sleep(Constants.CHARGING_STATION_ATG_AVAILABILITY_TIME)
346 }
347 }
348
349 private initializeConnectorsStatus (): void {
350 if (this.chargingStation.hasEvses) {
351 for (const [evseId, evseStatus] of this.chargingStation.evses) {
352 if (evseId > 0) {
353 for (const connectorId of evseStatus.connectors.keys()) {
354 this.connectorsStatus.set(connectorId, this.getConnectorStatus(connectorId))
355 }
356 }
357 }
358 } else {
359 for (const connectorId of this.chargingStation.connectors.keys()) {
360 if (connectorId > 0) {
361 this.connectorsStatus.set(connectorId, this.getConnectorStatus(connectorId))
362 }
363 }
364 }
365 }
366
367 private getConnectorStatus (connectorId: number): Status {
368 const connectorStatus =
369 this.chargingStation.getAutomaticTransactionGeneratorStatuses()?.[connectorId] != null
370 ? cloneObject<Status>(
371 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
372 this.chargingStation.getAutomaticTransactionGeneratorStatuses()![connectorId]
373 )
374 : undefined
375 this.resetConnectorStatus(connectorStatus)
376 return (
377 connectorStatus ?? {
378 start: false,
379 authorizeRequests: 0,
380 acceptedAuthorizeRequests: 0,
381 rejectedAuthorizeRequests: 0,
382 startTransactionRequests: 0,
383 acceptedStartTransactionRequests: 0,
384 rejectedStartTransactionRequests: 0,
385 stopTransactionRequests: 0,
386 acceptedStopTransactionRequests: 0,
387 rejectedStopTransactionRequests: 0,
388 skippedConsecutiveTransactions: 0,
389 skippedTransactions: 0
390 }
391 )
392 }
393
394 private resetConnectorStatus (connectorStatus: Status | undefined): void {
395 if (connectorStatus == null) {
396 return
397 }
398 delete connectorStatus.startDate
399 delete connectorStatus.lastRunDate
400 delete connectorStatus.stopDate
401 delete connectorStatus.stoppedDate
402 if (
403 !this.started &&
404 (connectorStatus.start ||
405 this.chargingStation.getAutomaticTransactionGeneratorConfiguration()?.enable !== true)
406 ) {
407 connectorStatus.start = false
408 }
409 }
410
411 private async startTransaction (
412 connectorId: number
413 ): Promise<StartTransactionResponse | undefined> {
414 const measureId = 'StartTransaction with ATG'
415 const beginId = PerformanceStatistics.beginMeasure(measureId)
416 let startResponse: StartTransactionResponse | undefined
417 if (this.chargingStation.hasIdTags()) {
418 const idTag = IdTagsCache.getInstance().getIdTag(
419 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
420 this.chargingStation.getAutomaticTransactionGeneratorConfiguration()!.idTagDistribution!,
421 this.chargingStation,
422 connectorId
423 )
424 const startTransactionLogMsg = `${this.logPrefix(
425 connectorId
426 )} start transaction with an idTag '${idTag}'`
427 if (this.getRequireAuthorize()) {
428 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
429 ++this.connectorsStatus.get(connectorId)!.authorizeRequests!
430 if (await isIdTagAuthorized(this.chargingStation, connectorId, idTag)) {
431 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
432 ++this.connectorsStatus.get(connectorId)!.acceptedAuthorizeRequests!
433 logger.info(startTransactionLogMsg)
434 // Start transaction
435 startResponse = await this.chargingStation.ocppRequestService.requestHandler<
436 StartTransactionRequest,
437 StartTransactionResponse
438 >(this.chargingStation, RequestCommand.START_TRANSACTION, {
439 connectorId,
440 idTag
441 })
442 this.handleStartTransactionResponse(connectorId, startResponse)
443 PerformanceStatistics.endMeasure(measureId, beginId)
444 return startResponse
445 }
446 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
447 ++this.connectorsStatus.get(connectorId)!.rejectedAuthorizeRequests!
448 PerformanceStatistics.endMeasure(measureId, beginId)
449 return startResponse
450 }
451 logger.info(startTransactionLogMsg)
452 // Start transaction
453 startResponse = await this.chargingStation.ocppRequestService.requestHandler<
454 StartTransactionRequest,
455 StartTransactionResponse
456 >(this.chargingStation, RequestCommand.START_TRANSACTION, {
457 connectorId,
458 idTag
459 })
460 this.handleStartTransactionResponse(connectorId, startResponse)
461 PerformanceStatistics.endMeasure(measureId, beginId)
462 return startResponse
463 }
464 logger.info(`${this.logPrefix(connectorId)} start transaction without an idTag`)
465 startResponse = await this.chargingStation.ocppRequestService.requestHandler<
466 StartTransactionRequest,
467 StartTransactionResponse
468 >(this.chargingStation, RequestCommand.START_TRANSACTION, { connectorId })
469 this.handleStartTransactionResponse(connectorId, startResponse)
470 PerformanceStatistics.endMeasure(measureId, beginId)
471 return startResponse
472 }
473
474 private async stopTransaction (
475 connectorId: number,
476 reason = StopTransactionReason.LOCAL
477 ): Promise<StopTransactionResponse | undefined> {
478 const measureId = 'StopTransaction with ATG'
479 const beginId = PerformanceStatistics.beginMeasure(measureId)
480 let stopResponse: StopTransactionResponse | undefined
481 if (this.chargingStation.getConnectorStatus(connectorId)?.transactionStarted === true) {
482 logger.info(
483 `${this.logPrefix(
484 connectorId
485 )} stop transaction with id ${this.chargingStation.getConnectorStatus(connectorId)
486 ?.transactionId}`
487 )
488 stopResponse = await this.chargingStation.stopTransactionOnConnector(connectorId, reason)
489 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
490 ++this.connectorsStatus.get(connectorId)!.stopTransactionRequests!
491 if (stopResponse.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
492 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
493 ++this.connectorsStatus.get(connectorId)!.acceptedStopTransactionRequests!
494 } else {
495 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
496 ++this.connectorsStatus.get(connectorId)!.rejectedStopTransactionRequests!
497 }
498 } else {
499 const transactionId = this.chargingStation.getConnectorStatus(connectorId)?.transactionId
500 logger.debug(
501 `${this.logPrefix(connectorId)} stopping a not started transaction${
502 transactionId != null ? ` with id ${transactionId}` : ''
503 }`
504 )
505 }
506 PerformanceStatistics.endMeasure(measureId, beginId)
507 return stopResponse
508 }
509
510 private getRequireAuthorize (): boolean {
511 return (
512 this.chargingStation.getAutomaticTransactionGeneratorConfiguration()?.requireAuthorize ?? true
513 )
514 }
515
516 private readonly logPrefix = (connectorId?: number): string => {
517 return logPrefix(
518 ` ${this.chargingStation.stationInfo?.chargingStationId} | ATG${
519 connectorId != null ? ` on connector #${connectorId}` : ''
520 }:`
521 )
522 }
523
524 private handleStartTransactionResponse (
525 connectorId: number,
526 startResponse: StartTransactionResponse
527 ): void {
528 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
529 ++this.connectorsStatus.get(connectorId)!.startTransactionRequests!
530 if (startResponse.idTagInfo.status === AuthorizationStatus.ACCEPTED) {
531 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
532 ++this.connectorsStatus.get(connectorId)!.acceptedStartTransactionRequests!
533 } else {
534 logger.warn(`${this.logPrefix(connectorId)} start transaction rejected`)
535 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
536 ++this.connectorsStatus.get(connectorId)!.rejectedStartTransactionRequests!
537 }
538 }
539 }