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