1 import { existsSync
, type FSWatcher
, readFileSync
, watch
} from
'node:fs'
2 import { dirname
, join
} from
'node:path'
3 import { env
} from
'node:process'
4 import { fileURLToPath
} from
'node:url'
6 import chalk from
'chalk'
7 import { mergeDeepRight
, once
} from
'rambda'
11 ApplicationProtocolVersion
,
12 type ConfigurationData
,
15 type LogConfiguration
,
16 type StationTemplateUrl
,
17 type StorageConfiguration
,
19 SupervisionUrlDistribution
,
20 type UIServerConfiguration
,
21 type WorkerConfiguration
22 } from
'../types/index.js'
24 DEFAULT_ELEMENT_ADD_DELAY
,
25 DEFAULT_POOL_MAX_SIZE
,
26 DEFAULT_POOL_MIN_SIZE
,
27 DEFAULT_WORKER_START_DELAY
,
29 } from
'../worker/index.js'
31 buildPerformanceUriFilePath
,
32 checkWorkerElementsPerWorker
,
33 checkWorkerProcessType
,
34 getDefaultPerformanceStorageUri
,
37 } from
'./ConfigurationUtils.js'
38 import { Constants
} from
'./Constants.js'
39 import { hasOwnProp
, isCFEnvironment
} from
'./Utils.js'
41 type ConfigurationSectionType
=
43 | StorageConfiguration
45 | UIServerConfiguration
47 const defaultUIServerConfiguration
: UIServerConfiguration
= {
49 type: ApplicationProtocol
.WS
,
50 version
: ApplicationProtocolVersion
.VERSION_11
,
52 host
: Constants
.DEFAULT_UI_SERVER_HOST
,
53 port
: Constants
.DEFAULT_UI_SERVER_PORT
57 const defaultStorageConfiguration
: StorageConfiguration
= {
59 type: StorageType
.NONE
62 const defaultLogConfiguration
: LogConfiguration
= {
64 file
: 'logs/combined.log',
65 errorFile
: 'logs/error.log',
66 statisticsInterval
: Constants
.DEFAULT_LOG_STATISTICS_INTERVAL
,
72 const defaultWorkerConfiguration
: WorkerConfiguration
= {
73 processType
: WorkerProcessType
.workerSet
,
74 startDelay
: DEFAULT_WORKER_START_DELAY
,
75 elementsPerWorker
: 'auto',
76 elementAddDelay
: DEFAULT_ELEMENT_ADD_DELAY
,
77 poolMinSize
: DEFAULT_POOL_MIN_SIZE
,
78 poolMaxSize
: DEFAULT_POOL_MAX_SIZE
81 // eslint-disable-next-line @typescript-eslint/no-extraneous-class
82 export class Configuration
{
83 public static configurationChangeCallback
?: () => Promise
<void>
85 private static configurationFile
: string | undefined
86 private static configurationFileReloading
= false
87 private static configurationData
?: ConfigurationData
88 private static configurationFileWatcher
?: FSWatcher
89 private static configurationSectionCache
: Map
<ConfigurationSection
, ConfigurationSectionType
>
92 const configurationFile
= join(dirname(fileURLToPath(import.meta
.url
)), 'assets', 'config.json')
93 if (existsSync(configurationFile
)) {
94 Configuration
.configurationFile
= configurationFile
97 `${chalk.green(logPrefix())} ${chalk.red(
98 "Configuration file './src/assets/config.json' not found, using default configuration"
101 Configuration
.configurationData
= {
102 stationTemplateUrls
: [
104 file
: 'siemens.station-template.json',
108 supervisionUrls
: 'ws://localhost:8180/steve/websocket/CentralSystemService',
109 supervisionUrlDistribution
: SupervisionUrlDistribution
.ROUND_ROBIN
,
110 uiServer
: defaultUIServerConfiguration
,
111 performanceStorage
: defaultStorageConfiguration
,
112 log
: defaultLogConfiguration
,
113 worker
: defaultWorkerConfiguration
116 Configuration
.configurationSectionCache
= new Map
<
117 ConfigurationSection
,
118 ConfigurationSectionType
120 [ConfigurationSection
.log
, Configuration
.buildLogSection()],
121 [ConfigurationSection
.performanceStorage
, Configuration
.buildPerformanceStorageSection()],
122 [ConfigurationSection
.worker
, Configuration
.buildWorkerSection()],
123 [ConfigurationSection
.uiServer
, Configuration
.buildUIServerSection()]
127 private constructor () {
128 // This is intentional
131 public static getConfigurationSection
<T
extends ConfigurationSectionType
>(
132 sectionName
: ConfigurationSection
134 if (!Configuration
.isConfigurationSectionCached(sectionName
)) {
135 Configuration
.cacheConfigurationSection(sectionName
)
137 return Configuration
.configurationSectionCache
.get(sectionName
) as T
140 public static getStationTemplateUrls (): StationTemplateUrl
[] | undefined {
141 const checkDeprecatedConfigurationKeysOnce
= once(
142 Configuration
.checkDeprecatedConfigurationKeys
.bind(Configuration
)
144 checkDeprecatedConfigurationKeysOnce()
145 return Configuration
.getConfigurationData()?.stationTemplateUrls
148 public static getSupervisionUrls (): string | string[] | undefined {
150 Configuration
.getConfigurationData()?.['supervisionURLs' as keyof ConfigurationData
] != null
152 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
153 Configuration
.getConfigurationData()!.supervisionUrls
= Configuration
.getConfigurationData()![
154 'supervisionURLs' as keyof ConfigurationData
155 ] as string | string[]
157 return Configuration
.getConfigurationData()?.supervisionUrls
160 public static getSupervisionUrlDistribution (): SupervisionUrlDistribution
| undefined {
161 return hasOwnProp(Configuration
.getConfigurationData(), 'supervisionUrlDistribution')
162 ? Configuration
.getConfigurationData()?.supervisionUrlDistribution
163 : SupervisionUrlDistribution
.ROUND_ROBIN
166 public static workerPoolInUse (): boolean {
167 return [WorkerProcessType
.dynamicPool
, WorkerProcessType
.fixedPool
].includes(
168 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
169 Configuration
.getConfigurationSection
<WorkerConfiguration
>(ConfigurationSection
.worker
)
174 public static workerDynamicPoolInUse (): boolean {
176 Configuration
.getConfigurationSection
<WorkerConfiguration
>(ConfigurationSection
.worker
)
177 .processType
=== WorkerProcessType
.dynamicPool
181 private static isConfigurationSectionCached (sectionName
: ConfigurationSection
): boolean {
182 return Configuration
.configurationSectionCache
.has(sectionName
)
185 private static cacheConfigurationSection (sectionName
: ConfigurationSection
): void {
186 switch (sectionName
) {
187 case ConfigurationSection
.log
:
188 Configuration
.configurationSectionCache
.set(sectionName
, Configuration
.buildLogSection())
190 case ConfigurationSection
.performanceStorage
:
191 Configuration
.configurationSectionCache
.set(
193 Configuration
.buildPerformanceStorageSection()
196 case ConfigurationSection
.worker
:
197 Configuration
.configurationSectionCache
.set(sectionName
, Configuration
.buildWorkerSection())
199 case ConfigurationSection
.uiServer
:
200 Configuration
.configurationSectionCache
.set(
202 Configuration
.buildUIServerSection()
206 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
207 throw new Error(`Unknown configuration section '${sectionName}'`)
211 private static buildUIServerSection (): UIServerConfiguration
{
212 let uiServerConfiguration
: UIServerConfiguration
= defaultUIServerConfiguration
213 if (hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.uiServer
)) {
214 uiServerConfiguration
= mergeDeepRight(
215 uiServerConfiguration
,
216 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
217 Configuration
.getConfigurationData()!.uiServer
!
220 if (isCFEnvironment()) {
221 delete uiServerConfiguration
.options
?.host
222 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
223 uiServerConfiguration
.options
!.port
= Number.parseInt(env
.PORT
!)
225 return uiServerConfiguration
228 private static buildPerformanceStorageSection (): StorageConfiguration
{
229 let storageConfiguration
: StorageConfiguration
230 switch (Configuration
.getConfigurationData()?.performanceStorage
?.type) {
231 case StorageType
.SQLITE
:
232 storageConfiguration
= {
234 type: StorageType
.SQLITE
,
235 uri
: getDefaultPerformanceStorageUri(StorageType
.SQLITE
)
238 case StorageType
.JSON_FILE
:
239 storageConfiguration
= {
241 type: StorageType
.JSON_FILE
,
242 uri
: getDefaultPerformanceStorageUri(StorageType
.JSON_FILE
)
245 case StorageType
.NONE
:
247 storageConfiguration
= defaultStorageConfiguration
250 if (hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.performanceStorage
)) {
251 storageConfiguration
= {
252 ...storageConfiguration
,
253 ...Configuration
.getConfigurationData()?.performanceStorage
,
254 ...((Configuration
.getConfigurationData()?.performanceStorage
?.type ===
255 StorageType
.JSON_FILE
||
256 Configuration
.getConfigurationData()?.performanceStorage
?.type === StorageType
.SQLITE
) &&
257 Configuration
.getConfigurationData()?.performanceStorage
?.uri
!= null && {
258 uri
: buildPerformanceUriFilePath(
259 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
260 new URL(Configuration
.getConfigurationData()!.performanceStorage
!.uri
!).pathname
265 return storageConfiguration
268 private static buildLogSection (): LogConfiguration
{
269 const deprecatedLogConfiguration
: LogConfiguration
= {
270 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logEnabled') && {
271 enabled
: Configuration
.getConfigurationData()?.logEnabled
273 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logFile') && {
274 file
: Configuration
.getConfigurationData()?.logFile
276 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logErrorFile') && {
277 errorFile
: Configuration
.getConfigurationData()?.logErrorFile
279 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logStatisticsInterval') && {
280 statisticsInterval
: Configuration
.getConfigurationData()?.logStatisticsInterval
282 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logLevel') && {
283 level
: Configuration
.getConfigurationData()?.logLevel
285 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logConsole') && {
286 console
: Configuration
.getConfigurationData()?.logConsole
288 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logFormat') && {
289 format
: Configuration
.getConfigurationData()?.logFormat
291 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logRotate') && {
292 rotate
: Configuration
.getConfigurationData()?.logRotate
294 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logMaxFiles') && {
295 maxFiles
: Configuration
.getConfigurationData()?.logMaxFiles
297 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logMaxSize') && {
298 maxSize
: Configuration
.getConfigurationData()?.logMaxSize
301 const logConfiguration
: LogConfiguration
= {
302 ...defaultLogConfiguration
,
303 ...deprecatedLogConfiguration
,
304 ...(hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.log
) &&
305 Configuration
.getConfigurationData()?.log
)
307 return logConfiguration
310 private static buildWorkerSection (): WorkerConfiguration
{
311 const deprecatedWorkerConfiguration
: WorkerConfiguration
= {
312 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerProcess') && {
313 processType
: Configuration
.getConfigurationData()?.workerProcess
315 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerStartDelay') && {
316 startDelay
: Configuration
.getConfigurationData()?.workerStartDelay
318 ...(hasOwnProp(Configuration
.getConfigurationData(), 'chargingStationsPerWorker') && {
319 elementsPerWorker
: Configuration
.getConfigurationData()?.chargingStationsPerWorker
321 ...(hasOwnProp(Configuration
.getConfigurationData(), 'elementAddDelay') && {
322 elementAddDelay
: Configuration
.getConfigurationData()?.elementAddDelay
324 ...(hasOwnProp(Configuration
.getConfigurationData()?.worker
, 'elementStartDelay') && {
325 elementAddDelay
: Configuration
.getConfigurationData()?.worker
?.elementStartDelay
327 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMinSize') && {
328 poolMinSize
: Configuration
.getConfigurationData()?.workerPoolMinSize
330 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMaxSize') && {
331 poolMaxSize
: Configuration
.getConfigurationData()?.workerPoolMaxSize
334 hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolStrategy') &&
335 delete Configuration
.getConfigurationData()?.workerPoolStrategy
336 const workerConfiguration
: WorkerConfiguration
= {
337 ...defaultWorkerConfiguration
,
338 ...deprecatedWorkerConfiguration
,
339 ...(hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.worker
) &&
340 Configuration
.getConfigurationData()?.worker
)
342 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
343 checkWorkerProcessType(workerConfiguration
.processType
!)
344 checkWorkerElementsPerWorker(workerConfiguration
.elementsPerWorker
)
345 return workerConfiguration
348 private static checkDeprecatedConfigurationKeys (): void {
349 // connection timeout
350 Configuration
.warnDeprecatedConfigurationKey(
351 'autoReconnectTimeout',
353 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead"
355 Configuration
.warnDeprecatedConfigurationKey(
358 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead"
360 // connection retries
361 Configuration
.warnDeprecatedConfigurationKey(
362 'autoReconnectMaxRetries',
364 'Use it in charging station template instead'
366 // station template url(s)
367 Configuration
.warnDeprecatedConfigurationKey(
368 'stationTemplateURLs',
370 "Use 'stationTemplateUrls' instead"
372 Configuration
.getConfigurationData()?.['stationTemplateURLs' as keyof ConfigurationData
] !=
374 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
375 (Configuration
.getConfigurationData()!.stationTemplateUrls
=
376 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
377 Configuration
.getConfigurationData()![
378 'stationTemplateURLs' as keyof ConfigurationData
379 ] as StationTemplateUrl
[])
380 Configuration
.getConfigurationData()?.stationTemplateUrls
.forEach(
381 (stationTemplateUrl
: StationTemplateUrl
) => {
382 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
383 if (stationTemplateUrl
['numberOfStation' as keyof StationTemplateUrl
] != null) {
385 `${chalk.green(logPrefix())} ${chalk.red(
386 `Deprecated configuration key
'numberOfStation' usage
for template file
'${stationTemplateUrl.file}' in 'stationTemplateUrls'. Use
'numberOfStations' instead
`
392 // supervision url(s)
393 Configuration
.warnDeprecatedConfigurationKey(
396 "Use 'supervisionUrls' instead"
398 // supervision urls distribution
399 Configuration
.warnDeprecatedConfigurationKey(
400 'distributeStationToTenantEqually',
402 "Use 'supervisionUrlDistribution' instead"
404 Configuration
.warnDeprecatedConfigurationKey(
405 'distributeStationsToTenantsEqually',
407 "Use 'supervisionUrlDistribution' instead"
410 Configuration
.warnDeprecatedConfigurationKey(
413 `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`
415 Configuration
.warnDeprecatedConfigurationKey(
418 `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`
420 Configuration
.warnDeprecatedConfigurationKey(
423 `Use '${ConfigurationSection.worker}' section to define the worker start delay instead`
425 Configuration
.warnDeprecatedConfigurationKey(
426 'chargingStationsPerWorker',
428 `Use '${ConfigurationSection.worker}' section to define the number of element(s) per worker instead`
430 Configuration
.warnDeprecatedConfigurationKey(
433 `Use '${ConfigurationSection.worker}' section to define the worker's element add delay instead`
435 Configuration
.warnDeprecatedConfigurationKey(
438 `Use '${ConfigurationSection.worker}' section to define the worker pool minimum size instead`
440 Configuration
.warnDeprecatedConfigurationKey(
443 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`
445 Configuration
.warnDeprecatedConfigurationKey(
448 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`
450 Configuration
.warnDeprecatedConfigurationKey(
451 'workerPoolStrategy',
453 `Use '${ConfigurationSection.worker}' section to define the worker pool strategy instead`
455 Configuration
.warnDeprecatedConfigurationKey(
457 ConfigurationSection
.worker
,
458 'Not publicly exposed to end users'
460 Configuration
.warnDeprecatedConfigurationKey(
462 ConfigurationSection
.worker
,
463 "Use 'elementAddDelay' instead"
466 Configuration
.getConfigurationData()?.worker
?.processType
===
467 ('staticPool' as WorkerProcessType
)
470 `${chalk.green(logPrefix())} ${chalk.red(
471 `Deprecated configuration
'staticPool' value usage
in worker section
'processType' field
. Use
'${WorkerProcessType.fixedPool}' value instead
`
476 Configuration
.warnDeprecatedConfigurationKey(
479 `Use '${ConfigurationSection.log}' section to define the logging enablement instead`
481 Configuration
.warnDeprecatedConfigurationKey(
484 `Use '${ConfigurationSection.log}' section to define the log file instead`
486 Configuration
.warnDeprecatedConfigurationKey(
489 `Use '${ConfigurationSection.log}' section to define the log error file instead`
491 Configuration
.warnDeprecatedConfigurationKey(
494 `Use '${ConfigurationSection.log}' section to define the console logging enablement instead`
496 Configuration
.warnDeprecatedConfigurationKey(
497 'logStatisticsInterval',
499 `Use '${ConfigurationSection.log}' section to define the log statistics interval instead`
501 Configuration
.warnDeprecatedConfigurationKey(
504 `Use '${ConfigurationSection.log}' section to define the log level instead`
506 Configuration
.warnDeprecatedConfigurationKey(
509 `Use '${ConfigurationSection.log}' section to define the log format instead`
511 Configuration
.warnDeprecatedConfigurationKey(
514 `Use '${ConfigurationSection.log}' section to define the log rotation enablement instead`
516 Configuration
.warnDeprecatedConfigurationKey(
519 `Use '${ConfigurationSection.log}' section to define the log maximum files instead`
521 Configuration
.warnDeprecatedConfigurationKey(
524 `Use '${ConfigurationSection.log}' section to define the log maximum size instead`
526 // performanceStorage section
527 Configuration
.warnDeprecatedConfigurationKey(
529 ConfigurationSection
.performanceStorage
,
533 if (hasOwnProp(Configuration
.getConfigurationData(), 'uiWebSocketServer')) {
535 `${chalk.green(logPrefix())} ${chalk.red(
536 `Deprecated configuration section
'uiWebSocketServer' usage
. Use
'${ConfigurationSection.uiServer}' instead
`
542 private static warnDeprecatedConfigurationKey (
544 configurationSection
?: ConfigurationSection
,
548 configurationSection
!= null &&
549 Configuration
.getConfigurationData()?.[configurationSection
as keyof ConfigurationData
] !=
552 Configuration
.getConfigurationData()?.[
553 configurationSection
as keyof ConfigurationData
554 ] as Record
<string, unknown
>
558 `${chalk.green(logPrefix())} ${chalk.red(
559 `Deprecated configuration key
'${key}' usage
in section
'${configurationSection}'$
{
560 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
564 } else if (Configuration
.getConfigurationData()?.[key
as keyof ConfigurationData
] != null) {
566 `${chalk.green(logPrefix())} ${chalk.red(
567 `Deprecated configuration key
'${key}' usage$
{
568 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
575 public static getConfigurationData (): ConfigurationData
| undefined {
577 Configuration
.configurationData
== null &&
578 Configuration
.configurationFile
!= null &&
579 Configuration
.configurationFile
.length
> 0
582 Configuration
.configurationData
= JSON
.parse(
583 readFileSync(Configuration
.configurationFile
, 'utf8')
584 ) as ConfigurationData
585 if (Configuration
.configurationFileWatcher
== null) {
586 Configuration
.configurationFileWatcher
= Configuration
.getConfigurationFileWatcher()
590 Configuration
.configurationFile
,
591 FileType
.Configuration
,
592 error
as NodeJS
.ErrnoException
,
597 return Configuration
.configurationData
600 private static getConfigurationFileWatcher (): FSWatcher
| undefined {
601 if (Configuration
.configurationFile
== null || Configuration
.configurationFile
.length
=== 0) {
605 return watch(Configuration
.configurationFile
, (event
, filename
): void => {
607 !Configuration
.configurationFileReloading
&&
608 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
609 filename
!.trim().length
> 0 &&
612 Configuration
.configurationFileReloading
= true
613 const consoleWarnOnce
= once(console
.warn
)
615 `${chalk.green(logPrefix())} ${chalk.yellow(
616 `${FileType.Configuration} ${this.configurationFile} file have changed
, reload
`
619 delete Configuration
.configurationData
620 Configuration
.configurationSectionCache
.clear()
621 if (Configuration
.configurationChangeCallback
!= null) {
622 Configuration
.configurationChangeCallback()
623 .catch((error
: unknown
) => {
624 throw typeof error
=== 'string' ? new Error(error
) : error
627 Configuration
.configurationFileReloading
= false
630 Configuration
.configurationFileReloading
= false
636 Configuration
.configurationFile
,
637 FileType
.Configuration
,
638 error
as NodeJS
.ErrnoException
,