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