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