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 return workerConfiguration
;
333 public static workerPoolInUse(): boolean {
334 return [WorkerProcessType
.dynamicPool
, WorkerProcessType
.staticPool
].includes(
335 Configuration
.getWorker().processType
!,
339 public static workerDynamicPoolInUse(): boolean {
340 return Configuration
.getWorker().processType
=== WorkerProcessType
.dynamicPool
;
343 public static getSupervisionUrls(): string | string[] | undefined {
344 Configuration
.warnDeprecatedConfigurationKey(
347 "Use 'supervisionUrls' instead",
349 // eslint-disable-next-line @typescript-eslint/dot-notation
350 if (!isUndefined(Configuration
.getConfig()!['supervisionURLs'])) {
351 // eslint-disable-next-line @typescript-eslint/dot-notation
352 Configuration
.getConfig()!.supervisionUrls
= Configuration
.getConfig()!['supervisionURLs'] as
357 return Configuration
.getConfig()?.supervisionUrls
;
360 public static getSupervisionUrlDistribution(): SupervisionUrlDistribution
| undefined {
361 Configuration
.warnDeprecatedConfigurationKey(
362 'distributeStationToTenantEqually',
364 "Use 'supervisionUrlDistribution' instead",
366 Configuration
.warnDeprecatedConfigurationKey(
367 'distributeStationsToTenantsEqually',
369 "Use 'supervisionUrlDistribution' instead",
371 return hasOwnProp(Configuration
.getConfig(), 'supervisionUrlDistribution')
372 ? Configuration
.getConfig()?.supervisionUrlDistribution
373 : SupervisionUrlDistribution
.ROUND_ROBIN
;
376 private static logPrefix
= (): string => {
377 return `${new Date().toLocaleString()} Simulator configuration |`;
380 private static warnDeprecatedConfigurationKey(
382 sectionName
?: string,
387 !isUndefined(Configuration
.getConfig()![sectionName
]) &&
388 !isUndefined((Configuration
.getConfig()![sectionName
] as Record
<string, unknown
>)[key
])
391 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
392 `Deprecated configuration key
'${key}' usage
in section
'${sectionName}'$
{
393 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
397 } else if (!isUndefined(Configuration
.getConfig()![key
])) {
399 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
400 `Deprecated configuration key
'${key}' usage$
{
401 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
408 // Read the config file
409 private static getConfig(): ConfigurationData
| null {
410 if (!Configuration
.configuration
) {
412 Configuration
.configuration
= JSON
.parse(
413 readFileSync(Configuration
.configurationFile
, 'utf8'),
414 ) as ConfigurationData
;
416 Configuration
.handleFileException(
417 Configuration
.configurationFile
,
418 FileType
.Configuration
,
419 error
as NodeJS
.ErrnoException
,
420 Configuration
.logPrefix(),
423 if (!Configuration
.configurationFileWatcher
) {
424 Configuration
.configurationFileWatcher
= Configuration
.getConfigurationFileWatcher();
427 return Configuration
.configuration
;
430 private static getConfigurationFileWatcher(): FSWatcher
| undefined {
432 return watch(Configuration
.configurationFile
, (event
, filename
): void => {
433 if (filename
!.trim()!.length
> 0 && event
=== 'change') {
434 // Nullify to force configuration file reading
435 Configuration
.configuration
= null;
436 if (!isUndefined(Configuration
.configurationChangeCallback
)) {
437 Configuration
.configurationChangeCallback().catch((error
) => {
438 throw typeof error
=== 'string' ? new Error(error
) : error
;
444 Configuration
.handleFileException(
445 Configuration
.configurationFile
,
446 FileType
.Configuration
,
447 error
as NodeJS
.ErrnoException
,
448 Configuration
.logPrefix(),
453 private static handleFileException(
456 error
: NodeJS
.ErrnoException
,
459 const prefix
= isNotEmptyString(logPrefix
) ? `${logPrefix} ` : '';
461 switch (error
.code
) {
463 logMsg
= `${fileType} file ${file} not found:`;
466 logMsg
= `${fileType} file ${file} already exists:`;
469 logMsg
= `${fileType} file ${file} access denied:`;
472 logMsg
= `${fileType} file ${file} permission denied:`;
475 logMsg
= `${fileType} file ${file} error:`;
477 console
.error(`${chalk.green(prefix)}${chalk.red(`${logMsg} `)}`, error);
481 private static getDefaultPerformanceStorageUri(storageType: StorageType) {
482 switch (storageType) {
483 case StorageType.JSON_FILE:
484 return Configuration.buildPerformanceUriFilePath(
485 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}
/${Constants.DEFAULT_PERFORMANCE_RECORDS_FILENAME}
`,
487 case StorageType.SQLITE:
488 return Configuration.buildPerformanceUriFilePath(
489 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}
/${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}
.db
`,
492 throw new Error(`Performance storage URI
is mandatory
with storage
type '${storageType}'`);
496 private static buildPerformanceUriFilePath(file: string) {
497 return `file
://${join(resolve(dirname(fileURLToPath(import.meta.url)), '../'), file)}`;