From a95873d8d308a20a7151346ac70d9a551f1a06f5 Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Sat, 12 Mar 2022 10:15:55 +0100 Subject: [PATCH] Add OCPP params file monitoring MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Reference: #196 Signed-off-by: Jérôme Benoit --- src/charging-station/ChargingStation.ts | 191 ++++++++---------- .../ocpp/1.6/OCPP16IncomingRequestService.ts | 11 +- .../ocpp/1.6/OCPP16ResponseService.ts | 7 +- src/performance/storage/JsonFileStorage.ts | 8 +- src/types/ChargingStationConfiguration.ts | 3 +- src/types/FileType.ts | 7 + src/types/JsonType.ts | 2 +- src/utils/Configuration.ts | 19 +- src/utils/Constants.ts | 1 - src/utils/FileUtils.ts | 55 ++++- 10 files changed, 166 insertions(+), 138 deletions(-) create mode 100644 src/types/FileType.ts diff --git a/src/charging-station/ChargingStation.ts b/src/charging-station/ChargingStation.ts index 6371501a..75094ceb 100644 --- a/src/charging-station/ChargingStation.ts +++ b/src/charging-station/ChargingStation.ts @@ -38,6 +38,7 @@ import Configuration from '../utils/Configuration'; import { ConnectorStatus } from '../types/ConnectorStatus'; import Constants from '../utils/Constants'; import { ErrorType } from '../types/ocpp/ErrorType'; +import { FileType } from '../types/FileType'; import FileUtils from '../utils/FileUtils'; import { JsonType } from '../types/JsonType'; import { MessageType } from '../types/ocpp/MessageType'; @@ -492,9 +493,57 @@ export default class ChargingStation { } this.openWSConnection(); // Monitor authorization file - this.startAuthorizationFileMonitoring(); - // Monitor station template file - this.startStationTemplateFileMonitoring(); + FileUtils.watchJsonFile( + this.logPrefix(), + FileType.Authorization, + this.getAuthorizationFile(), + this.authorizedTags + ); + // Monitor charging station template file + FileUtils.watchJsonFile( + this.logPrefix(), + FileType.ChargingStationTemplate, + this.stationTemplateFile, + null, + (event, filename): void => { + if (filename && event === 'change') { + try { + logger.debug( + `${this.logPrefix()} ${FileType.ChargingStationTemplate} ${ + this.stationTemplateFile + } file have changed, reload` + ); + // Initialize + this.initialize(); + // Restart the ATG + if ( + !this.stationInfo.AutomaticTransactionGenerator.enable && + this.automaticTransactionGenerator + ) { + this.automaticTransactionGenerator.stop(); + } + this.startAutomaticTransactionGenerator(); + if (this.getEnableStatistics()) { + this.performanceStatistics.restart(); + } else { + this.performanceStatistics.stop(); + } + // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed + } catch (error) { + logger.error( + `${this.logPrefix()} ${FileType.ChargingStationTemplate} file monitoring error: %j`, + error + ); + } + } + } + ); + FileUtils.watchJsonFile( + this.logPrefix(), + FileType.ChargingStationConfiguration, + this.configurationFile, + this.configuration + ); // Handle WebSocket message this.wsConnection.on( 'message', @@ -568,12 +617,27 @@ export default class ChargingStation { readonly: false, visible: true, reboot: false, - } + }, + params: { overwrite?: boolean; save?: boolean } = { overwrite: false, save: false } ): void { - const keyFound = this.getConfigurationKey(key); + if (!options || Utils.isEmptyObject(options)) { + options = { + readonly: false, + visible: true, + reboot: false, + }; + } const readonly = options.readonly; const visible = options.visible; const reboot = options.reboot; + let keyFound = this.getConfigurationKey(key); + if (keyFound && params?.overwrite) { + this.configuration.configurationKey.splice( + this.configuration.configurationKey.indexOf(keyFound), + 1 + ); + keyFound = undefined; + } if (!keyFound) { this.configuration.configurationKey.push({ key, @@ -582,6 +646,7 @@ export default class ChargingStation { visible, reboot, }); + params?.save && this.saveConfiguration(); } else { logger.error( `${this.logPrefix()} Trying to add an already existing configuration key: %j`, @@ -590,8 +655,12 @@ export default class ChargingStation { } } - public setConfigurationKeyValue(key: string | StandardParametersKey, value: string): void { - const keyFound = this.getConfigurationKey(key); + public setConfigurationKeyValue( + key: string | StandardParametersKey, + value: string, + caseInsensitive = false + ): void { + const keyFound = this.getConfigurationKey(key, caseInsensitive); if (keyFound) { const keyIndex = this.configuration.configurationKey.indexOf(keyFound); this.configuration.configurationKey[keyIndex].value = value; @@ -673,15 +742,13 @@ export default class ChargingStation { let stationTemplateFromFile: ChargingStationTemplate; try { // Load template file - const fileDescriptor = fs.openSync(this.stationTemplateFile, 'r'); stationTemplateFromFile = JSON.parse( - fs.readFileSync(fileDescriptor, 'utf8') + fs.readFileSync(this.stationTemplateFile, 'utf8') ) as ChargingStationTemplate; - fs.closeSync(fileDescriptor); } catch (error) { FileUtils.handleFileException( this.logPrefix(), - 'Template', + FileType.ChargingStationTemplate, this.stationTemplateFile, error as NodeJS.ErrnoException ); @@ -889,7 +956,7 @@ export default class ChargingStation { ) ) { this.addConfigurationKey( - VendorDefaultParametersKey.ConnectionUrl, + this.stationInfo.supervisionUrlOcppKey ?? VendorDefaultParametersKey.ConnectionUrl, this.getConfiguredSupervisionUrl().href, { reboot: true } ); @@ -903,7 +970,8 @@ export default class ChargingStation { this.addConfigurationKey( StandardParametersKey.NumberOfConnectors, this.getNumberOfConnectors().toString(), - { readonly: true } + { readonly: true }, + { overwrite: true } ); if (!this.getConfigurationKey(StandardParametersKey.MeterValuesSampledData)) { this.addConfigurationKey( @@ -959,15 +1027,13 @@ export default class ChargingStation { let configuration: ChargingStationConfiguration = null; if (this.configurationFile && fs.existsSync(this.configurationFile)) { try { - const fileDescriptor = fs.openSync(this.configurationFile, 'r'); configuration = JSON.parse( - fs.readFileSync(fileDescriptor, 'utf8') + fs.readFileSync(this.configurationFile, 'utf8') ) as ChargingStationConfiguration; - fs.closeSync(fileDescriptor); } catch (error) { FileUtils.handleFileException( this.logPrefix(), - 'Configuration', + FileType.ChargingStationConfiguration, this.configurationFile, error as NodeJS.ErrnoException ); @@ -988,7 +1054,7 @@ export default class ChargingStation { } catch (error) { FileUtils.handleFileException( this.logPrefix(), - 'Configuration', + FileType.ChargingStationConfiguration, this.configurationFile, error as NodeJS.ErrnoException ); @@ -1224,13 +1290,11 @@ export default class ChargingStation { if (authorizationFile) { try { // Load authorization file - const fileDescriptor = fs.openSync(authorizationFile, 'r'); - authorizedTags = JSON.parse(fs.readFileSync(fileDescriptor, 'utf8')) as string[]; - fs.closeSync(fileDescriptor); + authorizedTags = JSON.parse(fs.readFileSync(authorizationFile, 'utf8')) as string[]; } catch (error) { FileUtils.handleFileException( this.logPrefix(), - 'Authorization', + FileType.Authorization, authorizationFile, error as NodeJS.ErrnoException ); @@ -1616,89 +1680,6 @@ export default class ChargingStation { } } - private startAuthorizationFileMonitoring(): void { - const authorizationFile = this.getAuthorizationFile(); - if (authorizationFile) { - try { - fs.watch(authorizationFile, (event, filename) => { - if (filename && event === 'change') { - try { - logger.debug( - this.logPrefix() + - ' Authorization file ' + - authorizationFile + - ' have changed, reload' - ); - // Initialize authorizedTags - this.authorizedTags = this.getAuthorizedTags(); - } catch (error) { - logger.error(this.logPrefix() + ' Authorization file monitoring error: %j', error); - } - } - }); - } catch (error) { - FileUtils.handleFileException( - this.logPrefix(), - 'Authorization', - authorizationFile, - error as NodeJS.ErrnoException - ); - } - } else { - logger.info( - this.logPrefix() + - ' No authorization file given in template file ' + - this.stationTemplateFile + - '. Not monitoring changes' - ); - } - } - - private startStationTemplateFileMonitoring(): void { - try { - fs.watch(this.stationTemplateFile, (event, filename): void => { - if (filename && event === 'change') { - try { - logger.debug( - this.logPrefix() + - ' Template file ' + - this.stationTemplateFile + - ' have changed, reload' - ); - // Initialize - this.initialize(); - // Restart the ATG - if ( - !this.stationInfo.AutomaticTransactionGenerator.enable && - this.automaticTransactionGenerator - ) { - this.automaticTransactionGenerator.stop(); - } - this.startAutomaticTransactionGenerator(); - if (this.getEnableStatistics()) { - this.performanceStatistics.restart(); - } else { - this.performanceStatistics.stop(); - } - // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed - } catch (error) { - logger.error( - this.logPrefix() + ' Charging station template file monitoring error: %j', - error - ); - } - } - }); - } catch (error) { - FileUtils.handleFileException( - this.logPrefix(), - 'Template', - this.stationTemplateFile, - error as NodeJS.ErrnoException - ); - } - } - private getReconnectExponentialDelay(): boolean | undefined { return !Utils.isUndefined(this.stationInfo.reconnectExponentialDelay) ? this.stationInfo.reconnectExponentialDelay diff --git a/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts b/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts index 6f076865..bfb229be 100644 --- a/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts @@ -324,12 +324,13 @@ export default class OCPP16IncomingRequestService extends OCPPIncomingRequestSer } else if (keyToChange && keyToChange.readonly) { return Constants.OCPP_CONFIGURATION_RESPONSE_REJECTED; } else if (keyToChange && !keyToChange.readonly) { - const keyIndex = this.chargingStation.configuration.configurationKey.indexOf(keyToChange); let valueChanged = false; - if ( - this.chargingStation.configuration.configurationKey[keyIndex].value !== commandPayload.value - ) { - this.chargingStation.configuration.configurationKey[keyIndex].value = commandPayload.value; + if (keyToChange.value !== commandPayload.value) { + this.chargingStation.setConfigurationKeyValue( + commandPayload.key, + commandPayload.value, + true + ); valueChanged = true; } let triggerHeartbeatRestart = false; diff --git a/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts b/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts index aa48501e..c8d59eac 100644 --- a/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts @@ -104,12 +104,15 @@ export default class OCPP16ResponseService extends OCPPResponseService { if (payload.status === OCPP16RegistrationStatus.ACCEPTED) { this.chargingStation.addConfigurationKey( OCPP16StandardParametersKey.HeartBeatInterval, - payload.interval.toString() + payload.interval.toString(), + {}, + { overwrite: true, save: true } ); this.chargingStation.addConfigurationKey( OCPP16StandardParametersKey.HeartbeatInterval, payload.interval.toString(), - { visible: false } + { visible: false }, + { overwrite: true, save: true } ); this.chargingStation.heartbeatSetInterval ? this.chargingStation.restartHeartbeat() diff --git a/src/performance/storage/JsonFileStorage.ts b/src/performance/storage/JsonFileStorage.ts index 8a8808e9..17f5ad2f 100644 --- a/src/performance/storage/JsonFileStorage.ts +++ b/src/performance/storage/JsonFileStorage.ts @@ -1,6 +1,6 @@ // Copyright Jerome Benoit. 2021. All Rights Reserved. -import Constants from '../../utils/Constants'; +import { FileType } from '../../types/FileType'; import FileUtils from '../../utils/FileUtils'; import Statistics from '../../types/Statistics'; import { Storage } from './Storage'; @@ -46,7 +46,7 @@ export class JsonFileStorage extends Storage { } catch (error) { FileUtils.handleFileException( this.logPrefix, - Constants.PERFORMANCE_RECORDS_FILETYPE, + FileType.PerformanceRecords, this.dbName, error as NodeJS.ErrnoException ); @@ -66,7 +66,7 @@ export class JsonFileStorage extends Storage { } catch (error) { FileUtils.handleFileException( this.logPrefix, - Constants.PERFORMANCE_RECORDS_FILETYPE, + FileType.PerformanceRecords, this.dbName, error as NodeJS.ErrnoException ); @@ -82,7 +82,7 @@ export class JsonFileStorage extends Storage { } catch (error) { FileUtils.handleFileException( this.logPrefix, - Constants.PERFORMANCE_RECORDS_FILETYPE, + FileType.PerformanceRecords, this.dbName, error as NodeJS.ErrnoException ); diff --git a/src/types/ChargingStationConfiguration.ts b/src/types/ChargingStationConfiguration.ts index fca2ec4a..2e255f25 100644 --- a/src/types/ChargingStationConfiguration.ts +++ b/src/types/ChargingStationConfiguration.ts @@ -1,3 +1,4 @@ +import { JsonType } from './JsonType'; import { OCPPConfigurationKey } from './ocpp/Configuration'; export interface ConfigurationKey extends OCPPConfigurationKey { @@ -5,6 +6,6 @@ export interface ConfigurationKey extends OCPPConfigurationKey { reboot?: boolean; } -export default interface ChargingStationConfiguration { +export default interface ChargingStationConfiguration extends JsonType { configurationKey: ConfigurationKey[]; } diff --git a/src/types/FileType.ts b/src/types/FileType.ts new file mode 100644 index 00000000..b464bf8b --- /dev/null +++ b/src/types/FileType.ts @@ -0,0 +1,7 @@ +export enum FileType { + Authorization = 'authorization', + Configuration = 'configuration', + ChargingStationConfiguration = 'charging station configuration', + ChargingStationTemplate = 'charging station template', + PerformanceRecords = 'performance records', +} diff --git a/src/types/JsonType.ts b/src/types/JsonType.ts index 846eb763..8908b781 100644 --- a/src/types/JsonType.ts +++ b/src/types/JsonType.ts @@ -1,6 +1,6 @@ type JsonArray = Array; -type JsonValue = string | number | boolean | Date | JsonType | JsonArray; +export type JsonValue = string | number | boolean | Date | JsonType | JsonArray; export interface JsonType { [key: string]: JsonValue; diff --git a/src/utils/Configuration.ts b/src/utils/Configuration.ts index 9d873d2f..6da954d1 100644 --- a/src/utils/Configuration.ts +++ b/src/utils/Configuration.ts @@ -7,6 +7,7 @@ import ConfigurationData, { import Constants from './Constants'; import { EmptyObject } from '../types/EmptyObject'; +import { FileType } from '../types/FileType'; import { HandleErrorParams } from '../types/Error'; import { ServerOptions } from 'ws'; import { StorageType } from '../types/Storage'; @@ -18,7 +19,7 @@ import fs from 'fs'; import path from 'path'; export default class Configuration { - private static configurationFilePath = path.join( + private static configurationFile = path.join( path.resolve(__dirname, '../'), 'assets', 'config.json' @@ -322,13 +323,13 @@ export default class Configuration { if (!Configuration.configuration) { try { Configuration.configuration = JSON.parse( - fs.readFileSync(Configuration.configurationFilePath, 'utf8') + fs.readFileSync(Configuration.configurationFile, 'utf8') ) as ConfigurationData; } catch (error) { Configuration.handleFileException( Configuration.logPrefix(), - 'Configuration', - Configuration.configurationFilePath, + FileType.Configuration, + Configuration.configurationFile, error as NodeJS.ErrnoException ); } @@ -341,7 +342,7 @@ export default class Configuration { private static getConfigurationFileWatcher(): fs.FSWatcher { try { - return fs.watch(Configuration.configurationFilePath, (event, filename): void => { + return fs.watch(Configuration.configurationFile, (event, filename): void => { if (filename && event === 'change') { // Nullify to force configuration file reading Configuration.configuration = null; @@ -355,9 +356,9 @@ export default class Configuration { } catch (error) { Configuration.handleFileException( Configuration.logPrefix(), - 'Configuration', - Configuration.configurationFilePath, - error as Error + FileType.Configuration, + Configuration.configurationFile, + error as NodeJS.ErrnoException ); } } @@ -387,7 +388,7 @@ export default class Configuration { private static handleFileException( logPrefix: string, - fileType: string, + fileType: FileType, filePath: string, error: NodeJS.ErrnoException, params: HandleErrorParams = { throwError: true } diff --git a/src/utils/Constants.ts b/src/utils/Constants.ts index da41eaaf..76697c8a 100644 --- a/src/utils/Constants.ts +++ b/src/utils/Constants.ts @@ -107,7 +107,6 @@ export default class Constants { static readonly DEFAULT_FLUCTUATION_PERCENT = 5; - static readonly PERFORMANCE_RECORDS_FILETYPE = 'Performance records'; static readonly DEFAULT_PERFORMANCE_RECORDS_FILENAME = 'performanceRecords.json'; static readonly DEFAULT_PERFORMANCE_RECORDS_DB_NAME = 'charging-stations-simulator'; static readonly PERFORMANCE_RECORDS_TABLE = 'performance_records'; diff --git a/src/utils/FileUtils.ts b/src/utils/FileUtils.ts index 38df26a9..cab28941 100644 --- a/src/utils/FileUtils.ts +++ b/src/utils/FileUtils.ts @@ -1,14 +1,49 @@ +import { JsonType, JsonValue } from '../types/JsonType'; + import { EmptyObject } from '../types/EmptyObject'; +import { FileType } from '../types/FileType'; import { HandleErrorParams } from '../types/Error'; import Utils from './Utils'; import chalk from 'chalk'; +import fs from 'fs'; import logger from './Logger'; export default class FileUtils { + static watchJsonFile( + logPrefix: string, + fileType: FileType, + file: string, + attribute?: T, + listener: fs.WatchListener = (event, filename) => { + if (filename && event === 'change') { + try { + logger.debug(logPrefix + ' ' + fileType + ' file ' + file + ' have changed, reload'); + attribute = JSON.parse(fs.readFileSync(file, 'utf8')) as T; + } catch (error) { + FileUtils.handleFileException(logPrefix, fileType, file, error as NodeJS.ErrnoException, { + throwError: false, + }); + } + } + } + ) { + if (file) { + try { + fs.watch(file, listener); + } catch (error) { + FileUtils.handleFileException(logPrefix, fileType, file, error as NodeJS.ErrnoException, { + throwError: false, + }); + } + } else { + logger.info(`${logPrefix} No ${fileType} file to watch given. Not monitoring its changes`); + } + } + static handleFileException( logPrefix: string, - fileType: string, - filePath: string, + fileType: FileType, + file: string, error: NodeJS.ErrnoException, params: HandleErrorParams = { throwError: true, consoleOut: false } ): void { @@ -16,38 +51,38 @@ export default class FileUtils { if (error.code === 'ENOENT') { if (params?.consoleOut) { console.warn( - chalk.green(prefix) + chalk.yellow(fileType + ' file ' + filePath + ' not found: '), + chalk.green(prefix) + chalk.yellow(fileType + ' file ' + file + ' not found: '), error ); } else { - logger.warn(prefix + fileType + ' file ' + filePath + ' not found: %j', error); + logger.warn(prefix + fileType + ' file ' + file + ' not found: %j', error); } } else if (error.code === 'EEXIST') { if (params?.consoleOut) { console.warn( - chalk.green(prefix) + chalk.yellow(fileType + ' file ' + filePath + ' already exists: '), + chalk.green(prefix) + chalk.yellow(fileType + ' file ' + file + ' already exists: '), error ); } else { - logger.warn(prefix + fileType + ' file ' + filePath + ' already exists: %j', error); + logger.warn(prefix + fileType + ' file ' + file + ' already exists: %j', error); } } else if (error.code === 'EACCES') { if (params?.consoleOut) { console.warn( - chalk.green(prefix) + chalk.yellow(fileType + ' file ' + filePath + ' access denied: '), + chalk.green(prefix) + chalk.yellow(fileType + ' file ' + file + ' access denied: '), error ); } else { - logger.warn(prefix + fileType + ' file ' + filePath + ' access denied: %j', error); + logger.warn(prefix + fileType + ' file ' + file + ' access denied: %j', error); } } else { if (params?.consoleOut) { console.warn( - chalk.green(prefix) + chalk.yellow(fileType + ' file ' + filePath + ' error: '), + chalk.green(prefix) + chalk.yellow(fileType + ' file ' + file + ' error: '), error ); } else { - logger.warn(prefix + fileType + ' file ' + filePath + ' error: %j', error); + logger.warn(prefix + fileType + ' file ' + file + ' error: %j', error); } if (params?.throwError) { throw error; -- 2.34.1