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