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 export class Configuration
{
26 private static configurationFile
= join(
27 dirname(fileURLToPath(import.meta
.url
)),
32 private static configurationFileWatcher
: FSWatcher
| undefined;
33 private static configurationData
: ConfigurationData
| null = null;
34 private static configurationSectionCache
= new Map
<
36 LogConfiguration
| StorageConfiguration
| WorkerConfiguration
| UIServerConfiguration
38 [ConfigurationSection
.log
, Configuration
.buildLogSection()],
39 [ConfigurationSection
.performanceStorage
, Configuration
.buildPerformanceStorageSection()],
40 [ConfigurationSection
.worker
, Configuration
.buildWorkerSection()],
41 [ConfigurationSection
.uiServer
, Configuration
.buildUIServerSection()],
44 private static configurationChangeCallback
: () => Promise
<void>;
46 private constructor() {
47 // This is intentional
50 public static setConfigurationChangeCallback(cb
: () => Promise
<void>): void {
51 Configuration
.configurationChangeCallback
= cb
;
54 public static getConfigurationSection
<T
>(sectionName
: ConfigurationSection
): T
{
55 if (!Configuration
.configurationSectionCache
.has(sectionName
)) {
56 switch (sectionName
) {
57 case ConfigurationSection
.log
:
58 Configuration
.configurationSectionCache
.set(sectionName
, Configuration
.buildLogSection());
60 case ConfigurationSection
.performanceStorage
:
61 Configuration
.configurationSectionCache
.set(
63 Configuration
.buildPerformanceStorageSection(),
66 case ConfigurationSection
.worker
:
67 Configuration
.configurationSectionCache
.set(
69 Configuration
.buildWorkerSection(),
72 case ConfigurationSection
.uiServer
:
73 Configuration
.configurationSectionCache
.set(
75 Configuration
.buildUIServerSection(),
79 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
80 throw new Error(`Unknown configuration section '${sectionName}'`);
83 return Configuration
.configurationSectionCache
.get(sectionName
) as T
;
86 public static getAutoReconnectMaxRetries(): number | undefined {
87 Configuration
.warnDeprecatedConfigurationKey(
88 'autoReconnectTimeout',
90 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead",
92 Configuration
.warnDeprecatedConfigurationKey(
95 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead",
97 Configuration
.warnDeprecatedConfigurationKey(
98 'autoReconnectMaxRetries',
100 'Use it in charging station template instead',
102 if (hasOwnProp(Configuration
.getConfigurationData(), 'autoReconnectMaxRetries')) {
103 return Configuration
.getConfigurationData()?.autoReconnectMaxRetries
;
107 public static getStationTemplateUrls(): StationTemplateUrl
[] | undefined {
108 Configuration
.warnDeprecatedConfigurationKey(
109 'stationTemplateURLs',
111 "Use 'stationTemplateUrls' instead",
113 // eslint-disable-next-line @typescript-eslint/dot-notation
114 !isUndefined(Configuration
.getConfigurationData()!['stationTemplateURLs']) &&
115 (Configuration
.getConfigurationData()!.stationTemplateUrls
=
116 Configuration
.getConfigurationData()![
117 // eslint-disable-next-line @typescript-eslint/dot-notation
118 'stationTemplateURLs'
119 ] as StationTemplateUrl
[]);
120 Configuration
.getConfigurationData()!.stationTemplateUrls
.forEach(
121 (stationTemplateUrl
: StationTemplateUrl
) => {
122 // eslint-disable-next-line @typescript-eslint/dot-notation
123 if (!isUndefined(stationTemplateUrl
['numberOfStation'])) {
125 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
126 `Deprecated configuration key
'numberOfStation' usage
for template file
'${stationTemplateUrl.file}' in 'stationTemplateUrls'. Use
'numberOfStations' instead
`,
132 return Configuration
.getConfigurationData()?.stationTemplateUrls
;
135 public static getSupervisionUrls(): string | string[] | undefined {
136 Configuration
.warnDeprecatedConfigurationKey(
139 "Use 'supervisionUrls' instead",
141 // eslint-disable-next-line @typescript-eslint/dot-notation
142 if (!isUndefined(Configuration
.getConfigurationData()!['supervisionURLs'])) {
143 Configuration
.getConfigurationData()!.supervisionUrls
= Configuration
.getConfigurationData()![
144 // eslint-disable-next-line @typescript-eslint/dot-notation
146 ] as string | string[];
148 return Configuration
.getConfigurationData()?.supervisionUrls
;
151 public static getSupervisionUrlDistribution(): SupervisionUrlDistribution
| undefined {
152 Configuration
.warnDeprecatedConfigurationKey(
153 'distributeStationToTenantEqually',
155 "Use 'supervisionUrlDistribution' instead",
157 Configuration
.warnDeprecatedConfigurationKey(
158 'distributeStationsToTenantsEqually',
160 "Use 'supervisionUrlDistribution' instead",
162 return hasOwnProp(Configuration
.getConfigurationData(), 'supervisionUrlDistribution')
163 ? Configuration
.getConfigurationData()?.supervisionUrlDistribution
164 : SupervisionUrlDistribution
.ROUND_ROBIN
;
167 public static workerPoolInUse(): boolean {
168 return [WorkerProcessType
.dynamicPool
, WorkerProcessType
.staticPool
].includes(
169 Configuration
.buildWorkerSection().processType
!,
173 public static workerDynamicPoolInUse(): boolean {
174 return Configuration
.buildWorkerSection().processType
=== WorkerProcessType
.dynamicPool
;
177 private static buildUIServerSection(): UIServerConfiguration
{
178 if (hasOwnProp(Configuration
.getConfigurationData(), 'uiWebSocketServer')) {
180 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
181 `Deprecated configuration section
'uiWebSocketServer' usage
. Use
'${ConfigurationSection.uiServer}' instead
`,
185 let uiServerConfiguration
: UIServerConfiguration
= {
187 type: ApplicationProtocol
.WS
,
189 host
: Constants
.DEFAULT_UI_SERVER_HOST
,
190 port
: Constants
.DEFAULT_UI_SERVER_PORT
,
193 if (hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.uiServer
)) {
194 uiServerConfiguration
= merge
<UIServerConfiguration
>(
195 uiServerConfiguration
,
196 Configuration
.getConfigurationData()!.uiServer
!,
199 if (isCFEnvironment() === true) {
200 delete uiServerConfiguration
.options
?.host
;
201 uiServerConfiguration
.options
!.port
= parseInt(process
.env
.PORT
!);
203 return uiServerConfiguration
;
206 private static buildPerformanceStorageSection(): StorageConfiguration
{
207 Configuration
.warnDeprecatedConfigurationKey(
209 ConfigurationSection
.performanceStorage
,
212 let storageConfiguration
: StorageConfiguration
= {
214 type: StorageType
.JSON_FILE
,
215 uri
: this.getDefaultPerformanceStorageUri(StorageType
.JSON_FILE
),
217 if (hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.performanceStorage
)) {
218 storageConfiguration
= {
219 ...storageConfiguration
,
220 ...Configuration
.getConfigurationData()?.performanceStorage
,
221 ...(Configuration
.getConfigurationData()?.performanceStorage
?.type ===
222 StorageType
.JSON_FILE
&&
223 Configuration
.getConfigurationData()?.performanceStorage
?.uri
&& {
224 uri
: Configuration
.buildPerformanceUriFilePath(
225 new URL(Configuration
.getConfigurationData()!.performanceStorage
!.uri
!).pathname
,
230 return storageConfiguration
;
233 private static buildLogSection(): LogConfiguration
{
234 Configuration
.warnDeprecatedConfigurationKey(
237 `Use '${ConfigurationSection.log}' section to define the logging enablement instead`,
239 Configuration
.warnDeprecatedConfigurationKey(
242 `Use '${ConfigurationSection.log}' section to define the log file instead`,
244 Configuration
.warnDeprecatedConfigurationKey(
247 `Use '${ConfigurationSection.log}' section to define the log error file instead`,
249 Configuration
.warnDeprecatedConfigurationKey(
252 `Use '${ConfigurationSection.log}' section to define the console logging enablement instead`,
254 Configuration
.warnDeprecatedConfigurationKey(
255 'logStatisticsInterval',
257 `Use '${ConfigurationSection.log}' section to define the log statistics interval instead`,
259 Configuration
.warnDeprecatedConfigurationKey(
262 `Use '${ConfigurationSection.log}' section to define the log level instead`,
264 Configuration
.warnDeprecatedConfigurationKey(
267 `Use '${ConfigurationSection.log}' section to define the log format instead`,
269 Configuration
.warnDeprecatedConfigurationKey(
272 `Use '${ConfigurationSection.log}' section to define the log rotation enablement instead`,
274 Configuration
.warnDeprecatedConfigurationKey(
277 `Use '${ConfigurationSection.log}' section to define the log maximum files instead`,
279 Configuration
.warnDeprecatedConfigurationKey(
282 `Use '${ConfigurationSection.log}' section to define the log maximum size instead`,
284 const defaultLogConfiguration
: LogConfiguration
= {
286 file
: 'logs/combined.log',
287 errorFile
: 'logs/error.log',
288 statisticsInterval
: Constants
.DEFAULT_LOG_STATISTICS_INTERVAL
,
293 const deprecatedLogConfiguration
: LogConfiguration
= {
294 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logEnabled') && {
295 enabled
: Configuration
.getConfigurationData()?.logEnabled
,
297 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logFile') && {
298 file
: Configuration
.getConfigurationData()?.logFile
,
300 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logErrorFile') && {
301 errorFile
: Configuration
.getConfigurationData()?.logErrorFile
,
303 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logStatisticsInterval') && {
304 statisticsInterval
: Configuration
.getConfigurationData()?.logStatisticsInterval
,
306 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logLevel') && {
307 level
: Configuration
.getConfigurationData()?.logLevel
,
309 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logConsole') && {
310 console
: Configuration
.getConfigurationData()?.logConsole
,
312 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logFormat') && {
313 format
: Configuration
.getConfigurationData()?.logFormat
,
315 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logRotate') && {
316 rotate
: Configuration
.getConfigurationData()?.logRotate
,
318 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logMaxFiles') && {
319 maxFiles
: Configuration
.getConfigurationData()?.logMaxFiles
,
321 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logMaxSize') && {
322 maxSize
: Configuration
.getConfigurationData()?.logMaxSize
,
325 const logConfiguration
: LogConfiguration
= {
326 ...defaultLogConfiguration
,
327 ...deprecatedLogConfiguration
,
328 ...(hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.log
) &&
329 Configuration
.getConfigurationData()?.log
),
331 return logConfiguration
;
334 private static buildWorkerSection(): WorkerConfiguration
{
335 Configuration
.warnDeprecatedConfigurationKey(
338 `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`,
340 Configuration
.warnDeprecatedConfigurationKey(
343 `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`,
345 Configuration
.warnDeprecatedConfigurationKey(
348 `Use '${ConfigurationSection.worker}' section to define the worker start delay instead`,
350 Configuration
.warnDeprecatedConfigurationKey(
351 'chargingStationsPerWorker',
353 `Use '${ConfigurationSection.worker}' section to define the number of element(s) per worker instead`,
355 Configuration
.warnDeprecatedConfigurationKey(
358 `Use '${ConfigurationSection.worker}' section to define the worker's element start delay instead`,
360 Configuration
.warnDeprecatedConfigurationKey(
363 `Use '${ConfigurationSection.worker}' section to define the worker pool minimum size instead`,
365 Configuration
.warnDeprecatedConfigurationKey(
368 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`,
370 Configuration
.warnDeprecatedConfigurationKey(
371 'workerPoolMaxSize;',
373 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`,
375 Configuration
.warnDeprecatedConfigurationKey(
376 'workerPoolStrategy;',
378 `Use '${ConfigurationSection.worker}' section to define the worker pool strategy instead`,
380 const defaultWorkerConfiguration
: WorkerConfiguration
= {
381 processType
: WorkerProcessType
.workerSet
,
382 startDelay
: WorkerConstants
.DEFAULT_WORKER_START_DELAY
,
383 elementsPerWorker
: 'auto',
384 elementStartDelay
: WorkerConstants
.DEFAULT_ELEMENT_START_DELAY
,
385 poolMinSize
: WorkerConstants
.DEFAULT_POOL_MIN_SIZE
,
386 poolMaxSize
: WorkerConstants
.DEFAULT_POOL_MAX_SIZE
,
388 hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolStrategy') &&
389 delete Configuration
.getConfigurationData()?.workerPoolStrategy
;
390 const deprecatedWorkerConfiguration
: WorkerConfiguration
= {
391 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerProcess') && {
392 processType
: Configuration
.getConfigurationData()?.workerProcess
,
394 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerStartDelay') && {
395 startDelay
: Configuration
.getConfigurationData()?.workerStartDelay
,
397 ...(hasOwnProp(Configuration
.getConfigurationData(), 'chargingStationsPerWorker') && {
398 elementsPerWorker
: Configuration
.getConfigurationData()?.chargingStationsPerWorker
,
400 ...(hasOwnProp(Configuration
.getConfigurationData(), 'elementStartDelay') && {
401 elementStartDelay
: Configuration
.getConfigurationData()?.elementStartDelay
,
403 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMinSize') && {
404 poolMinSize
: Configuration
.getConfigurationData()?.workerPoolMinSize
,
406 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMaxSize') && {
407 poolMaxSize
: Configuration
.getConfigurationData()?.workerPoolMaxSize
,
410 Configuration
.warnDeprecatedConfigurationKey(
412 ConfigurationSection
.worker
,
413 'Not publicly exposed to end users',
415 const workerConfiguration
: WorkerConfiguration
= {
416 ...defaultWorkerConfiguration
,
417 ...deprecatedWorkerConfiguration
,
418 ...(hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.worker
) &&
419 Configuration
.getConfigurationData()?.worker
),
421 if (!Object.values(WorkerProcessType
).includes(workerConfiguration
.processType
!)) {
422 throw new SyntaxError(
423 `Invalid worker process type '${workerConfiguration.processType}' defined in configuration`,
426 return workerConfiguration
;
429 private static logPrefix
= (): string => {
430 return `${new Date().toLocaleString()} Simulator configuration |`;
433 private static warnDeprecatedConfigurationKey(
435 sectionName
?: string,
440 !isUndefined(Configuration
.getConfigurationData()![sectionName
]) &&
442 (Configuration
.getConfigurationData()![sectionName
] as Record
<string, unknown
>)[key
],
446 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
447 `Deprecated configuration key
'${key}' usage
in section
'${sectionName}'$
{
448 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
452 } else if (!isUndefined(Configuration
.getConfigurationData()![key
])) {
454 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
455 `Deprecated configuration key
'${key}' usage$
{
456 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
463 private static getConfigurationData(): ConfigurationData
| null {
464 if (!Configuration
.configurationData
) {
466 Configuration
.configurationData
= JSON
.parse(
467 readFileSync(Configuration
.configurationFile
, 'utf8'),
468 ) as ConfigurationData
;
470 Configuration
.handleFileException(
471 Configuration
.configurationFile
,
472 FileType
.Configuration
,
473 error
as NodeJS
.ErrnoException
,
474 Configuration
.logPrefix(),
477 if (!Configuration
.configurationFileWatcher
) {
478 Configuration
.configurationFileWatcher
= Configuration
.getConfigurationFileWatcher();
481 return Configuration
.configurationData
;
484 private static getConfigurationFileWatcher(): FSWatcher
| undefined {
486 return watch(Configuration
.configurationFile
, (event
, filename
): void => {
487 if (filename
!.trim()!.length
> 0 && event
=== 'change') {
488 // Nullify to force configuration file reading
489 Configuration
.configurationData
= null;
490 Configuration
.configurationSectionCache
.clear();
491 if (!isUndefined(Configuration
.configurationChangeCallback
)) {
492 Configuration
.configurationChangeCallback().catch((error
) => {
493 throw typeof error
=== 'string' ? new Error(error
) : error
;
499 Configuration
.handleFileException(
500 Configuration
.configurationFile
,
501 FileType
.Configuration
,
502 error
as NodeJS
.ErrnoException
,
503 Configuration
.logPrefix(),
508 private static handleFileException(
511 error
: NodeJS
.ErrnoException
,
514 const prefix
= isNotEmptyString(logPrefix
) ? `${logPrefix} ` : '';
516 switch (error
.code
) {
518 logMsg
= `${fileType} file ${file} not found:`;
521 logMsg
= `${fileType} file ${file} already exists:`;
524 logMsg
= `${fileType} file ${file} access denied:`;
527 logMsg
= `${fileType} file ${file} permission denied:`;
530 logMsg
= `${fileType} file ${file} error:`;
532 console
.error(`${chalk.green(prefix)}${chalk.red(`${logMsg} `)}`, error);
536 private static getDefaultPerformanceStorageUri(storageType: StorageType) {
537 switch (storageType) {
538 case StorageType.JSON_FILE:
539 return Configuration.buildPerformanceUriFilePath(
540 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}
/${Constants.DEFAULT_PERFORMANCE_RECORDS_FILENAME}
`,
542 case StorageType.SQLITE:
543 return Configuration.buildPerformanceUriFilePath(
544 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}
/${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}
.db
`,
547 throw new Error(`Performance storage URI
is mandatory
with storage
type '${storageType}'`);
551 private static buildPerformanceUriFilePath(file: string) {
552 return `file
://${join(resolve(dirname(fileURLToPath(import.meta.url)), '../'), file)}`;