1 import { type FSWatcher
, readFileSync
, watch
} from
'node:fs';
2 import { dirname
, join
, resolve
} from
'node:path';
3 import { fileURLToPath
} from
'node:url';
5 import chalk from
'chalk';
6 import merge from
'just-merge';
8 import { Constants
} from
'./Constants';
9 import { hasOwnProp
, isCFEnvironment
, isNotEmptyString
, isUndefined
} from
'./Utils';
12 type ConfigurationData
,
15 type LogConfiguration
,
16 type StationTemplateUrl
,
17 type StorageConfiguration
,
19 SupervisionUrlDistribution
,
20 type UIServerConfiguration
,
21 type WorkerConfiguration
,
24 DEFAULT_ELEMENT_START_DELAY
,
25 DEFAULT_POOL_MAX_SIZE
,
26 DEFAULT_POOL_MIN_SIZE
,
27 DEFAULT_WORKER_START_DELAY
,
31 type ConfigurationSectionType
=
33 | StorageConfiguration
35 | UIServerConfiguration
;
37 export class Configuration
{
38 private static configurationFile
= join(
39 dirname(fileURLToPath(import.meta
.url
)),
44 private static configurationFileWatcher
?: FSWatcher
;
45 private static configurationData
?: ConfigurationData
;
46 private static configurationSectionCache
= new Map
<
48 ConfigurationSectionType
50 [ConfigurationSection
.log
, Configuration
.buildLogSection()],
51 [ConfigurationSection
.performanceStorage
, Configuration
.buildPerformanceStorageSection()],
52 [ConfigurationSection
.worker
, Configuration
.buildWorkerSection()],
53 [ConfigurationSection
.uiServer
, Configuration
.buildUIServerSection()],
56 private static configurationChangeCallback
?: () => Promise
<void>;
58 private constructor() {
59 // This is intentional
62 public static setConfigurationChangeCallback(cb
: () => Promise
<void>): void {
63 Configuration
.configurationChangeCallback
= cb
;
66 public static getConfigurationSection
<T
extends ConfigurationSectionType
>(
67 sectionName
: ConfigurationSection
,
69 if (!Configuration
.isConfigurationSectionCached(sectionName
)) {
70 Configuration
.cacheConfigurationSection(sectionName
);
72 return Configuration
.configurationSectionCache
.get(sectionName
) as T
;
75 public static getAutoReconnectMaxRetries(): number | undefined {
76 Configuration
.warnDeprecatedConfigurationKey(
77 'autoReconnectTimeout',
79 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead",
81 Configuration
.warnDeprecatedConfigurationKey(
84 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead",
86 Configuration
.warnDeprecatedConfigurationKey(
87 'autoReconnectMaxRetries',
89 'Use it in charging station template instead',
91 if (hasOwnProp(Configuration
.getConfigurationData(), 'autoReconnectMaxRetries')) {
92 return Configuration
.getConfigurationData()?.autoReconnectMaxRetries
;
96 public static getStationTemplateUrls(): StationTemplateUrl
[] | undefined {
97 Configuration
.warnDeprecatedConfigurationKey(
98 'stationTemplateURLs',
100 "Use 'stationTemplateUrls' instead",
102 // eslint-disable-next-line @typescript-eslint/dot-notation
104 Configuration
.getConfigurationData()!['stationTemplateURLs' as keyof ConfigurationData
],
106 (Configuration
.getConfigurationData()!.stationTemplateUrls
=
107 Configuration
.getConfigurationData()![
108 // eslint-disable-next-line @typescript-eslint/dot-notation
109 'stationTemplateURLs' as keyof ConfigurationData
110 ] as StationTemplateUrl
[]);
111 Configuration
.getConfigurationData()!.stationTemplateUrls
.forEach(
112 (stationTemplateUrl
: StationTemplateUrl
) => {
113 // eslint-disable-next-line @typescript-eslint/dot-notation
114 if (!isUndefined(stationTemplateUrl
['numberOfStation' as keyof StationTemplateUrl
])) {
116 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
117 `Deprecated configuration key
'numberOfStation' usage
for template file
'${stationTemplateUrl.file}' in 'stationTemplateUrls'. Use
'numberOfStations' instead
`,
123 return Configuration
.getConfigurationData()?.stationTemplateUrls
;
126 public static getSupervisionUrls(): string | string[] | undefined {
127 Configuration
.warnDeprecatedConfigurationKey(
130 "Use 'supervisionUrls' instead",
132 // eslint-disable-next-line @typescript-eslint/dot-notation
135 Configuration
.getConfigurationData()!['supervisionURLs' as keyof ConfigurationData
],
138 Configuration
.getConfigurationData()!.supervisionUrls
= Configuration
.getConfigurationData()![
139 // eslint-disable-next-line @typescript-eslint/dot-notation
140 'supervisionURLs' as keyof ConfigurationData
141 ] as string | string[];
143 return Configuration
.getConfigurationData()?.supervisionUrls
;
146 public static getSupervisionUrlDistribution(): SupervisionUrlDistribution
| undefined {
147 Configuration
.warnDeprecatedConfigurationKey(
148 'distributeStationToTenantEqually',
150 "Use 'supervisionUrlDistribution' instead",
152 Configuration
.warnDeprecatedConfigurationKey(
153 'distributeStationsToTenantsEqually',
155 "Use 'supervisionUrlDistribution' instead",
157 return hasOwnProp(Configuration
.getConfigurationData(), 'supervisionUrlDistribution')
158 ? Configuration
.getConfigurationData()?.supervisionUrlDistribution
159 : SupervisionUrlDistribution
.ROUND_ROBIN
;
162 public static workerPoolInUse(): boolean {
163 return [WorkerProcessType
.dynamicPool
, WorkerProcessType
.staticPool
].includes(
164 Configuration
.buildWorkerSection().processType
!,
168 public static workerDynamicPoolInUse(): boolean {
169 return Configuration
.buildWorkerSection().processType
=== WorkerProcessType
.dynamicPool
;
172 private static isConfigurationSectionCached(sectionName
: ConfigurationSection
): boolean {
173 return Configuration
.configurationSectionCache
.has(sectionName
);
176 private static cacheConfigurationSection(sectionName
: ConfigurationSection
): void {
177 switch (sectionName
) {
178 case ConfigurationSection
.log
:
179 Configuration
.configurationSectionCache
.set(sectionName
, Configuration
.buildLogSection());
181 case ConfigurationSection
.performanceStorage
:
182 Configuration
.configurationSectionCache
.set(
184 Configuration
.buildPerformanceStorageSection(),
187 case ConfigurationSection
.worker
:
188 Configuration
.configurationSectionCache
.set(
190 Configuration
.buildWorkerSection(),
193 case ConfigurationSection
.uiServer
:
194 Configuration
.configurationSectionCache
.set(
196 Configuration
.buildUIServerSection(),
200 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
201 throw new Error(`Unknown configuration section '${sectionName}'`);
205 private static buildUIServerSection(): UIServerConfiguration
{
206 if (hasOwnProp(Configuration
.getConfigurationData(), 'uiWebSocketServer')) {
208 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
209 `Deprecated configuration section
'uiWebSocketServer' usage
. Use
'${ConfigurationSection.uiServer}' instead
`,
213 let uiServerConfiguration
: UIServerConfiguration
= {
215 type: ApplicationProtocol
.WS
,
217 host
: Constants
.DEFAULT_UI_SERVER_HOST
,
218 port
: Constants
.DEFAULT_UI_SERVER_PORT
,
221 if (hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.uiServer
)) {
222 uiServerConfiguration
= merge
<UIServerConfiguration
>(
223 uiServerConfiguration
,
224 Configuration
.getConfigurationData()!.uiServer
!,
227 if (isCFEnvironment() === true) {
228 delete uiServerConfiguration
.options
?.host
;
229 uiServerConfiguration
.options
!.port
= parseInt(process
.env
.PORT
!);
231 return uiServerConfiguration
;
234 private static buildPerformanceStorageSection(): StorageConfiguration
{
235 Configuration
.warnDeprecatedConfigurationKey(
237 ConfigurationSection
.performanceStorage
,
240 let storageConfiguration
: StorageConfiguration
= {
242 type: StorageType
.JSON_FILE
,
243 uri
: Configuration
.getDefaultPerformanceStorageUri(StorageType
.JSON_FILE
),
245 if (hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.performanceStorage
)) {
246 storageConfiguration
= {
247 ...storageConfiguration
,
248 ...Configuration
.getConfigurationData()?.performanceStorage
,
249 ...(Configuration
.getConfigurationData()?.performanceStorage
?.type ===
250 StorageType
.JSON_FILE
&&
251 Configuration
.getConfigurationData()?.performanceStorage
?.uri
&& {
252 uri
: Configuration
.buildPerformanceUriFilePath(
253 new URL(Configuration
.getConfigurationData()!.performanceStorage
!.uri
!).pathname
,
258 return storageConfiguration
;
261 private static buildLogSection(): LogConfiguration
{
262 Configuration
.warnDeprecatedConfigurationKey(
265 `Use '${ConfigurationSection.log}' section to define the logging enablement instead`,
267 Configuration
.warnDeprecatedConfigurationKey(
270 `Use '${ConfigurationSection.log}' section to define the log file instead`,
272 Configuration
.warnDeprecatedConfigurationKey(
275 `Use '${ConfigurationSection.log}' section to define the log error file instead`,
277 Configuration
.warnDeprecatedConfigurationKey(
280 `Use '${ConfigurationSection.log}' section to define the console logging enablement instead`,
282 Configuration
.warnDeprecatedConfigurationKey(
283 'logStatisticsInterval',
285 `Use '${ConfigurationSection.log}' section to define the log statistics interval instead`,
287 Configuration
.warnDeprecatedConfigurationKey(
290 `Use '${ConfigurationSection.log}' section to define the log level instead`,
292 Configuration
.warnDeprecatedConfigurationKey(
295 `Use '${ConfigurationSection.log}' section to define the log format instead`,
297 Configuration
.warnDeprecatedConfigurationKey(
300 `Use '${ConfigurationSection.log}' section to define the log rotation enablement instead`,
302 Configuration
.warnDeprecatedConfigurationKey(
305 `Use '${ConfigurationSection.log}' section to define the log maximum files instead`,
307 Configuration
.warnDeprecatedConfigurationKey(
310 `Use '${ConfigurationSection.log}' section to define the log maximum size instead`,
312 const defaultLogConfiguration
: LogConfiguration
= {
314 file
: 'logs/combined.log',
315 errorFile
: 'logs/error.log',
316 statisticsInterval
: Constants
.DEFAULT_LOG_STATISTICS_INTERVAL
,
321 const deprecatedLogConfiguration
: LogConfiguration
= {
322 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logEnabled') && {
323 enabled
: Configuration
.getConfigurationData()?.logEnabled
,
325 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logFile') && {
326 file
: Configuration
.getConfigurationData()?.logFile
,
328 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logErrorFile') && {
329 errorFile
: Configuration
.getConfigurationData()?.logErrorFile
,
331 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logStatisticsInterval') && {
332 statisticsInterval
: Configuration
.getConfigurationData()?.logStatisticsInterval
,
334 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logLevel') && {
335 level
: Configuration
.getConfigurationData()?.logLevel
,
337 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logConsole') && {
338 console
: Configuration
.getConfigurationData()?.logConsole
,
340 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logFormat') && {
341 format
: Configuration
.getConfigurationData()?.logFormat
,
343 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logRotate') && {
344 rotate
: Configuration
.getConfigurationData()?.logRotate
,
346 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logMaxFiles') && {
347 maxFiles
: Configuration
.getConfigurationData()?.logMaxFiles
,
349 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logMaxSize') && {
350 maxSize
: Configuration
.getConfigurationData()?.logMaxSize
,
353 const logConfiguration
: LogConfiguration
= {
354 ...defaultLogConfiguration
,
355 ...deprecatedLogConfiguration
,
356 ...(hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.log
) &&
357 Configuration
.getConfigurationData()?.log
),
359 return logConfiguration
;
362 private static buildWorkerSection(): WorkerConfiguration
{
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 type of worker process model instead`,
373 Configuration
.warnDeprecatedConfigurationKey(
376 `Use '${ConfigurationSection.worker}' section to define the worker start delay instead`,
378 Configuration
.warnDeprecatedConfigurationKey(
379 'chargingStationsPerWorker',
381 `Use '${ConfigurationSection.worker}' section to define the number of element(s) per worker instead`,
383 Configuration
.warnDeprecatedConfigurationKey(
386 `Use '${ConfigurationSection.worker}' section to define the worker's element start delay instead`,
388 Configuration
.warnDeprecatedConfigurationKey(
391 `Use '${ConfigurationSection.worker}' section to define the worker pool minimum size instead`,
393 Configuration
.warnDeprecatedConfigurationKey(
396 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`,
398 Configuration
.warnDeprecatedConfigurationKey(
399 'workerPoolMaxSize;',
401 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`,
403 Configuration
.warnDeprecatedConfigurationKey(
404 'workerPoolStrategy;',
406 `Use '${ConfigurationSection.worker}' section to define the worker pool strategy instead`,
408 const defaultWorkerConfiguration
: WorkerConfiguration
= {
409 processType
: WorkerProcessType
.workerSet
,
410 startDelay
: DEFAULT_WORKER_START_DELAY
,
411 elementsPerWorker
: 'auto',
412 elementStartDelay
: DEFAULT_ELEMENT_START_DELAY
,
413 poolMinSize
: DEFAULT_POOL_MIN_SIZE
,
414 poolMaxSize
: DEFAULT_POOL_MAX_SIZE
,
416 hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolStrategy') &&
417 delete Configuration
.getConfigurationData()?.workerPoolStrategy
;
418 const deprecatedWorkerConfiguration
: WorkerConfiguration
= {
419 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerProcess') && {
420 processType
: Configuration
.getConfigurationData()?.workerProcess
,
422 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerStartDelay') && {
423 startDelay
: Configuration
.getConfigurationData()?.workerStartDelay
,
425 ...(hasOwnProp(Configuration
.getConfigurationData(), 'chargingStationsPerWorker') && {
426 elementsPerWorker
: Configuration
.getConfigurationData()?.chargingStationsPerWorker
,
428 ...(hasOwnProp(Configuration
.getConfigurationData(), 'elementStartDelay') && {
429 elementStartDelay
: Configuration
.getConfigurationData()?.elementStartDelay
,
431 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMinSize') && {
432 poolMinSize
: Configuration
.getConfigurationData()?.workerPoolMinSize
,
434 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMaxSize') && {
435 poolMaxSize
: Configuration
.getConfigurationData()?.workerPoolMaxSize
,
438 Configuration
.warnDeprecatedConfigurationKey(
440 ConfigurationSection
.worker
,
441 'Not publicly exposed to end users',
443 const workerConfiguration
: WorkerConfiguration
= {
444 ...defaultWorkerConfiguration
,
445 ...deprecatedWorkerConfiguration
,
446 ...(hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.worker
) &&
447 Configuration
.getConfigurationData()?.worker
),
449 if (!Object.values(WorkerProcessType
).includes(workerConfiguration
.processType
!)) {
450 throw new SyntaxError(
451 `Invalid worker process type '${workerConfiguration.processType}' defined in configuration`,
454 return workerConfiguration
;
457 private static logPrefix
= (): string => {
458 return `${new Date().toLocaleString()} Simulator configuration |`;
461 private static warnDeprecatedConfigurationKey(
463 sectionName
?: string,
468 !isUndefined(Configuration
.getConfigurationData()![sectionName
as keyof ConfigurationData
]) &&
471 Configuration
.getConfigurationData()![sectionName
as keyof ConfigurationData
] as Record
<
479 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
480 `Deprecated configuration key
'${key}' usage
in section
'${sectionName}'$
{
481 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
486 !isUndefined(Configuration
.getConfigurationData()![key
as keyof ConfigurationData
])
489 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
490 `Deprecated configuration key
'${key}' usage$
{
491 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
498 private static getConfigurationData(): ConfigurationData
| undefined {
499 if (!Configuration
.configurationData
) {
501 Configuration
.configurationData
= JSON
.parse(
502 readFileSync(Configuration
.configurationFile
, 'utf8'),
503 ) as ConfigurationData
;
504 if (!Configuration
.configurationFileWatcher
) {
505 Configuration
.configurationFileWatcher
= Configuration
.getConfigurationFileWatcher();
508 Configuration
.handleFileException(
509 Configuration
.configurationFile
,
510 FileType
.Configuration
,
511 error
as NodeJS
.ErrnoException
,
512 Configuration
.logPrefix(),
516 return Configuration
.configurationData
;
519 private static getConfigurationFileWatcher(): FSWatcher
| undefined {
521 return watch(Configuration
.configurationFile
, (event
, filename
): void => {
522 if (filename
!.trim()!.length
> 0 && event
=== 'change') {
523 delete Configuration
.configurationData
;
524 Configuration
.configurationSectionCache
.clear();
525 if (!isUndefined(Configuration
.configurationChangeCallback
)) {
526 Configuration
.configurationChangeCallback
!().catch((error
) => {
527 throw typeof error
=== 'string' ? new Error(error
) : error
;
533 Configuration
.handleFileException(
534 Configuration
.configurationFile
,
535 FileType
.Configuration
,
536 error
as NodeJS
.ErrnoException
,
537 Configuration
.logPrefix(),
542 private static handleFileException(
545 error
: NodeJS
.ErrnoException
,
548 const prefix
= isNotEmptyString(logPrefix
) ? `${logPrefix} ` : '';
550 switch (error
.code
) {
552 logMsg
= `${fileType} file ${file} not found:`;
555 logMsg
= `${fileType} file ${file} already exists:`;
558 logMsg
= `${fileType} file ${file} access denied:`;
561 logMsg
= `${fileType} file ${file} permission denied:`;
564 logMsg
= `${fileType} file ${file} error:`;
566 console
.error(`${chalk.green(prefix)}${chalk.red(`${logMsg} `)}`, error);
570 private static getDefaultPerformanceStorageUri(storageType: StorageType) {
571 switch (storageType) {
572 case StorageType.JSON_FILE:
573 return Configuration.buildPerformanceUriFilePath(
574 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}
/${Constants.DEFAULT_PERFORMANCE_RECORDS_FILENAME}
`,
576 case StorageType.SQLITE:
577 return Configuration.buildPerformanceUriFilePath(
578 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}
/${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}
.db
`,
581 throw new Error(`Unsupported storage
type '${storageType}'`);
585 private static buildPerformanceUriFilePath(file: string) {
586 return `file
://${join(resolve(dirname(fileURLToPath(import.meta.url)), '../'), file)}`;