using Applications.FDC; using AutoMapper; using Edge.Core.Database; using Edge.Core.Processor; using Edge.Core.IndustryStandardInterface.ATG; using Edge.Core.UniversalApi; using Dfs.WayneChina.FairbanksRTData.Support; using Dfs.WayneChina.FairbanksRTData.UniversalApiModels; using FdcServerHost; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; using System.Reflection; using System.Text; using System.Text.Json; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using System.Timers; using Edge.Core.Database.Models; using Edge.Core.Processor.Dispatcher.Attributes; namespace Dfs.WayneChina.FairbanksRTData { [MetaPartsDescriptor( "lang-zh-cn:Fairbanks实时数据上传应用lang-en-us:Fairbank real time data App", "lang-zh-cn:用于传输加油站实时数据,如:加油、液位仪、告警等数据到Fairbanks服务器" + "lang-en-us:Used for uploading real time data in specified format to Fairbanks server", new[] { "lang-zh-cn:Fairbankslang-en-us:Fairbanks" })] public class FairbanksRealTimeDataApp : IAppProcessor { #region Fields public string MetaConfigName { get; set; } private ILogger appLogger = NullLogger.Instance; private IServiceProvider services; private FdcServerHostApp fdcServer; private System.Timers.Timer tankReadTimer; private System.Timers.Timer uploadTimer; private string host; private int port; private string username; private string password; private string deviceId; private string siteName; private int tankReadInterval; private int uploadInterval; private string hostUploadFolder; private IEnumerable autoTankGaugeControllers; private string transactionsFileName = "transactions"; private string transactionsFilePath; private string tankReadingFileName = "stockLevels"; private string stockLevelsFilePath; private string alarmsFileName = "alarms"; private string alarmsFilePath; private string deliveriesFileName = "deliveries"; private string deliveriesFilePath; private string requiredFolder; private string tempFolder; private string uploadedFilesFolder; private string folderSeperator = "/"; private FairbanksSftpClient fbSftpClient; // Persistence private IMapper objMapper; private readonly SqliteDbContext dbContext; private AppConfig appConfig; private DbHelper dbHelper; private ConcurrentQueue csvFileQueue = new ConcurrentQueue(); private ReaderWriterLockSlim fileLocker = new ReaderWriterLockSlim(); private int sortToken = 0; #endregion #region Tank State Dictionary private Dictionary tankStateDict = new Dictionary(); #endregion #region Logger NLog.Logger logger = NLog.LogManager.LoadConfiguration("NLog.config").GetLogger("Fairbanks"); #endregion #region Constructor [ParamsJsonSchemas("appCtorParamsJsonSchema")] public FairbanksRealTimeDataApp(IServiceProvider services, FairbanksAppConfigV1 config) { this.services = services; var loggerFactory = services.GetRequiredService(); appLogger = loggerFactory.CreateLogger("Application"); objMapper = services.GetRequiredService(); dbContext = services.GetRequiredService(); if (services != null) dbHelper = new DbHelper(services); host = config.Host; port = config.Port; username = config.Username; password = config.Password; deviceId = config.DeviceId; siteName = config.SiteName; tankReadInterval = config.Interval; uploadInterval = config.UploadInterval; hostUploadFolder = config.HostUploadFolder; fbSftpClient = new FairbanksSftpClient(host, username, password); } public FairbanksRealTimeDataApp(IServiceProvider services, string host, int port, string username, string password, string deviceId, string siteName, int interval, int uploadInterval, string hostUploadFolder) { this.services = services; var loggerFactory = services.GetRequiredService(); appLogger = loggerFactory.CreateLogger("Application"); objMapper = services.GetRequiredService(); dbContext = services.GetRequiredService(); if (services != null) dbHelper = new DbHelper(services); this.host = host; this.port = port; this.username = username; this.password = password; this.deviceId = deviceId; this.siteName = siteName; this.tankReadInterval = interval; this.uploadInterval = uploadInterval; this.hostUploadFolder = hostUploadFolder; fbSftpClient = new FairbanksSftpClient(host, username, password); } #endregion #region Init public void Init(IEnumerable processors) { //get the FdcServer instance during init foreach (dynamic processor in processors) { if (processor is IAppProcessor) { FdcServerHostApp fdcServer = processor as FdcServerHostApp; if (fdcServer != null) { logger.Info("FdcServerHostApp retrieved as an IApplication instance!"); this.fdcServer = processor; } continue; } } if (fdcServer != null) logger.Info("Fdc Server instance alive"); var atgControllers = processors.WithHandlerOrApp() .Select(p => p.ProcessorDescriptor().DeviceHandlerOrApp) .Cast(); autoTankGaugeControllers = atgControllers; if (autoTankGaugeControllers != null) { foreach (var controller in autoTankGaugeControllers) { controller.OnStateChange += Controller_OnStateChange; controller.OnAlarm += Controller_OnAlarm; } } } #region Alarms private async void Controller_OnAlarm(object sender, AtgAlarmEventArg e) { logger.Info("Incoming alarm"); var currentGauge = sender as IAutoTankGaugeController; if (currentGauge != null) { var alarms = new List(); foreach (var alarm in e.Alarms) { logger.Info($"Gauge: {currentGauge.DeviceId}"); logger.Info($" tank number: {alarm.TankNumber}, description: {alarm.Description}"); logger.Info($" alarm id: {alarm.Id}, priority: {alarm.Priority}, type: {alarm.Type}"); logger.Info($" created time: {alarm.CreatedTimeStamp}, cleared time: {alarm.ClearedTimeStamp}"); var fbAlarm = new FbAlarm(); fbAlarm.ibank = deviceId; fbAlarm.DateTime = alarm.CreatedTimeStamp; fbAlarm.CodedAlarm = "1"; fbAlarm.Category = "TLS_ALARM"; fbAlarm.Type = ""; // can be left blank or null fbAlarm.AlarmCode = string.Concat("TLS_", "02_", ((byte)alarm.Type).ToString().PadLeft(2, '0')); fbAlarm.TankAlarm = "1"; //1=tank alarm, 0=other device alarm or things fbAlarm.Tank = alarm.TankNumber; fbAlarm.State = 0; // active = 0, cleared = 1 fbAlarm.Note = alarm.Description; alarms.Add(fbAlarm); } await RecordAlarmsAsync(alarms); } } private async Task RecordAlarmsAsync(IEnumerable alarms) { try { fileLocker.TryEnterReadLock(500); SetAlarmsFile(requiredFolder); } catch (Exception ex) { logger.Error(ex.ToString()); } finally { fileLocker.ExitReadLock(); } if (!File.Exists(alarmsFilePath)) { logger.Info("Creating alarms file path"); await WriteCsvAsync(new List(), alarmsFilePath); } logger.Info("Writing alarms info into file"); await WriteCsvAsync(alarms, alarmsFilePath); } #endregion #region Fuel deliveries private async void Controller_OnStateChange(object sender, AtgStateChangeEventArg e) { if (e.State == AtgState.TanksReloaded) { logger.Info("Tank reloaded, ready for use"); } if (e.TargetTank != null) { if (e.TargetTank.State == TankState.Delivering) { logger.Info("Delivering fuel..."); if (!tankStateDict.ContainsKey(e.TargetTank.TankNumber)) { logger.Info($"Adding tank {e.TargetTank.TankNumber.ToString()} to the tank state dict"); tankStateDict.Add(e.TargetTank.TankNumber, e.TargetTank.State); } } else if (e.TargetTank.State == TankState.Idle) { logger.Info("Tank state: Idle"); var currentTankState = tankStateDict[e.TargetTank.TankNumber]; if (currentTankState == TankState.Delivering && e.TargetTank.State == TankState.Idle) { logger.Info($"Tank {e.TargetTank.TankNumber.ToString()} state changed, Delivering -> Idle, check fuel delivery"); tankStateDict[e.TargetTank.TankNumber] = TankState.Idle; var currentAtg = sender as IAutoTankGaugeController; if (currentAtg != null) { await RecordFuelDelivery(currentAtg, e.TargetTank.TankNumber); } } } } } private async Task RecordFuelDelivery(IAutoTankGaugeController autoTankGauge, int tankNumber) { var deliveries = await autoTankGauge.GetTankDeliveryAsync(tankNumber); var fbFuelDeliveries = new List(); foreach (var delivery in deliveries) { logger.Info($"Tank: {delivery.TankNumber}, FuelHeight, start: {delivery.StartingFuelHeight}, end: {delivery.EndingFuelHeight}"); logger.Info($" Fuel TC Volume, start: {delivery.StartingFuelTCVolume}, end: {delivery.EndingFuelTCVolume}"); logger.Info($" Fuel Volume, start: {delivery.StartingFuelVolume}, end: {delivery.EndingFuelVolume}"); var fbFuelDelivery = new FbFuelDelivery(); fbFuelDelivery.ibank = deviceId; fbFuelDelivery.Tank = delivery.TankNumber; fbFuelDelivery.StartDateTime = delivery.StartingDateTime; fbFuelDelivery.StartVolume = (decimal)delivery.StartingFuelVolume; fbFuelDelivery.StartWater = (decimal)delivery.StartingWaterHeight; fbFuelDelivery.StartTemperature = (decimal)delivery.StartingTemperture; fbFuelDelivery.EndDateTime = (DateTime)delivery.EndingDateTime; fbFuelDelivery.EndVolume = (decimal)delivery.EndingFuelVolume; fbFuelDelivery.EndWater = (decimal)delivery.EndingWaterHeight; fbFuelDelivery.EndTemperature = (decimal)delivery.EndingTemperture; fbFuelDelivery.Quantity = 0m; fbFuelDelivery.ConfirmedQuantity = 0m; fbFuelDeliveries.Add(fbFuelDelivery); } try { fileLocker.TryEnterReadLock(500); SetDeliveriesFile(requiredFolder); } catch (Exception ex) { logger.Error(ex.ToString()); } finally { fileLocker.ExitReadLock(); } if (!File.Exists(deliveriesFilePath)) { logger.Info("Creating deliveries file path"); await WriteCsvAsync(new List(), deliveriesFilePath); } logger.Info("Writing deliveries info into file"); await WriteCsvAsync(fbFuelDeliveries, deliveriesFilePath); } #endregion #endregion #region Folder and file name handling private void EnsureFolder() { string path = Directory.GetCurrentDirectory(); requiredFolder = path + folderSeperator + "RealTimeData"; if (!Directory.Exists(requiredFolder)) Directory.CreateDirectory(requiredFolder); tempFolder = requiredFolder + folderSeperator + "temp"; if (!Directory.Exists(requiredFolder + folderSeperator + "temp")) { Directory.CreateDirectory(tempFolder); } uploadedFilesFolder = requiredFolder + folderSeperator + "archives"; if (!Directory.Exists(uploadedFilesFolder)) Directory.CreateDirectory(uploadedFilesFolder); SetAlarmsFile(requiredFolder); SetDeliveriesFile(requiredFolder); SetTransactionFile(requiredFolder); SetTankReadingFile(requiredFolder); } private string CreateFileNameWithoutExt(string fileTypeName, string dateSection, string hourSection) { return string.Concat(requiredFolder, folderSeperator, /*deviceId, "_",*/ fileTypeName, "_", dateSection, "_", hourSection); } private void SetAlarmsFile(string requiredFolder) { var dateSection = DateTime.Now.Date.ToString("yyyy-MM-dd"); var hourSection = DateTime.Now.Hour.ToString("D2"); var fileNameWithoutExtension = CreateFileNameWithoutExt(alarmsFileName, dateSection, hourSection); logger.Debug($"FileNameWithoutExt: {fileNameWithoutExtension}"); var filesInProcessing = csvFileQueue.Where(f => f.Contains(dateSection + "_" + hourSection)).ToList(); foreach (var file in filesInProcessing) { logger.Debug($" Alarms files in queue: {file}"); } var candidateFiles = GetMatchingFilesFromFolder(alarmsFileName, uploadedFilesFolder) .Concat(filesInProcessing); foreach (var cf in candidateFiles) { logger.Debug($"\tcdi: {cf}"); } if (candidateFiles.Any(f => f.Contains('['))) { Interlocked.Increment(ref sortToken); var currentSortToken = sortToken; logger.Info($"Sort [{currentSortToken}] the candidates to find last Alarms file name"); var lastFile = candidateFiles.OrderBy(f => f, new FileNameSuffixComparer()).Last(); logger.Info($"Sort [{currentSortToken}] done, Last alarms file full name: {lastFile}"); int startIndex = lastFile.IndexOf('['); int endIndex = lastFile.IndexOf(']'); int number = Convert.ToInt32(lastFile.Substring(startIndex + 1, endIndex - startIndex - 1)); alarmsFilePath = fileNameWithoutExtension + "[" + Convert.ToString(number + 1) + "]" + ".csv"; logger.Debug($"Expected new alarms file: {alarmsFilePath}"); } else if (candidateFiles.Any(f => Path.GetFileNameWithoutExtension(f) == Path.GetFileNameWithoutExtension(fileNameWithoutExtension) || Path.GetFileNameWithoutExtension(f) == Path.GetFileNameWithoutExtension(fileNameWithoutExtension + ".csv"))) { logger.Info("Found a alarms file in `processing file`"); alarmsFilePath = fileNameWithoutExtension + "[0]" + ".csv"; } else { logger.Info("No previous alarms file"); alarmsFilePath = fileNameWithoutExtension + ".csv"; } if (csvFileQueue.Count > 5) { string dqFile; if (csvFileQueue.TryDequeue(out dqFile)) logger.Info($"Dequeued alarms file: {dqFile}"); } } private void SetDeliveriesFile(string requiredFolder) { //It's not possible that within an hour there are multiple fuel deliveries, is it? var dateSection = DateTime.Now.Date.ToString("yyyy-MM-dd"); var hourSection = DateTime.Now.Hour.ToString("D2"); var fileNameWithoutExtension = requiredFolder + folderSeperator + /*deviceId + "_" +*/ deliveriesFileName + "_" + dateSection + "_" + hourSection; deliveriesFilePath = fileNameWithoutExtension + ".csv"; } private void SetTransactionFile(string requiredFolder) { var dateSection = DateTime.Now.Date.ToString("yyyy-MM-dd"); var hourSection = DateTime.Now.Hour.ToString("D2"); var fileNameWithoutExtension = requiredFolder + folderSeperator + /*deviceId + "_" +*/ transactionsFileName + "_" + dateSection + "_" + hourSection; logger.Debug($"FileNameWithoutExt: {fileNameWithoutExtension}"); var filesInProcessing = csvFileQueue.Where(f => f.Contains(dateSection + "_" + hourSection)).ToList(); foreach (var file in filesInProcessing) { logger.Debug($" Transaction files in queue: {file}"); } var candidateFiles = GetMatchingFilesFromFolder(transactionsFileName, uploadedFilesFolder) .Concat(filesInProcessing); foreach (var cf in candidateFiles) { logger.Debug($"\tcdi: {cf}"); } if (candidateFiles.Any(f => f.Contains('['))) { Interlocked.Increment(ref sortToken); var currentSortToken = sortToken; logger.Info($"Sort [{currentSortToken}] the candidates to find last transaction file name"); var lastFile = candidateFiles.OrderBy(f => f, new FileNameSuffixComparer()).Last(); logger.Info($"Sort [{currentSortToken}] done, Last transaction file full name: {lastFile}"); int startIndex = lastFile.IndexOf('['); int endIndex = lastFile.IndexOf(']'); int number = Convert.ToInt32(lastFile.Substring(startIndex + 1, endIndex - startIndex - 1)); transactionsFilePath = fileNameWithoutExtension + "[" + Convert.ToString(number + 1) + "]" + ".csv"; logger.Debug($"Expected new transaction file: {stockLevelsFilePath}"); } else if (candidateFiles.Any(f => Path.GetFileNameWithoutExtension(f) == Path.GetFileNameWithoutExtension(fileNameWithoutExtension) || Path.GetFileNameWithoutExtension(f) == Path.GetFileNameWithoutExtension(fileNameWithoutExtension + ".csv"))) { logger.Info("Found a transaction file in `processing file`"); transactionsFilePath = fileNameWithoutExtension + "[0]" + ".csv"; } else { logger.Info("No previous transaction file"); transactionsFilePath = fileNameWithoutExtension + ".csv"; } if (csvFileQueue.Count > 5) { string dqFile; if (csvFileQueue.TryDequeue(out dqFile)) logger.Info($"Dequeued transaction file: {dqFile}"); } } private void SetTankReadingFile(string folder) { logger.Debug($"Determining TankReading file name, operating folder is: {folder}"); var dateSection = DateTime.Now.Date.ToString("yyyy-MM-dd"); var hourSection = DateTime.Now.Hour.ToString("D2"); var fileNameWithoutExtension = folder + folderSeperator + /*deviceId + "_" +*/ tankReadingFileName + "_" + dateSection + "_" + hourSection; logger.Debug($"FileNameWithoutExt: {fileNameWithoutExtension}"); var filesInProcessing = csvFileQueue.Where(f => f.Contains(dateSection + "_" + hourSection)).ToList(); foreach (var file in filesInProcessing) { logger.Debug($" file in queue: {file}"); } var candidateFiles = GetMatchingFilesFromFolder(tankReadingFileName, uploadedFilesFolder).Concat(filesInProcessing); foreach (var cf in candidateFiles) { logger.Debug($"\tcdi: {cf}"); } if (candidateFiles.Any(f => f.Contains('['))) { Interlocked.Increment(ref sortToken); var currentSortToken = sortToken; logger.Info($"Sort [{currentSortToken}] the candidates to find last stock reading file name"); var lastFile = candidateFiles.OrderBy(f => f, new FileNameSuffixComparer()).Last(); logger.Info($"Sort [{currentSortToken}] done, Last stock reading file full name: {lastFile}"); int startIndex = lastFile.IndexOf('['); int endIndex = lastFile.IndexOf(']'); int number = Convert.ToInt32(lastFile.Substring(startIndex + 1, endIndex - startIndex - 1)); stockLevelsFilePath = fileNameWithoutExtension + "[" + Convert.ToString(number + 1) + "]" + ".csv"; logger.Debug($"Expected new stock reading file: {stockLevelsFilePath}"); } else if (candidateFiles.Any(f => Path.GetFileNameWithoutExtension(f) == Path.GetFileNameWithoutExtension(fileNameWithoutExtension) || Path.GetFileNameWithoutExtension(f) == Path.GetFileNameWithoutExtension(fileNameWithoutExtension + ".csv"))) { logger.Info("Found an in processing file"); stockLevelsFilePath = fileNameWithoutExtension + "[0]" + ".csv"; } else { logger.Info("No previous tank reading file"); stockLevelsFilePath = fileNameWithoutExtension + ".csv"; } if (csvFileQueue.Count > 5) { string dqFile; if (csvFileQueue.TryDequeue(out dqFile)) logger.Info($"Dequeued file: {dqFile}"); } } private void CleanOldFiles(string folder, string fileName) { var di = new DirectoryInfo(folder); var files = di.GetFiles().Where(f => f.Name.Contains(fileName)).OrderByDescending(f => f.LastAccessTime); var oldFiles = files.Skip(10); foreach (var file in oldFiles) { logger.Debug($"Deleting file: {file.FullName}"); file.Delete(); } } private List GetMatchingFilesFromFolder(string fileName, string folder) { logger.Info($"Finding matching {fileName} in folder: {folder}"); var dateSection = DateTime.Now.Date.ToString("yyyy-MM-dd"); var hourSection = DateTime.Now.Hour.ToString("D2"); CleanOldFiles(folder, fileName); var archivedFiles = Directory.GetFiles(folder, "*.zip"); logger.Info($"Archived files count = {archivedFiles.Length}"); var matchedFileNames = archivedFiles .Where(f => f.Contains(dateSection + "_" + hourSection) && f.Contains(fileName) && Regex.IsMatch(Path.GetFileNameWithoutExtension(f), @"[a-zA-Z]{11}_\d{4}-\d{2}-\d{2}_\d{2}\[(.*?)\]")); logger.Info($"Matched files count = {matchedFileNames.Count()}"); if (!matchedFileNames.Any()) { var fileNameWithoutExtension = folder + folderSeperator + fileName + "_" + dateSection + "_" + hourSection; if (File.Exists(fileNameWithoutExtension + ".zip")) return new List { fileNameWithoutExtension }; } return matchedFileNames.ToList(); } private List GetMatchingTransactionFilesFromFolder(string fileName, string folder) { logger.Info($"Finding matching {fileName} in folder: {folder}"); var dateSection = DateTime.Now.Date.ToString("yyyy-MM-dd"); var hourSection = DateTime.Now.Hour.ToString("D2"); CleanOldFiles(folder, fileName); var archivedFiles = Directory.GetFiles(folder, "*.zip"); logger.Info($"Archived files count = {archivedFiles.Length}"); var matchedFileNames = archivedFiles .Where(f => f.Contains(dateSection + "_" + hourSection) && f.Contains(fileName) && Regex.IsMatch(Path.GetFileNameWithoutExtension(f), @"[a-zA-Z]{11}_\d{4}-\d{2}-\d{2}_\d{2}\[(.*?)\]")); logger.Info($"Matched files count = {matchedFileNames.Count()}"); if (!matchedFileNames.Any()) { var fileNameWithoutExtension = folder + folderSeperator + transactionsFileName + "_" + dateSection + "_" + hourSection; if (File.Exists(fileNameWithoutExtension + ".zip")) return new List { fileNameWithoutExtension }; } return matchedFileNames.ToList(); } #endregion #region Start public Task Start() { EnsureFolder(); if (fdcServer != null) fdcServer.OnCurrentFuellingStatusChange += FdcServer_OnCurrentFuellingStatusChange; uploadTimer = new System.Timers.Timer(); uploadTimer.Interval = uploadInterval * 60 * 1000; uploadTimer.Elapsed += Timer_Elapsed; uploadTimer.Enabled = true; tankReadTimer = new System.Timers.Timer(); tankReadTimer.Interval = tankReadInterval * 1000; tankReadTimer.Elapsed += TankReadTimer_Elapsed; tankReadTimer.Enabled = true; logger.Info("\n"); logger.Info("Fairbanks real time data client started"); var config = GetAppConfig(); RefreshConfig(config); return Task.FromResult(true); } #endregion #region Process records //Tank reading private async void TankReadTimer_Elapsed(object sender, ElapsedEventArgs e) { if (autoTankGaugeControllers != null) { var readings = new List(); var atgController = autoTankGaugeControllers.First(); if (atgController.Tanks == null) return; foreach (var tank in atgController.Tanks) { try { var inventory = await atgController.GetTankInventoryAsync(tank.TankNumber, 1); if (inventory != null) { var record = inventory.First(); logger.Info($"Tank No: {record.TankNumber}, Time stamp: {record.TimeStamp}, " + $"Fuel Volume: {record.FuelVolume}, Fuel TCVolume: {record.FuelTCVolume}, " + $"Fuel Height: {record.FuelHeight}, Temperature: {record.Temperture}," + $"Water Height: {record.WaterHeight}, Fuel Height: {record.FuelHeight}"); var reading = new FbTankReading(); reading.ibank = deviceId; reading.DateTime = record.TimeStamp; reading.Tank = record.TankNumber; reading.QTY = Math.Round(Convert.ToDecimal(record.FuelVolume), 2); reading.Temperature = Math.Round(Convert.ToDecimal(record.Temperture), 2); reading.Water = Math.Round(Convert.ToDecimal(record.WaterHeight), 2); reading.Height = Math.Round(Convert.ToDecimal(record.FuelHeight), 2); readings.Add(reading); } } catch (Exception ex) { logger.Error($"Exception in getting tank inventory on Tank {tank.TankNumber}, ex: " + ex.ToString()); } } if (readings.Count > 0) { await AddTankReadingsAsync(readings); } } } //Fuelling transactions private async void FdcServer_OnCurrentFuellingStatusChange(object sender, FdcServerTransactionDoneEventArg e) { if (e.Transaction.Finished) { logger.Info("A finished filling arrived and block the upload timer"); await AddFillingTransactionAsync(e); } } private async Task AddTankReadingsAsync(List readings) { try { fileLocker.TryEnterReadLock(500); SetTankReadingFile(requiredFolder); } catch (Exception ex) { logger.Error(ex.ToString()); } finally { fileLocker.ExitReadLock(); } if (!File.Exists(stockLevelsFilePath)) await WriteCsvAsync(new List(), stockLevelsFilePath); await WriteCsvAsync(readings, stockLevelsFilePath); } private async Task AddFillingTransactionAsync(FdcServerTransactionDoneEventArg e) { var filling = new FbFillingTransaction(); filling.ibank = deviceId; filling.StartDateTime = DateTime.Now; filling.EndDateTime = e.FuelingEndTime.HasValue ? e.FuelingEndTime.Value : DateTime.Now; filling.Pump = e.Transaction.Nozzle.PumpId; filling.Nozzle = e.Transaction.Nozzle.LogicalId; filling.QTY = Convert.ToDecimal(e.Transaction.Volumn) / 100; var fillings = new List(); fillings.Add(filling); logger.Info("Converted fueling transaction"); try { fileLocker.TryEnterReadLock(500); SetTransactionFile(requiredFolder); } catch (Exception ex) { logger.Error("Exception in adding filling transaction, ex: " + ex.ToString()); } finally { fileLocker.ExitReadLock(); } if (!File.Exists(transactionsFilePath)) await WriteCsvAsync(new List(), transactionsFilePath); await WriteCsvAsync(fillings, transactionsFilePath); } public async Task WriteCsvAsync(IEnumerable objInstances, string fileName) { logger.Debug($"Started to write record to file, File name: {fileName}"); Type itemType = typeof(T); var props = itemType.GetProperties(BindingFlags.Public | BindingFlags.Instance); using (FileStream fs = new FileStream(fileName, FileMode.Append, FileAccess.Write)) using (var writer = new StreamWriter(fs, Encoding.UTF8)) { if (objInstances.Count() == 0) { logger.Debug($"Formatting Header"); await writer.WriteLineAsync(string.Join(",", props.Select(p => p.Name))); } else { foreach (var obj in objInstances) { var propertyValues = props.Select(p => p.GetValue(obj, null)).ToList(); StringBuilder sb = new StringBuilder(); for (int i = 0; i < propertyValues.Count(); i++) { if (propertyValues[i] is DateTime) sb.Append(((DateTime)propertyValues[i]).ToString("yyyy-MM-dd HH:mm:ss")); else sb.Append(propertyValues[i]); if (i <= propertyValues.Count() - 2) sb.Append(","); } //sb.Append("\r"); await writer.WriteLineAsync(sb.ToString()); } } } logger.Debug("Writing done"); } private async void Timer_Elapsed(object sender, ElapsedEventArgs e) { List files = new List(); try { fileLocker.TryEnterReadLock(500); files = Directory.GetFiles(requiredFolder, "*.csv").ToList(); foreach (var file in files) { if (!csvFileQueue.Contains(file)) { logger.Debug($" Enqueuing CSV file: {file}"); csvFileQueue.Enqueue(file); } } } catch (Exception ex) { logger.Error(ex.ToString()); } finally { fileLocker.ExitReadLock(); } var fileCount = files.Count(); logger.Debug($"File count: {fileCount}"); fbSftpClient.Connect(); foreach (var currentFile in files) { logger.Info($"File full name: {currentFile}"); File.Move(currentFile, tempFolder + folderSeperator + Path.GetFileName(currentFile)); var zippedFileName = Path.GetFileNameWithoutExtension(currentFile) + ".zip"; ZipFile.CreateFromDirectory(tempFolder, requiredFolder + folderSeperator + zippedFileName); File.Delete(tempFolder + folderSeperator + Path.GetFileName(currentFile)); var sendResult = fbSftpClient.UploadSingleFile(requiredFolder + folderSeperator + zippedFileName, hostUploadFolder); await SaveUploadHistory(zippedFileName, sendResult); ArchiveFile(requiredFolder + folderSeperator + zippedFileName); } fbSftpClient.Disconnect(); } private void ArchiveFile(string currentFile) { logger.Info($"Archiving file: {currentFile}"); try { File.Move(currentFile, uploadedFilesFolder + folderSeperator + Path.GetFileName(currentFile)); } catch (IOException ex) { logger.Error("Error in archiving file: " + currentFile + "\n" + ex.ToString()); } } #endregion #region Upload history [UniversalApi(Description = "Leave input null to retrieve latest Day's(24 hours) upload history" + "
input parameters are as follows, " + "
para.Name==\"startTime\", format should be yyyyMMddHHmmss")] public async Task> GetUploadHistoryAsync(ApiData input) { if (input == null || (input?.IsEmpty() ?? false)) { return await dbHelper.GetLatestUploadHistoryAsync(DateTime.Now.Subtract(new TimeSpan(24, 0, 0))); } else { var startTime = input.Get("starttime", DateTime.Now.Subtract(new TimeSpan(24, 0, 0))); return await dbHelper.GetLatestUploadHistoryAsync(startTime); } } #endregion #region Stop public Task Stop() { return Task.FromResult(true); } #endregion #region Config handling [UniversalApi(Description = "Leave input null to retrieve Fairbanks related Config" + "
First para.Name==\"template\" will return an sample Config for reference.")] public async Task GetAppConfigAsync(ApiData input) { var uploadingConfig = new AppConfig(); if (input?.Get("template") != null) { logger.Info($"GetAppConfigAsync with template"); } else { uploadingConfig = GetAppConfig(); } logger.Info($"GetConfigAsync, Fairbanks app config: {uploadingConfig.ToString()}"); return await Task.FromResult(new[] { new { Name = "FairbanksAppConfig", Value = uploadingConfig } }); } [UniversalApi(Description = "First para.Name is the updating config name, such as: FairbanksAppConfig, " + "
First para.Value is the new config's json serialized string value")] public async Task PutConfigAsync(ApiData input) { //Check input if (input == null || input.Parameters == null || !input.Parameters.Any()) { throw new ArgumentException(nameof(input)); } try { AppConfig appConfig = GetConfig(input.Parameters.First().Name, input.Parameters.First().Value, new JsonSerializerOptions { PropertyNameCaseInsensitive = true, }); bool appConfigExists = objMapper .Map>(dbContext.GenericDatas .Where(gd => gd.Type == AutoMapperProfile.UploadConfig && gd.Owner == AutoMapperProfile.AppDataOwner)) .FirstOrDefault() != null; if (!appConfigExists) { //Create the config record await SaveDbAsync(appConfig); logger.Info($"PutConfigAsync created config type: {appConfig.GetType().Name}, details: {appConfig.ToString()}"); } else { //Update the config record await UpdateDbAsync(appConfig); logger.Info($"PutConfigAsync updated config type: {appConfig.GetType().Name}, details: {appConfig.ToString()}"); } RefreshConfig(appConfig); } catch (Exception ex) { logger.Error(ex.ToString()); return await Task.FromResult(new { Name = "FairbanksAppConfig", Value = false }); } return await Task.FromResult(new { Name = "FairbanksAppConfig", Value = true }); } static AppConfig GetConfig(string name, string value, JsonSerializerOptions options) => name switch { "FairbanksAppConfig" => JsonSerializer.Deserialize(value, options), _ => throw new InvalidOperationException("Unknown Config name: " + (name ?? "")) }; public AppConfig GetAppConfig() { return objMapper .Map>(dbContext.GenericDatas .Where(d => d.Type == AutoMapperProfile.UploadConfig && d.Owner == AutoMapperProfile.AppDataOwner)) .FirstOrDefault() ?? new AppConfig { DeviceId = this.deviceId, Host = this.host, Port = this.port, UserName = this.username, Password = this.password, SiteId = this.siteName, TankReadInterval = this.tankReadInterval, UploadInterval = this.uploadInterval }; } #endregion #region Private methods private void RefreshConfig(AppConfig config) { if (config == null) return; logger.Info("Refreshing config"); deviceId = config.DeviceId; siteName = config.SiteId; tankReadTimer.Stop(); tankReadTimer.Interval = config.TankReadInterval * 1000; tankReadTimer.Start(); uploadTimer.Stop(); uploadTimer.Interval = config.UploadInterval * 60 * 1000; uploadTimer.Start(); fbSftpClient.RefreshConfig(config.Host, config.UserName, config.Password); } private async Task SaveUploadHistory(string fileName, bool status) { var currentUploadRecord = new UploadRecord(); currentUploadRecord.TimeStamp = DateTime.Now; currentUploadRecord.FileName = fileName; currentUploadRecord.Status = status ? "成功" : "失败"; currentUploadRecord.Remark = dbHelper.RecordUploadTried(fileName) ? "重试" : "首次上传"; await SaveDbAsync(currentUploadRecord); } private async Task SaveDbAsync(T data) { var dbModel = objMapper.Map(data); dbContext.GenericDatas.Add(dbModel); await dbContext.SaveChangesAsync(); } private async Task UpdateDbAsync(T data) { var dbModel = objMapper.Map(data); dbContext.GenericDatas.Update(dbModel); await dbContext.SaveChangesAsync(); } #endregion } public class FairbanksAppConfigV1 { /// /// FTP host /// public string Host { get; set; } /// /// FTP port /// public int Port { get; set; } /// /// FTP user name /// public string Username { get; set; } /// /// FTP user password /// public string Password { get; set; } /// /// Device ID /// public string DeviceId { get; set; } /// /// Site name /// public string SiteName { get; set; } /// /// Tank reading interval, in seconds. /// public int Interval { get; set; } /// /// Upload interval, in minutes. /// public int UploadInterval { get; set; } /// /// Host folder path /// public string HostUploadFolder { get; set; } } }