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