1 import { 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
} from
'rambda'
10 buildPerformanceUriFilePath
,
11 checkWorkerElementsPerWorker
,
12 checkWorkerProcessType
,
13 getDefaultPerformanceStorageUri
,
16 } from
'./ConfigurationUtils.js'
17 import { Constants
} from
'./Constants.js'
18 import { hasOwnProp
, isCFEnvironment
, once
} from
'./Utils.js'
21 ApplicationProtocolVersion
,
22 type ConfigurationData
,
25 type LogConfiguration
,
26 type StationTemplateUrl
,
27 type StorageConfiguration
,
29 SupervisionUrlDistribution
,
30 type UIServerConfiguration
,
31 type WorkerConfiguration
32 } from
'../types/index.js'
34 DEFAULT_ELEMENT_START_DELAY
,
35 DEFAULT_POOL_MAX_SIZE
,
36 DEFAULT_POOL_MIN_SIZE
,
37 DEFAULT_WORKER_START_DELAY
,
39 } from
'../worker/index.js'
41 type ConfigurationSectionType
=
43 | StorageConfiguration
45 | UIServerConfiguration
47 // eslint-disable-next-line @typescript-eslint/no-extraneous-class
48 export class Configuration
{
49 public static configurationChangeCallback
?: () => Promise
<void>
51 private static readonly configurationFile
= join(
52 dirname(fileURLToPath(import.meta
.url
)),
57 private static configurationFileReloading
= false
58 private static configurationData
?: ConfigurationData
59 private static configurationFileWatcher
?: FSWatcher
60 private static readonly configurationSectionCache
= new Map
<
62 ConfigurationSectionType
64 [ConfigurationSection
.log
, Configuration
.buildLogSection()],
65 [ConfigurationSection
.performanceStorage
, Configuration
.buildPerformanceStorageSection()],
66 [ConfigurationSection
.worker
, Configuration
.buildWorkerSection()],
67 [ConfigurationSection
.uiServer
, Configuration
.buildUIServerSection()]
70 private constructor () {
71 // This is intentional
74 public static getConfigurationSection
<T
extends ConfigurationSectionType
>(
75 sectionName
: ConfigurationSection
77 if (!Configuration
.isConfigurationSectionCached(sectionName
)) {
78 Configuration
.cacheConfigurationSection(sectionName
)
80 return Configuration
.configurationSectionCache
.get(sectionName
) as T
83 public static getStationTemplateUrls (): StationTemplateUrl
[] | undefined {
84 const checkDeprecatedConfigurationKeysOnce
= once(
85 Configuration
.checkDeprecatedConfigurationKeys
.bind(Configuration
),
88 checkDeprecatedConfigurationKeysOnce()
89 return Configuration
.getConfigurationData()?.stationTemplateUrls
92 public static getSupervisionUrls (): string | string[] | undefined {
94 Configuration
.getConfigurationData()?.['supervisionURLs' as keyof ConfigurationData
] != null
96 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
97 Configuration
.getConfigurationData()!.supervisionUrls
= Configuration
.getConfigurationData()![
98 'supervisionURLs' as keyof ConfigurationData
99 ] as string | string[]
101 return Configuration
.getConfigurationData()?.supervisionUrls
104 public static getSupervisionUrlDistribution (): SupervisionUrlDistribution
| undefined {
105 return hasOwnProp(Configuration
.getConfigurationData(), 'supervisionUrlDistribution')
106 ? Configuration
.getConfigurationData()?.supervisionUrlDistribution
107 : SupervisionUrlDistribution
.ROUND_ROBIN
110 public static workerPoolInUse (): boolean {
111 return [WorkerProcessType
.dynamicPool
, WorkerProcessType
.fixedPool
].includes(
112 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
113 Configuration
.getConfigurationSection
<WorkerConfiguration
>(ConfigurationSection
.worker
)
118 public static workerDynamicPoolInUse (): boolean {
120 Configuration
.getConfigurationSection
<WorkerConfiguration
>(ConfigurationSection
.worker
)
121 .processType
=== WorkerProcessType
.dynamicPool
125 private static isConfigurationSectionCached (sectionName
: ConfigurationSection
): boolean {
126 return Configuration
.configurationSectionCache
.has(sectionName
)
129 private static cacheConfigurationSection (sectionName
: ConfigurationSection
): void {
130 switch (sectionName
) {
131 case ConfigurationSection
.log
:
132 Configuration
.configurationSectionCache
.set(sectionName
, Configuration
.buildLogSection())
134 case ConfigurationSection
.performanceStorage
:
135 Configuration
.configurationSectionCache
.set(
137 Configuration
.buildPerformanceStorageSection()
140 case ConfigurationSection
.worker
:
141 Configuration
.configurationSectionCache
.set(sectionName
, Configuration
.buildWorkerSection())
143 case ConfigurationSection
.uiServer
:
144 Configuration
.configurationSectionCache
.set(
146 Configuration
.buildUIServerSection()
150 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
151 throw new Error(`Unknown configuration section '${sectionName}'`)
155 private static buildUIServerSection (): UIServerConfiguration
{
156 let uiServerConfiguration
: UIServerConfiguration
= {
158 type: ApplicationProtocol
.WS
,
159 version
: ApplicationProtocolVersion
.VERSION_11
,
161 host
: Constants
.DEFAULT_UI_SERVER_HOST
,
162 port
: Constants
.DEFAULT_UI_SERVER_PORT
165 if (hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.uiServer
)) {
166 uiServerConfiguration
= mergeDeepRight(
167 uiServerConfiguration
,
168 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
169 Configuration
.getConfigurationData()!.uiServer
!
172 if (isCFEnvironment()) {
173 delete uiServerConfiguration
.options
?.host
174 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
175 uiServerConfiguration
.options
!.port
= parseInt(env
.PORT
!)
177 return uiServerConfiguration
180 private static buildPerformanceStorageSection (): StorageConfiguration
{
181 let storageConfiguration
: StorageConfiguration
182 switch (Configuration
.getConfigurationData()?.performanceStorage
?.type) {
183 case StorageType
.SQLITE
:
184 storageConfiguration
= {
186 type: StorageType
.SQLITE
,
187 uri
: getDefaultPerformanceStorageUri(StorageType
.SQLITE
)
190 case StorageType
.JSON_FILE
:
191 storageConfiguration
= {
193 type: StorageType
.JSON_FILE
,
194 uri
: getDefaultPerformanceStorageUri(StorageType
.JSON_FILE
)
197 case StorageType
.NONE
:
199 storageConfiguration
= {
201 type: StorageType
.NONE
205 if (hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.performanceStorage
)) {
206 storageConfiguration
= {
207 ...storageConfiguration
,
208 ...Configuration
.getConfigurationData()?.performanceStorage
,
209 ...((Configuration
.getConfigurationData()?.performanceStorage
?.type ===
210 StorageType
.JSON_FILE
||
211 Configuration
.getConfigurationData()?.performanceStorage
?.type === StorageType
.SQLITE
) &&
212 Configuration
.getConfigurationData()?.performanceStorage
?.uri
!= null && {
213 uri
: buildPerformanceUriFilePath(
214 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
215 new URL(Configuration
.getConfigurationData()!.performanceStorage
!.uri
!).pathname
220 return storageConfiguration
223 private static buildLogSection (): LogConfiguration
{
224 const defaultLogConfiguration
: LogConfiguration
= {
226 file
: 'logs/combined.log',
227 errorFile
: 'logs/error.log',
228 statisticsInterval
: Constants
.DEFAULT_LOG_STATISTICS_INTERVAL
,
233 const deprecatedLogConfiguration
: LogConfiguration
= {
234 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logEnabled') && {
235 enabled
: Configuration
.getConfigurationData()?.logEnabled
237 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logFile') && {
238 file
: Configuration
.getConfigurationData()?.logFile
240 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logErrorFile') && {
241 errorFile
: Configuration
.getConfigurationData()?.logErrorFile
243 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logStatisticsInterval') && {
244 statisticsInterval
: Configuration
.getConfigurationData()?.logStatisticsInterval
246 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logLevel') && {
247 level
: Configuration
.getConfigurationData()?.logLevel
249 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logConsole') && {
250 console
: Configuration
.getConfigurationData()?.logConsole
252 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logFormat') && {
253 format
: Configuration
.getConfigurationData()?.logFormat
255 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logRotate') && {
256 rotate
: Configuration
.getConfigurationData()?.logRotate
258 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logMaxFiles') && {
259 maxFiles
: Configuration
.getConfigurationData()?.logMaxFiles
261 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logMaxSize') && {
262 maxSize
: Configuration
.getConfigurationData()?.logMaxSize
265 const logConfiguration
: LogConfiguration
= {
266 ...defaultLogConfiguration
,
267 ...deprecatedLogConfiguration
,
268 ...(hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.log
) &&
269 Configuration
.getConfigurationData()?.log
)
271 return logConfiguration
274 private static buildWorkerSection (): WorkerConfiguration
{
275 const defaultWorkerConfiguration
: WorkerConfiguration
= {
276 processType
: WorkerProcessType
.workerSet
,
277 startDelay
: DEFAULT_WORKER_START_DELAY
,
278 elementsPerWorker
: 'auto',
279 elementStartDelay
: DEFAULT_ELEMENT_START_DELAY
,
280 poolMinSize
: DEFAULT_POOL_MIN_SIZE
,
281 poolMaxSize
: DEFAULT_POOL_MAX_SIZE
283 const deprecatedWorkerConfiguration
: WorkerConfiguration
= {
284 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerProcess') && {
285 processType
: Configuration
.getConfigurationData()?.workerProcess
287 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerStartDelay') && {
288 startDelay
: Configuration
.getConfigurationData()?.workerStartDelay
290 ...(hasOwnProp(Configuration
.getConfigurationData(), 'chargingStationsPerWorker') && {
291 elementsPerWorker
: Configuration
.getConfigurationData()?.chargingStationsPerWorker
293 ...(hasOwnProp(Configuration
.getConfigurationData(), 'elementStartDelay') && {
294 elementStartDelay
: Configuration
.getConfigurationData()?.elementStartDelay
296 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMinSize') && {
297 poolMinSize
: Configuration
.getConfigurationData()?.workerPoolMinSize
299 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMaxSize') && {
300 poolMaxSize
: Configuration
.getConfigurationData()?.workerPoolMaxSize
303 hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolStrategy') &&
304 delete Configuration
.getConfigurationData()?.workerPoolStrategy
305 const workerConfiguration
: WorkerConfiguration
= {
306 ...defaultWorkerConfiguration
,
307 ...deprecatedWorkerConfiguration
,
308 ...(hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.worker
) &&
309 Configuration
.getConfigurationData()?.worker
)
311 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
312 checkWorkerProcessType(workerConfiguration
.processType
!)
313 checkWorkerElementsPerWorker(workerConfiguration
.elementsPerWorker
)
314 return workerConfiguration
317 private static checkDeprecatedConfigurationKeys (): void {
318 // connection timeout
319 Configuration
.warnDeprecatedConfigurationKey(
320 'autoReconnectTimeout',
322 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead"
324 Configuration
.warnDeprecatedConfigurationKey(
327 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead"
329 // connection retries
330 Configuration
.warnDeprecatedConfigurationKey(
331 'autoReconnectMaxRetries',
333 'Use it in charging station template instead'
335 // station template url(s)
336 Configuration
.warnDeprecatedConfigurationKey(
337 'stationTemplateURLs',
339 "Use 'stationTemplateUrls' instead"
341 Configuration
.getConfigurationData()?.['stationTemplateURLs' as keyof ConfigurationData
] !=
343 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
344 (Configuration
.getConfigurationData()!.stationTemplateUrls
=
345 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
346 Configuration
.getConfigurationData()![
347 'stationTemplateURLs' as keyof ConfigurationData
348 ] as StationTemplateUrl
[])
349 Configuration
.getConfigurationData()?.stationTemplateUrls
.forEach(
350 (stationTemplateUrl
: StationTemplateUrl
) => {
351 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
352 if (stationTemplateUrl
['numberOfStation' as keyof StationTemplateUrl
] != null) {
354 `${chalk.green(logPrefix())} ${chalk.red(
355 `Deprecated configuration key
'numberOfStation' usage
for template file
'${stationTemplateUrl.file}' in 'stationTemplateUrls'. Use
'numberOfStations' instead
`
361 // supervision url(s)
362 Configuration
.warnDeprecatedConfigurationKey(
365 "Use 'supervisionUrls' instead"
367 // supervision urls distribution
368 Configuration
.warnDeprecatedConfigurationKey(
369 'distributeStationToTenantEqually',
371 "Use 'supervisionUrlDistribution' instead"
373 Configuration
.warnDeprecatedConfigurationKey(
374 'distributeStationsToTenantsEqually',
376 "Use 'supervisionUrlDistribution' instead"
379 Configuration
.warnDeprecatedConfigurationKey(
382 `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`
384 Configuration
.warnDeprecatedConfigurationKey(
387 `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`
389 Configuration
.warnDeprecatedConfigurationKey(
392 `Use '${ConfigurationSection.worker}' section to define the worker start delay instead`
394 Configuration
.warnDeprecatedConfigurationKey(
395 'chargingStationsPerWorker',
397 `Use '${ConfigurationSection.worker}' section to define the number of element(s) per worker instead`
399 Configuration
.warnDeprecatedConfigurationKey(
402 `Use '${ConfigurationSection.worker}' section to define the worker's element start delay instead`
404 Configuration
.warnDeprecatedConfigurationKey(
407 `Use '${ConfigurationSection.worker}' section to define the worker pool minimum size instead`
409 Configuration
.warnDeprecatedConfigurationKey(
412 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`
414 Configuration
.warnDeprecatedConfigurationKey(
417 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`
419 Configuration
.warnDeprecatedConfigurationKey(
420 'workerPoolStrategy',
422 `Use '${ConfigurationSection.worker}' section to define the worker pool strategy instead`
424 Configuration
.warnDeprecatedConfigurationKey(
426 ConfigurationSection
.worker
,
427 'Not publicly exposed to end users'
430 Configuration
.getConfigurationData()?.worker
?.processType
===
431 ('staticPool' as WorkerProcessType
)
434 `${chalk.green(logPrefix())} ${chalk.red(
435 `Deprecated configuration
'staticPool' value usage
in worker section
'processType' field
. Use
'${WorkerProcessType.fixedPool}' value instead
`
440 Configuration
.warnDeprecatedConfigurationKey(
443 `Use '${ConfigurationSection.log}' section to define the logging enablement instead`
445 Configuration
.warnDeprecatedConfigurationKey(
448 `Use '${ConfigurationSection.log}' section to define the log file instead`
450 Configuration
.warnDeprecatedConfigurationKey(
453 `Use '${ConfigurationSection.log}' section to define the log error file instead`
455 Configuration
.warnDeprecatedConfigurationKey(
458 `Use '${ConfigurationSection.log}' section to define the console logging enablement instead`
460 Configuration
.warnDeprecatedConfigurationKey(
461 'logStatisticsInterval',
463 `Use '${ConfigurationSection.log}' section to define the log statistics interval instead`
465 Configuration
.warnDeprecatedConfigurationKey(
468 `Use '${ConfigurationSection.log}' section to define the log level instead`
470 Configuration
.warnDeprecatedConfigurationKey(
473 `Use '${ConfigurationSection.log}' section to define the log format instead`
475 Configuration
.warnDeprecatedConfigurationKey(
478 `Use '${ConfigurationSection.log}' section to define the log rotation enablement instead`
480 Configuration
.warnDeprecatedConfigurationKey(
483 `Use '${ConfigurationSection.log}' section to define the log maximum files instead`
485 Configuration
.warnDeprecatedConfigurationKey(
488 `Use '${ConfigurationSection.log}' section to define the log maximum size instead`
490 // performanceStorage section
491 Configuration
.warnDeprecatedConfigurationKey(
493 ConfigurationSection
.performanceStorage
,
497 if (hasOwnProp(Configuration
.getConfigurationData(), 'uiWebSocketServer')) {
499 `${chalk.green(logPrefix())} ${chalk.red(
500 `Deprecated configuration section
'uiWebSocketServer' usage
. Use
'${ConfigurationSection.uiServer}' instead
`
506 private static warnDeprecatedConfigurationKey (
508 sectionName
?: string,
512 sectionName
!= null &&
513 Configuration
.getConfigurationData()?.[sectionName
as keyof ConfigurationData
] != null &&
515 Configuration
.getConfigurationData()?.[sectionName
as keyof ConfigurationData
] as Record
<
522 `${chalk.green(logPrefix())} ${chalk.red(
523 `Deprecated configuration key
'${key}' usage
in section
'${sectionName}'$
{
524 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
528 } else if (Configuration
.getConfigurationData()?.[key
as keyof ConfigurationData
] != null) {
530 `${chalk.green(logPrefix())} ${chalk.red(
531 `Deprecated configuration key
'${key}' usage$
{
532 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
539 private static getConfigurationData (): ConfigurationData
| undefined {
540 if (Configuration
.configurationData
== null) {
542 Configuration
.configurationData
= JSON
.parse(
543 readFileSync(Configuration
.configurationFile
, 'utf8')
544 ) as ConfigurationData
545 if (Configuration
.configurationFileWatcher
== null) {
546 Configuration
.configurationFileWatcher
= Configuration
.getConfigurationFileWatcher()
550 Configuration
.configurationFile
,
551 FileType
.Configuration
,
552 error
as NodeJS
.ErrnoException
,
557 return Configuration
.configurationData
560 private static getConfigurationFileWatcher (): FSWatcher
| undefined {
562 return watch(Configuration
.configurationFile
, (event
, filename
): void => {
564 !Configuration
.configurationFileReloading
&&
565 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
566 filename
!.trim().length
> 0 &&
569 Configuration
.configurationFileReloading
= true
570 const consoleWarnOnce
= once(console
.warn
, this)
572 `${chalk.green(logPrefix())} ${chalk.yellow(
573 `${FileType.Configuration} ${this.configurationFile} file have changed
, reload
`
576 delete Configuration
.configurationData
577 Configuration
.configurationSectionCache
.clear()
578 if (Configuration
.configurationChangeCallback
!= null) {
579 Configuration
.configurationChangeCallback()
581 throw typeof error
=== 'string' ? new Error(error
) : error
584 Configuration
.configurationFileReloading
= false
587 Configuration
.configurationFileReloading
= false
593 Configuration
.configurationFile
,
594 FileType
.Configuration
,
595 error
as NodeJS
.ErrnoException
,