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
,
23 import { WorkerConstants
, WorkerProcessType
} from
'../worker';
25 type ConfigurationSectionType
=
27 | StorageConfiguration
29 | UIServerConfiguration
;
31 export class Configuration
{
32 private static configurationFile
= join(
33 dirname(fileURLToPath(import.meta
.url
)),
38 private static configurationFileWatcher
?: FSWatcher
;
39 private static configurationData
?: ConfigurationData
;
40 private static configurationSectionCache
= new Map
<
42 ConfigurationSectionType
44 [ConfigurationSection
.log
, Configuration
.buildLogSection()],
45 [ConfigurationSection
.performanceStorage
, Configuration
.buildPerformanceStorageSection()],
46 [ConfigurationSection
.worker
, Configuration
.buildWorkerSection()],
47 [ConfigurationSection
.uiServer
, Configuration
.buildUIServerSection()],
50 private static configurationChangeCallback
: () => Promise
<void>;
52 private constructor() {
53 // This is intentional
56 public static setConfigurationChangeCallback(cb
: () => Promise
<void>): void {
57 Configuration
.configurationChangeCallback
= cb
;
60 public static getConfigurationSection
<T
extends ConfigurationSectionType
>(
61 sectionName
: ConfigurationSection
,
63 if (!Configuration
.configurationSectionCache
.has(sectionName
)) {
64 switch (sectionName
) {
65 case ConfigurationSection
.log
:
66 Configuration
.configurationSectionCache
.set(sectionName
, Configuration
.buildLogSection());
68 case ConfigurationSection
.performanceStorage
:
69 Configuration
.configurationSectionCache
.set(
71 Configuration
.buildPerformanceStorageSection(),
74 case ConfigurationSection
.worker
:
75 Configuration
.configurationSectionCache
.set(
77 Configuration
.buildWorkerSection(),
80 case ConfigurationSection
.uiServer
:
81 Configuration
.configurationSectionCache
.set(
83 Configuration
.buildUIServerSection(),
87 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
88 throw new Error(`Unknown configuration section '${sectionName}'`);
91 return Configuration
.configurationSectionCache
.get(sectionName
) as T
;
94 public static getAutoReconnectMaxRetries(): number | undefined {
95 Configuration
.warnDeprecatedConfigurationKey(
96 'autoReconnectTimeout',
98 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead",
100 Configuration
.warnDeprecatedConfigurationKey(
103 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead",
105 Configuration
.warnDeprecatedConfigurationKey(
106 'autoReconnectMaxRetries',
108 'Use it in charging station template instead',
110 if (hasOwnProp(Configuration
.getConfigurationData(), 'autoReconnectMaxRetries')) {
111 return Configuration
.getConfigurationData()?.autoReconnectMaxRetries
;
115 public static getStationTemplateUrls(): StationTemplateUrl
[] | undefined {
116 Configuration
.warnDeprecatedConfigurationKey(
117 'stationTemplateURLs',
119 "Use 'stationTemplateUrls' instead",
121 // eslint-disable-next-line @typescript-eslint/dot-notation
123 Configuration
.getConfigurationData()!['stationTemplateURLs' as keyof ConfigurationData
],
125 (Configuration
.getConfigurationData()!.stationTemplateUrls
=
126 Configuration
.getConfigurationData()![
127 // eslint-disable-next-line @typescript-eslint/dot-notation
128 'stationTemplateURLs' as keyof ConfigurationData
129 ] as StationTemplateUrl
[]);
130 Configuration
.getConfigurationData()!.stationTemplateUrls
.forEach(
131 (stationTemplateUrl
: StationTemplateUrl
) => {
132 // eslint-disable-next-line @typescript-eslint/dot-notation
133 if (!isUndefined(stationTemplateUrl
['numberOfStation' as keyof StationTemplateUrl
])) {
135 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
136 `Deprecated configuration key
'numberOfStation' usage
for template file
'${stationTemplateUrl.file}' in 'stationTemplateUrls'. Use
'numberOfStations' instead
`,
142 return Configuration
.getConfigurationData()?.stationTemplateUrls
;
145 public static getSupervisionUrls(): string | string[] | undefined {
146 Configuration
.warnDeprecatedConfigurationKey(
149 "Use 'supervisionUrls' instead",
151 // eslint-disable-next-line @typescript-eslint/dot-notation
154 Configuration
.getConfigurationData()!['supervisionURLs' as keyof ConfigurationData
],
157 Configuration
.getConfigurationData()!.supervisionUrls
= Configuration
.getConfigurationData()![
158 // eslint-disable-next-line @typescript-eslint/dot-notation
159 'supervisionURLs' as keyof ConfigurationData
160 ] as string | string[];
162 return Configuration
.getConfigurationData()?.supervisionUrls
;
165 public static getSupervisionUrlDistribution(): SupervisionUrlDistribution
| undefined {
166 Configuration
.warnDeprecatedConfigurationKey(
167 'distributeStationToTenantEqually',
169 "Use 'supervisionUrlDistribution' instead",
171 Configuration
.warnDeprecatedConfigurationKey(
172 'distributeStationsToTenantsEqually',
174 "Use 'supervisionUrlDistribution' instead",
176 return hasOwnProp(Configuration
.getConfigurationData(), 'supervisionUrlDistribution')
177 ? Configuration
.getConfigurationData()?.supervisionUrlDistribution
178 : SupervisionUrlDistribution
.ROUND_ROBIN
;
181 public static workerPoolInUse(): boolean {
182 return [WorkerProcessType
.dynamicPool
, WorkerProcessType
.staticPool
].includes(
183 Configuration
.buildWorkerSection().processType
!,
187 public static workerDynamicPoolInUse(): boolean {
188 return Configuration
.buildWorkerSection().processType
=== WorkerProcessType
.dynamicPool
;
191 private static buildUIServerSection(): UIServerConfiguration
{
192 if (hasOwnProp(Configuration
.getConfigurationData(), 'uiWebSocketServer')) {
194 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
195 `Deprecated configuration section
'uiWebSocketServer' usage
. Use
'${ConfigurationSection.uiServer}' instead
`,
199 let uiServerConfiguration
: UIServerConfiguration
= {
201 type: ApplicationProtocol
.WS
,
203 host
: Constants
.DEFAULT_UI_SERVER_HOST
,
204 port
: Constants
.DEFAULT_UI_SERVER_PORT
,
207 if (hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.uiServer
)) {
208 uiServerConfiguration
= merge
<UIServerConfiguration
>(
209 uiServerConfiguration
,
210 Configuration
.getConfigurationData()!.uiServer
!,
213 if (isCFEnvironment() === true) {
214 delete uiServerConfiguration
.options
?.host
;
215 uiServerConfiguration
.options
!.port
= parseInt(process
.env
.PORT
!);
217 return uiServerConfiguration
;
220 private static buildPerformanceStorageSection(): StorageConfiguration
{
221 Configuration
.warnDeprecatedConfigurationKey(
223 ConfigurationSection
.performanceStorage
,
226 let storageConfiguration
: StorageConfiguration
= {
228 type: StorageType
.JSON_FILE
,
229 uri
: Configuration
.getDefaultPerformanceStorageUri(StorageType
.JSON_FILE
),
231 if (hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.performanceStorage
)) {
232 storageConfiguration
= {
233 ...storageConfiguration
,
234 ...Configuration
.getConfigurationData()?.performanceStorage
,
235 ...(Configuration
.getConfigurationData()?.performanceStorage
?.type ===
236 StorageType
.JSON_FILE
&&
237 Configuration
.getConfigurationData()?.performanceStorage
?.uri
&& {
238 uri
: Configuration
.buildPerformanceUriFilePath(
239 new URL(Configuration
.getConfigurationData()!.performanceStorage
!.uri
!).pathname
,
244 return storageConfiguration
;
247 private static buildLogSection(): LogConfiguration
{
248 Configuration
.warnDeprecatedConfigurationKey(
251 `Use '${ConfigurationSection.log}' section to define the logging enablement instead`,
253 Configuration
.warnDeprecatedConfigurationKey(
256 `Use '${ConfigurationSection.log}' section to define the log file instead`,
258 Configuration
.warnDeprecatedConfigurationKey(
261 `Use '${ConfigurationSection.log}' section to define the log error file instead`,
263 Configuration
.warnDeprecatedConfigurationKey(
266 `Use '${ConfigurationSection.log}' section to define the console logging enablement instead`,
268 Configuration
.warnDeprecatedConfigurationKey(
269 'logStatisticsInterval',
271 `Use '${ConfigurationSection.log}' section to define the log statistics interval instead`,
273 Configuration
.warnDeprecatedConfigurationKey(
276 `Use '${ConfigurationSection.log}' section to define the log level instead`,
278 Configuration
.warnDeprecatedConfigurationKey(
281 `Use '${ConfigurationSection.log}' section to define the log format instead`,
283 Configuration
.warnDeprecatedConfigurationKey(
286 `Use '${ConfigurationSection.log}' section to define the log rotation enablement instead`,
288 Configuration
.warnDeprecatedConfigurationKey(
291 `Use '${ConfigurationSection.log}' section to define the log maximum files instead`,
293 Configuration
.warnDeprecatedConfigurationKey(
296 `Use '${ConfigurationSection.log}' section to define the log maximum size instead`,
298 const defaultLogConfiguration
: LogConfiguration
= {
300 file
: 'logs/combined.log',
301 errorFile
: 'logs/error.log',
302 statisticsInterval
: Constants
.DEFAULT_LOG_STATISTICS_INTERVAL
,
307 const deprecatedLogConfiguration
: LogConfiguration
= {
308 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logEnabled') && {
309 enabled
: Configuration
.getConfigurationData()?.logEnabled
,
311 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logFile') && {
312 file
: Configuration
.getConfigurationData()?.logFile
,
314 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logErrorFile') && {
315 errorFile
: Configuration
.getConfigurationData()?.logErrorFile
,
317 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logStatisticsInterval') && {
318 statisticsInterval
: Configuration
.getConfigurationData()?.logStatisticsInterval
,
320 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logLevel') && {
321 level
: Configuration
.getConfigurationData()?.logLevel
,
323 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logConsole') && {
324 console
: Configuration
.getConfigurationData()?.logConsole
,
326 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logFormat') && {
327 format
: Configuration
.getConfigurationData()?.logFormat
,
329 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logRotate') && {
330 rotate
: Configuration
.getConfigurationData()?.logRotate
,
332 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logMaxFiles') && {
333 maxFiles
: Configuration
.getConfigurationData()?.logMaxFiles
,
335 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logMaxSize') && {
336 maxSize
: Configuration
.getConfigurationData()?.logMaxSize
,
339 const logConfiguration
: LogConfiguration
= {
340 ...defaultLogConfiguration
,
341 ...deprecatedLogConfiguration
,
342 ...(hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.log
) &&
343 Configuration
.getConfigurationData()?.log
),
345 return logConfiguration
;
348 private static buildWorkerSection(): WorkerConfiguration
{
349 Configuration
.warnDeprecatedConfigurationKey(
352 `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`,
354 Configuration
.warnDeprecatedConfigurationKey(
357 `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`,
359 Configuration
.warnDeprecatedConfigurationKey(
362 `Use '${ConfigurationSection.worker}' section to define the worker start delay instead`,
364 Configuration
.warnDeprecatedConfigurationKey(
365 'chargingStationsPerWorker',
367 `Use '${ConfigurationSection.worker}' section to define the number of element(s) per worker instead`,
369 Configuration
.warnDeprecatedConfigurationKey(
372 `Use '${ConfigurationSection.worker}' section to define the worker's element start delay instead`,
374 Configuration
.warnDeprecatedConfigurationKey(
377 `Use '${ConfigurationSection.worker}' section to define the worker pool minimum size instead`,
379 Configuration
.warnDeprecatedConfigurationKey(
382 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`,
384 Configuration
.warnDeprecatedConfigurationKey(
385 'workerPoolMaxSize;',
387 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`,
389 Configuration
.warnDeprecatedConfigurationKey(
390 'workerPoolStrategy;',
392 `Use '${ConfigurationSection.worker}' section to define the worker pool strategy instead`,
394 const defaultWorkerConfiguration
: WorkerConfiguration
= {
395 processType
: WorkerProcessType
.workerSet
,
396 startDelay
: WorkerConstants
.DEFAULT_WORKER_START_DELAY
,
397 elementsPerWorker
: 'auto',
398 elementStartDelay
: WorkerConstants
.DEFAULT_ELEMENT_START_DELAY
,
399 poolMinSize
: WorkerConstants
.DEFAULT_POOL_MIN_SIZE
,
400 poolMaxSize
: WorkerConstants
.DEFAULT_POOL_MAX_SIZE
,
402 hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolStrategy') &&
403 delete Configuration
.getConfigurationData()?.workerPoolStrategy
;
404 const deprecatedWorkerConfiguration
: WorkerConfiguration
= {
405 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerProcess') && {
406 processType
: Configuration
.getConfigurationData()?.workerProcess
,
408 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerStartDelay') && {
409 startDelay
: Configuration
.getConfigurationData()?.workerStartDelay
,
411 ...(hasOwnProp(Configuration
.getConfigurationData(), 'chargingStationsPerWorker') && {
412 elementsPerWorker
: Configuration
.getConfigurationData()?.chargingStationsPerWorker
,
414 ...(hasOwnProp(Configuration
.getConfigurationData(), 'elementStartDelay') && {
415 elementStartDelay
: Configuration
.getConfigurationData()?.elementStartDelay
,
417 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMinSize') && {
418 poolMinSize
: Configuration
.getConfigurationData()?.workerPoolMinSize
,
420 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMaxSize') && {
421 poolMaxSize
: Configuration
.getConfigurationData()?.workerPoolMaxSize
,
424 Configuration
.warnDeprecatedConfigurationKey(
426 ConfigurationSection
.worker
,
427 'Not publicly exposed to end users',
429 const workerConfiguration
: WorkerConfiguration
= {
430 ...defaultWorkerConfiguration
,
431 ...deprecatedWorkerConfiguration
,
432 ...(hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.worker
) &&
433 Configuration
.getConfigurationData()?.worker
),
435 if (!Object.values(WorkerProcessType
).includes(workerConfiguration
.processType
!)) {
436 throw new SyntaxError(
437 `Invalid worker process type '${workerConfiguration.processType}' defined in configuration`,
440 return workerConfiguration
;
443 private static logPrefix
= (): string => {
444 return `${new Date().toLocaleString()} Simulator configuration |`;
447 private static warnDeprecatedConfigurationKey(
449 sectionName
?: string,
454 !isUndefined(Configuration
.getConfigurationData()![sectionName
as keyof ConfigurationData
]) &&
457 Configuration
.getConfigurationData()![sectionName
as keyof ConfigurationData
] as Record
<
465 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
466 `Deprecated configuration key
'${key}' usage
in section
'${sectionName}'$
{
467 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
472 !isUndefined(Configuration
.getConfigurationData()![key
as keyof ConfigurationData
])
475 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
476 `Deprecated configuration key
'${key}' usage$
{
477 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
484 private static getConfigurationData(): ConfigurationData
| undefined {
485 if (!Configuration
.configurationData
) {
487 Configuration
.configurationData
= JSON
.parse(
488 readFileSync(Configuration
.configurationFile
, 'utf8'),
489 ) as ConfigurationData
;
490 if (!Configuration
.configurationFileWatcher
) {
491 Configuration
.configurationFileWatcher
= Configuration
.getConfigurationFileWatcher();
494 Configuration
.handleFileException(
495 Configuration
.configurationFile
,
496 FileType
.Configuration
,
497 error
as NodeJS
.ErrnoException
,
498 Configuration
.logPrefix(),
502 return Configuration
.configurationData
;
505 private static getConfigurationFileWatcher(): FSWatcher
| undefined {
507 return watch(Configuration
.configurationFile
, (event
, filename
): void => {
508 if (filename
!.trim()!.length
> 0 && event
=== 'change') {
509 delete Configuration
.configurationData
;
510 Configuration
.configurationSectionCache
.clear();
511 if (!isUndefined(Configuration
.configurationChangeCallback
)) {
512 Configuration
.configurationChangeCallback().catch((error
) => {
513 throw typeof error
=== 'string' ? new Error(error
) : error
;
519 Configuration
.handleFileException(
520 Configuration
.configurationFile
,
521 FileType
.Configuration
,
522 error
as NodeJS
.ErrnoException
,
523 Configuration
.logPrefix(),
528 private static handleFileException(
531 error
: NodeJS
.ErrnoException
,
534 const prefix
= isNotEmptyString(logPrefix
) ? `${logPrefix} ` : '';
536 switch (error
.code
) {
538 logMsg
= `${fileType} file ${file} not found:`;
541 logMsg
= `${fileType} file ${file} already exists:`;
544 logMsg
= `${fileType} file ${file} access denied:`;
547 logMsg
= `${fileType} file ${file} permission denied:`;
550 logMsg
= `${fileType} file ${file} error:`;
552 console
.error(`${chalk.green(prefix)}${chalk.red(`${logMsg} `)}`, error);
556 private static getDefaultPerformanceStorageUri(storageType: StorageType) {
557 switch (storageType) {
558 case StorageType.JSON_FILE:
559 return Configuration.buildPerformanceUriFilePath(
560 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}
/${Constants.DEFAULT_PERFORMANCE_RECORDS_FILENAME}
`,
562 case StorageType.SQLITE:
563 return Configuration.buildPerformanceUriFilePath(
564 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}
/${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}
.db
`,
567 throw new Error(`Performance storage URI
is mandatory
with storage
type '${storageType}'`);
571 private static buildPerformanceUriFilePath(file: string) {
572 return `file
://${join(resolve(dirname(fileURLToPath(import.meta.url)), '../'), file)}`;