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
'${configurationFile}' not found
, using
default configuration
`
101 Configuration
.configurationData
= {
102 stationTemplateUrls
: [],
103 supervisionUrls
: 'ws://localhost:8180/steve/websocket/CentralSystemService',
104 supervisionUrlDistribution
: SupervisionUrlDistribution
.ROUND_ROBIN
,
105 uiServer
: defaultUIServerConfiguration
,
106 performanceStorage
: defaultStorageConfiguration
,
107 log
: defaultLogConfiguration
,
108 worker
: defaultWorkerConfiguration
111 Configuration
.configurationSectionCache
= new Map
<
112 ConfigurationSection
,
113 ConfigurationSectionType
115 [ConfigurationSection
.log
, Configuration
.buildLogSection()],
116 [ConfigurationSection
.performanceStorage
, Configuration
.buildPerformanceStorageSection()],
117 [ConfigurationSection
.worker
, Configuration
.buildWorkerSection()],
118 [ConfigurationSection
.uiServer
, Configuration
.buildUIServerSection()]
122 private constructor () {
123 // This is intentional
126 public static getConfigurationSection
<T
extends ConfigurationSectionType
>(
127 sectionName
: ConfigurationSection
129 if (!Configuration
.isConfigurationSectionCached(sectionName
)) {
130 Configuration
.cacheConfigurationSection(sectionName
)
132 return Configuration
.configurationSectionCache
.get(sectionName
) as T
135 public static getStationTemplateUrls (): StationTemplateUrl
[] | undefined {
136 const checkDeprecatedConfigurationKeysOnce
= once(
137 Configuration
.checkDeprecatedConfigurationKeys
.bind(Configuration
)
139 checkDeprecatedConfigurationKeysOnce()
140 return Configuration
.getConfigurationData()?.stationTemplateUrls
143 public static getSupervisionUrls (): string | string[] | undefined {
145 Configuration
.getConfigurationData()?.['supervisionURLs' as keyof ConfigurationData
] != null
147 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
148 Configuration
.getConfigurationData()!.supervisionUrls
= Configuration
.getConfigurationData()![
149 'supervisionURLs' as keyof ConfigurationData
150 ] as string | string[]
152 return Configuration
.getConfigurationData()?.supervisionUrls
155 public static getSupervisionUrlDistribution (): SupervisionUrlDistribution
| undefined {
156 return hasOwnProp(Configuration
.getConfigurationData(), 'supervisionUrlDistribution')
157 ? Configuration
.getConfigurationData()?.supervisionUrlDistribution
158 : SupervisionUrlDistribution
.ROUND_ROBIN
161 public static workerPoolInUse (): boolean {
162 return [WorkerProcessType
.dynamicPool
, WorkerProcessType
.fixedPool
].includes(
163 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
164 Configuration
.getConfigurationSection
<WorkerConfiguration
>(ConfigurationSection
.worker
)
169 public static workerDynamicPoolInUse (): boolean {
171 Configuration
.getConfigurationSection
<WorkerConfiguration
>(ConfigurationSection
.worker
)
172 .processType
=== WorkerProcessType
.dynamicPool
176 private static isConfigurationSectionCached (sectionName
: ConfigurationSection
): boolean {
177 return Configuration
.configurationSectionCache
.has(sectionName
)
180 private static cacheConfigurationSection (sectionName
: ConfigurationSection
): void {
181 switch (sectionName
) {
182 case ConfigurationSection
.log
:
183 Configuration
.configurationSectionCache
.set(sectionName
, Configuration
.buildLogSection())
185 case ConfigurationSection
.performanceStorage
:
186 Configuration
.configurationSectionCache
.set(
188 Configuration
.buildPerformanceStorageSection()
191 case ConfigurationSection
.worker
:
192 Configuration
.configurationSectionCache
.set(sectionName
, Configuration
.buildWorkerSection())
194 case ConfigurationSection
.uiServer
:
195 Configuration
.configurationSectionCache
.set(
197 Configuration
.buildUIServerSection()
201 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
202 throw new Error(`Unknown configuration section '${sectionName}'`)
206 private static buildUIServerSection (): UIServerConfiguration
{
207 let uiServerConfiguration
: UIServerConfiguration
= defaultUIServerConfiguration
208 if (hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.uiServer
)) {
209 uiServerConfiguration
= mergeDeepRight(
210 uiServerConfiguration
,
211 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
212 Configuration
.getConfigurationData()!.uiServer
!
215 if (isCFEnvironment()) {
216 delete uiServerConfiguration
.options
?.host
217 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
218 uiServerConfiguration
.options
!.port
= Number.parseInt(env
.PORT
!)
220 return uiServerConfiguration
223 private static buildPerformanceStorageSection (): StorageConfiguration
{
224 let storageConfiguration
: StorageConfiguration
225 switch (Configuration
.getConfigurationData()?.performanceStorage
?.type) {
226 case StorageType
.SQLITE
:
227 storageConfiguration
= {
229 type: StorageType
.SQLITE
,
230 uri
: getDefaultPerformanceStorageUri(StorageType
.SQLITE
)
233 case StorageType
.JSON_FILE
:
234 storageConfiguration
= {
236 type: StorageType
.JSON_FILE
,
237 uri
: getDefaultPerformanceStorageUri(StorageType
.JSON_FILE
)
240 case StorageType
.NONE
:
242 storageConfiguration
= defaultStorageConfiguration
245 if (hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.performanceStorage
)) {
246 storageConfiguration
= {
247 ...storageConfiguration
,
248 ...Configuration
.getConfigurationData()?.performanceStorage
,
249 ...((Configuration
.getConfigurationData()?.performanceStorage
?.type ===
250 StorageType
.JSON_FILE
||
251 Configuration
.getConfigurationData()?.performanceStorage
?.type === StorageType
.SQLITE
) &&
252 Configuration
.getConfigurationData()?.performanceStorage
?.uri
!= null && {
253 uri
: buildPerformanceUriFilePath(
254 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
255 new URL(Configuration
.getConfigurationData()!.performanceStorage
!.uri
!).pathname
260 return storageConfiguration
263 private static buildLogSection (): LogConfiguration
{
264 const deprecatedLogConfiguration
: LogConfiguration
= {
265 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logEnabled') && {
266 enabled
: Configuration
.getConfigurationData()?.logEnabled
268 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logFile') && {
269 file
: Configuration
.getConfigurationData()?.logFile
271 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logErrorFile') && {
272 errorFile
: Configuration
.getConfigurationData()?.logErrorFile
274 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logStatisticsInterval') && {
275 statisticsInterval
: Configuration
.getConfigurationData()?.logStatisticsInterval
277 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logLevel') && {
278 level
: Configuration
.getConfigurationData()?.logLevel
280 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logConsole') && {
281 console
: Configuration
.getConfigurationData()?.logConsole
283 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logFormat') && {
284 format
: Configuration
.getConfigurationData()?.logFormat
286 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logRotate') && {
287 rotate
: Configuration
.getConfigurationData()?.logRotate
289 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logMaxFiles') && {
290 maxFiles
: Configuration
.getConfigurationData()?.logMaxFiles
292 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logMaxSize') && {
293 maxSize
: Configuration
.getConfigurationData()?.logMaxSize
296 const logConfiguration
: LogConfiguration
= {
297 ...defaultLogConfiguration
,
298 ...deprecatedLogConfiguration
,
299 ...(hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.log
) &&
300 Configuration
.getConfigurationData()?.log
)
302 return logConfiguration
305 private static buildWorkerSection (): WorkerConfiguration
{
306 const deprecatedWorkerConfiguration
: WorkerConfiguration
= {
307 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerProcess') && {
308 processType
: Configuration
.getConfigurationData()?.workerProcess
310 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerStartDelay') && {
311 startDelay
: Configuration
.getConfigurationData()?.workerStartDelay
313 ...(hasOwnProp(Configuration
.getConfigurationData(), 'chargingStationsPerWorker') && {
314 elementsPerWorker
: Configuration
.getConfigurationData()?.chargingStationsPerWorker
316 ...(hasOwnProp(Configuration
.getConfigurationData(), 'elementAddDelay') && {
317 elementAddDelay
: Configuration
.getConfigurationData()?.elementAddDelay
319 ...(hasOwnProp(Configuration
.getConfigurationData()?.worker
, 'elementStartDelay') && {
320 elementAddDelay
: Configuration
.getConfigurationData()?.worker
?.elementStartDelay
322 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMinSize') && {
323 poolMinSize
: Configuration
.getConfigurationData()?.workerPoolMinSize
325 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMaxSize') && {
326 poolMaxSize
: Configuration
.getConfigurationData()?.workerPoolMaxSize
329 hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolStrategy') &&
330 delete Configuration
.getConfigurationData()?.workerPoolStrategy
331 const workerConfiguration
: WorkerConfiguration
= {
332 ...defaultWorkerConfiguration
,
333 ...deprecatedWorkerConfiguration
,
334 ...(hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.worker
) &&
335 Configuration
.getConfigurationData()?.worker
)
337 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
338 checkWorkerProcessType(workerConfiguration
.processType
!)
339 checkWorkerElementsPerWorker(workerConfiguration
.elementsPerWorker
)
340 return workerConfiguration
343 private static checkDeprecatedConfigurationKeys (): void {
344 // connection timeout
345 Configuration
.warnDeprecatedConfigurationKey(
346 'autoReconnectTimeout',
348 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead"
350 Configuration
.warnDeprecatedConfigurationKey(
353 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead"
355 // connection retries
356 Configuration
.warnDeprecatedConfigurationKey(
357 'autoReconnectMaxRetries',
359 'Use it in charging station template instead'
361 // station template url(s)
362 Configuration
.warnDeprecatedConfigurationKey(
363 'stationTemplateURLs',
365 "Use 'stationTemplateUrls' instead"
367 Configuration
.getConfigurationData()?.['stationTemplateURLs' as keyof ConfigurationData
] !=
369 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
370 (Configuration
.getConfigurationData()!.stationTemplateUrls
=
371 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
372 Configuration
.getConfigurationData()![
373 'stationTemplateURLs' as keyof ConfigurationData
374 ] as StationTemplateUrl
[])
375 Configuration
.getConfigurationData()?.stationTemplateUrls
.forEach(
376 (stationTemplateUrl
: StationTemplateUrl
) => {
377 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
378 if (stationTemplateUrl
['numberOfStation' as keyof StationTemplateUrl
] != null) {
380 `${chalk.green(logPrefix())} ${chalk.red(
381 `Deprecated configuration key
'numberOfStation' usage
for template file
'${stationTemplateUrl.file}' in 'stationTemplateUrls'. Use
'numberOfStations' instead
`
387 // supervision url(s)
388 Configuration
.warnDeprecatedConfigurationKey(
391 "Use 'supervisionUrls' instead"
393 // supervision urls distribution
394 Configuration
.warnDeprecatedConfigurationKey(
395 'distributeStationToTenantEqually',
397 "Use 'supervisionUrlDistribution' instead"
399 Configuration
.warnDeprecatedConfigurationKey(
400 'distributeStationsToTenantsEqually',
402 "Use 'supervisionUrlDistribution' instead"
405 Configuration
.warnDeprecatedConfigurationKey(
408 `Use '${ConfigurationSection.worker}' section to define the type of worker process model 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 worker start delay instead`
420 Configuration
.warnDeprecatedConfigurationKey(
421 'chargingStationsPerWorker',
423 `Use '${ConfigurationSection.worker}' section to define the number of element(s) per worker instead`
425 Configuration
.warnDeprecatedConfigurationKey(
428 `Use '${ConfigurationSection.worker}' section to define the worker's element add delay instead`
430 Configuration
.warnDeprecatedConfigurationKey(
433 `Use '${ConfigurationSection.worker}' section to define the worker pool minimum size instead`
435 Configuration
.warnDeprecatedConfigurationKey(
438 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`
440 Configuration
.warnDeprecatedConfigurationKey(
443 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`
445 Configuration
.warnDeprecatedConfigurationKey(
446 'workerPoolStrategy',
448 `Use '${ConfigurationSection.worker}' section to define the worker pool strategy instead`
450 Configuration
.warnDeprecatedConfigurationKey(
452 ConfigurationSection
.worker
,
453 'Not publicly exposed to end users'
455 Configuration
.warnDeprecatedConfigurationKey(
457 ConfigurationSection
.worker
,
458 "Use 'elementAddDelay' instead"
461 Configuration
.getConfigurationData()?.worker
?.processType
===
462 ('staticPool' as WorkerProcessType
)
465 `${chalk.green(logPrefix())} ${chalk.red(
466 `Deprecated configuration
'staticPool' value usage
in worker section
'processType' field
. Use
'${WorkerProcessType.fixedPool}' value instead
`
471 Configuration
.warnDeprecatedConfigurationKey(
474 `Use '${ConfigurationSection.log}' section to define the logging enablement instead`
476 Configuration
.warnDeprecatedConfigurationKey(
479 `Use '${ConfigurationSection.log}' section to define the log file instead`
481 Configuration
.warnDeprecatedConfigurationKey(
484 `Use '${ConfigurationSection.log}' section to define the log error file instead`
486 Configuration
.warnDeprecatedConfigurationKey(
489 `Use '${ConfigurationSection.log}' section to define the console logging enablement instead`
491 Configuration
.warnDeprecatedConfigurationKey(
492 'logStatisticsInterval',
494 `Use '${ConfigurationSection.log}' section to define the log statistics interval instead`
496 Configuration
.warnDeprecatedConfigurationKey(
499 `Use '${ConfigurationSection.log}' section to define the log level instead`
501 Configuration
.warnDeprecatedConfigurationKey(
504 `Use '${ConfigurationSection.log}' section to define the log format instead`
506 Configuration
.warnDeprecatedConfigurationKey(
509 `Use '${ConfigurationSection.log}' section to define the log rotation enablement instead`
511 Configuration
.warnDeprecatedConfigurationKey(
514 `Use '${ConfigurationSection.log}' section to define the log maximum files instead`
516 Configuration
.warnDeprecatedConfigurationKey(
519 `Use '${ConfigurationSection.log}' section to define the log maximum size instead`
521 // performanceStorage section
522 Configuration
.warnDeprecatedConfigurationKey(
524 ConfigurationSection
.performanceStorage
,
528 if (hasOwnProp(Configuration
.getConfigurationData(), 'uiWebSocketServer')) {
530 `${chalk.green(logPrefix())} ${chalk.red(
531 `Deprecated configuration section
'uiWebSocketServer' usage
. Use
'${ConfigurationSection.uiServer}' instead
`
537 private static warnDeprecatedConfigurationKey (
539 configurationSection
?: ConfigurationSection
,
543 configurationSection
!= null &&
544 Configuration
.getConfigurationData()?.[configurationSection
as keyof ConfigurationData
] !=
547 Configuration
.getConfigurationData()?.[
548 configurationSection
as keyof ConfigurationData
549 ] as Record
<string, unknown
>
553 `${chalk.green(logPrefix())} ${chalk.red(
554 `Deprecated configuration key
'${key}' usage
in section
'${configurationSection}'$
{
555 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
559 } else if (Configuration
.getConfigurationData()?.[key
as keyof ConfigurationData
] != null) {
561 `${chalk.green(logPrefix())} ${chalk.red(
562 `Deprecated configuration key
'${key}' usage$
{
563 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
570 public static getConfigurationData (): ConfigurationData
| undefined {
572 Configuration
.configurationData
== null &&
573 Configuration
.configurationFile
!= null &&
574 Configuration
.configurationFile
.length
> 0
577 Configuration
.configurationData
= JSON
.parse(
578 readFileSync(Configuration
.configurationFile
, 'utf8')
579 ) as ConfigurationData
580 if (Configuration
.configurationFileWatcher
== null) {
581 Configuration
.configurationFileWatcher
= Configuration
.getConfigurationFileWatcher()
585 Configuration
.configurationFile
,
586 FileType
.Configuration
,
587 error
as NodeJS
.ErrnoException
,
592 return Configuration
.configurationData
595 private static getConfigurationFileWatcher (): FSWatcher
| undefined {
596 if (Configuration
.configurationFile
== null || Configuration
.configurationFile
.length
=== 0) {
600 return watch(Configuration
.configurationFile
, (event
, filename
): void => {
602 !Configuration
.configurationFileReloading
&&
603 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
604 filename
!.trim().length
> 0 &&
607 Configuration
.configurationFileReloading
= true
608 const consoleWarnOnce
= once(console
.warn
)
610 `${chalk.green(logPrefix())} ${chalk.yellow(
611 `${FileType.Configuration} ${this.configurationFile} file have changed
, reload
`
614 delete Configuration
.configurationData
615 Configuration
.configurationSectionCache
.clear()
616 if (Configuration
.configurationChangeCallback
!= null) {
617 Configuration
.configurationChangeCallback()
618 .catch((error
: unknown
) => {
619 throw typeof error
=== 'string' ? new Error(error
) : error
622 Configuration
.configurationFileReloading
= false
625 Configuration
.configurationFileReloading
= false
631 Configuration
.configurationFile
,
632 FileType
.Configuration
,
633 error
as NodeJS
.ErrnoException
,