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
, 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_START_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 // 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
)
87 checkDeprecatedConfigurationKeysOnce()
88 return Configuration
.getConfigurationData()?.stationTemplateUrls
91 public static getSupervisionUrls (): string | string[] | undefined {
93 Configuration
.getConfigurationData()?.['supervisionURLs' as keyof ConfigurationData
] != null
95 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
96 Configuration
.getConfigurationData()!.supervisionUrls
= Configuration
.getConfigurationData()![
97 'supervisionURLs' as keyof ConfigurationData
98 ] as string | string[]
100 return Configuration
.getConfigurationData()?.supervisionUrls
103 public static getSupervisionUrlDistribution (): SupervisionUrlDistribution
| undefined {
104 return hasOwnProp(Configuration
.getConfigurationData(), 'supervisionUrlDistribution')
105 ? Configuration
.getConfigurationData()?.supervisionUrlDistribution
106 : SupervisionUrlDistribution
.ROUND_ROBIN
109 public static workerPoolInUse (): boolean {
110 return [WorkerProcessType
.dynamicPool
, WorkerProcessType
.fixedPool
].includes(
111 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
112 Configuration
.getConfigurationSection
<WorkerConfiguration
>(ConfigurationSection
.worker
)
117 public static workerDynamicPoolInUse (): boolean {
119 Configuration
.getConfigurationSection
<WorkerConfiguration
>(ConfigurationSection
.worker
)
120 .processType
=== WorkerProcessType
.dynamicPool
124 private static isConfigurationSectionCached (sectionName
: ConfigurationSection
): boolean {
125 return Configuration
.configurationSectionCache
.has(sectionName
)
128 private static cacheConfigurationSection (sectionName
: ConfigurationSection
): void {
129 switch (sectionName
) {
130 case ConfigurationSection
.log
:
131 Configuration
.configurationSectionCache
.set(sectionName
, Configuration
.buildLogSection())
133 case ConfigurationSection
.performanceStorage
:
134 Configuration
.configurationSectionCache
.set(
136 Configuration
.buildPerformanceStorageSection()
139 case ConfigurationSection
.worker
:
140 Configuration
.configurationSectionCache
.set(sectionName
, Configuration
.buildWorkerSection())
142 case ConfigurationSection
.uiServer
:
143 Configuration
.configurationSectionCache
.set(
145 Configuration
.buildUIServerSection()
149 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
150 throw new Error(`Unknown configuration section '${sectionName}'`)
154 private static buildUIServerSection (): UIServerConfiguration
{
155 let uiServerConfiguration
: UIServerConfiguration
= {
157 type: ApplicationProtocol
.WS
,
158 version
: ApplicationProtocolVersion
.VERSION_11
,
160 host
: Constants
.DEFAULT_UI_SERVER_HOST
,
161 port
: Constants
.DEFAULT_UI_SERVER_PORT
164 if (hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.uiServer
)) {
165 uiServerConfiguration
= mergeDeepRight(
166 uiServerConfiguration
,
167 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
168 Configuration
.getConfigurationData()!.uiServer
!
171 if (isCFEnvironment()) {
172 delete uiServerConfiguration
.options
?.host
173 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
174 uiServerConfiguration
.options
!.port
= parseInt(env
.PORT
!)
176 return uiServerConfiguration
179 private static buildPerformanceStorageSection (): StorageConfiguration
{
180 let storageConfiguration
: StorageConfiguration
181 switch (Configuration
.getConfigurationData()?.performanceStorage
?.type) {
182 case StorageType
.SQLITE
:
183 storageConfiguration
= {
185 type: StorageType
.SQLITE
,
186 uri
: getDefaultPerformanceStorageUri(StorageType
.SQLITE
)
189 case StorageType
.JSON_FILE
:
190 storageConfiguration
= {
192 type: StorageType
.JSON_FILE
,
193 uri
: getDefaultPerformanceStorageUri(StorageType
.JSON_FILE
)
196 case StorageType
.NONE
:
198 storageConfiguration
= {
200 type: StorageType
.NONE
204 if (hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.performanceStorage
)) {
205 storageConfiguration
= {
206 ...storageConfiguration
,
207 ...Configuration
.getConfigurationData()?.performanceStorage
,
208 ...((Configuration
.getConfigurationData()?.performanceStorage
?.type ===
209 StorageType
.JSON_FILE
||
210 Configuration
.getConfigurationData()?.performanceStorage
?.type === StorageType
.SQLITE
) &&
211 Configuration
.getConfigurationData()?.performanceStorage
?.uri
!= null && {
212 uri
: buildPerformanceUriFilePath(
213 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
214 new URL(Configuration
.getConfigurationData()!.performanceStorage
!.uri
!).pathname
219 return storageConfiguration
222 private static buildLogSection (): LogConfiguration
{
223 const defaultLogConfiguration
: LogConfiguration
= {
225 file
: 'logs/combined.log',
226 errorFile
: 'logs/error.log',
227 statisticsInterval
: Constants
.DEFAULT_LOG_STATISTICS_INTERVAL
,
232 const deprecatedLogConfiguration
: LogConfiguration
= {
233 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logEnabled') && {
234 enabled
: Configuration
.getConfigurationData()?.logEnabled
236 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logFile') && {
237 file
: Configuration
.getConfigurationData()?.logFile
239 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logErrorFile') && {
240 errorFile
: Configuration
.getConfigurationData()?.logErrorFile
242 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logStatisticsInterval') && {
243 statisticsInterval
: Configuration
.getConfigurationData()?.logStatisticsInterval
245 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logLevel') && {
246 level
: Configuration
.getConfigurationData()?.logLevel
248 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logConsole') && {
249 console
: Configuration
.getConfigurationData()?.logConsole
251 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logFormat') && {
252 format
: Configuration
.getConfigurationData()?.logFormat
254 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logRotate') && {
255 rotate
: Configuration
.getConfigurationData()?.logRotate
257 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logMaxFiles') && {
258 maxFiles
: Configuration
.getConfigurationData()?.logMaxFiles
260 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logMaxSize') && {
261 maxSize
: Configuration
.getConfigurationData()?.logMaxSize
264 const logConfiguration
: LogConfiguration
= {
265 ...defaultLogConfiguration
,
266 ...deprecatedLogConfiguration
,
267 ...(hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.log
) &&
268 Configuration
.getConfigurationData()?.log
)
270 return logConfiguration
273 private static buildWorkerSection (): WorkerConfiguration
{
274 const defaultWorkerConfiguration
: WorkerConfiguration
= {
275 processType
: WorkerProcessType
.workerSet
,
276 startDelay
: DEFAULT_WORKER_START_DELAY
,
277 elementsPerWorker
: 'auto',
278 elementStartDelay
: DEFAULT_ELEMENT_START_DELAY
,
279 poolMinSize
: DEFAULT_POOL_MIN_SIZE
,
280 poolMaxSize
: DEFAULT_POOL_MAX_SIZE
282 const deprecatedWorkerConfiguration
: WorkerConfiguration
= {
283 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerProcess') && {
284 processType
: Configuration
.getConfigurationData()?.workerProcess
286 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerStartDelay') && {
287 startDelay
: Configuration
.getConfigurationData()?.workerStartDelay
289 ...(hasOwnProp(Configuration
.getConfigurationData(), 'chargingStationsPerWorker') && {
290 elementsPerWorker
: Configuration
.getConfigurationData()?.chargingStationsPerWorker
292 ...(hasOwnProp(Configuration
.getConfigurationData(), 'elementStartDelay') && {
293 elementStartDelay
: Configuration
.getConfigurationData()?.elementStartDelay
295 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMinSize') && {
296 poolMinSize
: Configuration
.getConfigurationData()?.workerPoolMinSize
298 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMaxSize') && {
299 poolMaxSize
: Configuration
.getConfigurationData()?.workerPoolMaxSize
302 hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolStrategy') &&
303 delete Configuration
.getConfigurationData()?.workerPoolStrategy
304 const workerConfiguration
: WorkerConfiguration
= {
305 ...defaultWorkerConfiguration
,
306 ...deprecatedWorkerConfiguration
,
307 ...(hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.worker
) &&
308 Configuration
.getConfigurationData()?.worker
)
310 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
311 checkWorkerProcessType(workerConfiguration
.processType
!)
312 checkWorkerElementsPerWorker(workerConfiguration
.elementsPerWorker
)
313 return workerConfiguration
316 private static checkDeprecatedConfigurationKeys (): void {
317 // connection timeout
318 Configuration
.warnDeprecatedConfigurationKey(
319 'autoReconnectTimeout',
321 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead"
323 Configuration
.warnDeprecatedConfigurationKey(
326 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead"
328 // connection retries
329 Configuration
.warnDeprecatedConfigurationKey(
330 'autoReconnectMaxRetries',
332 'Use it in charging station template instead'
334 // station template url(s)
335 Configuration
.warnDeprecatedConfigurationKey(
336 'stationTemplateURLs',
338 "Use 'stationTemplateUrls' instead"
340 Configuration
.getConfigurationData()?.['stationTemplateURLs' as keyof ConfigurationData
] !=
342 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
343 (Configuration
.getConfigurationData()!.stationTemplateUrls
=
344 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
345 Configuration
.getConfigurationData()![
346 'stationTemplateURLs' as keyof ConfigurationData
347 ] as StationTemplateUrl
[])
348 Configuration
.getConfigurationData()?.stationTemplateUrls
.forEach(
349 (stationTemplateUrl
: StationTemplateUrl
) => {
350 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
351 if (stationTemplateUrl
['numberOfStation' as keyof StationTemplateUrl
] != null) {
353 `${chalk.green(logPrefix())} ${chalk.red(
354 `Deprecated configuration key
'numberOfStation' usage
for template file
'${stationTemplateUrl.file}' in 'stationTemplateUrls'. Use
'numberOfStations' instead
`
360 // supervision url(s)
361 Configuration
.warnDeprecatedConfigurationKey(
364 "Use 'supervisionUrls' instead"
366 // supervision urls distribution
367 Configuration
.warnDeprecatedConfigurationKey(
368 'distributeStationToTenantEqually',
370 "Use 'supervisionUrlDistribution' instead"
372 Configuration
.warnDeprecatedConfigurationKey(
373 'distributeStationsToTenantsEqually',
375 "Use 'supervisionUrlDistribution' instead"
378 Configuration
.warnDeprecatedConfigurationKey(
381 `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`
383 Configuration
.warnDeprecatedConfigurationKey(
386 `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`
388 Configuration
.warnDeprecatedConfigurationKey(
391 `Use '${ConfigurationSection.worker}' section to define the worker start delay instead`
393 Configuration
.warnDeprecatedConfigurationKey(
394 'chargingStationsPerWorker',
396 `Use '${ConfigurationSection.worker}' section to define the number of element(s) per worker instead`
398 Configuration
.warnDeprecatedConfigurationKey(
401 `Use '${ConfigurationSection.worker}' section to define the worker's element start delay instead`
403 Configuration
.warnDeprecatedConfigurationKey(
406 `Use '${ConfigurationSection.worker}' section to define the worker pool minimum size instead`
408 Configuration
.warnDeprecatedConfigurationKey(
411 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`
413 Configuration
.warnDeprecatedConfigurationKey(
416 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`
418 Configuration
.warnDeprecatedConfigurationKey(
419 'workerPoolStrategy',
421 `Use '${ConfigurationSection.worker}' section to define the worker pool strategy instead`
423 Configuration
.warnDeprecatedConfigurationKey(
425 ConfigurationSection
.worker
,
426 'Not publicly exposed to end users'
429 Configuration
.getConfigurationData()?.worker
?.processType
===
430 ('staticPool' as WorkerProcessType
)
433 `${chalk.green(logPrefix())} ${chalk.red(
434 `Deprecated configuration
'staticPool' value usage
in worker section
'processType' field
. Use
'${WorkerProcessType.fixedPool}' value instead
`
439 Configuration
.warnDeprecatedConfigurationKey(
442 `Use '${ConfigurationSection.log}' section to define the logging enablement instead`
444 Configuration
.warnDeprecatedConfigurationKey(
447 `Use '${ConfigurationSection.log}' section to define the log file instead`
449 Configuration
.warnDeprecatedConfigurationKey(
452 `Use '${ConfigurationSection.log}' section to define the log error file instead`
454 Configuration
.warnDeprecatedConfigurationKey(
457 `Use '${ConfigurationSection.log}' section to define the console logging enablement instead`
459 Configuration
.warnDeprecatedConfigurationKey(
460 'logStatisticsInterval',
462 `Use '${ConfigurationSection.log}' section to define the log statistics interval instead`
464 Configuration
.warnDeprecatedConfigurationKey(
467 `Use '${ConfigurationSection.log}' section to define the log level instead`
469 Configuration
.warnDeprecatedConfigurationKey(
472 `Use '${ConfigurationSection.log}' section to define the log format instead`
474 Configuration
.warnDeprecatedConfigurationKey(
477 `Use '${ConfigurationSection.log}' section to define the log rotation enablement instead`
479 Configuration
.warnDeprecatedConfigurationKey(
482 `Use '${ConfigurationSection.log}' section to define the log maximum files instead`
484 Configuration
.warnDeprecatedConfigurationKey(
487 `Use '${ConfigurationSection.log}' section to define the log maximum size instead`
489 // performanceStorage section
490 Configuration
.warnDeprecatedConfigurationKey(
492 ConfigurationSection
.performanceStorage
,
496 if (hasOwnProp(Configuration
.getConfigurationData(), 'uiWebSocketServer')) {
498 `${chalk.green(logPrefix())} ${chalk.red(
499 `Deprecated configuration section
'uiWebSocketServer' usage
. Use
'${ConfigurationSection.uiServer}' instead
`
505 private static warnDeprecatedConfigurationKey (
507 sectionName
?: string,
511 sectionName
!= null &&
512 Configuration
.getConfigurationData()?.[sectionName
as keyof ConfigurationData
] != null &&
514 Configuration
.getConfigurationData()?.[sectionName
as keyof ConfigurationData
] as Record
<
521 `${chalk.green(logPrefix())} ${chalk.red(
522 `Deprecated configuration key
'${key}' usage
in section
'${sectionName}'$
{
523 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
527 } else if (Configuration
.getConfigurationData()?.[key
as keyof ConfigurationData
] != null) {
529 `${chalk.green(logPrefix())} ${chalk.red(
530 `Deprecated configuration key
'${key}' usage$
{
531 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
538 private static getConfigurationData (): ConfigurationData
| undefined {
539 if (Configuration
.configurationData
== null) {
541 Configuration
.configurationData
= JSON
.parse(
542 readFileSync(Configuration
.configurationFile
, 'utf8')
543 ) as ConfigurationData
544 if (Configuration
.configurationFileWatcher
== null) {
545 Configuration
.configurationFileWatcher
= Configuration
.getConfigurationFileWatcher()
549 Configuration
.configurationFile
,
550 FileType
.Configuration
,
551 error
as NodeJS
.ErrnoException
,
556 return Configuration
.configurationData
559 private static getConfigurationFileWatcher (): FSWatcher
| undefined {
561 return watch(Configuration
.configurationFile
, (event
, filename
): void => {
563 !Configuration
.configurationFileReloading
&&
564 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
565 filename
!.trim().length
> 0 &&
568 Configuration
.configurationFileReloading
= true
569 const consoleWarnOnce
= once(console
.warn
)
571 `${chalk.green(logPrefix())} ${chalk.yellow(
572 `${FileType.Configuration} ${this.configurationFile} file have changed
, reload
`
575 delete Configuration
.configurationData
576 Configuration
.configurationSectionCache
.clear()
577 if (Configuration
.configurationChangeCallback
!= null) {
578 Configuration
.configurationChangeCallback()
580 throw typeof error
=== 'string' ? new Error(error
) : error
583 Configuration
.configurationFileReloading
= false
586 Configuration
.configurationFileReloading
= false
592 Configuration
.configurationFile
,
593 FileType
.Configuration
,
594 error
as NodeJS
.ErrnoException
,