refactor: improve error logging if ATG connector status is not found
[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 (): 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()
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): 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).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 (): 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)
143 }
144 }
145 }
146 } else {
147 for (const connectorId of this.chargingStation.connectors.keys()) {
148 if (connectorId > 0) {
149 this.startConnector(connectorId)
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 (connectorId: number): Promise<void> {
174 this.setStartConnectorStatus(connectorId)
175 logger.info(
176 `${this.logPrefix(
177 connectorId
178 )} started on connector and will run for ${formatDurationMilliSeconds(
179 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
180 this.connectorsStatus.get(connectorId)!.stopDate!.getTime() -
181 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
182 this.connectorsStatus.get(connectorId)!.startDate!.getTime()
183 )}`
184 )
185 while (this.connectorsStatus.get(connectorId)?.start === true) {
186 await this.waitChargingStationAvailable(connectorId)
187 await this.waitConnectorAvailable(connectorId)
188 if (!this.canStartConnector(connectorId)) {
189 this.stopConnector(connectorId)
190 break
191 }
192 const wait = secondsToMilliseconds(
193 getRandomInteger(
194 this.chargingStation.getAutomaticTransactionGeneratorConfiguration()
195 ?.maxDelayBetweenTwoTransactions,
196 this.chargingStation.getAutomaticTransactionGeneratorConfiguration()
197 ?.minDelayBetweenTwoTransactions
198 )
199 )
200 logger.info(`${this.logPrefix(connectorId)} waiting for ${formatDurationMilliSeconds(wait)}`)
201 await sleep(wait)
202 const start = secureRandom()
203 if (
204 start <
205 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
206 this.chargingStation.getAutomaticTransactionGeneratorConfiguration()!.probabilityOfStart
207 ) {
208 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
209 this.connectorsStatus.get(connectorId)!.skippedConsecutiveTransactions = 0
210 // Start transaction
211 const startResponse = await this.startTransaction(connectorId)
212 if (startResponse?.idTagInfo.status === AuthorizationStatus.ACCEPTED) {
213 // Wait until end of transaction
214 const waitTrxEnd = secondsToMilliseconds(
215 getRandomInteger(
216 this.chargingStation.getAutomaticTransactionGeneratorConfiguration()?.maxDuration,
217 this.chargingStation.getAutomaticTransactionGeneratorConfiguration()?.minDuration
218 )
219 )
220 logger.info(
221 `${this.logPrefix(connectorId)} transaction started with id ${
222 this.chargingStation.getConnectorStatus(connectorId)?.transactionId
223 } and will stop in ${formatDurationMilliSeconds(waitTrxEnd)}`
224 )
225 await sleep(waitTrxEnd)
226 await this.stopTransaction(connectorId)
227 }
228 } else {
229 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
230 ++this.connectorsStatus.get(connectorId)!.skippedConsecutiveTransactions
231 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
232 ++this.connectorsStatus.get(connectorId)!.skippedTransactions
233 logger.info(
234 `${this.logPrefix(connectorId)} skipped consecutively ${
235 this.connectorsStatus.get(connectorId)?.skippedConsecutiveTransactions
236 }/${this.connectorsStatus.get(connectorId)?.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)!.startDate = new Date()
263 if (
264 this.chargingStation.getAutomaticTransactionGeneratorConfiguration()?.stopAbsoluteDuration ===
265 false ||
266 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
267 !isValidDate(this.connectorsStatus.get(connectorId)!.stopDate)
268 ) {
269 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
270 this.connectorsStatus.get(connectorId)!.stopDate = new Date(
271 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
272 this.connectorsStatus.get(connectorId)!.startDate!.getTime() +
273 hoursToMilliseconds(
274 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
275 this.chargingStation.getAutomaticTransactionGeneratorConfiguration()!.stopAfterHours
276 )
277 )
278 }
279 delete this.connectorsStatus.get(connectorId)?.stoppedDate
280 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
281 this.connectorsStatus.get(connectorId)!.skippedConsecutiveTransactions = 0
282 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
283 this.connectorsStatus.get(connectorId)!.start = true
284 }
285
286 private canStartConnector (connectorId: number): boolean {
287 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
288 if (new Date() > this.connectorsStatus.get(connectorId)!.stopDate!) {
289 logger.info(
290 `${this.logPrefix(
291 connectorId
292 )} entered in transaction loop while the ATG stop date has been reached`
293 )
294 return false
295 }
296 if (!this.chargingStation.inAcceptedState()) {
297 logger.error(
298 `${this.logPrefix(
299 connectorId
300 )} entered in transaction loop while the charging station is not in accepted state`
301 )
302 return false
303 }
304 if (!this.chargingStation.isChargingStationAvailable()) {
305 logger.info(
306 `${this.logPrefix(
307 connectorId
308 )} entered in transaction loop while the charging station is unavailable`
309 )
310 return false
311 }
312 if (!this.chargingStation.isConnectorAvailable(connectorId)) {
313 logger.info(
314 `${this.logPrefix(
315 connectorId
316 )} entered in transaction loop while the connector ${connectorId} is unavailable`
317 )
318 return false
319 }
320 return true
321 }
322
323 private async waitChargingStationAvailable (connectorId: number): Promise<void> {
324 let logged = false
325 while (!this.chargingStation.isChargingStationAvailable()) {
326 if (!logged) {
327 logger.info(
328 `${this.logPrefix(
329 connectorId
330 )} transaction loop waiting for charging station to be available`
331 )
332 logged = true
333 }
334 await sleep(Constants.CHARGING_STATION_ATG_AVAILABILITY_TIME)
335 }
336 }
337
338 private async waitConnectorAvailable (connectorId: number): Promise<void> {
339 let logged = false
340 while (!this.chargingStation.isConnectorAvailable(connectorId)) {
341 if (!logged) {
342 logger.info(
343 `${this.logPrefix(
344 connectorId
345 )} transaction loop waiting for connector ${connectorId} to be available`
346 )
347 logged = true
348 }
349 await sleep(Constants.CHARGING_STATION_ATG_AVAILABILITY_TIME)
350 }
351 }
352
353 private initializeConnectorsStatus (): void {
354 if (this.chargingStation.hasEvses) {
355 for (const [evseId, evseStatus] of this.chargingStation.evses) {
356 if (evseId > 0) {
357 for (const connectorId of evseStatus.connectors.keys()) {
358 this.connectorsStatus.set(connectorId, this.getConnectorStatus(connectorId))
359 }
360 }
361 }
362 } else {
363 for (const connectorId of this.chargingStation.connectors.keys()) {
364 if (connectorId > 0) {
365 this.connectorsStatus.set(connectorId, this.getConnectorStatus(connectorId))
366 }
367 }
368 }
369 }
370
371 private getConnectorStatus (connectorId: number): Status {
372 const statusIndex = connectorId - 1
373 let connectorStatus: Status | undefined
374 if (this.chargingStation.getAutomaticTransactionGeneratorStatuses()?.[statusIndex] != null) {
375 connectorStatus = clone<Status>(
376 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
377 this.chargingStation.getAutomaticTransactionGeneratorStatuses()![statusIndex]
378 )
379 } else if (this.chargingStation.getAutomaticTransactionGeneratorStatuses() != null) {
380 logger.error(`${this.logPrefix(connectorId)} no status found for connector #${connectorId}`)
381 }
382 if (connectorStatus != null) {
383 connectorStatus.startDate = convertToDate(connectorStatus.startDate)
384 connectorStatus.lastRunDate = convertToDate(connectorStatus.lastRunDate)
385 connectorStatus.stopDate = convertToDate(connectorStatus.stopDate)
386 connectorStatus.stoppedDate = convertToDate(connectorStatus.stoppedDate)
387 if (
388 !this.started &&
389 (connectorStatus.start ||
390 this.chargingStation.getAutomaticTransactionGeneratorConfiguration()?.enable !== true)
391 ) {
392 connectorStatus.start = false
393 }
394 }
395 return (
396 connectorStatus ?? {
397 start: false,
398 authorizeRequests: 0,
399 acceptedAuthorizeRequests: 0,
400 rejectedAuthorizeRequests: 0,
401 startTransactionRequests: 0,
402 acceptedStartTransactionRequests: 0,
403 rejectedStartTransactionRequests: 0,
404 stopTransactionRequests: 0,
405 acceptedStopTransactionRequests: 0,
406 rejectedStopTransactionRequests: 0,
407 skippedConsecutiveTransactions: 0,
408 skippedTransactions: 0
409 }
410 )
411 }
412
413 private async startTransaction (
414 connectorId: number
415 ): Promise<StartTransactionResponse | undefined> {
416 const measureId = 'StartTransaction with ATG'
417 const beginId = PerformanceStatistics.beginMeasure(measureId)
418 let startResponse: StartTransactionResponse | undefined
419 if (this.chargingStation.hasIdTags()) {
420 const idTag = IdTagsCache.getInstance().getIdTag(
421 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
422 this.chargingStation.getAutomaticTransactionGeneratorConfiguration()!.idTagDistribution!,
423 this.chargingStation,
424 connectorId
425 )
426 const startTransactionLogMsg = `${this.logPrefix(
427 connectorId
428 )} start transaction with an idTag '${idTag}'`
429 if (this.getRequireAuthorize()) {
430 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
431 ++this.connectorsStatus.get(connectorId)!.authorizeRequests
432 if (await isIdTagAuthorized(this.chargingStation, connectorId, idTag)) {
433 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
434 ++this.connectorsStatus.get(connectorId)!.acceptedAuthorizeRequests
435 logger.info(startTransactionLogMsg)
436 // Start transaction
437 startResponse = await this.chargingStation.ocppRequestService.requestHandler<
438 StartTransactionRequest,
439 StartTransactionResponse
440 >(this.chargingStation, RequestCommand.START_TRANSACTION, {
441 connectorId,
442 idTag
443 })
444 this.handleStartTransactionResponse(connectorId, startResponse)
445 PerformanceStatistics.endMeasure(measureId, beginId)
446 return startResponse
447 }
448 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
449 ++this.connectorsStatus.get(connectorId)!.rejectedAuthorizeRequests
450 PerformanceStatistics.endMeasure(measureId, beginId)
451 return startResponse
452 }
453 logger.info(startTransactionLogMsg)
454 // Start transaction
455 startResponse = await this.chargingStation.ocppRequestService.requestHandler<
456 StartTransactionRequest,
457 StartTransactionResponse
458 >(this.chargingStation, RequestCommand.START_TRANSACTION, {
459 connectorId,
460 idTag
461 })
462 this.handleStartTransactionResponse(connectorId, startResponse)
463 PerformanceStatistics.endMeasure(measureId, beginId)
464 return startResponse
465 }
466 logger.info(`${this.logPrefix(connectorId)} start transaction without an idTag`)
467 startResponse = await this.chargingStation.ocppRequestService.requestHandler<
468 StartTransactionRequest,
469 StartTransactionResponse
470 >(this.chargingStation, RequestCommand.START_TRANSACTION, { connectorId })
471 this.handleStartTransactionResponse(connectorId, startResponse)
472 PerformanceStatistics.endMeasure(measureId, beginId)
473 return startResponse
474 }
475
476 private async stopTransaction (
477 connectorId: number,
478 reason = StopTransactionReason.LOCAL
479 ): Promise<StopTransactionResponse | undefined> {
480 const measureId = 'StopTransaction with ATG'
481 const beginId = PerformanceStatistics.beginMeasure(measureId)
482 let stopResponse: StopTransactionResponse | undefined
483 if (this.chargingStation.getConnectorStatus(connectorId)?.transactionStarted === true) {
484 logger.info(
485 `${this.logPrefix(connectorId)} stop transaction with id ${
486 this.chargingStation.getConnectorStatus(connectorId)?.transactionId
487 }`
488 )
489 stopResponse = await this.chargingStation.stopTransactionOnConnector(connectorId, reason)
490 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
491 ++this.connectorsStatus.get(connectorId)!.stopTransactionRequests
492 if (stopResponse.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
493 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
494 ++this.connectorsStatus.get(connectorId)!.acceptedStopTransactionRequests
495 } else {
496 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
497 ++this.connectorsStatus.get(connectorId)!.rejectedStopTransactionRequests
498 }
499 } else {
500 const transactionId = this.chargingStation.getConnectorStatus(connectorId)?.transactionId
501 logger.debug(
502 `${this.logPrefix(connectorId)} stopping a not started transaction${
503 transactionId != null ? ` with id ${transactionId}` : ''
504 }`
505 )
506 }
507 PerformanceStatistics.endMeasure(measureId, beginId)
508 return stopResponse
509 }
510
511 private getRequireAuthorize (): boolean {
512 return (
513 this.chargingStation.getAutomaticTransactionGeneratorConfiguration()?.requireAuthorize ?? true
514 )
515 }
516
517 private readonly logPrefix = (connectorId?: number): string => {
518 return logPrefix(
519 ` ${this.chargingStation.stationInfo?.chargingStationId} | ATG${
520 connectorId != null ? ` on connector #${connectorId}` : ''
521 }:`
522 )
523 }
524
525 private handleStartTransactionResponse (
526 connectorId: number,
527 startResponse: StartTransactionResponse
528 ): void {
529 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
530 ++this.connectorsStatus.get(connectorId)!.startTransactionRequests
531 if (startResponse.idTagInfo.status === AuthorizationStatus.ACCEPTED) {
532 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
533 ++this.connectorsStatus.get(connectorId)!.acceptedStartTransactionRequests
534 } else {
535 logger.warn(`${this.logPrefix(connectorId)} start transaction rejected`)
536 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
537 ++this.connectorsStatus.get(connectorId)!.rejectedStartTransactionRequests
538 }
539 }
540 }