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 // Avoid ESM race condition at class initialization
46 const configurationLogPrefix
= (): string => {
47 return logPrefix(' Simulator configuration |');
50 export class Configuration
{
51 public static configurationChangeCallback
: () => Promise
<void>;
53 private static configurationFile
= join(
54 dirname(fileURLToPath(import.meta
.url
)),
59 private static configurationData
?: ConfigurationData
;
60 private static configurationFileWatcher
?: FSWatcher
;
61 private static configurationSectionCache
= new Map
<
63 ConfigurationSectionType
65 [ConfigurationSection
.log
, Configuration
.buildLogSection()],
66 [ConfigurationSection
.performanceStorage
, Configuration
.buildPerformanceStorageSection()],
67 [ConfigurationSection
.worker
, Configuration
.buildWorkerSection()],
68 [ConfigurationSection
.uiServer
, Configuration
.buildUIServerSection()],
71 private constructor() {
72 // This is intentional
75 public static getConfigurationSection
<T
extends ConfigurationSectionType
>(
76 sectionName
: ConfigurationSection
,
78 if (!Configuration
.isConfigurationSectionCached(sectionName
)) {
79 Configuration
.cacheConfigurationSection(sectionName
);
81 return Configuration
.configurationSectionCache
.get(sectionName
) as T
;
84 public static getStationTemplateUrls(): StationTemplateUrl
[] | undefined {
85 const checkDeprecatedConfigurationKeysOnce
= once(
86 Configuration
.checkDeprecatedConfigurationKeys
.bind(Configuration
),
89 checkDeprecatedConfigurationKeysOnce();
90 return Configuration
.getConfigurationData()?.stationTemplateUrls
;
93 public static getSupervisionUrls(): string | string[] | undefined {
96 Configuration
.getConfigurationData()?.['supervisionURLs' as keyof ConfigurationData
],
99 Configuration
.getConfigurationData()!.supervisionUrls
= Configuration
.getConfigurationData()![
100 'supervisionURLs' as keyof ConfigurationData
101 ] as string | string[];
103 return Configuration
.getConfigurationData()?.supervisionUrls
;
106 public static getSupervisionUrlDistribution(): SupervisionUrlDistribution
| undefined {
107 return hasOwnProp(Configuration
.getConfigurationData(), 'supervisionUrlDistribution')
108 ? Configuration
.getConfigurationData()?.supervisionUrlDistribution
109 : SupervisionUrlDistribution
.ROUND_ROBIN
;
112 public static workerPoolInUse(): boolean {
113 return [WorkerProcessType
.dynamicPool
, WorkerProcessType
.fixedPool
].includes(
114 Configuration
.getConfigurationSection
<WorkerConfiguration
>(ConfigurationSection
.worker
)
119 public static workerDynamicPoolInUse(): boolean {
121 Configuration
.getConfigurationSection
<WorkerConfiguration
>(ConfigurationSection
.worker
)
122 .processType
=== WorkerProcessType
.dynamicPool
126 private static isConfigurationSectionCached(sectionName
: ConfigurationSection
): boolean {
127 return Configuration
.configurationSectionCache
.has(sectionName
);
130 private static cacheConfigurationSection(sectionName
: ConfigurationSection
): void {
131 switch (sectionName
) {
132 case ConfigurationSection
.log
:
133 Configuration
.configurationSectionCache
.set(sectionName
, Configuration
.buildLogSection());
135 case ConfigurationSection
.performanceStorage
:
136 Configuration
.configurationSectionCache
.set(
138 Configuration
.buildPerformanceStorageSection(),
141 case ConfigurationSection
.worker
:
142 Configuration
.configurationSectionCache
.set(
144 Configuration
.buildWorkerSection(),
147 case ConfigurationSection
.uiServer
:
148 Configuration
.configurationSectionCache
.set(
150 Configuration
.buildUIServerSection(),
154 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
155 throw new Error(`Unknown configuration section '${sectionName}'`);
159 private static buildUIServerSection(): UIServerConfiguration
{
160 let uiServerConfiguration
: UIServerConfiguration
= {
162 type: ApplicationProtocol
.WS
,
164 host
: Constants
.DEFAULT_UI_SERVER_HOST
,
165 port
: Constants
.DEFAULT_UI_SERVER_PORT
,
168 if (hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.uiServer
)) {
169 uiServerConfiguration
= merge
<UIServerConfiguration
>(
170 uiServerConfiguration
,
171 Configuration
.getConfigurationData()!.uiServer
!,
174 if (isCFEnvironment() === true) {
175 delete uiServerConfiguration
.options
?.host
;
176 uiServerConfiguration
.options
!.port
= parseInt(env
.PORT
!);
178 return uiServerConfiguration
;
181 private static buildPerformanceStorageSection(): StorageConfiguration
{
182 let storageConfiguration
: StorageConfiguration
= {
184 type: StorageType
.JSON_FILE
,
185 uri
: Configuration
.getDefaultPerformanceStorageUri(StorageType
.JSON_FILE
),
187 if (hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.performanceStorage
)) {
188 storageConfiguration
= {
189 ...storageConfiguration
,
190 ...Configuration
.getConfigurationData()?.performanceStorage
,
191 ...(Configuration
.getConfigurationData()?.performanceStorage
?.type ===
192 StorageType
.JSON_FILE
&&
193 Configuration
.getConfigurationData()?.performanceStorage
?.uri
&& {
194 uri
: Configuration
.buildPerformanceUriFilePath(
195 new URL(Configuration
.getConfigurationData()!.performanceStorage
!.uri
!).pathname
,
200 return storageConfiguration
;
203 private static buildLogSection(): LogConfiguration
{
204 const defaultLogConfiguration
: LogConfiguration
= {
206 file
: 'logs/combined.log',
207 errorFile
: 'logs/error.log',
208 statisticsInterval
: Constants
.DEFAULT_LOG_STATISTICS_INTERVAL
,
213 const deprecatedLogConfiguration
: LogConfiguration
= {
214 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logEnabled') && {
215 enabled
: Configuration
.getConfigurationData()?.logEnabled
,
217 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logFile') && {
218 file
: Configuration
.getConfigurationData()?.logFile
,
220 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logErrorFile') && {
221 errorFile
: Configuration
.getConfigurationData()?.logErrorFile
,
223 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logStatisticsInterval') && {
224 statisticsInterval
: Configuration
.getConfigurationData()?.logStatisticsInterval
,
226 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logLevel') && {
227 level
: Configuration
.getConfigurationData()?.logLevel
,
229 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logConsole') && {
230 console
: Configuration
.getConfigurationData()?.logConsole
,
232 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logFormat') && {
233 format
: Configuration
.getConfigurationData()?.logFormat
,
235 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logRotate') && {
236 rotate
: Configuration
.getConfigurationData()?.logRotate
,
238 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logMaxFiles') && {
239 maxFiles
: Configuration
.getConfigurationData()?.logMaxFiles
,
241 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logMaxSize') && {
242 maxSize
: Configuration
.getConfigurationData()?.logMaxSize
,
245 const logConfiguration
: LogConfiguration
= {
246 ...defaultLogConfiguration
,
247 ...deprecatedLogConfiguration
,
248 ...(hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.log
) &&
249 Configuration
.getConfigurationData()?.log
),
251 return logConfiguration
;
254 private static buildWorkerSection(): WorkerConfiguration
{
255 const defaultWorkerConfiguration
: WorkerConfiguration
= {
256 processType
: WorkerProcessType
.workerSet
,
257 startDelay
: DEFAULT_WORKER_START_DELAY
,
258 elementsPerWorker
: 'auto',
259 elementStartDelay
: DEFAULT_ELEMENT_START_DELAY
,
260 poolMinSize
: DEFAULT_POOL_MIN_SIZE
,
261 poolMaxSize
: DEFAULT_POOL_MAX_SIZE
,
263 const deprecatedWorkerConfiguration
: WorkerConfiguration
= {
264 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerProcess') && {
265 processType
: Configuration
.getConfigurationData()?.workerProcess
,
267 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerStartDelay') && {
268 startDelay
: Configuration
.getConfigurationData()?.workerStartDelay
,
270 ...(hasOwnProp(Configuration
.getConfigurationData(), 'chargingStationsPerWorker') && {
271 elementsPerWorker
: Configuration
.getConfigurationData()?.chargingStationsPerWorker
,
273 ...(hasOwnProp(Configuration
.getConfigurationData(), 'elementStartDelay') && {
274 elementStartDelay
: Configuration
.getConfigurationData()?.elementStartDelay
,
276 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMinSize') && {
277 poolMinSize
: Configuration
.getConfigurationData()?.workerPoolMinSize
,
279 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMaxSize') && {
280 poolMaxSize
: Configuration
.getConfigurationData()?.workerPoolMaxSize
,
283 hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolStrategy') &&
284 delete Configuration
.getConfigurationData()?.workerPoolStrategy
;
285 const workerConfiguration
: WorkerConfiguration
= {
286 ...defaultWorkerConfiguration
,
287 ...deprecatedWorkerConfiguration
,
288 ...(hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.worker
) &&
289 Configuration
.getConfigurationData()?.worker
),
291 if (!Object.values(WorkerProcessType
).includes(workerConfiguration
.processType
!)) {
292 throw new SyntaxError(
293 `Invalid worker process type '${workerConfiguration.processType}' defined in configuration`,
296 return workerConfiguration
;
299 private static checkDeprecatedConfigurationKeys() {
300 // connection timeout
301 Configuration
.warnDeprecatedConfigurationKey(
302 'autoReconnectTimeout',
304 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead",
306 Configuration
.warnDeprecatedConfigurationKey(
309 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead",
311 // connection retries
312 Configuration
.warnDeprecatedConfigurationKey(
313 'autoReconnectMaxRetries',
315 'Use it in charging station template instead',
317 // station template url(s)
318 Configuration
.warnDeprecatedConfigurationKey(
319 'stationTemplateURLs',
321 "Use 'stationTemplateUrls' instead",
324 Configuration
.getConfigurationData()?.['stationTemplateURLs' as keyof ConfigurationData
],
326 (Configuration
.getConfigurationData()!.stationTemplateUrls
=
327 Configuration
.getConfigurationData()![
328 'stationTemplateURLs' as keyof ConfigurationData
329 ] as StationTemplateUrl
[]);
330 Configuration
.getConfigurationData()?.stationTemplateUrls
.forEach(
331 (stationTemplateUrl
: StationTemplateUrl
) => {
332 if (!isUndefined(stationTemplateUrl
?.['numberOfStation' as keyof StationTemplateUrl
])) {
334 `${chalk.green(configurationLogPrefix())} ${chalk.red(
335 `Deprecated configuration key
'numberOfStation' usage
for template file
'${stationTemplateUrl.file}' in 'stationTemplateUrls'. Use
'numberOfStations' instead
`,
341 // supervision url(s)
342 Configuration
.warnDeprecatedConfigurationKey(
345 "Use 'supervisionUrls' instead",
347 // supervision urls distribution
348 Configuration
.warnDeprecatedConfigurationKey(
349 'distributeStationToTenantEqually',
351 "Use 'supervisionUrlDistribution' instead",
353 Configuration
.warnDeprecatedConfigurationKey(
354 'distributeStationsToTenantsEqually',
356 "Use 'supervisionUrlDistribution' instead",
359 Configuration
.warnDeprecatedConfigurationKey(
362 `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`,
364 Configuration
.warnDeprecatedConfigurationKey(
367 `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`,
369 Configuration
.warnDeprecatedConfigurationKey(
372 `Use '${ConfigurationSection.worker}' section to define the worker start delay instead`,
374 Configuration
.warnDeprecatedConfigurationKey(
375 'chargingStationsPerWorker',
377 `Use '${ConfigurationSection.worker}' section to define the number of element(s) per worker instead`,
379 Configuration
.warnDeprecatedConfigurationKey(
382 `Use '${ConfigurationSection.worker}' section to define the worker's element start delay instead`,
384 Configuration
.warnDeprecatedConfigurationKey(
387 `Use '${ConfigurationSection.worker}' section to define the worker pool minimum size instead`,
389 Configuration
.warnDeprecatedConfigurationKey(
392 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`,
394 Configuration
.warnDeprecatedConfigurationKey(
397 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`,
399 Configuration
.warnDeprecatedConfigurationKey(
400 'workerPoolStrategy',
402 `Use '${ConfigurationSection.worker}' section to define the worker pool strategy instead`,
404 Configuration
.warnDeprecatedConfigurationKey(
406 ConfigurationSection
.worker
,
407 'Not publicly exposed to end users',
410 Configuration
.getConfigurationData()?.worker
?.processType
===
411 ('staticPool' as WorkerProcessType
)
414 `${chalk.green(configurationLogPrefix())} ${chalk.red(
415 `Deprecated configuration
'staticPool' value usage
in worker section
'processType' field
. Use
'${WorkerProcessType.fixedPool}' value instead
`,
420 Configuration
.warnDeprecatedConfigurationKey(
423 `Use '${ConfigurationSection.log}' section to define the logging enablement instead`,
425 Configuration
.warnDeprecatedConfigurationKey(
428 `Use '${ConfigurationSection.log}' section to define the log file instead`,
430 Configuration
.warnDeprecatedConfigurationKey(
433 `Use '${ConfigurationSection.log}' section to define the log error file instead`,
435 Configuration
.warnDeprecatedConfigurationKey(
438 `Use '${ConfigurationSection.log}' section to define the console logging enablement instead`,
440 Configuration
.warnDeprecatedConfigurationKey(
441 'logStatisticsInterval',
443 `Use '${ConfigurationSection.log}' section to define the log statistics interval instead`,
445 Configuration
.warnDeprecatedConfigurationKey(
448 `Use '${ConfigurationSection.log}' section to define the log level instead`,
450 Configuration
.warnDeprecatedConfigurationKey(
453 `Use '${ConfigurationSection.log}' section to define the log format instead`,
455 Configuration
.warnDeprecatedConfigurationKey(
458 `Use '${ConfigurationSection.log}' section to define the log rotation enablement instead`,
460 Configuration
.warnDeprecatedConfigurationKey(
463 `Use '${ConfigurationSection.log}' section to define the log maximum files instead`,
465 Configuration
.warnDeprecatedConfigurationKey(
468 `Use '${ConfigurationSection.log}' section to define the log maximum size instead`,
470 // performanceStorage section
471 Configuration
.warnDeprecatedConfigurationKey(
473 ConfigurationSection
.performanceStorage
,
477 if (hasOwnProp(Configuration
.getConfigurationData(), 'uiWebSocketServer')) {
479 `${chalk.green(configurationLogPrefix())} ${chalk.red(
480 `Deprecated configuration section
'uiWebSocketServer' usage
. Use
'${ConfigurationSection.uiServer}' instead
`,
486 private static warnDeprecatedConfigurationKey(
488 sectionName
?: string,
494 Configuration
.getConfigurationData()?.[sectionName
as keyof ConfigurationData
],
498 Configuration
.getConfigurationData()?.[sectionName
as keyof ConfigurationData
] as Record
<
506 `${chalk.green(configurationLogPrefix())} ${chalk.red(
507 `Deprecated configuration key
'${key}' usage
in section
'${sectionName}'$
{
508 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
513 !isUndefined(Configuration
.getConfigurationData()?.[key
as keyof ConfigurationData
])
516 `${chalk.green(configurationLogPrefix())} ${chalk.red(
517 `Deprecated configuration key
'${key}' usage$
{
518 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
525 private static getConfigurationData(): ConfigurationData
| undefined {
526 if (!Configuration
.configurationData
) {
528 Configuration
.configurationData
= JSON
.parse(
529 readFileSync(Configuration
.configurationFile
, 'utf8'),
530 ) as ConfigurationData
;
531 if (!Configuration
.configurationFileWatcher
) {
532 Configuration
.configurationFileWatcher
= Configuration
.getConfigurationFileWatcher();
535 Configuration
.handleFileException(
536 Configuration
.configurationFile
,
537 FileType
.Configuration
,
538 error
as NodeJS
.ErrnoException
,
539 configurationLogPrefix(),
543 return Configuration
.configurationData
;
546 private static getConfigurationFileWatcher(): FSWatcher
| undefined {
548 return watch(Configuration
.configurationFile
, (event
, filename
): void => {
549 if (filename
!.trim()!.length
> 0 && event
=== 'change') {
550 delete Configuration
.configurationData
;
551 Configuration
.configurationSectionCache
.clear();
552 if (!isUndefined(Configuration
.configurationChangeCallback
)) {
553 Configuration
.configurationChangeCallback().catch((error
) => {
554 throw typeof error
=== 'string' ? new Error(error
) : error
;
560 Configuration
.handleFileException(
561 Configuration
.configurationFile
,
562 FileType
.Configuration
,
563 error
as NodeJS
.ErrnoException
,
564 configurationLogPrefix(),
569 private static handleFileException(
572 error
: NodeJS
.ErrnoException
,
575 const prefix
= isNotEmptyString(logPfx
) ? `${logPfx} ` : '';
577 switch (error
.code
) {
579 logMsg
= `${fileType} file ${file} not found: `;
582 logMsg
= `${fileType} file ${file} already exists: `;
585 logMsg
= `${fileType} file ${file} access denied: `;
588 logMsg
= `${fileType} file ${file} permission denied: `;
591 logMsg
= `${fileType} file ${file} error: `;
593 console
.error(`${chalk.green(prefix)}${chalk.red(logMsg)}`, error
);
597 private static getDefaultPerformanceStorageUri(storageType
: StorageType
) {
598 switch (storageType
) {
599 case StorageType
.JSON_FILE
:
600 return Configuration
.buildPerformanceUriFilePath(
601 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}/${Constants.DEFAULT_PERFORMANCE_RECORDS_FILENAME}`,
603 case StorageType
.SQLITE
:
604 return Configuration
.buildPerformanceUriFilePath(
605 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}/${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}.db`,
608 throw new Error(`Unsupported storage type '${storageType}'`);
612 private static buildPerformanceUriFilePath(file
: string) {
613 return `file://${join(resolve(dirname(fileURLToPath(import.meta.url)), '../'), file)}`;