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
,
14 type LogConfiguration
,
15 type StationTemplateUrl
,
16 type StorageConfiguration
,
18 SupervisionUrlDistribution
,
19 type UIServerConfiguration
,
20 type WorkerConfiguration
,
22 import { WorkerConstants
, WorkerProcessType
} from
'../worker';
24 enum ConfigurationSection
{
26 performanceStorage
= 'storage',
28 uiServer
= 'uiServer',
31 export class Configuration
{
32 private static configurationFile
= join(
33 dirname(fileURLToPath(import.meta
.url
)),
38 private static configurationFileWatcher
: FSWatcher
| undefined;
39 private static configuration
: ConfigurationData
| null = null;
40 private static configurationSectionCache
= new Map
<
42 LogConfiguration
| StorageConfiguration
| WorkerConfiguration
| UIServerConfiguration
45 private static configurationChangeCallback
: () => Promise
<void>;
47 private constructor() {
48 // This is intentional
51 public static setConfigurationChangeCallback(cb
: () => Promise
<void>): void {
52 Configuration
.configurationChangeCallback
= cb
;
55 public static getUIServer(): UIServerConfiguration
{
56 if (hasOwnProp(Configuration
.getConfig(), 'uiWebSocketServer')) {
58 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
59 `Deprecated configuration section
'uiWebSocketServer' usage
. Use
'${ConfigurationSection.uiServer}' instead
`,
63 let uiServerConfiguration
: UIServerConfiguration
= {
65 type: ApplicationProtocol
.WS
,
67 host
: Constants
.DEFAULT_UI_SERVER_HOST
,
68 port
: Constants
.DEFAULT_UI_SERVER_PORT
,
71 if (hasOwnProp(Configuration
.getConfig(), ConfigurationSection
.uiServer
)) {
72 uiServerConfiguration
= merge
<UIServerConfiguration
>(
73 uiServerConfiguration
,
74 Configuration
.getConfig()!.uiServer
!,
77 if (isCFEnvironment() === true) {
78 delete uiServerConfiguration
.options
?.host
;
79 uiServerConfiguration
.options
!.port
= parseInt(process
.env
.PORT
!);
81 return Configuration
.getConfigurationSection
<UIServerConfiguration
>(
82 ConfigurationSection
.uiServer
,
83 uiServerConfiguration
,
87 public static getPerformanceStorage(): StorageConfiguration
{
88 Configuration
.warnDeprecatedConfigurationKey(
90 ConfigurationSection
.performanceStorage
,
93 let storageConfiguration
: StorageConfiguration
= {
95 type: StorageType
.JSON_FILE
,
96 uri
: this.getDefaultPerformanceStorageUri(StorageType
.JSON_FILE
),
98 if (hasOwnProp(Configuration
.getConfig(), ConfigurationSection
.performanceStorage
)) {
99 storageConfiguration
= {
100 ...storageConfiguration
,
101 ...Configuration
.getConfig()?.performanceStorage
,
102 ...(Configuration
.getConfig()?.performanceStorage
?.type === StorageType
.JSON_FILE
&&
103 Configuration
.getConfig()?.performanceStorage
?.uri
&& {
104 uri
: Configuration
.buildPerformanceUriFilePath(
105 new URL(Configuration
.getConfig()!.performanceStorage
!.uri
!).pathname
,
110 return Configuration
.getConfigurationSection
<StorageConfiguration
>(
111 ConfigurationSection
.performanceStorage
,
112 storageConfiguration
,
116 public static getAutoReconnectMaxRetries(): number | undefined {
117 Configuration
.warnDeprecatedConfigurationKey(
118 'autoReconnectTimeout',
120 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead",
122 Configuration
.warnDeprecatedConfigurationKey(
125 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead",
127 Configuration
.warnDeprecatedConfigurationKey(
128 'autoReconnectMaxRetries',
130 'Use it in charging station template instead',
133 if (hasOwnProp(Configuration
.getConfig(), 'autoReconnectMaxRetries')) {
134 return Configuration
.getConfig()?.autoReconnectMaxRetries
;
138 public static getStationTemplateUrls(): StationTemplateUrl
[] | undefined {
139 Configuration
.warnDeprecatedConfigurationKey(
140 'stationTemplateURLs',
142 "Use 'stationTemplateUrls' instead",
144 // eslint-disable-next-line @typescript-eslint/dot-notation
145 !isUndefined(Configuration
.getConfig()!['stationTemplateURLs']) &&
146 (Configuration
.getConfig()!.stationTemplateUrls
= Configuration
.getConfig()![
147 // eslint-disable-next-line @typescript-eslint/dot-notation
148 'stationTemplateURLs'
149 ] as StationTemplateUrl
[]);
150 Configuration
.getConfig()!.stationTemplateUrls
.forEach(
151 (stationTemplateUrl
: StationTemplateUrl
) => {
152 // eslint-disable-next-line @typescript-eslint/dot-notation
153 if (!isUndefined(stationTemplateUrl
['numberOfStation'])) {
155 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
156 `Deprecated configuration key
'numberOfStation' usage
for template file
'${stationTemplateUrl.file}' in 'stationTemplateUrls'. Use
'numberOfStations' instead
`,
163 return Configuration
.getConfig()?.stationTemplateUrls
;
166 public static getLog(): LogConfiguration
{
167 Configuration
.warnDeprecatedConfigurationKey(
170 `Use '${ConfigurationSection.log}' section to define the logging enablement instead`,
172 Configuration
.warnDeprecatedConfigurationKey(
175 `Use '${ConfigurationSection.log}' section to define the log file instead`,
177 Configuration
.warnDeprecatedConfigurationKey(
180 `Use '${ConfigurationSection.log}' section to define the log error file instead`,
182 Configuration
.warnDeprecatedConfigurationKey(
185 `Use '${ConfigurationSection.log}' section to define the console logging enablement instead`,
187 Configuration
.warnDeprecatedConfigurationKey(
188 'logStatisticsInterval',
190 `Use '${ConfigurationSection.log}' section to define the log statistics interval instead`,
192 Configuration
.warnDeprecatedConfigurationKey(
195 `Use '${ConfigurationSection.log}' section to define the log level instead`,
197 Configuration
.warnDeprecatedConfigurationKey(
200 `Use '${ConfigurationSection.log}' section to define the log format instead`,
202 Configuration
.warnDeprecatedConfigurationKey(
205 `Use '${ConfigurationSection.log}' section to define the log rotation enablement instead`,
207 Configuration
.warnDeprecatedConfigurationKey(
210 `Use '${ConfigurationSection.log}' section to define the log maximum files instead`,
212 Configuration
.warnDeprecatedConfigurationKey(
215 `Use '${ConfigurationSection.log}' section to define the log maximum size instead`,
217 const defaultLogConfiguration
: LogConfiguration
= {
219 file
: 'logs/combined.log',
220 errorFile
: 'logs/error.log',
221 statisticsInterval
: Constants
.DEFAULT_LOG_STATISTICS_INTERVAL
,
226 const deprecatedLogConfiguration
: LogConfiguration
= {
227 ...(hasOwnProp(Configuration
.getConfig(), 'logEnabled') && {
228 enabled
: Configuration
.getConfig()?.logEnabled
,
230 ...(hasOwnProp(Configuration
.getConfig(), 'logFile') && {
231 file
: Configuration
.getConfig()?.logFile
,
233 ...(hasOwnProp(Configuration
.getConfig(), 'logErrorFile') && {
234 errorFile
: Configuration
.getConfig()?.logErrorFile
,
236 ...(hasOwnProp(Configuration
.getConfig(), 'logStatisticsInterval') && {
237 statisticsInterval
: Configuration
.getConfig()?.logStatisticsInterval
,
239 ...(hasOwnProp(Configuration
.getConfig(), 'logLevel') && {
240 level
: Configuration
.getConfig()?.logLevel
,
242 ...(hasOwnProp(Configuration
.getConfig(), 'logConsole') && {
243 console
: Configuration
.getConfig()?.logConsole
,
245 ...(hasOwnProp(Configuration
.getConfig(), 'logFormat') && {
246 format
: Configuration
.getConfig()?.logFormat
,
248 ...(hasOwnProp(Configuration
.getConfig(), 'logRotate') && {
249 rotate
: Configuration
.getConfig()?.logRotate
,
251 ...(hasOwnProp(Configuration
.getConfig(), 'logMaxFiles') && {
252 maxFiles
: Configuration
.getConfig()?.logMaxFiles
,
254 ...(hasOwnProp(Configuration
.getConfig(), 'logMaxSize') && {
255 maxSize
: Configuration
.getConfig()?.logMaxSize
,
258 const logConfiguration
: LogConfiguration
= {
259 ...defaultLogConfiguration
,
260 ...deprecatedLogConfiguration
,
261 ...(hasOwnProp(Configuration
.getConfig(), ConfigurationSection
.log
) &&
262 Configuration
.getConfig()?.log
),
264 return Configuration
.getConfigurationSection
<LogConfiguration
>(
265 ConfigurationSection
.log
,
270 public static getWorker(): WorkerConfiguration
{
271 Configuration
.warnDeprecatedConfigurationKey(
274 `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`,
276 Configuration
.warnDeprecatedConfigurationKey(
279 `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`,
281 Configuration
.warnDeprecatedConfigurationKey(
284 `Use '${ConfigurationSection.worker}' section to define the worker start delay instead`,
286 Configuration
.warnDeprecatedConfigurationKey(
287 'chargingStationsPerWorker',
289 `Use '${ConfigurationSection.worker}' section to define the number of element(s) per worker instead`,
291 Configuration
.warnDeprecatedConfigurationKey(
294 `Use '${ConfigurationSection.worker}' section to define the worker's element start delay instead`,
296 Configuration
.warnDeprecatedConfigurationKey(
299 `Use '${ConfigurationSection.worker}' section to define the worker pool minimum size instead`,
301 Configuration
.warnDeprecatedConfigurationKey(
304 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`,
306 Configuration
.warnDeprecatedConfigurationKey(
307 'workerPoolMaxSize;',
309 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`,
311 Configuration
.warnDeprecatedConfigurationKey(
312 'workerPoolStrategy;',
314 `Use '${ConfigurationSection.worker}' section to define the worker pool strategy instead`,
316 const defaultWorkerConfiguration
: WorkerConfiguration
= {
317 processType
: WorkerProcessType
.workerSet
,
318 startDelay
: WorkerConstants
.DEFAULT_WORKER_START_DELAY
,
319 elementsPerWorker
: 'auto',
320 elementStartDelay
: WorkerConstants
.DEFAULT_ELEMENT_START_DELAY
,
321 poolMinSize
: WorkerConstants
.DEFAULT_POOL_MIN_SIZE
,
322 poolMaxSize
: WorkerConstants
.DEFAULT_POOL_MAX_SIZE
,
324 hasOwnProp(Configuration
.getConfig(), 'workerPoolStrategy') &&
325 delete Configuration
.getConfig()?.workerPoolStrategy
;
326 const deprecatedWorkerConfiguration
: WorkerConfiguration
= {
327 ...(hasOwnProp(Configuration
.getConfig(), 'workerProcess') && {
328 processType
: Configuration
.getConfig()?.workerProcess
,
330 ...(hasOwnProp(Configuration
.getConfig(), 'workerStartDelay') && {
331 startDelay
: Configuration
.getConfig()?.workerStartDelay
,
333 ...(hasOwnProp(Configuration
.getConfig(), 'chargingStationsPerWorker') && {
334 elementsPerWorker
: Configuration
.getConfig()?.chargingStationsPerWorker
,
336 ...(hasOwnProp(Configuration
.getConfig(), 'elementStartDelay') && {
337 elementStartDelay
: Configuration
.getConfig()?.elementStartDelay
,
339 ...(hasOwnProp(Configuration
.getConfig(), 'workerPoolMinSize') && {
340 poolMinSize
: Configuration
.getConfig()?.workerPoolMinSize
,
342 ...(hasOwnProp(Configuration
.getConfig(), 'workerPoolMaxSize') && {
343 poolMaxSize
: Configuration
.getConfig()?.workerPoolMaxSize
,
346 Configuration
.warnDeprecatedConfigurationKey(
348 ConfigurationSection
.worker
,
349 'Not publicly exposed to end users',
351 const workerConfiguration
: WorkerConfiguration
= {
352 ...defaultWorkerConfiguration
,
353 ...deprecatedWorkerConfiguration
,
354 ...(hasOwnProp(Configuration
.getConfig(), ConfigurationSection
.worker
) &&
355 Configuration
.getConfig()?.worker
),
357 if (!Object.values(WorkerProcessType
).includes(workerConfiguration
.processType
!)) {
358 throw new SyntaxError(
359 `Invalid worker process type '${workerConfiguration.processType}' defined in configuration`,
362 return Configuration
.getConfigurationSection
<WorkerConfiguration
>(
363 ConfigurationSection
.worker
,
368 public static workerPoolInUse(): boolean {
369 return [WorkerProcessType
.dynamicPool
, WorkerProcessType
.staticPool
].includes(
370 Configuration
.getWorker().processType
!,
374 public static workerDynamicPoolInUse(): boolean {
375 return Configuration
.getWorker().processType
=== WorkerProcessType
.dynamicPool
;
378 public static getSupervisionUrls(): string | string[] | undefined {
379 Configuration
.warnDeprecatedConfigurationKey(
382 "Use 'supervisionUrls' instead",
384 // eslint-disable-next-line @typescript-eslint/dot-notation
385 if (!isUndefined(Configuration
.getConfig()!['supervisionURLs'])) {
386 // eslint-disable-next-line @typescript-eslint/dot-notation
387 Configuration
.getConfig()!.supervisionUrls
= Configuration
.getConfig()!['supervisionURLs'] as
392 return Configuration
.getConfig()?.supervisionUrls
;
395 public static getSupervisionUrlDistribution(): SupervisionUrlDistribution
| undefined {
396 Configuration
.warnDeprecatedConfigurationKey(
397 'distributeStationToTenantEqually',
399 "Use 'supervisionUrlDistribution' instead",
401 Configuration
.warnDeprecatedConfigurationKey(
402 'distributeStationsToTenantsEqually',
404 "Use 'supervisionUrlDistribution' instead",
406 return hasOwnProp(Configuration
.getConfig(), 'supervisionUrlDistribution')
407 ? Configuration
.getConfig()?.supervisionUrlDistribution
408 : SupervisionUrlDistribution
.ROUND_ROBIN
;
411 private static logPrefix
= (): string => {
412 return `${new Date().toLocaleString()} Simulator configuration |`;
415 private static warnDeprecatedConfigurationKey(
417 sectionName
?: string,
422 !isUndefined(Configuration
.getConfig()![sectionName
]) &&
423 !isUndefined((Configuration
.getConfig()![sectionName
] as Record
<string, unknown
>)[key
])
426 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
427 `Deprecated configuration key
'${key}' usage
in section
'${sectionName}'$
{
428 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
432 } else if (!isUndefined(Configuration
.getConfig()![key
])) {
434 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
435 `Deprecated configuration key
'${key}' usage$
{
436 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
443 private static getConfigurationSection
<T
>(
444 sectionName
: ConfigurationSection
,
445 sectionConfiguration
?: T
,
447 if (!Configuration
.configurationSectionCache
.has(sectionName
) && sectionConfiguration
) {
448 Configuration
.configurationSectionCache
.set(sectionName
, sectionConfiguration
);
450 return Configuration
.configurationSectionCache
.get(sectionName
) as T
;
453 private static getConfig(): ConfigurationData
| null {
454 if (!Configuration
.configuration
) {
456 Configuration
.configuration
= JSON
.parse(
457 readFileSync(Configuration
.configurationFile
, 'utf8'),
458 ) as ConfigurationData
;
460 Configuration
.handleFileException(
461 Configuration
.configurationFile
,
462 FileType
.Configuration
,
463 error
as NodeJS
.ErrnoException
,
464 Configuration
.logPrefix(),
467 if (!Configuration
.configurationFileWatcher
) {
468 Configuration
.configurationFileWatcher
= Configuration
.getConfigurationFileWatcher();
471 return Configuration
.configuration
;
474 private static getConfigurationFileWatcher(): FSWatcher
| undefined {
476 return watch(Configuration
.configurationFile
, (event
, filename
): void => {
477 if (filename
!.trim()!.length
> 0 && event
=== 'change') {
478 // Nullify to force configuration file reading
479 Configuration
.configuration
= null;
480 Configuration
.configurationSectionCache
.clear();
481 if (!isUndefined(Configuration
.configurationChangeCallback
)) {
482 Configuration
.configurationChangeCallback().catch((error
) => {
483 throw typeof error
=== 'string' ? new Error(error
) : error
;
489 Configuration
.handleFileException(
490 Configuration
.configurationFile
,
491 FileType
.Configuration
,
492 error
as NodeJS
.ErrnoException
,
493 Configuration
.logPrefix(),
498 private static handleFileException(
501 error
: NodeJS
.ErrnoException
,
504 const prefix
= isNotEmptyString(logPrefix
) ? `${logPrefix} ` : '';
506 switch (error
.code
) {
508 logMsg
= `${fileType} file ${file} not found:`;
511 logMsg
= `${fileType} file ${file} already exists:`;
514 logMsg
= `${fileType} file ${file} access denied:`;
517 logMsg
= `${fileType} file ${file} permission denied:`;
520 logMsg
= `${fileType} file ${file} error:`;
522 console
.error(`${chalk.green(prefix)}${chalk.red(`${logMsg} `)}`, error);
526 private static getDefaultPerformanceStorageUri(storageType: StorageType) {
527 switch (storageType) {
528 case StorageType.JSON_FILE:
529 return Configuration.buildPerformanceUriFilePath(
530 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}
/${Constants.DEFAULT_PERFORMANCE_RECORDS_FILENAME}
`,
532 case StorageType.SQLITE:
533 return Configuration.buildPerformanceUriFilePath(
534 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}
/${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}
.db
`,
537 throw new Error(`Performance storage URI
is mandatory
with storage
type '${storageType}'`);
541 private static buildPerformanceUriFilePath(file: string) {
542 return `file
://${join(resolve(dirname(fileURLToPath(import.meta.url)), '../'), file)}`;