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 merge from
'just-merge';
10 buildPerformanceUriFilePath
,
11 checkWorkerElementsPerWorker
,
12 checkWorkerProcessType
,
13 getDefaultPerformanceStorageUri
,
16 } from
'./ConfigurationUtils';
17 import { Constants
} from
'./Constants';
18 import { hasOwnProp
, isCFEnvironment
, isUndefined
, once
} from
'./Utils';
21 type ConfigurationData
,
24 type LogConfiguration
,
25 type StationTemplateUrl
,
26 type StorageConfiguration
,
28 SupervisionUrlDistribution
,
29 type UIServerConfiguration
,
30 type WorkerConfiguration
,
33 DEFAULT_ELEMENT_START_DELAY
,
34 DEFAULT_POOL_MAX_SIZE
,
35 DEFAULT_POOL_MIN_SIZE
,
36 DEFAULT_WORKER_START_DELAY
,
40 type ConfigurationSectionType
=
42 | StorageConfiguration
44 | UIServerConfiguration
;
46 export class Configuration
{
47 public static configurationChangeCallback
: () => Promise
<void>;
49 private static configurationFile
= join(
50 dirname(fileURLToPath(import.meta
.url
)),
55 private static configurationFileReloading
= false;
56 private static configurationData
?: ConfigurationData
;
57 private static configurationFileWatcher
?: FSWatcher
;
58 private static configurationSectionCache
= new Map
<
60 ConfigurationSectionType
62 [ConfigurationSection
.log
, Configuration
.buildLogSection()],
63 [ConfigurationSection
.performanceStorage
, Configuration
.buildPerformanceStorageSection()],
64 [ConfigurationSection
.worker
, Configuration
.buildWorkerSection()],
65 [ConfigurationSection
.uiServer
, Configuration
.buildUIServerSection()],
68 private constructor() {
69 // This is intentional
72 public static getConfigurationSection
<T
extends ConfigurationSectionType
>(
73 sectionName
: ConfigurationSection
,
75 if (!Configuration
.isConfigurationSectionCached(sectionName
)) {
76 Configuration
.cacheConfigurationSection(sectionName
);
78 return Configuration
.configurationSectionCache
.get(sectionName
) as T
;
81 public static getStationTemplateUrls(): StationTemplateUrl
[] | undefined {
82 const checkDeprecatedConfigurationKeysOnce
= once(
83 Configuration
.checkDeprecatedConfigurationKeys
.bind(Configuration
),
86 checkDeprecatedConfigurationKeysOnce();
87 return Configuration
.getConfigurationData()?.stationTemplateUrls
;
90 public static getSupervisionUrls(): string | string[] | undefined {
93 Configuration
.getConfigurationData()?.['supervisionURLs' as keyof ConfigurationData
],
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 Configuration
.getConfigurationSection
<WorkerConfiguration
>(ConfigurationSection
.worker
)
116 public static workerDynamicPoolInUse(): boolean {
118 Configuration
.getConfigurationSection
<WorkerConfiguration
>(ConfigurationSection
.worker
)
119 .processType
=== WorkerProcessType
.dynamicPool
123 private static isConfigurationSectionCached(sectionName
: ConfigurationSection
): boolean {
124 return Configuration
.configurationSectionCache
.has(sectionName
);
127 private static cacheConfigurationSection(sectionName
: ConfigurationSection
): void {
128 switch (sectionName
) {
129 case ConfigurationSection
.log
:
130 Configuration
.configurationSectionCache
.set(sectionName
, Configuration
.buildLogSection());
132 case ConfigurationSection
.performanceStorage
:
133 Configuration
.configurationSectionCache
.set(
135 Configuration
.buildPerformanceStorageSection(),
138 case ConfigurationSection
.worker
:
139 Configuration
.configurationSectionCache
.set(
141 Configuration
.buildWorkerSection(),
144 case ConfigurationSection
.uiServer
:
145 Configuration
.configurationSectionCache
.set(
147 Configuration
.buildUIServerSection(),
151 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
152 throw new Error(`Unknown configuration section '${sectionName}'`);
156 private static buildUIServerSection(): UIServerConfiguration
{
157 let uiServerConfiguration
: UIServerConfiguration
= {
159 type: ApplicationProtocol
.WS
,
161 host
: Constants
.DEFAULT_UI_SERVER_HOST
,
162 port
: Constants
.DEFAULT_UI_SERVER_PORT
,
165 if (hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.uiServer
)) {
166 uiServerConfiguration
= merge
<UIServerConfiguration
>(
167 uiServerConfiguration
,
168 Configuration
.getConfigurationData()!.uiServer
!,
171 if (isCFEnvironment() === true) {
172 delete uiServerConfiguration
.options
?.host
;
173 uiServerConfiguration
.options
!.port
= parseInt(env
.PORT
!);
175 return uiServerConfiguration
;
178 private static buildPerformanceStorageSection(): StorageConfiguration
{
179 let storageConfiguration
: StorageConfiguration
= {
181 type: StorageType
.JSON_FILE
,
182 uri
: getDefaultPerformanceStorageUri(StorageType
.JSON_FILE
),
184 if (hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.performanceStorage
)) {
185 storageConfiguration
= {
186 ...storageConfiguration
,
187 ...Configuration
.getConfigurationData()?.performanceStorage
,
188 ...(Configuration
.getConfigurationData()?.performanceStorage
?.type ===
189 StorageType
.JSON_FILE
&&
190 Configuration
.getConfigurationData()?.performanceStorage
?.uri
&& {
191 uri
: buildPerformanceUriFilePath(
192 new URL(Configuration
.getConfigurationData()!.performanceStorage
!.uri
!).pathname
,
197 return storageConfiguration
;
200 private static buildLogSection(): LogConfiguration
{
201 const defaultLogConfiguration
: LogConfiguration
= {
203 file
: 'logs/combined.log',
204 errorFile
: 'logs/error.log',
205 statisticsInterval
: Constants
.DEFAULT_LOG_STATISTICS_INTERVAL
,
210 const deprecatedLogConfiguration
: LogConfiguration
= {
211 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logEnabled') && {
212 enabled
: Configuration
.getConfigurationData()?.logEnabled
,
214 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logFile') && {
215 file
: Configuration
.getConfigurationData()?.logFile
,
217 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logErrorFile') && {
218 errorFile
: Configuration
.getConfigurationData()?.logErrorFile
,
220 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logStatisticsInterval') && {
221 statisticsInterval
: Configuration
.getConfigurationData()?.logStatisticsInterval
,
223 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logLevel') && {
224 level
: Configuration
.getConfigurationData()?.logLevel
,
226 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logConsole') && {
227 console
: Configuration
.getConfigurationData()?.logConsole
,
229 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logFormat') && {
230 format
: Configuration
.getConfigurationData()?.logFormat
,
232 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logRotate') && {
233 rotate
: Configuration
.getConfigurationData()?.logRotate
,
235 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logMaxFiles') && {
236 maxFiles
: Configuration
.getConfigurationData()?.logMaxFiles
,
238 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logMaxSize') && {
239 maxSize
: Configuration
.getConfigurationData()?.logMaxSize
,
242 const logConfiguration
: LogConfiguration
= {
243 ...defaultLogConfiguration
,
244 ...deprecatedLogConfiguration
,
245 ...(hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.log
) &&
246 Configuration
.getConfigurationData()?.log
),
248 return logConfiguration
;
251 private static buildWorkerSection(): WorkerConfiguration
{
252 const defaultWorkerConfiguration
: WorkerConfiguration
= {
253 processType
: WorkerProcessType
.workerSet
,
254 startDelay
: DEFAULT_WORKER_START_DELAY
,
255 elementsPerWorker
: 'auto',
256 elementStartDelay
: DEFAULT_ELEMENT_START_DELAY
,
257 poolMinSize
: DEFAULT_POOL_MIN_SIZE
,
258 poolMaxSize
: DEFAULT_POOL_MAX_SIZE
,
260 const deprecatedWorkerConfiguration
: WorkerConfiguration
= {
261 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerProcess') && {
262 processType
: Configuration
.getConfigurationData()?.workerProcess
,
264 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerStartDelay') && {
265 startDelay
: Configuration
.getConfigurationData()?.workerStartDelay
,
267 ...(hasOwnProp(Configuration
.getConfigurationData(), 'chargingStationsPerWorker') && {
268 elementsPerWorker
: Configuration
.getConfigurationData()?.chargingStationsPerWorker
,
270 ...(hasOwnProp(Configuration
.getConfigurationData(), 'elementStartDelay') && {
271 elementStartDelay
: Configuration
.getConfigurationData()?.elementStartDelay
,
273 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMinSize') && {
274 poolMinSize
: Configuration
.getConfigurationData()?.workerPoolMinSize
,
276 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMaxSize') && {
277 poolMaxSize
: Configuration
.getConfigurationData()?.workerPoolMaxSize
,
280 hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolStrategy') &&
281 delete Configuration
.getConfigurationData()?.workerPoolStrategy
;
282 const workerConfiguration
: WorkerConfiguration
= {
283 ...defaultWorkerConfiguration
,
284 ...deprecatedWorkerConfiguration
,
285 ...(hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.worker
) &&
286 Configuration
.getConfigurationData()?.worker
),
288 checkWorkerProcessType(workerConfiguration
.processType
!);
289 checkWorkerElementsPerWorker(workerConfiguration
.elementsPerWorker
);
290 return workerConfiguration
;
293 private static checkDeprecatedConfigurationKeys() {
294 // connection timeout
295 Configuration
.warnDeprecatedConfigurationKey(
296 'autoReconnectTimeout',
298 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead",
300 Configuration
.warnDeprecatedConfigurationKey(
303 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead",
305 // connection retries
306 Configuration
.warnDeprecatedConfigurationKey(
307 'autoReconnectMaxRetries',
309 'Use it in charging station template instead',
311 // station template url(s)
312 Configuration
.warnDeprecatedConfigurationKey(
313 'stationTemplateURLs',
315 "Use 'stationTemplateUrls' instead",
318 Configuration
.getConfigurationData()?.['stationTemplateURLs' as keyof ConfigurationData
],
320 (Configuration
.getConfigurationData()!.stationTemplateUrls
=
321 Configuration
.getConfigurationData()![
322 'stationTemplateURLs' as keyof ConfigurationData
323 ] as StationTemplateUrl
[]);
324 Configuration
.getConfigurationData()?.stationTemplateUrls
.forEach(
325 (stationTemplateUrl
: StationTemplateUrl
) => {
326 if (!isUndefined(stationTemplateUrl
?.['numberOfStation' as keyof StationTemplateUrl
])) {
328 `${chalk.green(logPrefix())} ${chalk.red(
329 `Deprecated configuration key
'numberOfStation' usage
for template file
'${stationTemplateUrl.file}' in 'stationTemplateUrls'. Use
'numberOfStations' instead
`,
335 // supervision url(s)
336 Configuration
.warnDeprecatedConfigurationKey(
339 "Use 'supervisionUrls' instead",
341 // supervision urls distribution
342 Configuration
.warnDeprecatedConfigurationKey(
343 'distributeStationToTenantEqually',
345 "Use 'supervisionUrlDistribution' instead",
347 Configuration
.warnDeprecatedConfigurationKey(
348 'distributeStationsToTenantsEqually',
350 "Use 'supervisionUrlDistribution' instead",
353 Configuration
.warnDeprecatedConfigurationKey(
356 `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`,
358 Configuration
.warnDeprecatedConfigurationKey(
361 `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`,
363 Configuration
.warnDeprecatedConfigurationKey(
366 `Use '${ConfigurationSection.worker}' section to define the worker start delay instead`,
368 Configuration
.warnDeprecatedConfigurationKey(
369 'chargingStationsPerWorker',
371 `Use '${ConfigurationSection.worker}' section to define the number of element(s) per worker instead`,
373 Configuration
.warnDeprecatedConfigurationKey(
376 `Use '${ConfigurationSection.worker}' section to define the worker's element start delay instead`,
378 Configuration
.warnDeprecatedConfigurationKey(
381 `Use '${ConfigurationSection.worker}' section to define the worker pool minimum size instead`,
383 Configuration
.warnDeprecatedConfigurationKey(
386 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`,
388 Configuration
.warnDeprecatedConfigurationKey(
391 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`,
393 Configuration
.warnDeprecatedConfigurationKey(
394 'workerPoolStrategy',
396 `Use '${ConfigurationSection.worker}' section to define the worker pool strategy instead`,
398 Configuration
.warnDeprecatedConfigurationKey(
400 ConfigurationSection
.worker
,
401 'Not publicly exposed to end users',
404 Configuration
.getConfigurationData()?.worker
?.processType
===
405 ('staticPool' as WorkerProcessType
)
408 `${chalk.green(logPrefix())} ${chalk.red(
409 `Deprecated configuration
'staticPool' value usage
in worker section
'processType' field
. Use
'${WorkerProcessType.fixedPool}' value instead
`,
414 Configuration
.warnDeprecatedConfigurationKey(
417 `Use '${ConfigurationSection.log}' section to define the logging enablement instead`,
419 Configuration
.warnDeprecatedConfigurationKey(
422 `Use '${ConfigurationSection.log}' section to define the log file instead`,
424 Configuration
.warnDeprecatedConfigurationKey(
427 `Use '${ConfigurationSection.log}' section to define the log error file instead`,
429 Configuration
.warnDeprecatedConfigurationKey(
432 `Use '${ConfigurationSection.log}' section to define the console logging enablement instead`,
434 Configuration
.warnDeprecatedConfigurationKey(
435 'logStatisticsInterval',
437 `Use '${ConfigurationSection.log}' section to define the log statistics interval instead`,
439 Configuration
.warnDeprecatedConfigurationKey(
442 `Use '${ConfigurationSection.log}' section to define the log level instead`,
444 Configuration
.warnDeprecatedConfigurationKey(
447 `Use '${ConfigurationSection.log}' section to define the log format instead`,
449 Configuration
.warnDeprecatedConfigurationKey(
452 `Use '${ConfigurationSection.log}' section to define the log rotation enablement instead`,
454 Configuration
.warnDeprecatedConfigurationKey(
457 `Use '${ConfigurationSection.log}' section to define the log maximum files instead`,
459 Configuration
.warnDeprecatedConfigurationKey(
462 `Use '${ConfigurationSection.log}' section to define the log maximum size instead`,
464 // performanceStorage section
465 Configuration
.warnDeprecatedConfigurationKey(
467 ConfigurationSection
.performanceStorage
,
471 if (hasOwnProp(Configuration
.getConfigurationData(), 'uiWebSocketServer')) {
473 `${chalk.green(logPrefix())} ${chalk.red(
474 `Deprecated configuration section
'uiWebSocketServer' usage
. Use
'${ConfigurationSection.uiServer}' instead
`,
480 private static warnDeprecatedConfigurationKey(
482 sectionName
?: string,
488 Configuration
.getConfigurationData()?.[sectionName
as keyof ConfigurationData
],
492 Configuration
.getConfigurationData()?.[sectionName
as keyof ConfigurationData
] as Record
<
500 `${chalk.green(logPrefix())} ${chalk.red(
501 `Deprecated configuration key
'${key}' usage
in section
'${sectionName}'$
{
502 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
507 !isUndefined(Configuration
.getConfigurationData()?.[key
as keyof ConfigurationData
])
510 `${chalk.green(logPrefix())} ${chalk.red(
511 `Deprecated configuration key
'${key}' usage$
{
512 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
519 private static getConfigurationData(): ConfigurationData
| undefined {
520 if (!Configuration
.configurationData
) {
522 Configuration
.configurationData
= JSON
.parse(
523 readFileSync(Configuration
.configurationFile
, 'utf8'),
524 ) as ConfigurationData
;
525 if (!Configuration
.configurationFileWatcher
) {
526 Configuration
.configurationFileWatcher
= Configuration
.getConfigurationFileWatcher();
530 Configuration
.configurationFile
,
531 FileType
.Configuration
,
532 error
as NodeJS
.ErrnoException
,
537 return Configuration
.configurationData
;
540 private static getConfigurationFileWatcher(): FSWatcher
| undefined {
542 return watch(Configuration
.configurationFile
, (event
, filename
): void => {
544 !Configuration
.configurationFileReloading
&&
545 filename
!.trim()!.length
> 0 &&
548 Configuration
.configurationFileReloading
= true;
549 const consoleWarnOnce
= once(console
.warn
, this);
551 `${chalk.green(logPrefix())} ${chalk.yellow(
552 `${FileType.Configuration} ${this.configurationFile} file have changed
, reload
`,
555 delete Configuration
.configurationData
;
556 Configuration
.configurationSectionCache
.clear();
557 if (!isUndefined(Configuration
.configurationChangeCallback
)) {
558 Configuration
.configurationChangeCallback()
560 throw typeof error
=== 'string' ? new Error(error
) : error
;
563 Configuration
.configurationFileReloading
= false;
566 Configuration
.configurationFileReloading
= false;
572 Configuration
.configurationFile
,
573 FileType
.Configuration
,
574 error
as NodeJS
.ErrnoException
,