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