feat: ensure charging station add op return its station info
[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'
8114d10e 8
66a7748d 9import chalk from 'chalk'
4c3f6c20
JB
10import { availableParallelism, type MessageHandler } from 'poolifier'
11import type { Worker } from 'worker_threads'
8114d10e 12
66a7748d
JB
13import { version } from '../../package.json'
14import { BaseError } from '../exception/index.js'
15import { type Storage, StorageFactory } from '../performance/index.js'
e7aeea18 16import {
bbe10d5f 17 type ChargingStationData,
3b09e788 18 type ChargingStationInfo,
71ac2bd7 19 type ChargingStationOptions,
bbe10d5f 20 type ChargingStationWorkerData,
244c1396 21 type ChargingStationWorkerEventError,
bbe10d5f
JB
22 type ChargingStationWorkerMessage,
23 type ChargingStationWorkerMessageData,
e7aeea18 24 ChargingStationWorkerMessageEvents,
5d049829 25 ConfigurationSection,
6bd808fd 26 ProcedureName,
e8237645 27 type SimulatorState,
268a74bb 28 type Statistics,
5d049829 29 type StorageConfiguration,
276e05ae 30 type TemplateStatistics,
5d049829 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,
4c3f6c20
JB
43 logger,
44 logPrefix
66a7748d
JB
45} from '../utils/index.js'
46import { type WorkerAbstract, WorkerFactory } from '../worker/index.js'
4c3f6c20
JB
47import { buildTemplateName, waitChargingStationEvents } from './Helpers.js'
48import type { AbstractUIServer } from './ui-server/AbstractUIServer.js'
49import { UIServerFactory } from './ui-server/UIServerFactory.js'
ded13d97 50
66a7748d 51const moduleName = 'Bootstrap'
32de5a57 52
a307349b 53enum exitCodes {
a51a4ead 54 succeeded = 0,
a307349b 55 missingChargingStationsConfiguration = 1,
2f989136
JB
56 duplicateChargingStationTemplateUrls = 2,
57 noChargingStationTemplates = 3,
58 gracefulShutdownError = 4
a307349b 59}
e4cb2c14 60
f130b8e6 61export class Bootstrap extends EventEmitter {
66a7748d 62 private static instance: Bootstrap | null = null
3b09e788 63 private workerImplementation?: WorkerAbstract<ChargingStationWorkerData, ChargingStationInfo>
a1cfaa16 64 private readonly uiServer: AbstractUIServer
66a7748d 65 private storage?: Storage
276e05ae 66 private readonly templateStatistics: Map<string, TemplateStatistics>
66a7748d 67 private readonly version: string = version
66a7748d
JB
68 private started: boolean
69 private starting: boolean
70 private stopping: boolean
a1cfaa16 71 private uiServerStarted: boolean
ded13d97 72
66a7748d
JB
73 private constructor () {
74 super()
6bd808fd 75 for (const signal of ['SIGINT', 'SIGQUIT', 'SIGTERM']) {
66a7748d 76 process.on(signal, this.gracefulShutdown.bind(this))
6bd808fd 77 }
4724a293 78 // Enable unconditionally for now
66a7748d
JB
79 handleUnhandledRejection()
80 handleUncaughtException()
81 this.started = false
82 this.starting = false
83 this.stopping = false
a1cfaa16 84 this.uiServerStarted = false
24dc52e9 85 this.templateStatistics = new Map<string, TemplateStatistics>()
36adaf06 86 this.uiServer = UIServerFactory.getUIServerImplementation(
66a7748d
JB
87 Configuration.getConfigurationSection<UIServerConfiguration>(ConfigurationSection.uiServer)
88 )
42e341c4 89 this.initializeCounters()
2bb3c92f
JB
90 this.initializeWorkerImplementation(
91 Configuration.getConfigurationSection<WorkerConfiguration>(ConfigurationSection.worker)
92 )
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,
276e05ae 122 templateStatistics: 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 )
24dc52e9
JB
179 // eslint-disable-next-line @typescript-eslint/unbound-method
180 if (isAsyncFunction(this.workerImplementation?.start)) {
181 await this.workerImplementation.start()
182 } else {
183 (this.workerImplementation?.start as () => void)()
184 }
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 }
24dc52e9
JB
225 const workerConfiguration = Configuration.getConfigurationSection<WorkerConfiguration>(
226 ConfigurationSection.worker
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 276 await this.workerImplementation?.stop()
66a7748d 277 this.removeAllListeners()
a1cfaa16 278 this.uiServer.clearCaches()
66a7748d
JB
279 await this.storage?.close()
280 delete this.storage
66a7748d
JB
281 this.started = false
282 this.stopping = false
82e9c15a 283 } else {
66a7748d 284 console.error(chalk.red('Cannot stop an already stopping charging stations simulator'))
82e9c15a 285 }
b322b8b4 286 } else {
66a7748d 287 console.error(chalk.red('Cannot stop an already stopped charging stations simulator'))
ded13d97 288 }
ded13d97
JB
289 }
290
c5ecc04d
JB
291 private async restart (): Promise<void> {
292 await this.stop()
a1cfaa16
JB
293 if (
294 this.uiServerStarted &&
295 Configuration.getConfigurationSection<UIServerConfiguration>(ConfigurationSection.uiServer)
296 .enabled !== true
297 ) {
298 this.uiServer.stop()
299 this.uiServerStarted = false
300 }
2bb3c92f
JB
301 this.initializeCounters()
302 // FIXME: initialize worker implementation only if the worker section has changed
303 this.initializeWorkerImplementation(
304 Configuration.getConfigurationSection<WorkerConfiguration>(ConfigurationSection.worker)
305 )
66a7748d 306 await this.start()
ded13d97
JB
307 }
308
66a7748d 309 private async waitChargingStationsStopped (): Promise<string> {
ea32ea05 310 return await new Promise<string>((resolve, reject: (reason?: unknown) => void) => {
5b2721db 311 const waitTimeout = setTimeout(() => {
a01134ed 312 const timeoutMessage = `Timeout ${formatDurationMilliSeconds(
66a7748d
JB
313 Constants.STOP_CHARGING_STATIONS_TIMEOUT
314 )} reached at stopping charging stations`
a01134ed
JB
315 console.warn(chalk.yellow(timeoutMessage))
316 reject(new Error(timeoutMessage))
66a7748d 317 }, Constants.STOP_CHARGING_STATIONS_TIMEOUT)
36adaf06
JB
318 waitChargingStationEvents(
319 this,
320 ChargingStationWorkerMessageEvents.stopped,
a01134ed 321 this.numberOfStartedChargingStations
5b2721db
JB
322 )
323 .then(() => {
66a7748d 324 resolve('Charging stations stopped')
5b2721db 325 })
b7ee97c1 326 .catch(reject)
5b2721db 327 .finally(() => {
66a7748d
JB
328 clearTimeout(waitTimeout)
329 })
330 })
36adaf06
JB
331 }
332
66a7748d 333 private initializeWorkerImplementation (workerConfiguration: WorkerConfiguration): void {
c5ecc04d
JB
334 if (!isMainThread) {
335 return
336 }
1feac591 337 let elementsPerWorker: number
5199f9fd 338 switch (workerConfiguration.elementsPerWorker) {
1feac591
JB
339 case 'all':
340 elementsPerWorker = this.numberOfConfiguredChargingStations
341 break
487f0dfd 342 case 'auto':
1feac591 343 default:
487f0dfd 344 elementsPerWorker =
2f989136
JB
345 this.numberOfConfiguredChargingStations > availableParallelism()
346 ? Math.round(this.numberOfConfiguredChargingStations / (availableParallelism() * 1.5))
66a7748d
JB
347 : 1
348 break
8603c1ca 349 }
3b09e788
JB
350 this.workerImplementation = WorkerFactory.getWorkerImplementation<
351 ChargingStationWorkerData,
352 ChargingStationInfo
353 >(
6d2b7d01
JB
354 join(
355 dirname(fileURLToPath(import.meta.url)),
66a7748d 356 `ChargingStationWorker${extname(fileURLToPath(import.meta.url))}`
6d2b7d01 357 ),
66a7748d 358 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
6d2b7d01
JB
359 workerConfiguration.processType!,
360 {
361 workerStartDelay: workerConfiguration.startDelay,
da47bc29 362 elementAddDelay: workerConfiguration.elementAddDelay,
66a7748d 363 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
6d2b7d01 364 poolMaxSize: workerConfiguration.poolMaxSize!,
66a7748d 365 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
6d2b7d01 366 poolMinSize: workerConfiguration.poolMinSize!,
1feac591 367 elementsPerWorker,
6d2b7d01 368 poolOptions: {
ba9a56a6 369 messageHandler: this.messageHandler.bind(this) as MessageHandler<Worker>,
56f94590
JB
370 ...(workerConfiguration.resourceLimits != null && {
371 workerOptions: { resourceLimits: workerConfiguration.resourceLimits }
372 })
66a7748d
JB
373 }
374 }
375 )
ded13d97 376 }
81797102 377
66a7748d
JB
378 private messageHandler (
379 msg: ChargingStationWorkerMessage<ChargingStationWorkerMessageData>
32de5a57
LM
380 ): void {
381 // logger.debug(
382 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
383 // msg,
4ed03b6e 384 // undefined,
66a7748d
JB
385 // 2
386 // )}`
387 // )
32de5a57 388 try {
8cc482a9 389 switch (msg.event) {
244c1396 390 case ChargingStationWorkerMessageEvents.added:
44fccdf0 391 this.emit(ChargingStationWorkerMessageEvents.added, msg.data)
244c1396 392 break
09e5a7a8
JB
393 case ChargingStationWorkerMessageEvents.deleted:
394 this.emit(ChargingStationWorkerMessageEvents.deleted, msg.data)
395 break
721646e9 396 case ChargingStationWorkerMessageEvents.started:
44fccdf0 397 this.emit(ChargingStationWorkerMessageEvents.started, msg.data)
66a7748d 398 break
721646e9 399 case ChargingStationWorkerMessageEvents.stopped:
44fccdf0 400 this.emit(ChargingStationWorkerMessageEvents.stopped, msg.data)
66a7748d 401 break
721646e9 402 case ChargingStationWorkerMessageEvents.updated:
44fccdf0 403 this.emit(ChargingStationWorkerMessageEvents.updated, msg.data)
66a7748d 404 break
721646e9 405 case ChargingStationWorkerMessageEvents.performanceStatistics:
44fccdf0 406 this.emit(ChargingStationWorkerMessageEvents.performanceStatistics, msg.data)
66a7748d 407 break
e1a3f3c1 408 case ChargingStationWorkerMessageEvents.addedWorkerElement:
a492245c 409 this.emit(ChargingStationWorkerMessageEvents.addWorkerElement, msg.data)
e1a3f3c1 410 break
244c1396 411 case ChargingStationWorkerMessageEvents.workerElementError:
244c1396 412 this.emit(ChargingStationWorkerMessageEvents.workerElementError, msg.data)
66a7748d 413 break
32de5a57
LM
414 default:
415 throw new BaseError(
f93dda6a
JB
416 `Unknown charging station worker event: '${
417 msg.event
66a7748d
JB
418 }' received with data: ${JSON.stringify(msg.data, undefined, 2)}`
419 )
32de5a57
LM
420 }
421 } catch (error) {
422 logger.error(
423 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
8cc482a9 424 msg.event
32de5a57 425 }' event:`,
66a7748d
JB
426 error
427 )
32de5a57
LM
428 }
429 }
430
244c1396 431 private readonly workerEventAdded = (data: ChargingStationData): void => {
a1cfaa16 432 this.uiServer.chargingStations.set(data.stationInfo.hashId, data)
244c1396
JB
433 logger.info(
434 `${this.logPrefix()} ${moduleName}.workerEventAdded: Charging station ${
435 data.stationInfo.chargingStationId
436 } (hashId: ${data.stationInfo.hashId}) added (${
437 this.numberOfAddedChargingStations
438 } added from ${this.numberOfConfiguredChargingStations} configured charging station(s))`
09e5a7a8
JB
439 )
440 }
441
442 private readonly workerEventDeleted = (data: ChargingStationData): void => {
a1cfaa16 443 this.uiServer.chargingStations.delete(data.stationInfo.hashId)
09e5a7a8 444 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
e8237645
JB
445 const templateStatistics = this.templateStatistics.get(data.stationInfo.templateName)!
446 --templateStatistics.added
447 templateStatistics.indexes.delete(data.stationInfo.templateIndex)
09e5a7a8
JB
448 logger.info(
449 `${this.logPrefix()} ${moduleName}.workerEventDeleted: Charging station ${
450 data.stationInfo.chargingStationId
451 } (hashId: ${data.stationInfo.hashId}) deleted (${
452 this.numberOfAddedChargingStations
453 } added from ${this.numberOfConfiguredChargingStations} configured charging station(s))`
244c1396
JB
454 )
455 }
456
66a7748d 457 private readonly workerEventStarted = (data: ChargingStationData): void => {
a1cfaa16 458 this.uiServer.chargingStations.set(data.stationInfo.hashId, data)
2f989136 459 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
e8237645 460 ++this.templateStatistics.get(data.stationInfo.templateName)!.started
56eb297e 461 logger.info(
e6159ce8 462 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
56eb297e 463 data.stationInfo.chargingStationId
e6159ce8 464 } (hashId: ${data.stationInfo.hashId}) started (${
56eb297e 465 this.numberOfStartedChargingStations
244c1396 466 } started from ${this.numberOfAddedChargingStations} added charging station(s))`
66a7748d
JB
467 )
468 }
32de5a57 469
66a7748d 470 private readonly workerEventStopped = (data: ChargingStationData): void => {
a1cfaa16 471 this.uiServer.chargingStations.set(data.stationInfo.hashId, data)
2f989136 472 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
e8237645 473 --this.templateStatistics.get(data.stationInfo.templateName)!.started
56eb297e 474 logger.info(
e6159ce8 475 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
56eb297e 476 data.stationInfo.chargingStationId
e6159ce8 477 } (hashId: ${data.stationInfo.hashId}) stopped (${
56eb297e 478 this.numberOfStartedChargingStations
244c1396 479 } started from ${this.numberOfAddedChargingStations} added charging station(s))`
66a7748d
JB
480 )
481 }
32de5a57 482
66a7748d 483 private readonly workerEventUpdated = (data: ChargingStationData): void => {
a1cfaa16 484 this.uiServer.chargingStations.set(data.stationInfo.hashId, data)
66a7748d 485 }
32de5a57 486
66a7748d 487 private readonly workerEventPerformanceStatistics = (data: Statistics): void => {
be0a4d4d
JB
488 // eslint-disable-next-line @typescript-eslint/unbound-method
489 if (isAsyncFunction(this.storage?.storePerformanceStatistics)) {
490 (
491 this.storage.storePerformanceStatistics as (
492 performanceStatistics: Statistics
493 ) => Promise<void>
494 )(data).catch(Constants.EMPTY_FUNCTION)
495 } else {
496 (this.storage?.storePerformanceStatistics as (performanceStatistics: Statistics) => void)(
497 data
498 )
499 }
66a7748d 500 }
32de5a57 501
66a7748d 502 private initializeCounters (): void {
2bb3c92f
JB
503 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
504 const stationTemplateUrls = Configuration.getStationTemplateUrls()!
505 if (isNotEmptyArray(stationTemplateUrls)) {
506 for (const stationTemplateUrl of stationTemplateUrls) {
507 const templateName = buildTemplateName(stationTemplateUrl.file)
508 this.templateStatistics.set(templateName, {
509 configured: stationTemplateUrl.numberOfStations,
510 added: 0,
511 started: 0,
512 indexes: new Set<number>()
513 })
514 this.uiServer.chargingStationTemplates.add(templateName)
a596d200 515 }
2bb3c92f 516 if (this.templateStatistics.size !== stationTemplateUrls.length) {
2f989136
JB
517 console.error(
518 chalk.red(
2bb3c92f 519 "'stationTemplateUrls' contains duplicate entries, please check your configuration"
2f989136
JB
520 )
521 )
2bb3c92f 522 exit(exitCodes.duplicateChargingStationTemplateUrls)
a596d200 523 }
2bb3c92f
JB
524 } else {
525 console.error(
526 chalk.red("'stationTemplateUrls' not defined or empty, please check your configuration")
527 )
528 exit(exitCodes.missingChargingStationsConfiguration)
529 }
530 if (
531 this.numberOfConfiguredChargingStations === 0 &&
532 Configuration.getConfigurationSection<UIServerConfiguration>(ConfigurationSection.uiServer)
533 .enabled !== true
534 ) {
535 console.error(
536 chalk.red(
537 "'stationTemplateUrls' has no charging station enabled and UI server is disabled, please check your configuration"
538 )
539 )
540 exit(exitCodes.noChargingStationTemplates)
846d2851 541 }
7c72977b
JB
542 }
543
71ac2bd7
JB
544 public async addChargingStation (
545 index: number,
a33026fe 546 templateFile: string,
71ac2bd7 547 options?: ChargingStationOptions
3b09e788 548 ): Promise<ChargingStationInfo | undefined> {
f6cb1767
JB
549 if (!this.started && !this.starting) {
550 throw new BaseError(
2762ad62 551 'Cannot add charging station while the charging stations simulator is not started'
f6cb1767
JB
552 )
553 }
3b09e788 554 const stationInfo = await this.workerImplementation?.addElement({
717c1e56 555 index,
d972af76
JB
556 templateFile: join(
557 dirname(fileURLToPath(import.meta.url)),
e7aeea18
JB
558 'assets',
559 'station-templates',
a33026fe 560 templateFile
71ac2bd7
JB
561 ),
562 options
66a7748d 563 })
c5ecc04d 564 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
a33026fe 565 const templateStatistics = this.templateStatistics.get(buildTemplateName(templateFile))!
e8237645
JB
566 ++templateStatistics.added
567 templateStatistics.indexes.add(index)
3b09e788 568 return stationInfo
717c1e56
JB
569 }
570
66a7748d 571 private gracefulShutdown (): void {
f130b8e6
JB
572 this.stop()
573 .then(() => {
5199f9fd 574 console.info(chalk.green('Graceful shutdown'))
a1cfaa16
JB
575 this.uiServer.stop()
576 this.uiServerStarted = false
36adaf06
JB
577 this.waitChargingStationsStopped()
578 .then(() => {
66a7748d 579 exit(exitCodes.succeeded)
36adaf06 580 })
5b2721db 581 .catch(() => {
66a7748d
JB
582 exit(exitCodes.gracefulShutdownError)
583 })
f130b8e6 584 })
ea32ea05 585 .catch((error: unknown) => {
66a7748d
JB
586 console.error(chalk.red('Error while shutdowning charging stations simulator: '), error)
587 exit(exitCodes.gracefulShutdownError)
588 })
36adaf06 589 }
f130b8e6 590
66a7748d
JB
591 private readonly logPrefix = (): string => {
592 return logPrefix(' Bootstrap |')
593 }
ded13d97 594}