fix: reset simulator state on stop/start cycle
[e-mobility-charging-stations-simulator.git] / src / charging-station / Bootstrap.ts
CommitLineData
a19b897d 1// Partial Copyright Jerome Benoit. 2021-2024. All Rights Reserved.
b4d34251 2
66a7748d 3import { EventEmitter } from 'node:events'
2f989136 4import { dirname, extname, join, parse } from 'node:path'
66a7748d
JB
5import process, { exit } from 'node:process'
6import { fileURLToPath } from 'node:url'
c5ecc04d 7import { isMainThread } from 'node:worker_threads'
ba9a56a6 8import type { Worker } from 'worker_threads'
8114d10e 9
66a7748d 10import chalk from 'chalk'
ba9a56a6 11import { type MessageHandler, availableParallelism } from 'poolifier'
8114d10e 12
66a7748d
JB
13import { waitChargingStationEvents } from './Helpers.js'
14import type { AbstractUIServer } from './ui-server/AbstractUIServer.js'
15import { UIServerFactory } from './ui-server/UIServerFactory.js'
16import { version } from '../../package.json'
17import { BaseError } from '../exception/index.js'
18import { type Storage, StorageFactory } from '../performance/index.js'
e7aeea18 19import {
bbe10d5f 20 type ChargingStationData,
71ac2bd7 21 type ChargingStationOptions,
bbe10d5f 22 type ChargingStationWorkerData,
244c1396 23 type ChargingStationWorkerEventError,
bbe10d5f
JB
24 type ChargingStationWorkerMessage,
25 type ChargingStationWorkerMessageData,
e7aeea18 26 ChargingStationWorkerMessageEvents,
5d049829 27 ConfigurationSection,
6bd808fd 28 ProcedureName,
268a74bb 29 type Statistics,
5d049829
JB
30 type StorageConfiguration,
31 type UIServerConfiguration,
66a7748d
JB
32 type WorkerConfiguration
33} from '../types/index.js'
fa5995d6
JB
34import {
35 Configuration,
36 Constants,
9bf0ef23
JB
37 formatDurationMilliSeconds,
38 generateUUID,
fa5995d6
JB
39 handleUncaughtException,
40 handleUnhandledRejection,
be0a4d4d 41 isAsyncFunction,
9bf0ef23 42 isNotEmptyArray,
9bf0ef23 43 logPrefix,
e375708d 44 logger
66a7748d
JB
45} from '../utils/index.js'
46import { type WorkerAbstract, WorkerFactory } from '../worker/index.js'
ded13d97 47
66a7748d 48const moduleName = 'Bootstrap'
32de5a57 49
a307349b 50enum exitCodes {
a51a4ead 51 succeeded = 0,
a307349b 52 missingChargingStationsConfiguration = 1,
2f989136
JB
53 duplicateChargingStationTemplateUrls = 2,
54 noChargingStationTemplates = 3,
55 gracefulShutdownError = 4
a307349b 56}
e4cb2c14 57
efc411f7
JB
58interface TemplateChargingStations {
59 configured: number
244c1396 60 added: number
efc411f7 61 started: number
e375708d 62 indexes: Set<number>
efc411f7
JB
63}
64
f130b8e6 65export class Bootstrap extends EventEmitter {
66a7748d 66 private static instance: Bootstrap | null = null
66a7748d
JB
67 private workerImplementation?: WorkerAbstract<ChargingStationWorkerData>
68 private readonly uiServer?: AbstractUIServer
69 private storage?: Storage
afbb8202 70 private readonly templatesChargingStations: Map<string, TemplateChargingStations>
66a7748d
JB
71 private readonly version: string = version
72 private initializedCounters: boolean
73 private started: boolean
74 private starting: boolean
75 private stopping: boolean
ded13d97 76
66a7748d
JB
77 private constructor () {
78 super()
6bd808fd 79 for (const signal of ['SIGINT', 'SIGQUIT', 'SIGTERM']) {
66a7748d 80 process.on(signal, this.gracefulShutdown.bind(this))
6bd808fd 81 }
4724a293 82 // Enable unconditionally for now
66a7748d
JB
83 handleUnhandledRejection()
84 handleUncaughtException()
85 this.started = false
86 this.starting = false
87 this.stopping = false
afbb8202 88 this.templatesChargingStations = new Map<string, TemplateChargingStations>()
36adaf06 89 this.uiServer = UIServerFactory.getUIServerImplementation(
66a7748d
JB
90 Configuration.getConfigurationSection<UIServerConfiguration>(ConfigurationSection.uiServer)
91 )
42e341c4
JB
92 this.initializedCounters = false
93 this.initializeCounters()
66a7748d 94 Configuration.configurationChangeCallback = async () => {
c5ecc04d
JB
95 if (isMainThread) {
96 await Bootstrap.getInstance().restart()
97 }
66a7748d 98 }
ded13d97
JB
99 }
100
66a7748d 101 public static getInstance (): Bootstrap {
1ca780f9 102 if (Bootstrap.instance === null) {
66a7748d 103 Bootstrap.instance = new Bootstrap()
ded13d97 104 }
66a7748d 105 return Bootstrap.instance
ded13d97
JB
106 }
107
2f989136 108 public get numberOfChargingStationTemplates (): number {
afbb8202 109 return this.templatesChargingStations.size
2f989136
JB
110 }
111
112 public get numberOfConfiguredChargingStations (): number {
afbb8202 113 return [...this.templatesChargingStations.values()].reduce(
2f989136
JB
114 (accumulator, value) => accumulator + value.configured,
115 0
116 )
117 }
118
c5ecc04d 119 public getLastIndex (templateName: string): number {
e375708d 120 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
afbb8202 121 const indexes = [...this.templatesChargingStations.get(templateName)!.indexes]
e375708d
JB
122 .concat(0)
123 .sort((a, b) => a - b)
124 for (let i = 0; i < indexes.length - 1; i++) {
125 if (indexes[i + 1] - indexes[i] !== 1) {
126 return indexes[i]
127 }
128 }
129 return indexes[indexes.length - 1]
c5ecc04d
JB
130 }
131
a66bbcfe
JB
132 public getPerformanceStatistics (): IterableIterator<Statistics> | undefined {
133 return this.storage?.getPerformanceStatistics()
134 }
135
244c1396 136 private get numberOfAddedChargingStations (): number {
afbb8202 137 return [...this.templatesChargingStations.values()].reduce(
244c1396
JB
138 (accumulator, value) => accumulator + value.added,
139 0
140 )
141 }
142
2f989136 143 private get numberOfStartedChargingStations (): number {
afbb8202 144 return [...this.templatesChargingStations.values()].reduce(
2f989136
JB
145 (accumulator, value) => accumulator + value.started,
146 0
147 )
148 }
149
66a7748d
JB
150 public async start (): Promise<void> {
151 if (!this.started) {
152 if (!this.starting) {
153 this.starting = true
244c1396 154 this.on(ChargingStationWorkerMessageEvents.added, this.workerEventAdded)
09e5a7a8 155 this.on(ChargingStationWorkerMessageEvents.deleted, this.workerEventDeleted)
66a7748d
JB
156 this.on(ChargingStationWorkerMessageEvents.started, this.workerEventStarted)
157 this.on(ChargingStationWorkerMessageEvents.stopped, this.workerEventStopped)
158 this.on(ChargingStationWorkerMessageEvents.updated, this.workerEventUpdated)
4354af5a
JB
159 this.on(
160 ChargingStationWorkerMessageEvents.performanceStatistics,
66a7748d
JB
161 this.workerEventPerformanceStatistics
162 )
44fccdf0
JB
163 this.on(
164 ChargingStationWorkerMessageEvents.workerElementError,
887a125e 165 (eventError: ChargingStationWorkerEventError) => {
44fccdf0 166 logger.error(
3ab32759 167 `${this.logPrefix()} ${moduleName}.start: Error occurred while handling '${eventError.event}' event on worker:`,
887a125e 168 eventError
44fccdf0
JB
169 )
170 }
171 )
66a7748d 172 this.initializeCounters()
5b373a23 173 const workerConfiguration = Configuration.getConfigurationSection<WorkerConfiguration>(
66a7748d
JB
174 ConfigurationSection.worker
175 )
176 this.initializeWorkerImplementation(workerConfiguration)
177 await this.workerImplementation?.start()
6d2b7d01
JB
178 const performanceStorageConfiguration =
179 Configuration.getConfigurationSection<StorageConfiguration>(
66a7748d
JB
180 ConfigurationSection.performanceStorage
181 )
6d2b7d01
JB
182 if (performanceStorageConfiguration.enabled === true) {
183 this.storage = StorageFactory.getStorage(
66a7748d 184 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
6d2b7d01 185 performanceStorageConfiguration.type!,
66a7748d 186 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
6d2b7d01 187 performanceStorageConfiguration.uri!,
66a7748d
JB
188 this.logPrefix()
189 )
190 await this.storage?.open()
6d2b7d01 191 }
36adaf06 192 Configuration.getConfigurationSection<UIServerConfiguration>(ConfigurationSection.uiServer)
66a7748d 193 .enabled === true && this.uiServer?.start()
82e9c15a 194 // Start ChargingStation object instance in worker thread
66a7748d 195 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
e1d9a0f4 196 for (const stationTemplateUrl of Configuration.getStationTemplateUrls()!) {
82e9c15a 197 try {
2f989136 198 const nbStations =
afbb8202
JB
199 this.templatesChargingStations.get(parse(stationTemplateUrl.file).name)?.configured ??
200 stationTemplateUrl.numberOfStations
82e9c15a 201 for (let index = 1; index <= nbStations; index++) {
c5ecc04d 202 await this.addChargingStation(index, stationTemplateUrl.file)
82e9c15a
JB
203 }
204 } catch (error) {
205 console.error(
206 chalk.red(
66a7748d 207 `Error at starting charging station with template file ${stationTemplateUrl.file}: `
82e9c15a 208 ),
66a7748d
JB
209 error
210 )
ded13d97 211 }
ded13d97 212 }
82e9c15a
JB
213 console.info(
214 chalk.green(
215 `Charging stations simulator ${
216 this.version
c5ecc04d 217 } started with ${this.numberOfConfiguredChargingStations} configured charging station(s) from ${this.numberOfChargingStationTemplates} charging station template(s) and ${
2f989136 218 Configuration.workerDynamicPoolInUse() ? `${workerConfiguration.poolMinSize}/` : ''
82e9c15a 219 }${this.workerImplementation?.size}${
2f989136 220 Configuration.workerPoolInUse() ? `/${workerConfiguration.poolMaxSize}` : ''
5b373a23 221 } worker(s) concurrently running in '${workerConfiguration.processType}' mode${
401fa922 222 this.workerImplementation?.maxElementsPerWorker != null
5199f9fd 223 ? ` (${this.workerImplementation.maxElementsPerWorker} charging station(s) per worker)`
82e9c15a 224 : ''
66a7748d
JB
225 }`
226 )
227 )
56e2e1ab
JB
228 Configuration.workerDynamicPoolInUse() &&
229 console.warn(
230 chalk.yellow(
66a7748d
JB
231 'Charging stations simulator is using dynamic pool mode. This is an experimental feature with known issues.\nPlease consider using fixed pool or worker set mode instead'
232 )
233 )
234 console.info(chalk.green('Worker set/pool information:'), this.workerImplementation?.info)
235 this.started = true
236 this.starting = false
82e9c15a 237 } else {
66a7748d 238 console.error(chalk.red('Cannot start an already starting charging stations simulator'))
ded13d97 239 }
b322b8b4 240 } else {
66a7748d 241 console.error(chalk.red('Cannot start an already started charging stations simulator'))
ded13d97
JB
242 }
243 }
244
c5ecc04d 245 public async stop (): Promise<void> {
66a7748d
JB
246 if (this.started) {
247 if (!this.stopping) {
248 this.stopping = true
c5ecc04d
JB
249 await this.uiServer?.sendInternalRequest(
250 this.uiServer.buildProtocolRequest(
251 generateUUID(),
252 ProcedureName.STOP_CHARGING_STATION,
253 Constants.EMPTY_FROZEN_OBJECT
66a7748d 254 )
c5ecc04d
JB
255 )
256 try {
257 await this.waitChargingStationsStopped()
258 } catch (error) {
259 console.error(chalk.red('Error while waiting for charging stations to stop: '), error)
ab7a96fa 260 }
66a7748d
JB
261 await this.workerImplementation?.stop()
262 delete this.workerImplementation
263 this.removeAllListeners()
5c0e9352 264 this.uiServer?.chargingStations.clear()
9d289c63 265 this.initializedCounters = false
66a7748d
JB
266 await this.storage?.close()
267 delete this.storage
66a7748d
JB
268 this.started = false
269 this.stopping = false
82e9c15a 270 } else {
66a7748d 271 console.error(chalk.red('Cannot stop an already stopping charging stations simulator'))
82e9c15a 272 }
b322b8b4 273 } else {
66a7748d 274 console.error(chalk.red('Cannot stop an already stopped charging stations simulator'))
ded13d97 275 }
ded13d97
JB
276 }
277
c5ecc04d
JB
278 private async restart (): Promise<void> {
279 await this.stop()
73edcc94 280 Configuration.getConfigurationSection<UIServerConfiguration>(ConfigurationSection.uiServer)
baf34a77 281 .enabled !== true && this.uiServer?.stop()
66a7748d 282 await this.start()
ded13d97
JB
283 }
284
66a7748d
JB
285 private async waitChargingStationsStopped (): Promise<string> {
286 return await new Promise<string>((resolve, reject) => {
5b2721db 287 const waitTimeout = setTimeout(() => {
a01134ed 288 const timeoutMessage = `Timeout ${formatDurationMilliSeconds(
66a7748d
JB
289 Constants.STOP_CHARGING_STATIONS_TIMEOUT
290 )} reached at stopping charging stations`
a01134ed
JB
291 console.warn(chalk.yellow(timeoutMessage))
292 reject(new Error(timeoutMessage))
66a7748d 293 }, Constants.STOP_CHARGING_STATIONS_TIMEOUT)
36adaf06
JB
294 waitChargingStationEvents(
295 this,
296 ChargingStationWorkerMessageEvents.stopped,
a01134ed 297 this.numberOfStartedChargingStations
5b2721db
JB
298 )
299 .then(() => {
66a7748d 300 resolve('Charging stations stopped')
5b2721db 301 })
b7ee97c1 302 .catch(reject)
5b2721db 303 .finally(() => {
66a7748d
JB
304 clearTimeout(waitTimeout)
305 })
306 })
36adaf06
JB
307 }
308
66a7748d 309 private initializeWorkerImplementation (workerConfiguration: WorkerConfiguration): void {
c5ecc04d
JB
310 if (!isMainThread) {
311 return
312 }
1feac591 313 let elementsPerWorker: number
5199f9fd 314 switch (workerConfiguration.elementsPerWorker) {
1feac591
JB
315 case 'all':
316 elementsPerWorker = this.numberOfConfiguredChargingStations
317 break
487f0dfd 318 case 'auto':
1feac591 319 default:
487f0dfd 320 elementsPerWorker =
2f989136
JB
321 this.numberOfConfiguredChargingStations > availableParallelism()
322 ? Math.round(this.numberOfConfiguredChargingStations / (availableParallelism() * 1.5))
66a7748d
JB
323 : 1
324 break
8603c1ca 325 }
6d2b7d01
JB
326 this.workerImplementation = WorkerFactory.getWorkerImplementation<ChargingStationWorkerData>(
327 join(
328 dirname(fileURLToPath(import.meta.url)),
66a7748d 329 `ChargingStationWorker${extname(fileURLToPath(import.meta.url))}`
6d2b7d01 330 ),
66a7748d 331 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
6d2b7d01
JB
332 workerConfiguration.processType!,
333 {
334 workerStartDelay: workerConfiguration.startDelay,
335 elementStartDelay: workerConfiguration.elementStartDelay,
66a7748d 336 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
6d2b7d01 337 poolMaxSize: workerConfiguration.poolMaxSize!,
66a7748d 338 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
6d2b7d01 339 poolMinSize: workerConfiguration.poolMinSize!,
1feac591 340 elementsPerWorker,
6d2b7d01 341 poolOptions: {
ba9a56a6 342 messageHandler: this.messageHandler.bind(this) as MessageHandler<Worker>,
66a7748d
JB
343 workerOptions: { resourceLimits: workerConfiguration.resourceLimits }
344 }
345 }
346 )
ded13d97 347 }
81797102 348
66a7748d
JB
349 private messageHandler (
350 msg: ChargingStationWorkerMessage<ChargingStationWorkerMessageData>
32de5a57
LM
351 ): void {
352 // logger.debug(
353 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
354 // msg,
4ed03b6e 355 // undefined,
66a7748d
JB
356 // 2
357 // )}`
358 // )
32de5a57 359 try {
8cc482a9 360 switch (msg.event) {
244c1396 361 case ChargingStationWorkerMessageEvents.added:
44fccdf0 362 this.emit(ChargingStationWorkerMessageEvents.added, msg.data)
244c1396 363 break
09e5a7a8
JB
364 case ChargingStationWorkerMessageEvents.deleted:
365 this.emit(ChargingStationWorkerMessageEvents.deleted, msg.data)
366 break
721646e9 367 case ChargingStationWorkerMessageEvents.started:
44fccdf0 368 this.emit(ChargingStationWorkerMessageEvents.started, msg.data)
66a7748d 369 break
721646e9 370 case ChargingStationWorkerMessageEvents.stopped:
44fccdf0 371 this.emit(ChargingStationWorkerMessageEvents.stopped, msg.data)
66a7748d 372 break
721646e9 373 case ChargingStationWorkerMessageEvents.updated:
44fccdf0 374 this.emit(ChargingStationWorkerMessageEvents.updated, msg.data)
66a7748d 375 break
721646e9 376 case ChargingStationWorkerMessageEvents.performanceStatistics:
44fccdf0 377 this.emit(ChargingStationWorkerMessageEvents.performanceStatistics, msg.data)
66a7748d 378 break
e1a3f3c1 379 case ChargingStationWorkerMessageEvents.addedWorkerElement:
a492245c 380 this.emit(ChargingStationWorkerMessageEvents.addWorkerElement, msg.data)
e1a3f3c1 381 break
244c1396 382 case ChargingStationWorkerMessageEvents.workerElementError:
244c1396 383 this.emit(ChargingStationWorkerMessageEvents.workerElementError, msg.data)
66a7748d 384 break
32de5a57
LM
385 default:
386 throw new BaseError(
f93dda6a
JB
387 `Unknown charging station worker event: '${
388 msg.event
66a7748d
JB
389 }' received with data: ${JSON.stringify(msg.data, undefined, 2)}`
390 )
32de5a57
LM
391 }
392 } catch (error) {
393 logger.error(
394 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
8cc482a9 395 msg.event
32de5a57 396 }' event:`,
66a7748d
JB
397 error
398 )
32de5a57
LM
399 }
400 }
401
244c1396
JB
402 private readonly workerEventAdded = (data: ChargingStationData): void => {
403 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data)
244c1396
JB
404 logger.info(
405 `${this.logPrefix()} ${moduleName}.workerEventAdded: Charging station ${
406 data.stationInfo.chargingStationId
407 } (hashId: ${data.stationInfo.hashId}) added (${
408 this.numberOfAddedChargingStations
409 } added from ${this.numberOfConfiguredChargingStations} configured charging station(s))`
09e5a7a8
JB
410 )
411 }
412
413 private readonly workerEventDeleted = (data: ChargingStationData): void => {
414 this.uiServer?.chargingStations.delete(data.stationInfo.hashId)
415 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
afbb8202
JB
416 const templateChargingStations = this.templatesChargingStations.get(
417 data.stationInfo.templateName
418 )!
419 --templateChargingStations.added
420 templateChargingStations.indexes.delete(data.stationInfo.templateIndex)
09e5a7a8
JB
421 logger.info(
422 `${this.logPrefix()} ${moduleName}.workerEventDeleted: Charging station ${
423 data.stationInfo.chargingStationId
424 } (hashId: ${data.stationInfo.hashId}) deleted (${
425 this.numberOfAddedChargingStations
426 } added from ${this.numberOfConfiguredChargingStations} configured charging station(s))`
244c1396
JB
427 )
428 }
429
66a7748d
JB
430 private readonly workerEventStarted = (data: ChargingStationData): void => {
431 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data)
2f989136 432 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
afbb8202 433 ++this.templatesChargingStations.get(data.stationInfo.templateName)!.started
56eb297e 434 logger.info(
e6159ce8 435 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
56eb297e 436 data.stationInfo.chargingStationId
e6159ce8 437 } (hashId: ${data.stationInfo.hashId}) started (${
56eb297e 438 this.numberOfStartedChargingStations
244c1396 439 } started from ${this.numberOfAddedChargingStations} added charging station(s))`
66a7748d
JB
440 )
441 }
32de5a57 442
66a7748d
JB
443 private readonly workerEventStopped = (data: ChargingStationData): void => {
444 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data)
2f989136 445 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
afbb8202 446 --this.templatesChargingStations.get(data.stationInfo.templateName)!.started
56eb297e 447 logger.info(
e6159ce8 448 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
56eb297e 449 data.stationInfo.chargingStationId
e6159ce8 450 } (hashId: ${data.stationInfo.hashId}) stopped (${
56eb297e 451 this.numberOfStartedChargingStations
244c1396 452 } started from ${this.numberOfAddedChargingStations} added charging station(s))`
66a7748d
JB
453 )
454 }
32de5a57 455
66a7748d
JB
456 private readonly workerEventUpdated = (data: ChargingStationData): void => {
457 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data)
458 }
32de5a57 459
66a7748d 460 private readonly workerEventPerformanceStatistics = (data: Statistics): void => {
be0a4d4d
JB
461 // eslint-disable-next-line @typescript-eslint/unbound-method
462 if (isAsyncFunction(this.storage?.storePerformanceStatistics)) {
463 (
464 this.storage.storePerformanceStatistics as (
465 performanceStatistics: Statistics
466 ) => Promise<void>
467 )(data).catch(Constants.EMPTY_FUNCTION)
468 } else {
469 (this.storage?.storePerformanceStatistics as (performanceStatistics: Statistics) => void)(
470 data
471 )
472 }
66a7748d 473 }
32de5a57 474
66a7748d
JB
475 private initializeCounters (): void {
476 if (!this.initializedCounters) {
66a7748d
JB
477 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
478 const stationTemplateUrls = Configuration.getStationTemplateUrls()!
9bf0ef23 479 if (isNotEmptyArray(stationTemplateUrls)) {
7436ee0d 480 for (const stationTemplateUrl of stationTemplateUrls) {
2f989136 481 const templateName = parse(stationTemplateUrl.file).name
afbb8202 482 this.templatesChargingStations.set(templateName, {
2f989136 483 configured: stationTemplateUrl.numberOfStations,
244c1396 484 added: 0,
c5ecc04d 485 started: 0,
e375708d 486 indexes: new Set<number>()
2f989136
JB
487 })
488 this.uiServer?.chargingStationTemplates.add(templateName)
489 }
afbb8202 490 if (this.templatesChargingStations.size !== stationTemplateUrls.length) {
2f989136
JB
491 console.error(
492 chalk.red(
493 "'stationTemplateUrls' contains duplicate entries, please check your configuration"
494 )
495 )
496 exit(exitCodes.duplicateChargingStationTemplateUrls)
7436ee0d 497 }
a596d200 498 } else {
2f989136
JB
499 console.error(
500 chalk.red("'stationTemplateUrls' not defined or empty, please check your configuration")
66a7748d
JB
501 )
502 exit(exitCodes.missingChargingStationsConfiguration)
a596d200 503 }
c5ecc04d
JB
504 if (
505 this.numberOfConfiguredChargingStations === 0 &&
506 Configuration.getConfigurationSection<UIServerConfiguration>(ConfigurationSection.uiServer)
baf34a77 507 .enabled !== true
c5ecc04d 508 ) {
2f989136
JB
509 console.error(
510 chalk.red(
c5ecc04d 511 "'stationTemplateUrls' has no charging station enabled and UI server is disabled, please check your configuration"
2f989136
JB
512 )
513 )
66a7748d 514 exit(exitCodes.noChargingStationTemplates)
a596d200 515 }
66a7748d 516 this.initializedCounters = true
846d2851 517 }
7c72977b
JB
518 }
519
71ac2bd7
JB
520 public async addChargingStation (
521 index: number,
522 stationTemplateFile: string,
523 options?: ChargingStationOptions
524 ): Promise<void> {
6ed3c845 525 await this.workerImplementation?.addElement({
717c1e56 526 index,
d972af76
JB
527 templateFile: join(
528 dirname(fileURLToPath(import.meta.url)),
e7aeea18
JB
529 'assets',
530 'station-templates',
c5ecc04d 531 stationTemplateFile
71ac2bd7
JB
532 ),
533 options
66a7748d 534 })
c5ecc04d 535 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
afbb8202
JB
536 const templateChargingStations = this.templatesChargingStations.get(
537 parse(stationTemplateFile).name
538 )!
539 ++templateChargingStations.added
540 templateChargingStations.indexes.add(index)
717c1e56
JB
541 }
542
66a7748d 543 private gracefulShutdown (): void {
f130b8e6
JB
544 this.stop()
545 .then(() => {
5199f9fd 546 console.info(chalk.green('Graceful shutdown'))
66a7748d 547 this.uiServer?.stop()
36adaf06
JB
548 this.waitChargingStationsStopped()
549 .then(() => {
66a7748d 550 exit(exitCodes.succeeded)
36adaf06 551 })
5b2721db 552 .catch(() => {
66a7748d
JB
553 exit(exitCodes.gracefulShutdownError)
554 })
f130b8e6 555 })
a974c8e4 556 .catch(error => {
66a7748d
JB
557 console.error(chalk.red('Error while shutdowning charging stations simulator: '), error)
558 exit(exitCodes.gracefulShutdownError)
559 })
36adaf06 560 }
f130b8e6 561
66a7748d
JB
562 private readonly logPrefix = (): string => {
563 return logPrefix(' Bootstrap |')
564 }
ded13d97 565}