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_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 // 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
= Number.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 elementAddDelay
: DEFAULT_ELEMENT_ADD_DELAY
,
279 poolMinSize
: DEFAULT_POOL_MIN_SIZE
,
280 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(), 'elementAddDelay') && {
294 elementAddDelay
: Configuration
.getConfigurationData()?.elementAddDelay
296 ...(hasOwnProp(Configuration
.getConfigurationData()?.worker
, 'elementStartDelay') && {
297 elementAddDelay
: Configuration
.getConfigurationData()?.worker
?.elementStartDelay
299 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMinSize') && {
300 poolMinSize
: Configuration
.getConfigurationData()?.workerPoolMinSize
302 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMaxSize') && {
303 poolMaxSize
: Configuration
.getConfigurationData()?.workerPoolMaxSize
306 hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolStrategy') &&
307 delete Configuration
.getConfigurationData()?.workerPoolStrategy
308 const workerConfiguration
: WorkerConfiguration
= {
309 ...defaultWorkerConfiguration
,
310 ...deprecatedWorkerConfiguration
,
311 ...(hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.worker
) &&
312 Configuration
.getConfigurationData()?.worker
)
314 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
315 checkWorkerProcessType(workerConfiguration
.processType
!)
316 checkWorkerElementsPerWorker(workerConfiguration
.elementsPerWorker
)
317 return workerConfiguration
320 private static checkDeprecatedConfigurationKeys (): void {
321 // connection timeout
322 Configuration
.warnDeprecatedConfigurationKey(
323 'autoReconnectTimeout',
325 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead"
327 Configuration
.warnDeprecatedConfigurationKey(
330 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead"
332 // connection retries
333 Configuration
.warnDeprecatedConfigurationKey(
334 'autoReconnectMaxRetries',
336 'Use it in charging station template instead'
338 // station template url(s)
339 Configuration
.warnDeprecatedConfigurationKey(
340 'stationTemplateURLs',
342 "Use 'stationTemplateUrls' instead"
344 Configuration
.getConfigurationData()?.['stationTemplateURLs' as keyof ConfigurationData
] !=
346 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
347 (Configuration
.getConfigurationData()!.stationTemplateUrls
=
348 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
349 Configuration
.getConfigurationData()![
350 'stationTemplateURLs' as keyof ConfigurationData
351 ] as StationTemplateUrl
[])
352 Configuration
.getConfigurationData()?.stationTemplateUrls
.forEach(
353 (stationTemplateUrl
: StationTemplateUrl
) => {
354 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
355 if (stationTemplateUrl
['numberOfStation' as keyof StationTemplateUrl
] != null) {
357 `${chalk.green(logPrefix())} ${chalk.red(
358 `Deprecated configuration key
'numberOfStation' usage
for template file
'${stationTemplateUrl.file}' in 'stationTemplateUrls'. Use
'numberOfStations' instead
`
364 // supervision url(s)
365 Configuration
.warnDeprecatedConfigurationKey(
368 "Use 'supervisionUrls' instead"
370 // supervision urls distribution
371 Configuration
.warnDeprecatedConfigurationKey(
372 'distributeStationToTenantEqually',
374 "Use 'supervisionUrlDistribution' instead"
376 Configuration
.warnDeprecatedConfigurationKey(
377 'distributeStationsToTenantsEqually',
379 "Use 'supervisionUrlDistribution' instead"
382 Configuration
.warnDeprecatedConfigurationKey(
385 `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`
387 Configuration
.warnDeprecatedConfigurationKey(
390 `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`
392 Configuration
.warnDeprecatedConfigurationKey(
395 `Use '${ConfigurationSection.worker}' section to define the worker start delay instead`
397 Configuration
.warnDeprecatedConfigurationKey(
398 'chargingStationsPerWorker',
400 `Use '${ConfigurationSection.worker}' section to define the number of element(s) per worker instead`
402 Configuration
.warnDeprecatedConfigurationKey(
405 `Use '${ConfigurationSection.worker}' section to define the worker's element add delay instead`
407 Configuration
.warnDeprecatedConfigurationKey(
410 `Use '${ConfigurationSection.worker}' section to define the worker pool minimum size instead`
412 Configuration
.warnDeprecatedConfigurationKey(
415 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`
417 Configuration
.warnDeprecatedConfigurationKey(
420 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`
422 Configuration
.warnDeprecatedConfigurationKey(
423 'workerPoolStrategy',
425 `Use '${ConfigurationSection.worker}' section to define the worker pool strategy instead`
427 Configuration
.warnDeprecatedConfigurationKey(
429 ConfigurationSection
.worker
,
430 'Not publicly exposed to end users'
432 Configuration
.warnDeprecatedConfigurationKey(
434 ConfigurationSection
.worker
,
435 "Use 'elementAddDelay' instead"
438 Configuration
.getConfigurationData()?.worker
?.processType
===
439 ('staticPool' as WorkerProcessType
)
442 `${chalk.green(logPrefix())} ${chalk.red(
443 `Deprecated configuration
'staticPool' value usage
in worker section
'processType' field
. Use
'${WorkerProcessType.fixedPool}' value instead
`
448 Configuration
.warnDeprecatedConfigurationKey(
451 `Use '${ConfigurationSection.log}' section to define the logging enablement instead`
453 Configuration
.warnDeprecatedConfigurationKey(
456 `Use '${ConfigurationSection.log}' section to define the log file instead`
458 Configuration
.warnDeprecatedConfigurationKey(
461 `Use '${ConfigurationSection.log}' section to define the log error file instead`
463 Configuration
.warnDeprecatedConfigurationKey(
466 `Use '${ConfigurationSection.log}' section to define the console logging enablement instead`
468 Configuration
.warnDeprecatedConfigurationKey(
469 'logStatisticsInterval',
471 `Use '${ConfigurationSection.log}' section to define the log statistics interval instead`
473 Configuration
.warnDeprecatedConfigurationKey(
476 `Use '${ConfigurationSection.log}' section to define the log level instead`
478 Configuration
.warnDeprecatedConfigurationKey(
481 `Use '${ConfigurationSection.log}' section to define the log format instead`
483 Configuration
.warnDeprecatedConfigurationKey(
486 `Use '${ConfigurationSection.log}' section to define the log rotation enablement instead`
488 Configuration
.warnDeprecatedConfigurationKey(
491 `Use '${ConfigurationSection.log}' section to define the log maximum files instead`
493 Configuration
.warnDeprecatedConfigurationKey(
496 `Use '${ConfigurationSection.log}' section to define the log maximum size instead`
498 // performanceStorage section
499 Configuration
.warnDeprecatedConfigurationKey(
501 ConfigurationSection
.performanceStorage
,
505 if (hasOwnProp(Configuration
.getConfigurationData(), 'uiWebSocketServer')) {
507 `${chalk.green(logPrefix())} ${chalk.red(
508 `Deprecated configuration section
'uiWebSocketServer' usage
. Use
'${ConfigurationSection.uiServer}' instead
`
514 private static warnDeprecatedConfigurationKey (
516 configurationSection
?: ConfigurationSection
,
520 configurationSection
!= null &&
521 Configuration
.getConfigurationData()?.[configurationSection
as keyof ConfigurationData
] !=
524 Configuration
.getConfigurationData()?.[
525 configurationSection
as keyof ConfigurationData
526 ] as Record
<string, unknown
>
530 `${chalk.green(logPrefix())} ${chalk.red(
531 `Deprecated configuration key
'${key}' usage
in section
'${configurationSection}'$
{
532 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
536 } else if (Configuration
.getConfigurationData()?.[key
as keyof ConfigurationData
] != null) {
538 `${chalk.green(logPrefix())} ${chalk.red(
539 `Deprecated configuration key
'${key}' usage$
{
540 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
547 public static getConfigurationData (): ConfigurationData
| undefined {
548 if (Configuration
.configurationData
== null) {
550 Configuration
.configurationData
= JSON
.parse(
551 readFileSync(Configuration
.configurationFile
, 'utf8')
552 ) as ConfigurationData
553 if (Configuration
.configurationFileWatcher
== null) {
554 Configuration
.configurationFileWatcher
= Configuration
.getConfigurationFileWatcher()
558 Configuration
.configurationFile
,
559 FileType
.Configuration
,
560 error
as NodeJS
.ErrnoException
,
565 return Configuration
.configurationData
568 private static getConfigurationFileWatcher (): FSWatcher
| undefined {
570 return watch(Configuration
.configurationFile
, (event
, filename
): void => {
572 !Configuration
.configurationFileReloading
&&
573 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
574 filename
!.trim().length
> 0 &&
577 Configuration
.configurationFileReloading
= true
578 const consoleWarnOnce
= once(console
.warn
)
580 `${chalk.green(logPrefix())} ${chalk.yellow(
581 `${FileType.Configuration} ${this.configurationFile} file have changed
, reload
`
584 delete Configuration
.configurationData
585 Configuration
.configurationSectionCache
.clear()
586 if (Configuration
.configurationChangeCallback
!= null) {
587 Configuration
.configurationChangeCallback()
588 .catch((error
: unknown
) => {
589 throw typeof error
=== 'string' ? new Error(error
) : error
592 Configuration
.configurationFileReloading
= false
595 Configuration
.configurationFileReloading
= false
601 Configuration
.configurationFile
,
602 FileType
.Configuration
,
603 error
as NodeJS
.ErrnoException
,