1 import chalk from
'chalk'
2 import { existsSync
, type FSWatcher
, readFileSync
, watch
} from
'node:fs'
3 import { dirname
, join
} from
'node:path'
4 import { env
} from
'node:process'
5 import { fileURLToPath
} from
'node:url'
6 import { mergeDeepRight
, once
} from
'rambda'
10 ApplicationProtocolVersion
,
11 type ConfigurationData
,
14 type LogConfiguration
,
15 type StationTemplateUrl
,
16 type StorageConfiguration
,
18 SupervisionUrlDistribution
,
19 type UIServerConfiguration
,
20 type WorkerConfiguration
,
21 } from
'../types/index.js'
23 DEFAULT_ELEMENT_ADD_DELAY
,
24 DEFAULT_POOL_MAX_SIZE
,
25 DEFAULT_POOL_MIN_SIZE
,
26 DEFAULT_WORKER_START_DELAY
,
28 } from
'../worker/index.js'
30 buildPerformanceUriFilePath
,
31 checkWorkerElementsPerWorker
,
32 checkWorkerProcessType
,
33 getDefaultPerformanceStorageUri
,
36 } from
'./ConfigurationUtils.js'
37 import { Constants
} from
'./Constants.js'
38 import { hasOwnProp
, isCFEnvironment
} from
'./Utils.js'
40 type ConfigurationSectionType
=
42 | StorageConfiguration
43 | UIServerConfiguration
46 const defaultUIServerConfiguration
: UIServerConfiguration
= {
49 host
: Constants
.DEFAULT_UI_SERVER_HOST
,
50 port
: Constants
.DEFAULT_UI_SERVER_PORT
,
52 type: ApplicationProtocol
.WS
,
53 version
: ApplicationProtocolVersion
.VERSION_11
,
56 const defaultStorageConfiguration
: StorageConfiguration
= {
58 type: StorageType
.NONE
,
61 const defaultLogConfiguration
: LogConfiguration
= {
63 errorFile
: 'logs/error.log',
64 file
: 'logs/combined.log',
68 statisticsInterval
: Constants
.DEFAULT_LOG_STATISTICS_INTERVAL
,
71 const defaultWorkerConfiguration
: WorkerConfiguration
= {
72 elementAddDelay
: DEFAULT_ELEMENT_ADD_DELAY
,
73 elementsPerWorker
: 'auto',
74 poolMaxSize
: DEFAULT_POOL_MAX_SIZE
,
75 poolMinSize
: DEFAULT_POOL_MIN_SIZE
,
76 processType
: WorkerProcessType
.workerSet
,
77 startDelay
: DEFAULT_WORKER_START_DELAY
,
80 // eslint-disable-next-line @typescript-eslint/no-extraneous-class
81 export class Configuration
{
82 public static configurationChangeCallback
?: () => Promise
<void>
84 private static configurationData
?: ConfigurationData
85 private static configurationFile
: string | undefined
86 private static configurationFileReloading
= false
87 private static configurationFileWatcher
?: FSWatcher
88 private static configurationSectionCache
: Map
<ConfigurationSection
, ConfigurationSectionType
>
91 const configurationFile
= join(dirname(fileURLToPath(import.meta
.url
)), 'assets', 'config.json')
92 if (existsSync(configurationFile
)) {
93 Configuration
.configurationFile
= configurationFile
96 `${chalk.green(logPrefix())} ${chalk.red(
97 "Configuration file './src/assets/config.json' not found, using default configuration"
100 Configuration
.configurationData
= {
101 log
: defaultLogConfiguration
,
102 performanceStorage
: defaultStorageConfiguration
,
103 stationTemplateUrls
: [
105 file
: 'siemens.station-template.json',
109 supervisionUrlDistribution
: SupervisionUrlDistribution
.ROUND_ROBIN
,
110 supervisionUrls
: 'ws://localhost:8180/steve/websocket/CentralSystemService',
111 uiServer
: defaultUIServerConfiguration
,
112 worker
: defaultWorkerConfiguration
,
115 Configuration
.configurationSectionCache
= new Map
<
116 ConfigurationSection
,
117 ConfigurationSectionType
119 [ConfigurationSection
.log
, Configuration
.buildLogSection()],
120 [ConfigurationSection
.performanceStorage
, Configuration
.buildPerformanceStorageSection()],
121 [ConfigurationSection
.uiServer
, Configuration
.buildUIServerSection()],
122 [ConfigurationSection
.worker
, Configuration
.buildWorkerSection()],
126 private constructor () {
127 // This is intentional
130 private static buildLogSection (): LogConfiguration
{
131 const deprecatedLogConfiguration
: LogConfiguration
= {
132 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logEnabled') && {
133 // eslint-disable-next-line @typescript-eslint/no-deprecated
134 enabled
: Configuration
.getConfigurationData()?.logEnabled
,
136 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logFile') && {
137 // eslint-disable-next-line @typescript-eslint/no-deprecated
138 file
: Configuration
.getConfigurationData()?.logFile
,
140 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logErrorFile') && {
141 // eslint-disable-next-line @typescript-eslint/no-deprecated
142 errorFile
: Configuration
.getConfigurationData()?.logErrorFile
,
144 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logStatisticsInterval') && {
145 // eslint-disable-next-line @typescript-eslint/no-deprecated
146 statisticsInterval
: Configuration
.getConfigurationData()?.logStatisticsInterval
,
148 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logLevel') && {
149 // eslint-disable-next-line @typescript-eslint/no-deprecated
150 level
: Configuration
.getConfigurationData()?.logLevel
,
152 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logConsole') && {
153 // eslint-disable-next-line @typescript-eslint/no-deprecated
154 console
: Configuration
.getConfigurationData()?.logConsole
,
156 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logFormat') && {
157 // eslint-disable-next-line @typescript-eslint/no-deprecated
158 format
: Configuration
.getConfigurationData()?.logFormat
,
160 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logRotate') && {
161 // eslint-disable-next-line @typescript-eslint/no-deprecated
162 rotate
: Configuration
.getConfigurationData()?.logRotate
,
164 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logMaxFiles') && {
165 // eslint-disable-next-line @typescript-eslint/no-deprecated
166 maxFiles
: Configuration
.getConfigurationData()?.logMaxFiles
,
168 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logMaxSize') && {
169 // eslint-disable-next-line @typescript-eslint/no-deprecated
170 maxSize
: Configuration
.getConfigurationData()?.logMaxSize
,
173 const logConfiguration
: LogConfiguration
= {
174 ...defaultLogConfiguration
,
175 ...deprecatedLogConfiguration
,
176 ...(hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.log
) &&
177 Configuration
.getConfigurationData()?.log
),
179 return logConfiguration
182 private static buildPerformanceStorageSection (): StorageConfiguration
{
183 let storageConfiguration
: StorageConfiguration
184 switch (Configuration
.getConfigurationData()?.performanceStorage
?.type) {
185 case StorageType
.SQLITE
:
186 storageConfiguration
= {
188 type: StorageType
.SQLITE
,
189 uri
: getDefaultPerformanceStorageUri(StorageType
.SQLITE
),
192 case StorageType
.JSON_FILE
:
193 storageConfiguration
= {
195 type: StorageType
.JSON_FILE
,
196 uri
: getDefaultPerformanceStorageUri(StorageType
.JSON_FILE
),
199 case StorageType
.NONE
:
201 storageConfiguration
= defaultStorageConfiguration
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 buildUIServerSection (): UIServerConfiguration
{
223 let uiServerConfiguration
: UIServerConfiguration
= defaultUIServerConfiguration
224 if (hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.uiServer
)) {
225 uiServerConfiguration
= mergeDeepRight(
226 uiServerConfiguration
,
227 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
228 Configuration
.getConfigurationData()!.uiServer
!
231 if (isCFEnvironment()) {
232 delete uiServerConfiguration
.options
?.host
233 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
234 uiServerConfiguration
.options
!.port
= Number.parseInt(env
.PORT
!)
236 return uiServerConfiguration
239 private static buildWorkerSection (): WorkerConfiguration
{
240 const deprecatedWorkerConfiguration
: WorkerConfiguration
= {
241 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerProcess') && {
242 // eslint-disable-next-line @typescript-eslint/no-deprecated
243 processType
: Configuration
.getConfigurationData()?.workerProcess
,
245 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerStartDelay') && {
246 // eslint-disable-next-line @typescript-eslint/no-deprecated
247 startDelay
: Configuration
.getConfigurationData()?.workerStartDelay
,
249 ...(hasOwnProp(Configuration
.getConfigurationData(), 'chargingStationsPerWorker') && {
250 // eslint-disable-next-line @typescript-eslint/no-deprecated
251 elementsPerWorker
: Configuration
.getConfigurationData()?.chargingStationsPerWorker
,
253 ...(hasOwnProp(Configuration
.getConfigurationData(), 'elementAddDelay') && {
254 // eslint-disable-next-line @typescript-eslint/no-deprecated
255 elementAddDelay
: Configuration
.getConfigurationData()?.elementAddDelay
,
257 ...(hasOwnProp(Configuration
.getConfigurationData()?.worker
, 'elementStartDelay') && {
258 // eslint-disable-next-line @typescript-eslint/no-deprecated
259 elementAddDelay
: Configuration
.getConfigurationData()?.worker
?.elementStartDelay
,
261 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMinSize') && {
262 // eslint-disable-next-line @typescript-eslint/no-deprecated
263 poolMinSize
: Configuration
.getConfigurationData()?.workerPoolMinSize
,
265 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMaxSize') && {
266 // eslint-disable-next-line @typescript-eslint/no-deprecated
267 poolMaxSize
: Configuration
.getConfigurationData()?.workerPoolMaxSize
,
270 hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolStrategy') &&
271 // eslint-disable-next-line @typescript-eslint/no-deprecated
272 delete Configuration
.getConfigurationData()?.workerPoolStrategy
273 const workerConfiguration
: WorkerConfiguration
= {
274 ...defaultWorkerConfiguration
,
275 ...deprecatedWorkerConfiguration
,
276 ...(hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.worker
) &&
277 Configuration
.getConfigurationData()?.worker
),
279 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
280 checkWorkerProcessType(workerConfiguration
.processType
!)
281 checkWorkerElementsPerWorker(workerConfiguration
.elementsPerWorker
)
282 return workerConfiguration
285 private static cacheConfigurationSection (sectionName
: ConfigurationSection
): void {
286 switch (sectionName
) {
287 case ConfigurationSection
.log
:
288 Configuration
.configurationSectionCache
.set(sectionName
, Configuration
.buildLogSection())
290 case ConfigurationSection
.performanceStorage
:
291 Configuration
.configurationSectionCache
.set(
293 Configuration
.buildPerformanceStorageSection()
296 case ConfigurationSection
.uiServer
:
297 Configuration
.configurationSectionCache
.set(
299 Configuration
.buildUIServerSection()
302 case ConfigurationSection
.worker
:
303 Configuration
.configurationSectionCache
.set(sectionName
, Configuration
.buildWorkerSection())
306 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
307 throw new Error(`Unknown configuration section '${sectionName}'`)
311 private static checkDeprecatedConfigurationKeys (): void {
312 // connection timeout
313 Configuration
.warnDeprecatedConfigurationKey(
314 'autoReconnectTimeout',
316 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead"
318 Configuration
.warnDeprecatedConfigurationKey(
321 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead"
323 // connection retries
324 Configuration
.warnDeprecatedConfigurationKey(
325 'autoReconnectMaxRetries',
327 'Use it in charging station template instead'
329 // station template url(s)
330 Configuration
.warnDeprecatedConfigurationKey(
331 'stationTemplateURLs',
333 "Use 'stationTemplateUrls' instead"
335 Configuration
.getConfigurationData()?.['stationTemplateURLs' as keyof ConfigurationData
] !=
337 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
338 (Configuration
.getConfigurationData()!.stationTemplateUrls
=
339 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
340 Configuration
.getConfigurationData()![
341 'stationTemplateURLs' as keyof ConfigurationData
342 ] as StationTemplateUrl
[])
343 Configuration
.getConfigurationData()?.stationTemplateUrls
.forEach(
344 (stationTemplateUrl
: StationTemplateUrl
) => {
345 if (stationTemplateUrl
['numberOfStation' as keyof StationTemplateUrl
] != null) {
347 `${chalk.green(logPrefix())} ${chalk.red(
348 `Deprecated configuration key
'numberOfStation' usage
for template file
'${stationTemplateUrl.file}' in 'stationTemplateUrls'. Use
'numberOfStations' instead
`
354 // supervision url(s)
355 Configuration
.warnDeprecatedConfigurationKey(
358 "Use 'supervisionUrls' instead"
360 // supervision urls distribution
361 Configuration
.warnDeprecatedConfigurationKey(
362 'distributeStationToTenantEqually',
364 "Use 'supervisionUrlDistribution' instead"
366 Configuration
.warnDeprecatedConfigurationKey(
367 'distributeStationsToTenantsEqually',
369 "Use 'supervisionUrlDistribution' instead"
372 Configuration
.warnDeprecatedConfigurationKey(
375 `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`
377 Configuration
.warnDeprecatedConfigurationKey(
380 `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`
382 Configuration
.warnDeprecatedConfigurationKey(
385 `Use '${ConfigurationSection.worker}' section to define the worker start delay instead`
387 Configuration
.warnDeprecatedConfigurationKey(
388 'chargingStationsPerWorker',
390 `Use '${ConfigurationSection.worker}' section to define the number of element(s) per worker instead`
392 Configuration
.warnDeprecatedConfigurationKey(
395 `Use '${ConfigurationSection.worker}' section to define the worker's element add delay instead`
397 Configuration
.warnDeprecatedConfigurationKey(
400 `Use '${ConfigurationSection.worker}' section to define the worker pool minimum size instead`
402 Configuration
.warnDeprecatedConfigurationKey(
405 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`
407 Configuration
.warnDeprecatedConfigurationKey(
410 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`
412 Configuration
.warnDeprecatedConfigurationKey(
413 'workerPoolStrategy',
415 `Use '${ConfigurationSection.worker}' section to define the worker pool strategy instead`
417 Configuration
.warnDeprecatedConfigurationKey(
419 ConfigurationSection
.worker
,
420 'Not publicly exposed to end users'
422 Configuration
.warnDeprecatedConfigurationKey(
424 ConfigurationSection
.worker
,
425 "Use 'elementAddDelay' instead"
428 Configuration
.getConfigurationData()?.worker
?.processType
===
429 ('staticPool' as WorkerProcessType
)
432 `${chalk.green(logPrefix())} ${chalk.red(
433 `Deprecated configuration
'staticPool' value usage
in worker section
'processType' field
. Use
'${WorkerProcessType.fixedPool}' value instead
`
438 Configuration
.warnDeprecatedConfigurationKey(
441 `Use '${ConfigurationSection.log}' section to define the logging enablement instead`
443 Configuration
.warnDeprecatedConfigurationKey(
446 `Use '${ConfigurationSection.log}' section to define the log file instead`
448 Configuration
.warnDeprecatedConfigurationKey(
451 `Use '${ConfigurationSection.log}' section to define the log error file instead`
453 Configuration
.warnDeprecatedConfigurationKey(
456 `Use '${ConfigurationSection.log}' section to define the console logging enablement instead`
458 Configuration
.warnDeprecatedConfigurationKey(
459 'logStatisticsInterval',
461 `Use '${ConfigurationSection.log}' section to define the log statistics interval instead`
463 Configuration
.warnDeprecatedConfigurationKey(
466 `Use '${ConfigurationSection.log}' section to define the log level instead`
468 Configuration
.warnDeprecatedConfigurationKey(
471 `Use '${ConfigurationSection.log}' section to define the log format instead`
473 Configuration
.warnDeprecatedConfigurationKey(
476 `Use '${ConfigurationSection.log}' section to define the log rotation enablement instead`
478 Configuration
.warnDeprecatedConfigurationKey(
481 `Use '${ConfigurationSection.log}' section to define the log maximum files instead`
483 Configuration
.warnDeprecatedConfigurationKey(
486 `Use '${ConfigurationSection.log}' section to define the log maximum size instead`
488 // performanceStorage section
489 Configuration
.warnDeprecatedConfigurationKey(
491 ConfigurationSection
.performanceStorage
,
495 if (hasOwnProp(Configuration
.getConfigurationData(), 'uiWebSocketServer')) {
497 `${chalk.green(logPrefix())} ${chalk.red(
498 `Deprecated configuration section
'uiWebSocketServer' usage
. Use
'${ConfigurationSection.uiServer}' instead
`
504 public static getConfigurationData (): ConfigurationData
| undefined {
506 Configuration
.configurationData
== null &&
507 Configuration
.configurationFile
!= null &&
508 Configuration
.configurationFile
.length
> 0
511 Configuration
.configurationData
= JSON
.parse(
512 readFileSync(Configuration
.configurationFile
, 'utf8')
513 ) as ConfigurationData
514 if (Configuration
.configurationFileWatcher
== null) {
515 Configuration
.configurationFileWatcher
= Configuration
.getConfigurationFileWatcher()
519 Configuration
.configurationFile
,
520 FileType
.Configuration
,
521 error
as NodeJS
.ErrnoException
,
526 return Configuration
.configurationData
529 private static getConfigurationFileWatcher (): FSWatcher
| undefined {
530 if (Configuration
.configurationFile
== null || Configuration
.configurationFile
.length
=== 0) {
534 return watch(Configuration
.configurationFile
, (event
, filename
): void => {
536 !Configuration
.configurationFileReloading
&&
537 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
538 filename
!.trim().length
> 0 &&
541 Configuration
.configurationFileReloading
= true
542 const consoleWarnOnce
= once(console
.warn
)
544 `${chalk.green(logPrefix())} ${chalk.yellow(
545 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
546 `${FileType.Configuration} ${this.configurationFile} file have changed
, reload
`
549 delete Configuration
.configurationData
550 Configuration
.configurationSectionCache
.clear()
551 if (Configuration
.configurationChangeCallback
!= null) {
552 Configuration
.configurationChangeCallback()
554 Configuration
.configurationFileReloading
= false
556 .catch((error
: unknown
) => {
557 throw typeof error
=== 'string' ? new Error(error
) : error
560 Configuration
.configurationFileReloading
= false
566 Configuration
.configurationFile
,
567 FileType
.Configuration
,
568 error
as NodeJS
.ErrnoException
,
574 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
575 public static getConfigurationSection
<T
extends ConfigurationSectionType
>(
576 sectionName
: ConfigurationSection
578 if (!Configuration
.isConfigurationSectionCached(sectionName
)) {
579 Configuration
.cacheConfigurationSection(sectionName
)
581 return Configuration
.configurationSectionCache
.get(sectionName
) as T
584 public static getStationTemplateUrls (): StationTemplateUrl
[] | undefined {
585 const checkDeprecatedConfigurationKeysOnce
= once(
586 Configuration
.checkDeprecatedConfigurationKeys
.bind(Configuration
)
588 checkDeprecatedConfigurationKeysOnce()
589 return Configuration
.getConfigurationData()?.stationTemplateUrls
592 public static getSupervisionUrlDistribution (): SupervisionUrlDistribution
| undefined {
593 return hasOwnProp(Configuration
.getConfigurationData(), 'supervisionUrlDistribution')
594 ? Configuration
.getConfigurationData()?.supervisionUrlDistribution
595 : SupervisionUrlDistribution
.ROUND_ROBIN
598 public static getSupervisionUrls (): string | string[] | undefined {
600 Configuration
.getConfigurationData()?.['supervisionURLs' as keyof ConfigurationData
] != null
602 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
603 Configuration
.getConfigurationData()!.supervisionUrls
= Configuration
.getConfigurationData()![
604 'supervisionURLs' as keyof ConfigurationData
605 ] as string | string[]
607 return Configuration
.getConfigurationData()?.supervisionUrls
610 private static isConfigurationSectionCached (sectionName
: ConfigurationSection
): boolean {
611 return Configuration
.configurationSectionCache
.has(sectionName
)
614 private static warnDeprecatedConfigurationKey (
616 configurationSection
?: ConfigurationSection
,
620 configurationSection
!= null &&
621 Configuration
.getConfigurationData()?.[configurationSection
as keyof ConfigurationData
] !=
624 Configuration
.getConfigurationData()?.[
625 configurationSection
as keyof ConfigurationData
626 ] as Record
<string, unknown
>
630 `${chalk.green(logPrefix())} ${chalk.red(
631 `Deprecated configuration key
'${key}' usage
in section
'${configurationSection}'$
{
632 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
636 } else if (Configuration
.getConfigurationData()?.[key
as keyof ConfigurationData
] != null) {
638 `${chalk.green(logPrefix())} ${chalk.red(
639 `Deprecated configuration key
'${key}' usage$
{
640 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
647 public static workerDynamicPoolInUse (): boolean {
649 Configuration
.getConfigurationSection
<WorkerConfiguration
>(ConfigurationSection
.worker
)
650 .processType
=== WorkerProcessType
.dynamicPool
654 public static workerPoolInUse (): boolean {
655 return [WorkerProcessType
.dynamicPool
, WorkerProcessType
.fixedPool
].includes(
656 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
657 Configuration
.getConfigurationSection
<WorkerConfiguration
>(ConfigurationSection
.worker
)