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 export class Configuration
{
25 private static configurationFile
= join(
26 dirname(fileURLToPath(import.meta
.url
)),
31 private static configurationFileWatcher
: FSWatcher
| undefined;
32 private static configuration
: ConfigurationData
| null = null;
33 private static configurationChangeCallback
: () => Promise
<void>;
35 private constructor() {
36 // This is intentional
39 public static setConfigurationChangeCallback(cb
: () => Promise
<void>): void {
40 Configuration
.configurationChangeCallback
= cb
;
43 public static getUIServer(): UIServerConfiguration
{
44 if (hasOwnProp(Configuration
.getConfig(), 'uiWebSocketServer')) {
46 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
47 "Deprecated configuration section 'uiWebSocketServer' usage. Use 'uiServer' instead",
51 let uiServerConfiguration
: UIServerConfiguration
= {
53 type: ApplicationProtocol
.WS
,
55 host
: Constants
.DEFAULT_UI_SERVER_HOST
,
56 port
: Constants
.DEFAULT_UI_SERVER_PORT
,
59 if (hasOwnProp(Configuration
.getConfig(), 'uiServer')) {
60 uiServerConfiguration
= merge
<UIServerConfiguration
>(
61 uiServerConfiguration
,
62 Configuration
.getConfig()!.uiServer
!,
65 if (isCFEnvironment() === true) {
66 delete uiServerConfiguration
.options
?.host
;
67 uiServerConfiguration
.options
!.port
= parseInt(process
.env
.PORT
!);
69 return uiServerConfiguration
;
72 public static getPerformanceStorage(): StorageConfiguration
{
73 Configuration
.warnDeprecatedConfigurationKey('URI', 'performanceStorage', "Use 'uri' instead");
74 let storageConfiguration
: StorageConfiguration
= {
76 type: StorageType
.JSON_FILE
,
77 uri
: this.getDefaultPerformanceStorageUri(StorageType
.JSON_FILE
),
79 if (hasOwnProp(Configuration
.getConfig(), 'performanceStorage')) {
80 storageConfiguration
= {
81 ...storageConfiguration
,
82 ...Configuration
.getConfig()?.performanceStorage
,
83 ...(Configuration
.getConfig()?.performanceStorage
?.type === StorageType
.JSON_FILE
&&
84 Configuration
.getConfig()?.performanceStorage
?.uri
&& {
85 uri
: Configuration
.buildPerformanceUriFilePath(
86 new URL(Configuration
.getConfig()!.performanceStorage
!.uri
!).pathname
,
91 return storageConfiguration
;
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',
111 if (hasOwnProp(Configuration
.getConfig(), 'autoReconnectMaxRetries')) {
112 return Configuration
.getConfig()?.autoReconnectMaxRetries
;
116 public static getStationTemplateUrls(): StationTemplateUrl
[] | undefined {
117 Configuration
.warnDeprecatedConfigurationKey(
118 'stationTemplateURLs',
120 "Use 'stationTemplateUrls' instead",
122 // eslint-disable-next-line @typescript-eslint/dot-notation
123 !isUndefined(Configuration
.getConfig()!['stationTemplateURLs']) &&
124 (Configuration
.getConfig()!.stationTemplateUrls
= Configuration
.getConfig()![
125 // eslint-disable-next-line @typescript-eslint/dot-notation
126 'stationTemplateURLs'
127 ] as StationTemplateUrl
[]);
128 Configuration
.getConfig()!.stationTemplateUrls
.forEach(
129 (stationTemplateUrl
: StationTemplateUrl
) => {
130 // eslint-disable-next-line @typescript-eslint/dot-notation
131 if (!isUndefined(stationTemplateUrl
['numberOfStation'])) {
133 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
134 `Deprecated configuration key
'numberOfStation' usage
for template file
'${stationTemplateUrl.file}' in 'stationTemplateUrls'. Use
'numberOfStations' instead
`,
141 return Configuration
.getConfig()?.stationTemplateUrls
;
144 public static getLog(): LogConfiguration
{
145 Configuration
.warnDeprecatedConfigurationKey(
148 "Use 'log' section to define the logging enablement instead",
150 Configuration
.warnDeprecatedConfigurationKey(
153 "Use 'log' section to define the log file instead",
155 Configuration
.warnDeprecatedConfigurationKey(
158 "Use 'log' section to define the log error file instead",
160 Configuration
.warnDeprecatedConfigurationKey(
163 "Use 'log' section to define the console logging enablement instead",
165 Configuration
.warnDeprecatedConfigurationKey(
166 'logStatisticsInterval',
168 "Use 'log' section to define the log statistics interval instead",
170 Configuration
.warnDeprecatedConfigurationKey(
173 "Use 'log' section to define the log level instead",
175 Configuration
.warnDeprecatedConfigurationKey(
178 "Use 'log' section to define the log format instead",
180 Configuration
.warnDeprecatedConfigurationKey(
183 "Use 'log' section to define the log rotation enablement instead",
185 Configuration
.warnDeprecatedConfigurationKey(
188 "Use 'log' section to define the log maximum files instead",
190 Configuration
.warnDeprecatedConfigurationKey(
193 "Use 'log' section to define the log maximum size instead",
195 const defaultLogConfiguration
: LogConfiguration
= {
197 file
: 'logs/combined.log',
198 errorFile
: 'logs/error.log',
199 statisticsInterval
: Constants
.DEFAULT_LOG_STATISTICS_INTERVAL
,
204 const deprecatedLogConfiguration
: LogConfiguration
= {
205 ...(hasOwnProp(Configuration
.getConfig(), 'logEnabled') && {
206 enabled
: Configuration
.getConfig()?.logEnabled
,
208 ...(hasOwnProp(Configuration
.getConfig(), 'logFile') && {
209 file
: Configuration
.getConfig()?.logFile
,
211 ...(hasOwnProp(Configuration
.getConfig(), 'logErrorFile') && {
212 errorFile
: Configuration
.getConfig()?.logErrorFile
,
214 ...(hasOwnProp(Configuration
.getConfig(), 'logStatisticsInterval') && {
215 statisticsInterval
: Configuration
.getConfig()?.logStatisticsInterval
,
217 ...(hasOwnProp(Configuration
.getConfig(), 'logLevel') && {
218 level
: Configuration
.getConfig()?.logLevel
,
220 ...(hasOwnProp(Configuration
.getConfig(), 'logConsole') && {
221 console
: Configuration
.getConfig()?.logConsole
,
223 ...(hasOwnProp(Configuration
.getConfig(), 'logFormat') && {
224 format
: Configuration
.getConfig()?.logFormat
,
226 ...(hasOwnProp(Configuration
.getConfig(), 'logRotate') && {
227 rotate
: Configuration
.getConfig()?.logRotate
,
229 ...(hasOwnProp(Configuration
.getConfig(), 'logMaxFiles') && {
230 maxFiles
: Configuration
.getConfig()?.logMaxFiles
,
232 ...(hasOwnProp(Configuration
.getConfig(), 'logMaxSize') && {
233 maxSize
: Configuration
.getConfig()?.logMaxSize
,
236 const logConfiguration
: LogConfiguration
= {
237 ...defaultLogConfiguration
,
238 ...deprecatedLogConfiguration
,
239 ...(hasOwnProp(Configuration
.getConfig(), 'log') && Configuration
.getConfig()?.log
),
241 return logConfiguration
;
244 public static getWorker(): WorkerConfiguration
{
245 Configuration
.warnDeprecatedConfigurationKey(
248 "Use 'worker' section to define the type of worker process model instead",
250 Configuration
.warnDeprecatedConfigurationKey(
253 "Use 'worker' section to define the type of worker process model instead",
255 Configuration
.warnDeprecatedConfigurationKey(
258 "Use 'worker' section to define the worker start delay instead",
260 Configuration
.warnDeprecatedConfigurationKey(
261 'chargingStationsPerWorker',
263 "Use 'worker' section to define the number of element(s) per worker instead",
265 Configuration
.warnDeprecatedConfigurationKey(
268 "Use 'worker' section to define the worker's element start delay instead",
270 Configuration
.warnDeprecatedConfigurationKey(
273 "Use 'worker' section to define the worker pool minimum size instead",
275 Configuration
.warnDeprecatedConfigurationKey(
278 "Use 'worker' section to define the worker pool maximum size instead",
280 Configuration
.warnDeprecatedConfigurationKey(
281 'workerPoolMaxSize;',
283 "Use 'worker' section to define the worker pool maximum size instead",
285 Configuration
.warnDeprecatedConfigurationKey(
286 'workerPoolStrategy;',
288 "Use 'worker' section to define the worker pool strategy instead",
290 const defaultWorkerConfiguration
: WorkerConfiguration
= {
291 processType
: WorkerProcessType
.workerSet
,
292 startDelay
: WorkerConstants
.DEFAULT_WORKER_START_DELAY
,
293 elementsPerWorker
: 'auto',
294 elementStartDelay
: WorkerConstants
.DEFAULT_ELEMENT_START_DELAY
,
295 poolMinSize
: WorkerConstants
.DEFAULT_POOL_MIN_SIZE
,
296 poolMaxSize
: WorkerConstants
.DEFAULT_POOL_MAX_SIZE
,
298 hasOwnProp(Configuration
.getConfig(), 'workerPoolStrategy') &&
299 delete Configuration
.getConfig()?.workerPoolStrategy
;
300 const deprecatedWorkerConfiguration
: WorkerConfiguration
= {
301 ...(hasOwnProp(Configuration
.getConfig(), 'workerProcess') && {
302 processType
: Configuration
.getConfig()?.workerProcess
,
304 ...(hasOwnProp(Configuration
.getConfig(), 'workerStartDelay') && {
305 startDelay
: Configuration
.getConfig()?.workerStartDelay
,
307 ...(hasOwnProp(Configuration
.getConfig(), 'chargingStationsPerWorker') && {
308 elementsPerWorker
: Configuration
.getConfig()?.chargingStationsPerWorker
,
310 ...(hasOwnProp(Configuration
.getConfig(), 'elementStartDelay') && {
311 elementStartDelay
: Configuration
.getConfig()?.elementStartDelay
,
313 ...(hasOwnProp(Configuration
.getConfig(), 'workerPoolMinSize') && {
314 poolMinSize
: Configuration
.getConfig()?.workerPoolMinSize
,
316 ...(hasOwnProp(Configuration
.getConfig(), 'workerPoolMaxSize') && {
317 poolMaxSize
: Configuration
.getConfig()?.workerPoolMaxSize
,
320 Configuration
.warnDeprecatedConfigurationKey(
323 'Not publicly exposed to end users',
325 const workerConfiguration
: WorkerConfiguration
= {
326 ...defaultWorkerConfiguration
,
327 ...deprecatedWorkerConfiguration
,
328 ...(hasOwnProp(Configuration
.getConfig(), 'worker') && Configuration
.getConfig()?.worker
),
330 if (!Object.values(WorkerProcessType
).includes(workerConfiguration
.processType
!)) {
331 throw new SyntaxError(
332 `Invalid worker process type '${workerConfiguration.processType}' defined in configuration`,
335 return workerConfiguration
;
338 public static workerPoolInUse(): boolean {
339 return [WorkerProcessType
.dynamicPool
, WorkerProcessType
.staticPool
].includes(
340 Configuration
.getWorker().processType
!,
344 public static workerDynamicPoolInUse(): boolean {
345 return Configuration
.getWorker().processType
=== WorkerProcessType
.dynamicPool
;
348 public static getSupervisionUrls(): string | string[] | undefined {
349 Configuration
.warnDeprecatedConfigurationKey(
352 "Use 'supervisionUrls' instead",
354 // eslint-disable-next-line @typescript-eslint/dot-notation
355 if (!isUndefined(Configuration
.getConfig()!['supervisionURLs'])) {
356 // eslint-disable-next-line @typescript-eslint/dot-notation
357 Configuration
.getConfig()!.supervisionUrls
= Configuration
.getConfig()!['supervisionURLs'] as
362 return Configuration
.getConfig()?.supervisionUrls
;
365 public static getSupervisionUrlDistribution(): SupervisionUrlDistribution
| undefined {
366 Configuration
.warnDeprecatedConfigurationKey(
367 'distributeStationToTenantEqually',
369 "Use 'supervisionUrlDistribution' instead",
371 Configuration
.warnDeprecatedConfigurationKey(
372 'distributeStationsToTenantsEqually',
374 "Use 'supervisionUrlDistribution' instead",
376 return hasOwnProp(Configuration
.getConfig(), 'supervisionUrlDistribution')
377 ? Configuration
.getConfig()?.supervisionUrlDistribution
378 : SupervisionUrlDistribution
.ROUND_ROBIN
;
381 private static logPrefix
= (): string => {
382 return `${new Date().toLocaleString()} Simulator configuration |`;
385 private static warnDeprecatedConfigurationKey(
387 sectionName
?: string,
392 !isUndefined(Configuration
.getConfig()![sectionName
]) &&
393 !isUndefined((Configuration
.getConfig()![sectionName
] as Record
<string, unknown
>)[key
])
396 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
397 `Deprecated configuration key
'${key}' usage
in section
'${sectionName}'$
{
398 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
402 } else if (!isUndefined(Configuration
.getConfig()![key
])) {
404 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
405 `Deprecated configuration key
'${key}' usage$
{
406 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
413 // Read the config file
414 private static getConfig(): ConfigurationData
| null {
415 if (!Configuration
.configuration
) {
417 Configuration
.configuration
= JSON
.parse(
418 readFileSync(Configuration
.configurationFile
, 'utf8'),
419 ) as ConfigurationData
;
421 Configuration
.handleFileException(
422 Configuration
.configurationFile
,
423 FileType
.Configuration
,
424 error
as NodeJS
.ErrnoException
,
425 Configuration
.logPrefix(),
428 if (!Configuration
.configurationFileWatcher
) {
429 Configuration
.configurationFileWatcher
= Configuration
.getConfigurationFileWatcher();
432 return Configuration
.configuration
;
435 private static getConfigurationFileWatcher(): FSWatcher
| undefined {
437 return watch(Configuration
.configurationFile
, (event
, filename
): void => {
438 if (filename
!.trim()!.length
> 0 && event
=== 'change') {
439 // Nullify to force configuration file reading
440 Configuration
.configuration
= null;
441 if (!isUndefined(Configuration
.configurationChangeCallback
)) {
442 Configuration
.configurationChangeCallback().catch((error
) => {
443 throw typeof error
=== 'string' ? new Error(error
) : error
;
449 Configuration
.handleFileException(
450 Configuration
.configurationFile
,
451 FileType
.Configuration
,
452 error
as NodeJS
.ErrnoException
,
453 Configuration
.logPrefix(),
458 private static handleFileException(
461 error
: NodeJS
.ErrnoException
,
464 const prefix
= isNotEmptyString(logPrefix
) ? `${logPrefix} ` : '';
466 switch (error
.code
) {
468 logMsg
= `${fileType} file ${file} not found:`;
471 logMsg
= `${fileType} file ${file} already exists:`;
474 logMsg
= `${fileType} file ${file} access denied:`;
477 logMsg
= `${fileType} file ${file} permission denied:`;
480 logMsg
= `${fileType} file ${file} error:`;
482 console
.error(`${chalk.green(prefix)}${chalk.red(`${logMsg} `)}`, error);
486 private static getDefaultPerformanceStorageUri(storageType: StorageType) {
487 switch (storageType) {
488 case StorageType.JSON_FILE:
489 return Configuration.buildPerformanceUriFilePath(
490 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}
/${Constants.DEFAULT_PERFORMANCE_RECORDS_FILENAME}
`,
492 case StorageType.SQLITE:
493 return Configuration.buildPerformanceUriFilePath(
494 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}
/${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}
.db
`,
497 throw new Error(`Performance storage URI
is mandatory
with storage
type '${storageType}'`);
501 private static buildPerformanceUriFilePath(file: string) {
502 return `file
://${join(resolve(dirname(fileURLToPath(import.meta.url)), '../'), file)}`;