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