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
= 'performanceStorage',
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 configurationData
: 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
.getConfigurationData(), '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
.getConfigurationData(), ConfigurationSection
.uiServer
)) {
72 uiServerConfiguration
= merge
<UIServerConfiguration
>(
73 uiServerConfiguration
,
74 Configuration
.getConfigurationData()!.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
.getConfigurationData(), ConfigurationSection
.performanceStorage
)) {
99 storageConfiguration
= {
100 ...storageConfiguration
,
101 ...Configuration
.getConfigurationData()?.performanceStorage
,
102 ...(Configuration
.getConfigurationData()?.performanceStorage
?.type ===
103 StorageType
.JSON_FILE
&&
104 Configuration
.getConfigurationData()?.performanceStorage
?.uri
&& {
105 uri
: Configuration
.buildPerformanceUriFilePath(
106 new URL(Configuration
.getConfigurationData()!.performanceStorage
!.uri
!).pathname
,
111 return Configuration
.getConfigurationSection
<StorageConfiguration
>(
112 ConfigurationSection
.performanceStorage
,
113 storageConfiguration
,
117 public static getAutoReconnectMaxRetries(): number | undefined {
118 Configuration
.warnDeprecatedConfigurationKey(
119 'autoReconnectTimeout',
121 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead",
123 Configuration
.warnDeprecatedConfigurationKey(
126 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead",
128 Configuration
.warnDeprecatedConfigurationKey(
129 'autoReconnectMaxRetries',
131 'Use it in charging station template instead',
133 if (hasOwnProp(Configuration
.getConfigurationData(), 'autoReconnectMaxRetries')) {
134 return Configuration
.getConfigurationData()?.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
.getConfigurationData()!['stationTemplateURLs']) &&
146 (Configuration
.getConfigurationData()!.stationTemplateUrls
=
147 Configuration
.getConfigurationData()![
148 // eslint-disable-next-line @typescript-eslint/dot-notation
149 'stationTemplateURLs'
150 ] as StationTemplateUrl
[]);
151 Configuration
.getConfigurationData()!.stationTemplateUrls
.forEach(
152 (stationTemplateUrl
: StationTemplateUrl
) => {
153 // eslint-disable-next-line @typescript-eslint/dot-notation
154 if (!isUndefined(stationTemplateUrl
['numberOfStation'])) {
156 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
157 `Deprecated configuration key
'numberOfStation' usage
for template file
'${stationTemplateUrl.file}' in 'stationTemplateUrls'. Use
'numberOfStations' instead
`,
163 return Configuration
.getConfigurationData()?.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
.getConfigurationData(), 'logEnabled') && {
228 enabled
: Configuration
.getConfigurationData()?.logEnabled
,
230 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logFile') && {
231 file
: Configuration
.getConfigurationData()?.logFile
,
233 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logErrorFile') && {
234 errorFile
: Configuration
.getConfigurationData()?.logErrorFile
,
236 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logStatisticsInterval') && {
237 statisticsInterval
: Configuration
.getConfigurationData()?.logStatisticsInterval
,
239 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logLevel') && {
240 level
: Configuration
.getConfigurationData()?.logLevel
,
242 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logConsole') && {
243 console
: Configuration
.getConfigurationData()?.logConsole
,
245 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logFormat') && {
246 format
: Configuration
.getConfigurationData()?.logFormat
,
248 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logRotate') && {
249 rotate
: Configuration
.getConfigurationData()?.logRotate
,
251 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logMaxFiles') && {
252 maxFiles
: Configuration
.getConfigurationData()?.logMaxFiles
,
254 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logMaxSize') && {
255 maxSize
: Configuration
.getConfigurationData()?.logMaxSize
,
258 const logConfiguration
: LogConfiguration
= {
259 ...defaultLogConfiguration
,
260 ...deprecatedLogConfiguration
,
261 ...(hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.log
) &&
262 Configuration
.getConfigurationData()?.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
.getConfigurationData(), 'workerPoolStrategy') &&
325 delete Configuration
.getConfigurationData()?.workerPoolStrategy
;
326 const deprecatedWorkerConfiguration
: WorkerConfiguration
= {
327 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerProcess') && {
328 processType
: Configuration
.getConfigurationData()?.workerProcess
,
330 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerStartDelay') && {
331 startDelay
: Configuration
.getConfigurationData()?.workerStartDelay
,
333 ...(hasOwnProp(Configuration
.getConfigurationData(), 'chargingStationsPerWorker') && {
334 elementsPerWorker
: Configuration
.getConfigurationData()?.chargingStationsPerWorker
,
336 ...(hasOwnProp(Configuration
.getConfigurationData(), 'elementStartDelay') && {
337 elementStartDelay
: Configuration
.getConfigurationData()?.elementStartDelay
,
339 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMinSize') && {
340 poolMinSize
: Configuration
.getConfigurationData()?.workerPoolMinSize
,
342 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMaxSize') && {
343 poolMaxSize
: Configuration
.getConfigurationData()?.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
.getConfigurationData(), ConfigurationSection
.worker
) &&
355 Configuration
.getConfigurationData()?.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
.getConfigurationData()!['supervisionURLs'])) {
386 Configuration
.getConfigurationData()!.supervisionUrls
= Configuration
.getConfigurationData()![
387 // eslint-disable-next-line @typescript-eslint/dot-notation
389 ] as string | string[];
391 return Configuration
.getConfigurationData()?.supervisionUrls
;
394 public static getSupervisionUrlDistribution(): SupervisionUrlDistribution
| undefined {
395 Configuration
.warnDeprecatedConfigurationKey(
396 'distributeStationToTenantEqually',
398 "Use 'supervisionUrlDistribution' instead",
400 Configuration
.warnDeprecatedConfigurationKey(
401 'distributeStationsToTenantsEqually',
403 "Use 'supervisionUrlDistribution' instead",
405 return hasOwnProp(Configuration
.getConfigurationData(), 'supervisionUrlDistribution')
406 ? Configuration
.getConfigurationData()?.supervisionUrlDistribution
407 : SupervisionUrlDistribution
.ROUND_ROBIN
;
410 private static logPrefix
= (): string => {
411 return `${new Date().toLocaleString()} Simulator configuration |`;
414 private static warnDeprecatedConfigurationKey(
416 sectionName
?: string,
421 !isUndefined(Configuration
.getConfigurationData()![sectionName
]) &&
423 (Configuration
.getConfigurationData()![sectionName
] as Record
<string, unknown
>)[key
],
427 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
428 `Deprecated configuration key
'${key}' usage
in section
'${sectionName}'$
{
429 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
433 } else if (!isUndefined(Configuration
.getConfigurationData()![key
])) {
435 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
436 `Deprecated configuration key
'${key}' usage$
{
437 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
444 private static getConfigurationSection
<T
>(
445 sectionName
: ConfigurationSection
,
446 sectionConfiguration
?: T
,
448 if (!Configuration
.configurationSectionCache
.has(sectionName
) && sectionConfiguration
) {
449 Configuration
.configurationSectionCache
.set(sectionName
, sectionConfiguration
);
451 return Configuration
.configurationSectionCache
.get(sectionName
) as T
;
454 private static getConfigurationData(): ConfigurationData
| null {
455 if (!Configuration
.configurationData
) {
457 Configuration
.configurationData
= JSON
.parse(
458 readFileSync(Configuration
.configurationFile
, 'utf8'),
459 ) as ConfigurationData
;
461 Configuration
.handleFileException(
462 Configuration
.configurationFile
,
463 FileType
.Configuration
,
464 error
as NodeJS
.ErrnoException
,
465 Configuration
.logPrefix(),
468 if (!Configuration
.configurationFileWatcher
) {
469 Configuration
.configurationFileWatcher
= Configuration
.getConfigurationFileWatcher();
472 return Configuration
.configurationData
;
475 private static getConfigurationFileWatcher(): FSWatcher
| undefined {
477 return watch(Configuration
.configurationFile
, (event
, filename
): void => {
478 if (filename
!.trim()!.length
> 0 && event
=== 'change') {
479 // Nullify to force configuration file reading
480 Configuration
.configurationData
= null;
481 Configuration
.configurationSectionCache
.clear();
482 if (!isUndefined(Configuration
.configurationChangeCallback
)) {
483 Configuration
.configurationChangeCallback().catch((error
) => {
484 throw typeof error
=== 'string' ? new Error(error
) : error
;
490 Configuration
.handleFileException(
491 Configuration
.configurationFile
,
492 FileType
.Configuration
,
493 error
as NodeJS
.ErrnoException
,
494 Configuration
.logPrefix(),
499 private static handleFileException(
502 error
: NodeJS
.ErrnoException
,
505 const prefix
= isNotEmptyString(logPrefix
) ? `${logPrefix} ` : '';
507 switch (error
.code
) {
509 logMsg
= `${fileType} file ${file} not found:`;
512 logMsg
= `${fileType} file ${file} already exists:`;
515 logMsg
= `${fileType} file ${file} access denied:`;
518 logMsg
= `${fileType} file ${file} permission denied:`;
521 logMsg
= `${fileType} file ${file} error:`;
523 console
.error(`${chalk.green(prefix)}${chalk.red(`${logMsg} `)}`, error);
527 private static getDefaultPerformanceStorageUri(storageType: StorageType) {
528 switch (storageType) {
529 case StorageType.JSON_FILE:
530 return Configuration.buildPerformanceUriFilePath(
531 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}
/${Constants.DEFAULT_PERFORMANCE_RECORDS_FILENAME}
`,
533 case StorageType.SQLITE:
534 return Configuration.buildPerformanceUriFilePath(
535 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}
/${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}
.db
`,
538 throw new Error(`Performance storage URI
is mandatory
with storage
type '${storageType}'`);
542 private static buildPerformanceUriFilePath(file: string) {
543 return `file
://${join(resolve(dirname(fileURLToPath(import.meta.url)), '../'), file)}`;