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 !isUndefined(Configuration
.getConfig()['stationTemplateURLs']) &&
123 (Configuration
.getConfig().stationTemplateUrls
= Configuration
.getConfig()[
124 'stationTemplateURLs'
125 ] as StationTemplateUrl
[]);
126 Configuration
.getConfig().stationTemplateUrls
.forEach(
127 (stationTemplateUrl
: StationTemplateUrl
) => {
128 if (!isUndefined(stationTemplateUrl
['numberOfStation'])) {
130 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
131 `Deprecated configuration key
'numberOfStation' usage
for template file
'${stationTemplateUrl.file}' in 'stationTemplateUrls'. Use
'numberOfStations' instead
`,
138 return Configuration
.getConfig()?.stationTemplateUrls
;
141 public static getLog(): LogConfiguration
{
142 Configuration
.warnDeprecatedConfigurationKey(
145 "Use 'log' section to define the logging enablement instead",
147 Configuration
.warnDeprecatedConfigurationKey(
150 "Use 'log' section to define the log file instead",
152 Configuration
.warnDeprecatedConfigurationKey(
155 "Use 'log' section to define the log error file instead",
157 Configuration
.warnDeprecatedConfigurationKey(
160 "Use 'log' section to define the console logging enablement instead",
162 Configuration
.warnDeprecatedConfigurationKey(
163 'logStatisticsInterval',
165 "Use 'log' section to define the log statistics interval instead",
167 Configuration
.warnDeprecatedConfigurationKey(
170 "Use 'log' section to define the log level instead",
172 Configuration
.warnDeprecatedConfigurationKey(
175 "Use 'log' section to define the log format instead",
177 Configuration
.warnDeprecatedConfigurationKey(
180 "Use 'log' section to define the log rotation enablement instead",
182 Configuration
.warnDeprecatedConfigurationKey(
185 "Use 'log' section to define the log maximum files instead",
187 Configuration
.warnDeprecatedConfigurationKey(
190 "Use 'log' section to define the log maximum size instead",
192 const defaultLogConfiguration
: LogConfiguration
= {
194 file
: 'logs/combined.log',
195 errorFile
: 'logs/error.log',
196 statisticsInterval
: Constants
.DEFAULT_LOG_STATISTICS_INTERVAL
,
201 const deprecatedLogConfiguration
: LogConfiguration
= {
202 ...(hasOwnProp(Configuration
.getConfig(), 'logEnabled') && {
203 enabled
: Configuration
.getConfig()?.logEnabled
,
205 ...(hasOwnProp(Configuration
.getConfig(), 'logFile') && {
206 file
: Configuration
.getConfig()?.logFile
,
208 ...(hasOwnProp(Configuration
.getConfig(), 'logErrorFile') && {
209 errorFile
: Configuration
.getConfig()?.logErrorFile
,
211 ...(hasOwnProp(Configuration
.getConfig(), 'logStatisticsInterval') && {
212 statisticsInterval
: Configuration
.getConfig()?.logStatisticsInterval
,
214 ...(hasOwnProp(Configuration
.getConfig(), 'logLevel') && {
215 level
: Configuration
.getConfig()?.logLevel
,
217 ...(hasOwnProp(Configuration
.getConfig(), 'logConsole') && {
218 console
: Configuration
.getConfig()?.logConsole
,
220 ...(hasOwnProp(Configuration
.getConfig(), 'logFormat') && {
221 format
: Configuration
.getConfig()?.logFormat
,
223 ...(hasOwnProp(Configuration
.getConfig(), 'logRotate') && {
224 rotate
: Configuration
.getConfig()?.logRotate
,
226 ...(hasOwnProp(Configuration
.getConfig(), 'logMaxFiles') && {
227 maxFiles
: Configuration
.getConfig()?.logMaxFiles
,
229 ...(hasOwnProp(Configuration
.getConfig(), 'logMaxSize') && {
230 maxSize
: Configuration
.getConfig()?.logMaxSize
,
233 const logConfiguration
: LogConfiguration
= {
234 ...defaultLogConfiguration
,
235 ...deprecatedLogConfiguration
,
236 ...(hasOwnProp(Configuration
.getConfig(), 'log') && Configuration
.getConfig()?.log
),
238 return logConfiguration
;
241 public static getWorker(): WorkerConfiguration
{
242 Configuration
.warnDeprecatedConfigurationKey(
245 "Use 'worker' section to define the type of worker process model instead",
247 Configuration
.warnDeprecatedConfigurationKey(
250 "Use 'worker' section to define the type of worker process model instead",
252 Configuration
.warnDeprecatedConfigurationKey(
255 "Use 'worker' section to define the worker start delay instead",
257 Configuration
.warnDeprecatedConfigurationKey(
258 'chargingStationsPerWorker',
260 "Use 'worker' section to define the number of element(s) per worker instead",
262 Configuration
.warnDeprecatedConfigurationKey(
265 "Use 'worker' section to define the worker's element start delay instead",
267 Configuration
.warnDeprecatedConfigurationKey(
270 "Use 'worker' section to define the worker pool minimum size instead",
272 Configuration
.warnDeprecatedConfigurationKey(
275 "Use 'worker' section to define the worker pool maximum size instead",
277 Configuration
.warnDeprecatedConfigurationKey(
278 'workerPoolMaxSize;',
280 "Use 'worker' section to define the worker pool maximum size instead",
282 Configuration
.warnDeprecatedConfigurationKey(
283 'workerPoolStrategy;',
285 "Use 'worker' section to define the worker pool strategy instead",
287 const defaultWorkerConfiguration
: WorkerConfiguration
= {
288 processType
: WorkerProcessType
.workerSet
,
289 startDelay
: WorkerConstants
.DEFAULT_WORKER_START_DELAY
,
290 elementsPerWorker
: 'auto',
291 elementStartDelay
: WorkerConstants
.DEFAULT_ELEMENT_START_DELAY
,
292 poolMinSize
: WorkerConstants
.DEFAULT_POOL_MIN_SIZE
,
293 poolMaxSize
: WorkerConstants
.DEFAULT_POOL_MAX_SIZE
,
295 hasOwnProp(Configuration
.getConfig(), 'workerPoolStrategy') &&
296 delete Configuration
.getConfig()?.workerPoolStrategy
;
297 const deprecatedWorkerConfiguration
: WorkerConfiguration
= {
298 ...(hasOwnProp(Configuration
.getConfig(), 'workerProcess') && {
299 processType
: Configuration
.getConfig()?.workerProcess
,
301 ...(hasOwnProp(Configuration
.getConfig(), 'workerStartDelay') && {
302 startDelay
: Configuration
.getConfig()?.workerStartDelay
,
304 ...(hasOwnProp(Configuration
.getConfig(), 'chargingStationsPerWorker') && {
305 elementsPerWorker
: Configuration
.getConfig()?.chargingStationsPerWorker
,
307 ...(hasOwnProp(Configuration
.getConfig(), 'elementStartDelay') && {
308 elementStartDelay
: Configuration
.getConfig()?.elementStartDelay
,
310 ...(hasOwnProp(Configuration
.getConfig(), 'workerPoolMinSize') && {
311 poolMinSize
: Configuration
.getConfig()?.workerPoolMinSize
,
313 ...(hasOwnProp(Configuration
.getConfig(), 'workerPoolMaxSize') && {
314 poolMaxSize
: Configuration
.getConfig()?.workerPoolMaxSize
,
317 Configuration
.warnDeprecatedConfigurationKey(
320 'Not publicly exposed to end users',
322 const workerConfiguration
: WorkerConfiguration
= {
323 ...defaultWorkerConfiguration
,
324 ...deprecatedWorkerConfiguration
,
325 ...(hasOwnProp(Configuration
.getConfig(), 'worker') && Configuration
.getConfig()?.worker
),
327 return workerConfiguration
;
330 public static workerPoolInUse(): boolean {
331 return [WorkerProcessType
.dynamicPool
, WorkerProcessType
.staticPool
].includes(
332 Configuration
.getWorker().processType
,
336 public static workerDynamicPoolInUse(): boolean {
337 return Configuration
.getWorker().processType
=== WorkerProcessType
.dynamicPool
;
340 public static getSupervisionUrls(): string | string[] | undefined {
341 Configuration
.warnDeprecatedConfigurationKey(
344 "Use 'supervisionUrls' instead",
346 !isUndefined(Configuration
.getConfig()['supervisionURLs']) &&
347 (Configuration
.getConfig().supervisionUrls
= Configuration
.getConfig()['supervisionURLs'] as
351 return Configuration
.getConfig()?.supervisionUrls
;
354 public static getSupervisionUrlDistribution(): SupervisionUrlDistribution
| undefined {
355 Configuration
.warnDeprecatedConfigurationKey(
356 'distributeStationToTenantEqually',
358 "Use 'supervisionUrlDistribution' instead",
360 Configuration
.warnDeprecatedConfigurationKey(
361 'distributeStationsToTenantsEqually',
363 "Use 'supervisionUrlDistribution' instead",
365 return hasOwnProp(Configuration
.getConfig(), 'supervisionUrlDistribution')
366 ? Configuration
.getConfig()?.supervisionUrlDistribution
367 : SupervisionUrlDistribution
.ROUND_ROBIN
;
370 private static logPrefix
= (): string => {
371 return `${new Date().toLocaleString()} Simulator configuration |`;
374 private static warnDeprecatedConfigurationKey(
376 sectionName
?: string,
381 !isUndefined(Configuration
.getConfig()[sectionName
]) &&
382 !isUndefined((Configuration
.getConfig()[sectionName
] as Record
<string, unknown
>)[key
])
385 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
386 `Deprecated configuration key
'${key}' usage
in section
'${sectionName}'$
{
387 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
391 } else if (!isUndefined(Configuration
.getConfig()[key
])) {
393 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
394 `Deprecated configuration key
'${key}' usage$
{
395 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
402 // Read the config file
403 private static getConfig(): ConfigurationData
| null {
404 if (!Configuration
.configuration
) {
406 Configuration
.configuration
= JSON
.parse(
407 readFileSync(Configuration
.configurationFile
, 'utf8'),
408 ) as ConfigurationData
;
410 Configuration
.handleFileException(
411 Configuration
.configurationFile
,
412 FileType
.Configuration
,
413 error
as NodeJS
.ErrnoException
,
414 Configuration
.logPrefix(),
417 if (!Configuration
.configurationFileWatcher
) {
418 Configuration
.configurationFileWatcher
= Configuration
.getConfigurationFileWatcher();
421 return Configuration
.configuration
;
424 private static getConfigurationFileWatcher(): FSWatcher
| undefined {
426 return watch(Configuration
.configurationFile
, (event
, filename
): void => {
427 if (filename
?.trim().length
> 0 && event
=== 'change') {
428 // Nullify to force configuration file reading
429 Configuration
.configuration
= null;
430 if (!isUndefined(Configuration
.configurationChangeCallback
)) {
431 Configuration
.configurationChangeCallback().catch((error
) => {
432 throw typeof error
=== 'string' ? new Error(error
) : error
;
438 Configuration
.handleFileException(
439 Configuration
.configurationFile
,
440 FileType
.Configuration
,
441 error
as NodeJS
.ErrnoException
,
442 Configuration
.logPrefix(),
447 private static handleFileException(
450 error
: NodeJS
.ErrnoException
,
453 const prefix
= isNotEmptyString(logPrefix
) ? `${logPrefix} ` : '';
455 switch (error
.code
) {
457 logMsg
= `${fileType} file ${file} not found:`;
460 logMsg
= `${fileType} file ${file} already exists:`;
463 logMsg
= `${fileType} file ${file} access denied:`;
466 logMsg
= `${fileType} file ${file} permission denied:`;
469 logMsg
= `${fileType} file ${file} error:`;
471 console
.error(`${chalk.green(prefix)}${chalk.red(`${logMsg} `)}`, error);
475 private static getDefaultPerformanceStorageUri(storageType: StorageType) {
476 switch (storageType) {
477 case StorageType.JSON_FILE:
478 return Configuration.buildPerformanceUriFilePath(
479 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}
/${Constants.DEFAULT_PERFORMANCE_RECORDS_FILENAME}
`,
481 case StorageType.SQLITE:
482 return Configuration.buildPerformanceUriFilePath(
483 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}
/${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}
.db
`,
486 throw new Error(`Performance storage URI
is mandatory
with storage
type '${storageType}'`);
490 private static buildPerformanceUriFilePath(file: string) {
491 return `file
://${join(resolve(dirname(fileURLToPath(import.meta.url)), '../'), file)}`;