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