1 import { type FSWatcher
, readFileSync
, watch
} from
'node:fs';
2 import { dirname
, join
, resolve
} 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';
9 import { Constants
} from
'./Constants';
20 type ConfigurationData
,
23 type LogConfiguration
,
24 type StationTemplateUrl
,
25 type StorageConfiguration
,
27 SupervisionUrlDistribution
,
28 type UIServerConfiguration
,
29 type WorkerConfiguration
,
32 DEFAULT_ELEMENT_START_DELAY
,
33 DEFAULT_POOL_MAX_SIZE
,
34 DEFAULT_POOL_MIN_SIZE
,
35 DEFAULT_WORKER_START_DELAY
,
39 type ConfigurationSectionType
=
41 | StorageConfiguration
43 | UIServerConfiguration
;
45 export class Configuration
{
46 public static configurationChangeCallback
: () => Promise
<void>;
48 private static configurationFile
= join(
49 dirname(fileURLToPath(import.meta
.url
)),
54 private static configurationData
?: ConfigurationData
;
55 private static configurationFileWatcher
?: FSWatcher
;
56 private static configurationSectionCache
= new Map
<
58 ConfigurationSectionType
60 [ConfigurationSection
.log
, Configuration
.buildLogSection()],
61 [ConfigurationSection
.performanceStorage
, Configuration
.buildPerformanceStorageSection()],
62 [ConfigurationSection
.worker
, Configuration
.buildWorkerSection()],
63 [ConfigurationSection
.uiServer
, Configuration
.buildUIServerSection()],
66 private constructor() {
67 // This is intentional
70 public static getConfigurationSection
<T
extends ConfigurationSectionType
>(
71 sectionName
: ConfigurationSection
,
73 if (!Configuration
.isConfigurationSectionCached(sectionName
)) {
74 Configuration
.cacheConfigurationSection(sectionName
);
76 return Configuration
.configurationSectionCache
.get(sectionName
) as T
;
79 public static getStationTemplateUrls(): StationTemplateUrl
[] | undefined {
80 const checkDeprecatedConfigurationKeysOnce
= once(
81 Configuration
.checkDeprecatedConfigurationKeys
.bind(Configuration
),
84 checkDeprecatedConfigurationKeysOnce();
85 return Configuration
.getConfigurationData()?.stationTemplateUrls
;
88 public static getSupervisionUrls(): string | string[] | undefined {
91 Configuration
.getConfigurationData()?.['supervisionURLs' as keyof ConfigurationData
],
94 Configuration
.getConfigurationData()!.supervisionUrls
= Configuration
.getConfigurationData()![
95 'supervisionURLs' as keyof ConfigurationData
96 ] as string | string[];
98 return Configuration
.getConfigurationData()?.supervisionUrls
;
101 public static getSupervisionUrlDistribution(): SupervisionUrlDistribution
| undefined {
102 return hasOwnProp(Configuration
.getConfigurationData(), 'supervisionUrlDistribution')
103 ? Configuration
.getConfigurationData()?.supervisionUrlDistribution
104 : SupervisionUrlDistribution
.ROUND_ROBIN
;
107 public static workerPoolInUse(): boolean {
108 return [WorkerProcessType
.dynamicPool
, WorkerProcessType
.fixedPool
].includes(
109 Configuration
.getConfigurationSection
<WorkerConfiguration
>(ConfigurationSection
.worker
)
114 public static workerDynamicPoolInUse(): boolean {
116 Configuration
.getConfigurationSection
<WorkerConfiguration
>(ConfigurationSection
.worker
)
117 .processType
=== WorkerProcessType
.dynamicPool
121 private static logPrefix
= (): string => {
122 return logPrefix(' Simulator configuration |');
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(
143 Configuration
.buildWorkerSection(),
146 case ConfigurationSection
.uiServer
:
147 Configuration
.configurationSectionCache
.set(
149 Configuration
.buildUIServerSection(),
153 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
154 throw new Error(`Unknown configuration section '${sectionName}'`);
158 private static buildUIServerSection(): UIServerConfiguration
{
159 let uiServerConfiguration
: UIServerConfiguration
= {
161 type: ApplicationProtocol
.WS
,
163 host
: Constants
.DEFAULT_UI_SERVER_HOST
,
164 port
: Constants
.DEFAULT_UI_SERVER_PORT
,
167 if (hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.uiServer
)) {
168 uiServerConfiguration
= merge
<UIServerConfiguration
>(
169 uiServerConfiguration
,
170 Configuration
.getConfigurationData()!.uiServer
!,
173 if (isCFEnvironment() === true) {
174 delete uiServerConfiguration
.options
?.host
;
175 uiServerConfiguration
.options
!.port
= parseInt(env
.PORT
!);
177 return uiServerConfiguration
;
180 private static buildPerformanceStorageSection(): StorageConfiguration
{
181 let storageConfiguration
: StorageConfiguration
= {
183 type: StorageType
.JSON_FILE
,
184 uri
: Configuration
.getDefaultPerformanceStorageUri(StorageType
.JSON_FILE
),
186 if (hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.performanceStorage
)) {
187 storageConfiguration
= {
188 ...storageConfiguration
,
189 ...Configuration
.getConfigurationData()?.performanceStorage
,
190 ...(Configuration
.getConfigurationData()?.performanceStorage
?.type ===
191 StorageType
.JSON_FILE
&&
192 Configuration
.getConfigurationData()?.performanceStorage
?.uri
&& {
193 uri
: Configuration
.buildPerformanceUriFilePath(
194 new URL(Configuration
.getConfigurationData()!.performanceStorage
!.uri
!).pathname
,
199 return storageConfiguration
;
202 private static buildLogSection(): LogConfiguration
{
203 const defaultLogConfiguration
: LogConfiguration
= {
205 file
: 'logs/combined.log',
206 errorFile
: 'logs/error.log',
207 statisticsInterval
: Constants
.DEFAULT_LOG_STATISTICS_INTERVAL
,
212 const deprecatedLogConfiguration
: LogConfiguration
= {
213 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logEnabled') && {
214 enabled
: Configuration
.getConfigurationData()?.logEnabled
,
216 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logFile') && {
217 file
: Configuration
.getConfigurationData()?.logFile
,
219 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logErrorFile') && {
220 errorFile
: Configuration
.getConfigurationData()?.logErrorFile
,
222 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logStatisticsInterval') && {
223 statisticsInterval
: Configuration
.getConfigurationData()?.logStatisticsInterval
,
225 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logLevel') && {
226 level
: Configuration
.getConfigurationData()?.logLevel
,
228 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logConsole') && {
229 console
: Configuration
.getConfigurationData()?.logConsole
,
231 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logFormat') && {
232 format
: Configuration
.getConfigurationData()?.logFormat
,
234 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logRotate') && {
235 rotate
: Configuration
.getConfigurationData()?.logRotate
,
237 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logMaxFiles') && {
238 maxFiles
: Configuration
.getConfigurationData()?.logMaxFiles
,
240 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logMaxSize') && {
241 maxSize
: Configuration
.getConfigurationData()?.logMaxSize
,
244 const logConfiguration
: LogConfiguration
= {
245 ...defaultLogConfiguration
,
246 ...deprecatedLogConfiguration
,
247 ...(hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.log
) &&
248 Configuration
.getConfigurationData()?.log
),
250 return logConfiguration
;
253 private static buildWorkerSection(): WorkerConfiguration
{
254 const defaultWorkerConfiguration
: WorkerConfiguration
= {
255 processType
: WorkerProcessType
.workerSet
,
256 startDelay
: DEFAULT_WORKER_START_DELAY
,
257 elementsPerWorker
: 'auto',
258 elementStartDelay
: DEFAULT_ELEMENT_START_DELAY
,
259 poolMinSize
: DEFAULT_POOL_MIN_SIZE
,
260 poolMaxSize
: DEFAULT_POOL_MAX_SIZE
,
262 const deprecatedWorkerConfiguration
: WorkerConfiguration
= {
263 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerProcess') && {
264 processType
: Configuration
.getConfigurationData()?.workerProcess
,
266 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerStartDelay') && {
267 startDelay
: Configuration
.getConfigurationData()?.workerStartDelay
,
269 ...(hasOwnProp(Configuration
.getConfigurationData(), 'chargingStationsPerWorker') && {
270 elementsPerWorker
: Configuration
.getConfigurationData()?.chargingStationsPerWorker
,
272 ...(hasOwnProp(Configuration
.getConfigurationData(), 'elementStartDelay') && {
273 elementStartDelay
: Configuration
.getConfigurationData()?.elementStartDelay
,
275 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMinSize') && {
276 poolMinSize
: Configuration
.getConfigurationData()?.workerPoolMinSize
,
278 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMaxSize') && {
279 poolMaxSize
: Configuration
.getConfigurationData()?.workerPoolMaxSize
,
282 hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolStrategy') &&
283 delete Configuration
.getConfigurationData()?.workerPoolStrategy
;
284 const workerConfiguration
: WorkerConfiguration
= {
285 ...defaultWorkerConfiguration
,
286 ...deprecatedWorkerConfiguration
,
287 ...(hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.worker
) &&
288 Configuration
.getConfigurationData()?.worker
),
290 if (!Object.values(WorkerProcessType
).includes(workerConfiguration
.processType
!)) {
291 throw new SyntaxError(
292 `Invalid worker process type '${workerConfiguration.processType}' defined in configuration`,
295 return workerConfiguration
;
298 private static checkDeprecatedConfigurationKeys() {
299 // connection timeout
300 Configuration
.warnDeprecatedConfigurationKey(
301 'autoReconnectTimeout',
303 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead",
305 Configuration
.warnDeprecatedConfigurationKey(
308 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead",
310 // connection retries
311 Configuration
.warnDeprecatedConfigurationKey(
312 'autoReconnectMaxRetries',
314 'Use it in charging station template instead',
316 // station template url(s)
317 Configuration
.warnDeprecatedConfigurationKey(
318 'stationTemplateURLs',
320 "Use 'stationTemplateUrls' instead",
323 Configuration
.getConfigurationData()?.['stationTemplateURLs' as keyof ConfigurationData
],
325 (Configuration
.getConfigurationData()!.stationTemplateUrls
=
326 Configuration
.getConfigurationData()![
327 'stationTemplateURLs' as keyof ConfigurationData
328 ] as StationTemplateUrl
[]);
329 Configuration
.getConfigurationData()?.stationTemplateUrls
.forEach(
330 (stationTemplateUrl
: StationTemplateUrl
) => {
331 if (!isUndefined(stationTemplateUrl
?.['numberOfStation' as keyof StationTemplateUrl
])) {
333 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
334 `Deprecated configuration key
'numberOfStation' usage
for template file
'${stationTemplateUrl.file}' in 'stationTemplateUrls'. Use
'numberOfStations' instead
`,
340 // supervision url(s)
341 Configuration
.warnDeprecatedConfigurationKey(
344 "Use 'supervisionUrls' instead",
346 // supervision urls distribution
347 Configuration
.warnDeprecatedConfigurationKey(
348 'distributeStationToTenantEqually',
350 "Use 'supervisionUrlDistribution' instead",
352 Configuration
.warnDeprecatedConfigurationKey(
353 'distributeStationsToTenantsEqually',
355 "Use 'supervisionUrlDistribution' 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 type of worker process model instead`,
368 Configuration
.warnDeprecatedConfigurationKey(
371 `Use '${ConfigurationSection.worker}' section to define the worker start delay instead`,
373 Configuration
.warnDeprecatedConfigurationKey(
374 'chargingStationsPerWorker',
376 `Use '${ConfigurationSection.worker}' section to define the number of element(s) per worker instead`,
378 Configuration
.warnDeprecatedConfigurationKey(
381 `Use '${ConfigurationSection.worker}' section to define the worker's element start delay instead`,
383 Configuration
.warnDeprecatedConfigurationKey(
386 `Use '${ConfigurationSection.worker}' section to define the worker pool minimum size instead`,
388 Configuration
.warnDeprecatedConfigurationKey(
391 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`,
393 Configuration
.warnDeprecatedConfigurationKey(
396 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`,
398 Configuration
.warnDeprecatedConfigurationKey(
399 'workerPoolStrategy',
401 `Use '${ConfigurationSection.worker}' section to define the worker pool strategy instead`,
403 Configuration
.warnDeprecatedConfigurationKey(
405 ConfigurationSection
.worker
,
406 'Not publicly exposed to end users',
409 Configuration
.getConfigurationData()?.worker
?.processType
===
410 ('staticPool' as WorkerProcessType
)
413 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
414 `Deprecated configuration
'staticPool' value usage
in worker section
'processType' field
. Use
'${WorkerProcessType.fixedPool}' value instead
`,
419 Configuration
.warnDeprecatedConfigurationKey(
422 `Use '${ConfigurationSection.log}' section to define the logging enablement instead`,
424 Configuration
.warnDeprecatedConfigurationKey(
427 `Use '${ConfigurationSection.log}' section to define the log file instead`,
429 Configuration
.warnDeprecatedConfigurationKey(
432 `Use '${ConfigurationSection.log}' section to define the log error file instead`,
434 Configuration
.warnDeprecatedConfigurationKey(
437 `Use '${ConfigurationSection.log}' section to define the console logging enablement instead`,
439 Configuration
.warnDeprecatedConfigurationKey(
440 'logStatisticsInterval',
442 `Use '${ConfigurationSection.log}' section to define the log statistics interval instead`,
444 Configuration
.warnDeprecatedConfigurationKey(
447 `Use '${ConfigurationSection.log}' section to define the log level instead`,
449 Configuration
.warnDeprecatedConfigurationKey(
452 `Use '${ConfigurationSection.log}' section to define the log format instead`,
454 Configuration
.warnDeprecatedConfigurationKey(
457 `Use '${ConfigurationSection.log}' section to define the log rotation enablement instead`,
459 Configuration
.warnDeprecatedConfigurationKey(
462 `Use '${ConfigurationSection.log}' section to define the log maximum files instead`,
464 Configuration
.warnDeprecatedConfigurationKey(
467 `Use '${ConfigurationSection.log}' section to define the log maximum size instead`,
469 // performanceStorage section
470 Configuration
.warnDeprecatedConfigurationKey(
472 ConfigurationSection
.performanceStorage
,
476 if (hasOwnProp(Configuration
.getConfigurationData(), 'uiWebSocketServer')) {
478 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
479 `Deprecated configuration section
'uiWebSocketServer' usage
. Use
'${ConfigurationSection.uiServer}' instead
`,
485 private static warnDeprecatedConfigurationKey(
487 sectionName
?: string,
493 Configuration
.getConfigurationData()?.[sectionName
as keyof ConfigurationData
],
497 Configuration
.getConfigurationData()?.[sectionName
as keyof ConfigurationData
] as Record
<
505 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
506 `Deprecated configuration key
'${key}' usage
in section
'${sectionName}'$
{
507 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
512 !isUndefined(Configuration
.getConfigurationData()?.[key
as keyof ConfigurationData
])
515 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
516 `Deprecated configuration key
'${key}' usage$
{
517 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
524 private static getConfigurationData(): ConfigurationData
| undefined {
525 if (!Configuration
.configurationData
) {
527 Configuration
.configurationData
= JSON
.parse(
528 readFileSync(Configuration
.configurationFile
, 'utf8'),
529 ) as ConfigurationData
;
530 if (!Configuration
.configurationFileWatcher
) {
531 Configuration
.configurationFileWatcher
= Configuration
.getConfigurationFileWatcher();
534 Configuration
.handleFileException(
535 Configuration
.configurationFile
,
536 FileType
.Configuration
,
537 error
as NodeJS
.ErrnoException
,
538 Configuration
.logPrefix(),
542 return Configuration
.configurationData
;
545 private static getConfigurationFileWatcher(): FSWatcher
| undefined {
547 return watch(Configuration
.configurationFile
, (event
, filename
): void => {
548 if (filename
!.trim()!.length
> 0 && event
=== 'change') {
549 delete Configuration
.configurationData
;
550 Configuration
.configurationSectionCache
.clear();
551 if (!isUndefined(Configuration
.configurationChangeCallback
)) {
552 Configuration
.configurationChangeCallback().catch((error
) => {
553 throw typeof error
=== 'string' ? new Error(error
) : error
;
559 Configuration
.handleFileException(
560 Configuration
.configurationFile
,
561 FileType
.Configuration
,
562 error
as NodeJS
.ErrnoException
,
563 Configuration
.logPrefix(),
568 private static handleFileException(
571 error
: NodeJS
.ErrnoException
,
574 const prefix
= isNotEmptyString(logPfx
) ? `${logPfx} ` : '';
576 switch (error
.code
) {
578 logMsg
= `${fileType} file ${file} not found: `;
581 logMsg
= `${fileType} file ${file} already exists: `;
584 logMsg
= `${fileType} file ${file} access denied: `;
587 logMsg
= `${fileType} file ${file} permission denied: `;
590 logMsg
= `${fileType} file ${file} error: `;
592 console
.error(`${chalk.green(prefix)}${chalk.red(logMsg)}`, error
);
596 private static getDefaultPerformanceStorageUri(storageType
: StorageType
) {
597 switch (storageType
) {
598 case StorageType
.JSON_FILE
:
599 return Configuration
.buildPerformanceUriFilePath(
600 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}/${Constants.DEFAULT_PERFORMANCE_RECORDS_FILENAME}`,
602 case StorageType
.SQLITE
:
603 return Configuration
.buildPerformanceUriFilePath(
604 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}/${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}.db`,
607 throw new Error(`Unsupported storage type '${storageType}'`);
611 private static buildPerformanceUriFilePath(file
: string) {
612 return `file://${join(resolve(dirname(fileURLToPath(import.meta.url)), '../'), file)}`;