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
,
15 type LogConfiguration
,
16 type StationTemplateUrl
,
17 type StorageConfiguration
,
19 SupervisionUrlDistribution
,
20 type UIServerConfiguration
,
21 type WorkerConfiguration
,
23 import { WorkerConstants
, WorkerProcessType
} from
'../worker';
25 export class Configuration
{
26 private static configurationFile
= join(
27 dirname(fileURLToPath(import.meta
.url
)),
32 private static configurationFileWatcher
: FSWatcher
| undefined;
33 private static configurationData
: ConfigurationData
| null = null;
34 private static configurationSectionCache
= new Map
<
36 LogConfiguration
| StorageConfiguration
| WorkerConfiguration
| UIServerConfiguration
38 [ConfigurationSection
.log
, Configuration
.buildLogSection()],
39 [ConfigurationSection
.performanceStorage
, Configuration
.buildPerformanceStorageSection()],
40 [ConfigurationSection
.worker
, Configuration
.buildWorkerSection()],
41 [ConfigurationSection
.uiServer
, Configuration
.buildUIServerSection()],
44 private static configurationChangeCallback
: () => Promise
<void>;
46 private constructor() {
47 // This is intentional
50 public static setConfigurationChangeCallback(cb
: () => Promise
<void>): void {
51 Configuration
.configurationChangeCallback
= cb
;
54 public static getConfigurationSection
<T
>(sectionName
: ConfigurationSection
): T
{
55 return Configuration
.configurationSectionCache
.get(sectionName
) as T
;
58 public static getAutoReconnectMaxRetries(): number | undefined {
59 Configuration
.warnDeprecatedConfigurationKey(
60 'autoReconnectTimeout',
62 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead",
64 Configuration
.warnDeprecatedConfigurationKey(
67 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead",
69 Configuration
.warnDeprecatedConfigurationKey(
70 'autoReconnectMaxRetries',
72 'Use it in charging station template instead',
74 if (hasOwnProp(Configuration
.getConfigurationData(), 'autoReconnectMaxRetries')) {
75 return Configuration
.getConfigurationData()?.autoReconnectMaxRetries
;
79 public static getStationTemplateUrls(): StationTemplateUrl
[] | undefined {
80 Configuration
.warnDeprecatedConfigurationKey(
81 'stationTemplateURLs',
83 "Use 'stationTemplateUrls' instead",
85 // eslint-disable-next-line @typescript-eslint/dot-notation
86 !isUndefined(Configuration
.getConfigurationData()!['stationTemplateURLs']) &&
87 (Configuration
.getConfigurationData()!.stationTemplateUrls
=
88 Configuration
.getConfigurationData()![
89 // eslint-disable-next-line @typescript-eslint/dot-notation
91 ] as StationTemplateUrl
[]);
92 Configuration
.getConfigurationData()!.stationTemplateUrls
.forEach(
93 (stationTemplateUrl
: StationTemplateUrl
) => {
94 // eslint-disable-next-line @typescript-eslint/dot-notation
95 if (!isUndefined(stationTemplateUrl
['numberOfStation'])) {
97 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
98 `Deprecated configuration key
'numberOfStation' usage
for template file
'${stationTemplateUrl.file}' in 'stationTemplateUrls'. Use
'numberOfStations' instead
`,
104 return Configuration
.getConfigurationData()?.stationTemplateUrls
;
107 public static workerPoolInUse(): boolean {
108 return [WorkerProcessType
.dynamicPool
, WorkerProcessType
.staticPool
].includes(
109 Configuration
.buildWorkerSection().processType
!,
113 public static workerDynamicPoolInUse(): boolean {
114 return Configuration
.buildWorkerSection().processType
=== WorkerProcessType
.dynamicPool
;
117 public static getSupervisionUrls(): string | string[] | undefined {
118 Configuration
.warnDeprecatedConfigurationKey(
121 "Use 'supervisionUrls' instead",
123 // eslint-disable-next-line @typescript-eslint/dot-notation
124 if (!isUndefined(Configuration
.getConfigurationData()!['supervisionURLs'])) {
125 Configuration
.getConfigurationData()!.supervisionUrls
= Configuration
.getConfigurationData()![
126 // eslint-disable-next-line @typescript-eslint/dot-notation
128 ] as string | string[];
130 return Configuration
.getConfigurationData()?.supervisionUrls
;
133 public static getSupervisionUrlDistribution(): SupervisionUrlDistribution
| undefined {
134 Configuration
.warnDeprecatedConfigurationKey(
135 'distributeStationToTenantEqually',
137 "Use 'supervisionUrlDistribution' instead",
139 Configuration
.warnDeprecatedConfigurationKey(
140 'distributeStationsToTenantsEqually',
142 "Use 'supervisionUrlDistribution' instead",
144 return hasOwnProp(Configuration
.getConfigurationData(), 'supervisionUrlDistribution')
145 ? Configuration
.getConfigurationData()?.supervisionUrlDistribution
146 : SupervisionUrlDistribution
.ROUND_ROBIN
;
149 private static buildUIServerSection(): UIServerConfiguration
{
150 if (hasOwnProp(Configuration
.getConfigurationData(), 'uiWebSocketServer')) {
152 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
153 `Deprecated configuration section
'uiWebSocketServer' usage
. Use
'${ConfigurationSection.uiServer}' instead
`,
157 let uiServerConfiguration
: UIServerConfiguration
= {
159 type: ApplicationProtocol
.WS
,
161 host
: Constants
.DEFAULT_UI_SERVER_HOST
,
162 port
: Constants
.DEFAULT_UI_SERVER_PORT
,
165 if (hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.uiServer
)) {
166 uiServerConfiguration
= merge
<UIServerConfiguration
>(
167 uiServerConfiguration
,
168 Configuration
.getConfigurationData()!.uiServer
!,
171 if (isCFEnvironment() === true) {
172 delete uiServerConfiguration
.options
?.host
;
173 uiServerConfiguration
.options
!.port
= parseInt(process
.env
.PORT
!);
175 return uiServerConfiguration
;
178 private static buildPerformanceStorageSection(): StorageConfiguration
{
179 Configuration
.warnDeprecatedConfigurationKey(
181 ConfigurationSection
.performanceStorage
,
184 let storageConfiguration
: StorageConfiguration
= {
186 type: StorageType
.JSON_FILE
,
187 uri
: this.getDefaultPerformanceStorageUri(StorageType
.JSON_FILE
),
189 if (hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.performanceStorage
)) {
190 storageConfiguration
= {
191 ...storageConfiguration
,
192 ...Configuration
.getConfigurationData()?.performanceStorage
,
193 ...(Configuration
.getConfigurationData()?.performanceStorage
?.type ===
194 StorageType
.JSON_FILE
&&
195 Configuration
.getConfigurationData()?.performanceStorage
?.uri
&& {
196 uri
: Configuration
.buildPerformanceUriFilePath(
197 new URL(Configuration
.getConfigurationData()!.performanceStorage
!.uri
!).pathname
,
202 return storageConfiguration
;
205 private static buildLogSection(): LogConfiguration
{
206 Configuration
.warnDeprecatedConfigurationKey(
209 `Use '${ConfigurationSection.log}' section to define the logging enablement instead`,
211 Configuration
.warnDeprecatedConfigurationKey(
214 `Use '${ConfigurationSection.log}' section to define the log file instead`,
216 Configuration
.warnDeprecatedConfigurationKey(
219 `Use '${ConfigurationSection.log}' section to define the log error file instead`,
221 Configuration
.warnDeprecatedConfigurationKey(
224 `Use '${ConfigurationSection.log}' section to define the console logging enablement instead`,
226 Configuration
.warnDeprecatedConfigurationKey(
227 'logStatisticsInterval',
229 `Use '${ConfigurationSection.log}' section to define the log statistics interval instead`,
231 Configuration
.warnDeprecatedConfigurationKey(
234 `Use '${ConfigurationSection.log}' section to define the log level instead`,
236 Configuration
.warnDeprecatedConfigurationKey(
239 `Use '${ConfigurationSection.log}' section to define the log format instead`,
241 Configuration
.warnDeprecatedConfigurationKey(
244 `Use '${ConfigurationSection.log}' section to define the log rotation enablement instead`,
246 Configuration
.warnDeprecatedConfigurationKey(
249 `Use '${ConfigurationSection.log}' section to define the log maximum files instead`,
251 Configuration
.warnDeprecatedConfigurationKey(
254 `Use '${ConfigurationSection.log}' section to define the log maximum size instead`,
256 const defaultLogConfiguration
: LogConfiguration
= {
258 file
: 'logs/combined.log',
259 errorFile
: 'logs/error.log',
260 statisticsInterval
: Constants
.DEFAULT_LOG_STATISTICS_INTERVAL
,
265 const deprecatedLogConfiguration
: LogConfiguration
= {
266 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logEnabled') && {
267 enabled
: Configuration
.getConfigurationData()?.logEnabled
,
269 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logFile') && {
270 file
: Configuration
.getConfigurationData()?.logFile
,
272 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logErrorFile') && {
273 errorFile
: Configuration
.getConfigurationData()?.logErrorFile
,
275 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logStatisticsInterval') && {
276 statisticsInterval
: Configuration
.getConfigurationData()?.logStatisticsInterval
,
278 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logLevel') && {
279 level
: Configuration
.getConfigurationData()?.logLevel
,
281 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logConsole') && {
282 console
: Configuration
.getConfigurationData()?.logConsole
,
284 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logFormat') && {
285 format
: Configuration
.getConfigurationData()?.logFormat
,
287 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logRotate') && {
288 rotate
: Configuration
.getConfigurationData()?.logRotate
,
290 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logMaxFiles') && {
291 maxFiles
: Configuration
.getConfigurationData()?.logMaxFiles
,
293 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logMaxSize') && {
294 maxSize
: Configuration
.getConfigurationData()?.logMaxSize
,
297 const logConfiguration
: LogConfiguration
= {
298 ...defaultLogConfiguration
,
299 ...deprecatedLogConfiguration
,
300 ...(hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.log
) &&
301 Configuration
.getConfigurationData()?.log
),
303 return logConfiguration
;
306 private static buildWorkerSection(): WorkerConfiguration
{
307 Configuration
.warnDeprecatedConfigurationKey(
310 `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`,
312 Configuration
.warnDeprecatedConfigurationKey(
315 `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`,
317 Configuration
.warnDeprecatedConfigurationKey(
320 `Use '${ConfigurationSection.worker}' section to define the worker start delay instead`,
322 Configuration
.warnDeprecatedConfigurationKey(
323 'chargingStationsPerWorker',
325 `Use '${ConfigurationSection.worker}' section to define the number of element(s) per worker instead`,
327 Configuration
.warnDeprecatedConfigurationKey(
330 `Use '${ConfigurationSection.worker}' section to define the worker's element start delay instead`,
332 Configuration
.warnDeprecatedConfigurationKey(
335 `Use '${ConfigurationSection.worker}' section to define the worker pool minimum size instead`,
337 Configuration
.warnDeprecatedConfigurationKey(
340 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`,
342 Configuration
.warnDeprecatedConfigurationKey(
343 'workerPoolMaxSize;',
345 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`,
347 Configuration
.warnDeprecatedConfigurationKey(
348 'workerPoolStrategy;',
350 `Use '${ConfigurationSection.worker}' section to define the worker pool strategy instead`,
352 const defaultWorkerConfiguration
: WorkerConfiguration
= {
353 processType
: WorkerProcessType
.workerSet
,
354 startDelay
: WorkerConstants
.DEFAULT_WORKER_START_DELAY
,
355 elementsPerWorker
: 'auto',
356 elementStartDelay
: WorkerConstants
.DEFAULT_ELEMENT_START_DELAY
,
357 poolMinSize
: WorkerConstants
.DEFAULT_POOL_MIN_SIZE
,
358 poolMaxSize
: WorkerConstants
.DEFAULT_POOL_MAX_SIZE
,
360 hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolStrategy') &&
361 delete Configuration
.getConfigurationData()?.workerPoolStrategy
;
362 const deprecatedWorkerConfiguration
: WorkerConfiguration
= {
363 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerProcess') && {
364 processType
: Configuration
.getConfigurationData()?.workerProcess
,
366 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerStartDelay') && {
367 startDelay
: Configuration
.getConfigurationData()?.workerStartDelay
,
369 ...(hasOwnProp(Configuration
.getConfigurationData(), 'chargingStationsPerWorker') && {
370 elementsPerWorker
: Configuration
.getConfigurationData()?.chargingStationsPerWorker
,
372 ...(hasOwnProp(Configuration
.getConfigurationData(), 'elementStartDelay') && {
373 elementStartDelay
: Configuration
.getConfigurationData()?.elementStartDelay
,
375 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMinSize') && {
376 poolMinSize
: Configuration
.getConfigurationData()?.workerPoolMinSize
,
378 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMaxSize') && {
379 poolMaxSize
: Configuration
.getConfigurationData()?.workerPoolMaxSize
,
382 Configuration
.warnDeprecatedConfigurationKey(
384 ConfigurationSection
.worker
,
385 'Not publicly exposed to end users',
387 const workerConfiguration
: WorkerConfiguration
= {
388 ...defaultWorkerConfiguration
,
389 ...deprecatedWorkerConfiguration
,
390 ...(hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.worker
) &&
391 Configuration
.getConfigurationData()?.worker
),
393 if (!Object.values(WorkerProcessType
).includes(workerConfiguration
.processType
!)) {
394 throw new SyntaxError(
395 `Invalid worker process type '${workerConfiguration.processType}' defined in configuration`,
398 return workerConfiguration
;
401 private static logPrefix
= (): string => {
402 return `${new Date().toLocaleString()} Simulator configuration |`;
405 private static warnDeprecatedConfigurationKey(
407 sectionName
?: string,
412 !isUndefined(Configuration
.getConfigurationData()![sectionName
]) &&
414 (Configuration
.getConfigurationData()![sectionName
] as Record
<string, unknown
>)[key
],
418 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
419 `Deprecated configuration key
'${key}' usage
in section
'${sectionName}'$
{
420 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
424 } else if (!isUndefined(Configuration
.getConfigurationData()![key
])) {
426 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
427 `Deprecated configuration key
'${key}' usage$
{
428 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
435 private static getConfigurationData(): ConfigurationData
| null {
436 if (!Configuration
.configurationData
) {
438 Configuration
.configurationData
= JSON
.parse(
439 readFileSync(Configuration
.configurationFile
, 'utf8'),
440 ) as ConfigurationData
;
442 Configuration
.handleFileException(
443 Configuration
.configurationFile
,
444 FileType
.Configuration
,
445 error
as NodeJS
.ErrnoException
,
446 Configuration
.logPrefix(),
449 if (!Configuration
.configurationFileWatcher
) {
450 Configuration
.configurationFileWatcher
= Configuration
.getConfigurationFileWatcher();
453 return Configuration
.configurationData
;
456 private static getConfigurationFileWatcher(): FSWatcher
| undefined {
458 return watch(Configuration
.configurationFile
, (event
, filename
): void => {
459 if (filename
!.trim()!.length
> 0 && event
=== 'change') {
460 // Nullify to force configuration file reading
461 Configuration
.configurationData
= null;
462 Configuration
.configurationSectionCache
.clear();
463 if (!isUndefined(Configuration
.configurationChangeCallback
)) {
464 Configuration
.configurationChangeCallback().catch((error
) => {
465 throw typeof error
=== 'string' ? new Error(error
) : error
;
471 Configuration
.handleFileException(
472 Configuration
.configurationFile
,
473 FileType
.Configuration
,
474 error
as NodeJS
.ErrnoException
,
475 Configuration
.logPrefix(),
480 private static handleFileException(
483 error
: NodeJS
.ErrnoException
,
486 const prefix
= isNotEmptyString(logPrefix
) ? `${logPrefix} ` : '';
488 switch (error
.code
) {
490 logMsg
= `${fileType} file ${file} not found:`;
493 logMsg
= `${fileType} file ${file} already exists:`;
496 logMsg
= `${fileType} file ${file} access denied:`;
499 logMsg
= `${fileType} file ${file} permission denied:`;
502 logMsg
= `${fileType} file ${file} error:`;
504 console
.error(`${chalk.green(prefix)}${chalk.red(`${logMsg} `)}`, error);
508 private static getDefaultPerformanceStorageUri(storageType: StorageType) {
509 switch (storageType) {
510 case StorageType.JSON_FILE:
511 return Configuration.buildPerformanceUriFilePath(
512 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}
/${Constants.DEFAULT_PERFORMANCE_RECORDS_FILENAME}
`,
514 case StorageType.SQLITE:
515 return Configuration.buildPerformanceUriFilePath(
516 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}
/${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}
.db
`,
519 throw new Error(`Performance storage URI
is mandatory
with storage
type '${storageType}'`);
523 private static buildPerformanceUriFilePath(file: string) {
524 return `file
://${join(resolve(dirname(fileURLToPath(import.meta.url)), '../'), file)}`;