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