using Edge.Core.Processor; using Edge.Core.IndustryStandardInterface.Pump; using Edge.Core.IndustryStandardInterface.ATG; using Edge.Core.UniversalApi; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using VeederRoot_ATG_Console.MessageEntity; using VeederRoot_ATG_Console.MessageEntity.DispenserInterface.Outgoing; using VeederRoot_ATG_Console.MessageEntity.Incoming; using VeederRoot_ATG_Console.MessageEntity.Outgoing; using Wayne.FDCPOSLibrary; using static VeederRoot_ATG_Console.MessageEntity.Incoming.QueryInTankInventoryReportResponse.InventoryData; using static VeederRoot_ATG_Console.MessageEntity.Incoming.QueryInTankStatusReportResponse; using Edge.Core.Processor.Dispatcher.Attributes; using Edge.Core.Processor.Communicator; namespace VeederRoot_ATG_Console { [UniversalApi(Name = "OnStateChange", EventDataType = typeof(AtgStateChangeEventArg))] [UniversalApi(Name = GenericAlarm.UniversalApiEventName, EventDataType = typeof(GenericAlarm[]), Description = "Fire GenericAlarms to AlarmBar for attracting users.")] [MetaPartsRequired(typeof(HalfDuplexActivePollingDeviceProcessor<,>))] [MetaPartsRequired(typeof(ComPortCommunicator<>))] [MetaPartsRequired(typeof(TcpClientCommunicator<>))] [MetaPartsRequired(typeof(TcpServerCommunicator<>))] [MetaPartsDescriptor( "lang-zh-cn:维德路特液位仪lang-en-us:VeederRoot ATG", "lang-zh-cn:用于驱动维德路特协议(或兼容)的液位仪控制台lang-en-us:Used for driven ATG console that use VeederRoot ATG protocol(or compatible with)", new[] { "lang-zh-cn:液位仪lang-en-us:ATG" })] public class Handler : TestableActivePollingDeviceHandler, IAutoTankGaugeController { private ILogger logger = NullLogger.Instance; /// /// Range from 0 to 9. /// Event Message Identifier. /// Start Events and Stop Events contain event IDs to help the dispenser /// interface module identify transmissions that are repeated as a result of /// communication errors. /// Once an event report (start or end) is successfully transmitted, /// the Event Message ID must change so the next event report (start or end) /// will get a new ID in the range 0 - 9. /// An event report must keep the same ID until it is successfully transmitted. /// The status report does not require an ID. /// public static byte nextRotateEventId; private DateTime lastLogicalDeviceStateReceivedTime = DateTime.Now; // by seconds, change this value need change the correlated deviceOfflineCountdownTimer's interval as well public const int lastLogicalDeviceStateExpiredTime = 15; private System.Timers.Timer deviceOfflineCountdownTimer; private IContext context; private IEnumerable tanks; /// /// private int loadAsync_Guard = 0; private IServiceProvider services; public string MetaConfigName => "VeederRoot_ATG_Console"; public IEnumerable Tanks => this.tanks; private int deviceId; public event EventHandler OnStateChange; public event EventHandler OnAlarm; public int DeviceId => this.deviceId; public SystemUnit SystemUnit { get; private set; } public AtgState State { get; private set; } = AtgState.Offline; #region UniversalApi Service [UniversalApi()] public Task> GetTanksAsync() { return Task.FromResult(this.tanks); } #endregion public Handler(int deviceId, IServiceProvider services) { this.services = services; var loggerFactory = services.GetRequiredService(); this.logger = loggerFactory.CreateLogger("PumpHandler"); this.deviceId = deviceId; this.deviceOfflineCountdownTimer = new System.Timers.Timer(3000); this.deviceOfflineCountdownTimer.Elapsed += async (_, __) => { if (DateTime.Now.Subtract(this.lastLogicalDeviceStateReceivedTime).TotalSeconds >= lastLogicalDeviceStateExpiredTime) { if (this.State != AtgState.Offline) { this.State = AtgState.Offline; logger.LogInformation("VeederRoot ATG Console with Id: " + this.deviceId + ", " + " State switched to OFFLINE due to long time no see data incoming"); var onStateChangeEventArg = new AtgStateChangeEventArg(this.State, "VR ATG switched to Offline due to device offline timer timeout reached"); this.OnStateChange?.Invoke(this, onStateChangeEventArg); var universalApiHub = this.services.GetRequiredService(); await universalApiHub.FireEvent(this.context.Processor, "OnStateChange", onStateChangeEventArg); } } }; this.deviceOfflineCountdownTimer.Start(); } /// /// Notify the Veeder Root ATG console that a fueling has started or done. /// /// start or stop event which for notify ATG console. /// public async Task NotifyFuelTrxEventToAtgConsoleAsync(MessageEntity.DispenserInterface.Outgoing.OutgoingMessageBase startOrStopEventReportRequest) { MessageEntity.DispenserInterface.Incoming.GenericResponse response = null; startOrStopEventReportRequest.EventId = nextRotateEventId++; if (nextRotateEventId > 9) nextRotateEventId = 0; await Task.Factory.StartNew(() => { ManualResetEvent block = new ManualResetEvent(false); this.context.Outgoing.WriteAsync( startOrStopEventReportRequest, (req, resp) => resp is MessageEntity.DispenserInterface.Incoming.GenericResponse, (req, resp) => { if (resp != null) { var data = resp as MessageEntity.DispenserInterface.Incoming.GenericResponse; response = data; } else logger.LogError("NotifyFuelTrxEventToAtgConsoleAsync failed with timed out"); block.Set(); }, 2000); block.WaitOne(5 * 1000); }); return response; } public override void Init(IContext context) { base.Init(context); this.context = context; this.context.Processor.Communicator.OnDisconnected += async (_, __) => { if (this.State != AtgState.Offline) { this.State = AtgState.Offline; logger.LogInformation("VeederRoot ATG Console with Id: " + this.deviceId + ", " + " State switched to OFFLINE due to Communicator.OnDisconnected event received"); var onStateChangeEventArg = new AtgStateChangeEventArg(this.State, "VR ATG switched to Offline due to Communicator OnDisconnected"); this.OnStateChange?.Invoke(this, onStateChangeEventArg); var universalApiHub = this.services.GetRequiredService(); await universalApiHub.FireEvent(this.context.Processor, "OnStateChange", onStateChangeEventArg); } }; var timeWindowWithActivePollingOutgoing = this.context.Outgoing as TimeWindowWithActivePollingOutgoing; DateTime? lastPollingRequestSendTime = null; VeederRoot_ATG_Console.MessageEntity.Outgoing.OutgoingMessageBase lastPollingMessage = null; timeWindowWithActivePollingOutgoing.PollingMsgProducer = () => { /* for quickly send out the request in polling queue to real device, * the polling interval configurated in TimeWindowWithActivePollingOutgoing would not be set long, say 500ms, * but, this also make this PollingMsgProducer called too fast which may not proper for this VR device, * so below we're trying to slow it down to 2 seconds. */ if (lastPollingRequestSendTime == null) { lastPollingRequestSendTime = DateTime.Now; var polling = new QueryTimeOfDayRequest(MessageBase.MessageFormat.Computer); return polling; } else if (DateTime.Now.Subtract(lastPollingRequestSendTime.Value).TotalSeconds >= 2) { /* every 2 seconds query tanks' alarms and status, the response handle will be in function: Process(..., ...) */ lastPollingRequestSendTime = DateTime.Now; if (lastPollingMessage == null || lastPollingMessage is QueryInTankStatusReportRequest) { /* for query tank status, like if in delivery or not*/ lastPollingMessage = new QueryInTankInventoryReportRequest(MessageBase.MessageFormat.Computer, 0); } else { /* for query tank alarms*/ lastPollingMessage = new QueryInTankStatusReportRequest(MessageBase.MessageFormat.Computer, 0); } return lastPollingMessage; } return null; }; } public override async Task Process(IContext context) { this.lastLogicalDeviceStateReceivedTime = DateTime.Now; if (this.State == AtgState.Offline) { if (1 == Interlocked.CompareExchange(ref this.loadAsync_Guard, 1, 0)) return; try { logger.LogInformation($"VR ATG is switching From Offline to Idle by received a Device Msg: {(context.Incoming.Message?.ToLogString() ?? "")}, will reloading tanks..."); this.tanks = await this.LoadAsync(); if (this.tanks == null) { logger.LogInformation($"VR ATG switching to Idle failed as failure in Load tanks, will treat ATG still in Offline"); return; } logger.LogInformation($"Loaded Tanks info(total: {this.tanks.Count()}): " + Environment.NewLine + this.tanks.Select(t => $"Tank with TankNumber: { t.TankNumber} => ProductCode: { (t.Product?.ProductCode ?? "")}, " + $"ProductLabel: {t.Product?.ProductLabel ?? ""}, Diameter: {t.Diameter ?? -1}, " + $"TankState: {t.State}, ProbeLength: {t.Probe.ProbeLength}, ProbeState: {t.Probe.State ?? ""}" + //$"ProbeReadings: ({t.Probe.ProbeReading.ToString()}) "").Aggregate((acc, n) => acc + Environment.NewLine + n)); this.State = AtgState.Idle; } catch (Exception eeee) { logger.LogError($"VR ATG switching to Idle got exception: {eeee}{Environment.NewLine}Will treat ATG still in Offline"); return; } finally { this.loadAsync_Guard = 0; } var onStateChangeEventArg = new AtgStateChangeEventArg(AtgState.TanksReloaded, "VR ATG switched from Offline to Online due to Process(...) get called"); this.OnStateChange?.Invoke(this, onStateChangeEventArg); var universalApiHub = this.services.GetRequiredService(); await universalApiHub.FireEvent(this.context.Processor, "OnStateChange", onStateChangeEventArg); } if (context.Incoming.Message is QueryInTankStatusReportResponse pollingQueryTankAlarmResponse) { var alarmsForTanks = pollingQueryTankAlarmResponse?.TanksOfAlarms.Select(ta => { var alarms = new List(); alarms.AddRange(ta?.Alarms.Select(aType => new Edge.Core.IndustryStandardInterface.ATG.Alarm() { TankNumber = (byte)ta.TankNumber, Priority = AlarmPriority.Alarm, Type = aType, CreatedTimeStamp = DateTime.Now, })); return alarms; }); foreach (var alarmsForTank in alarmsForTanks) { var alarmEventArg = new AtgAlarmEventArg( alarmsForTank.First().TankNumber, alarmsForTank.Select(alarm => new Alarm() { TankNumber = alarmsForTank.First().TankNumber, Priority = AlarmPriority.Alarm, Type = alarm.Type, CreatedTimeStamp = DateTime.Now, Description = alarmsForTank.First().TankNumber + "号油罐 报告其状态为:" + alarm.Type })); this.OnAlarm?.Invoke(this, alarmEventArg); } var universalApiHub = this.services.GetRequiredService(); await universalApiHub.FireEvent(this.context.Processor, GenericAlarm.UniversalApiEventName, alarmsForTanks.SelectMany(als => als, (als, al) => new GenericAlarm() { Category = "VeederRoot ATG Alarms from tanks", Title = $"Tank {al.TankNumber} is alarming", Detail = $"{al.Description}", Severity = (al.Priority == AlarmPriority.Alarm ? GenericAlarmSeverity.Error : GenericAlarmSeverity.Warning), })); } else if (context.Incoming.Message is QueryInTankInventoryReportResponse pollingQueryTankStateResponse && pollingQueryTankStateResponse.InventoryDatas != null) { foreach (var data in pollingQueryTankStateResponse.InventoryDatas) { var targetTank = this.tanks.FirstOrDefault(t => t.TankNumber == (byte)(data.TankNumber ?? -1)); if (targetTank == null) continue; var deliveryingStateReportedFromDevice = data.States?.Exists(s => s == QueryInTankInventoryReportResponse.InventoryData.TankState.DeliveryInProgress_LSB) ?? false; if (deliveryingStateReportedFromDevice && targetTank.State != Edge.Core.IndustryStandardInterface.ATG.TankState.Delivering) { targetTank.State = Edge.Core.IndustryStandardInterface.ATG.TankState.Delivering; this.logger.LogInformation("VR ATG has a tank entered AtgTankState.Delivering due to TankState.DeliveryInProgress_LSB reported for tankNumber: " + (data.TankNumber ?? -1)); } else if (!deliveryingStateReportedFromDevice && targetTank.State == Edge.Core.IndustryStandardInterface.ATG.TankState.Delivering) { targetTank.State = Edge.Core.IndustryStandardInterface.ATG.TankState.Idle; this.logger.LogInformation("VR ATG has a tank quit from AtgTankState.Delivering for tankNumber: " + (data.TankNumber ?? -1)); } else return; var evtArg = new AtgStateChangeEventArg(targetTank, this.State, "the target tank changed tank state to: " + targetTank.State); this.OnStateChange?.Invoke(this, evtArg); var universalApiHub = this.services.GetRequiredService(); await universalApiHub.FireEvent(this.context.Processor, "OnStateChange", evtArg); } } } private async Task> LoadAsync() { this.logger.LogInformation("VR ATG is starting LoadAsync()..."); var _ = await this.context.Outgoing.WriteAsync( new QuerySystemTypeAndLanguageFlagsRequest(MessageBase.MessageFormat.Computer), (req, testResp) => testResp is QueryOrSetSystemTypeAndLanguageFlagsResponse, 5000); if (_ is QueryOrSetSystemTypeAndLanguageFlagsResponse querySystemTypeAndLanguageFlagsResponse) { this.lastLogicalDeviceStateReceivedTime = DateTime.Now; this.logger.LogInformation($" VR ATG, querySystemTypeAndLanguageFlagsResponse: {querySystemTypeAndLanguageFlagsResponse.ToLogString()}"); var timeGapBySeconds = DateTime.Now.Subtract((querySystemTypeAndLanguageFlagsResponse.CurrentDateAndTime ?? DateTime.Now)).TotalSeconds; if (timeGapBySeconds >= 60) { this.logger.LogInformation($" VR ATG has its CurrentDateAndTime differency from fusion time with gap of TotalSeconds: " + $"{timeGapBySeconds}, will Send SetTimeOfDayRequest for align 2 sides..."); var setTimeResponse = await this.context.Outgoing.WriteAsync( new SetTimeOfDayRequest(MessageBase.MessageFormat.Computer, DateTime.Now), (req, testResp) => testResp is MessageBaseGeneric, 5000); if (setTimeResponse == null) this.logger.LogInformation($" VR ATG SetTimeOfDayRequest failed due to response timedout"); } } else { _ = await this.context.Outgoing.WriteAsync( new QuerySystemTypeAndLanguageFlags_Extended(MessageBase.MessageFormat.Computer), (req, testResp) => testResp is QueryOrSetSystemTypeAndLanguageFlagsResponse, 3000); if (_ is QueryOrSetSystemTypeAndLanguageFlagsResponse querySystemTypeAndLanguageFlagsResponse_Extended) { this.lastLogicalDeviceStateReceivedTime = DateTime.Now; this.logger.LogInformation($" VR ATG QuerySystemTypeAndLanguageFlags_Extended responded: {querySystemTypeAndLanguageFlagsResponse_Extended.ToLogString()}"); this.SystemUnit = querySystemTypeAndLanguageFlagsResponse_Extended?.SystemUnits ?? SystemUnit.Metric; } else this.logger.LogInformation(" Seems this ATG console does not " + "support 'QuerySystemTypeAndLanguageFlags' since the query returned with response: " + (_?.ToLogString() ?? "") + Environment.NewLine + "no way to know the ATG Console System's Unit"); } var queryInTankInventoryReportResponse = await this.context.Outgoing.WriteAsync( new QueryInTankInventoryReportRequest(MessageBase.MessageFormat.Computer, 0), (req, testResp) => testResp is QueryInTankInventoryReportResponse, 4000) as QueryInTankInventoryReportResponse; //var queryInTankDiagnosticReportResponse = _ as QueryInTankDiagnosticReportResponse; if (queryInTankInventoryReportResponse == null) { this.logger.LogInformation(" QueryInTankInventoryReportResponse timed out, treat as ReloadTanks failed."); return null; //below request is used for get probe info var queryInTankDiagnosticReportResponse = await this.context.Outgoing.WriteAsync( new QueryInTankDiagnosticReportRequest(MessageBase.MessageFormat.Computer, 0), (req, testResp) => testResp is QueryInTankDiagnosticReportResponse, 4000) as QueryInTankDiagnosticReportResponse; } _ = await this.context.Outgoing.WriteAsync( new QueryTankDiameterRequest(MessageBase.MessageFormat.Computer, 0), (req, testResp) => testResp is QueryOrSetTankDiameterResponse, 4000); var queryOrSetTankDiameterResponse = _ as QueryOrSetTankDiameterResponse; if (queryOrSetTankDiameterResponse == null) { this.logger.LogInformation(" QueryOrSetTankDiameterResponse timed out, so diameter value will not presents."); } _ = await this.context.Outgoing.WriteAsync( new QueryTankProductLabelRequest(MessageBase.MessageFormat.Computer, 0), (req, testResp) => testResp is QueryOrSetTankProductLabelResponse, 5000); var queryOrSetTankProductLabelResponse = _ as QueryOrSetTankProductLabelResponse; if (queryOrSetTankProductLabelResponse == null) { this.logger.LogInformation(" QueryOrSetTankProductLabelResponse timed out, so product name value will not presents."); } //this.logger.LogInformation(queryOrSetTankProductLabelResponse.ToLogString()); //var probeReadings = await this.GetTankProbeReadingsAsync(null); var innerTanks = new List(); foreach (var inventoryData in queryInTankInventoryReportResponse.InventoryDatas) { innerTanks.Add( new Tank() { TankNumber = (byte)(inventoryData?.TankNumber ?? -1), State = Edge.Core.IndustryStandardInterface.ATG.TankState.Idle, Diameter = queryOrSetTankDiameterResponse?.Diameters?.FirstOrDefault(pl => pl.Item1 == (inventoryData.TankNumber ?? -1))?.Item2 ?? -1, Product = new Product() { ProductCode = inventoryData.ProductCode?.ToString() ?? "", ProductLabel = queryOrSetTankProductLabelResponse?.ProductLabels?.FirstOrDefault(pl => pl.Item1 == (inventoryData.TankNumber ?? -1))?.Item2 ?? "" }, Limit = new TankLimit() { }, Probe = new Probe() { ProbeLength = -1, //ProbeReading = probeReadings.First(pl => pl.Item1 == probe.TankNumber).Item2, }, }); } return innerTanks; } /// /// /// /// 0 or null for query all tanks /// [UniversalApi(Description = "", InputParametersExampleJson = "[1]")] public async Task GetTankReadingAsync(int tankNumber) { //some china produced ATG may not support specific tank number query, so here have to pass in hardcoded 0 for query all tanks, and then filter out //the data for target tank var inventoryReportResponse = await this.context.Outgoing.WriteAsync( new QueryInTankInventoryReportRequest(MessageBase.MessageFormat.Computer, 0), (req, testResp) => testResp is QueryInTankInventoryReportResponse r, 5000) as QueryInTankInventoryReportResponse; if (inventoryReportResponse == null) throw new TimeoutException($"GetTankReadingAsync, QueryInTankInventoryReportRequest timedout"); this.lastLogicalDeviceStateReceivedTime = DateTime.Now; if (tankNumber != 0) { var targetTankData = inventoryReportResponse.InventoryDatas.FirstOrDefault(id => id.TankNumber == tankNumber); if (targetTankData == null) throw new ArgumentException($"GetTankReadingAsync, Target tank with number: {tankNumber} does not have InventoryReport queried, you may input the not existing tank number?"); } //if caller input tanknumber 0, then randomly give back a single data, otherwise filter out the data by tanknumber. return inventoryReportResponse.InventoryDatas.Where(id => tankNumber == 0 ? true : id.TankNumber == tankNumber) .Select(rd => { #region parse each fields int tankNumber = rd.TankNumber ?? -1; double? volume = rd.TankReadingDatas.ContainsKey(TankReadingDataName.Volume) ? rd.TankReadingDatas[TankReadingDataName.Volume] : default(double?); double? tc_Volume = rd.TankReadingDatas.ContainsKey(TankReadingDataName.TC_Volume) ? rd.TankReadingDatas[TankReadingDataName.TC_Volume] : default(double?); double? ullage = rd.TankReadingDatas.ContainsKey(TankReadingDataName.Ullage) ? rd.TankReadingDatas[TankReadingDataName.Ullage] : default(double?); double? height = rd.TankReadingDatas.ContainsKey(TankReadingDataName.Height) ? rd.TankReadingDatas[TankReadingDataName.Height] : default(double?); double? water = rd.TankReadingDatas.ContainsKey(TankReadingDataName.Water) ? rd.TankReadingDatas[TankReadingDataName.Water] : default(double?); double? temperature = rd.TankReadingDatas.ContainsKey(TankReadingDataName.Temperature) ? rd.TankReadingDatas[TankReadingDataName.Temperature] : default(double?); double? waterVolume = rd.TankReadingDatas.ContainsKey(TankReadingDataName.WaterVolume) ? rd.TankReadingDatas[TankReadingDataName.WaterVolume] : default(double?); #endregion //this.logger.LogInformation("QueryInTankInventoryReportDatas: " // + "TankNumber: " + ((rd?.TankNumber.Value.ToString()) ?? "") // + ", ProductCode: " + ((rd?.ProductCode.Value.ToString()) ?? "") // + ", State: " + (rd.States.Any() ? rd.States.First().ToString() : "") // + ", Volume: " + (volume ?? -1) // + ", TcVolume: " + (tc_Volume ?? -1) // + ", Ullage: " + (ullage ?? -1) // + ", Height: " + (height ?? -1) // + ", Water: " + (water ?? -1) // + ", Temperature: " + (temperature ?? -1) // + ", WaterVolume: " + (waterVolume ?? -1)); var tankReading = new TankReading() { TankNumber = tankNumber, Volume = volume, TcVolume = tc_Volume, Ullage = ullage, Height = height, Water = water, Temperature = temperature, WaterVolume = waterVolume }; return tankReading; }).FirstOrDefault(); } /// /// Get reading for all tanks. /// /// reading for all tanks [UniversalApi(Description = "Get all tanks' reading")] public async Task> GetTanksReadingAsync() { //some china produced ATG may not support specific tank number query, so here have to pass in hardcoded 0 for query all tanks, and then filter out //the data for target tank var inventoryReportResponse = await this.context.Outgoing.WriteAsync( new QueryInTankInventoryReportRequest(MessageBase.MessageFormat.Computer, 0), (req, testResp) => testResp is QueryInTankInventoryReportResponse r, 5000) as QueryInTankInventoryReportResponse; if (inventoryReportResponse == null) throw new TimeoutException($"GetTanksReadingAsync timedout"); this.lastLogicalDeviceStateReceivedTime = DateTime.Now; return inventoryReportResponse.InventoryDatas.Select(rd => { #region parse each fields int tankNumber = rd.TankNumber ?? -1; double? volume = rd.TankReadingDatas.ContainsKey(TankReadingDataName.Volume) ? rd.TankReadingDatas[TankReadingDataName.Volume] : default(double?); double? tc_Volume = rd.TankReadingDatas.ContainsKey(TankReadingDataName.TC_Volume) ? rd.TankReadingDatas[TankReadingDataName.TC_Volume] : default(double?); double? ullage = rd.TankReadingDatas.ContainsKey(TankReadingDataName.Ullage) ? rd.TankReadingDatas[TankReadingDataName.Ullage] : default(double?); double? height = rd.TankReadingDatas.ContainsKey(TankReadingDataName.Height) ? rd.TankReadingDatas[TankReadingDataName.Height] : default(double?); double? water = rd.TankReadingDatas.ContainsKey(TankReadingDataName.Water) ? rd.TankReadingDatas[TankReadingDataName.Water] : default(double?); double? temperature = rd.TankReadingDatas.ContainsKey(TankReadingDataName.Temperature) ? rd.TankReadingDatas[TankReadingDataName.Temperature] : default(double?); double? waterVolume = rd.TankReadingDatas.ContainsKey(TankReadingDataName.WaterVolume) ? rd.TankReadingDatas[TankReadingDataName.WaterVolume] : default(double?); #endregion //this.logger.LogInformation("QueryInTankInventoryReportDatas: " // + "TankNumber: " + ((rd?.TankNumber.Value.ToString()) ?? "") // + ", ProductCode: " + ((rd?.ProductCode.Value.ToString()) ?? "") // + ", State: " + (rd.States.Any() ? rd.States.First().ToString() : "") // + ", Volume: " + (volume ?? -1) // + ", TcVolume: " + (tc_Volume ?? -1) // + ", Ullage: " + (ullage ?? -1) // + ", Height: " + (height ?? -1) // + ", Water: " + (water ?? -1) // + ", Temperature: " + (temperature ?? -1) // + ", WaterVolume: " + (waterVolume ?? -1)); var tankReading = new TankReading() { TankNumber = tankNumber, Volume = volume, TcVolume = tc_Volume, Ullage = ullage, Height = height, Water = water, Temperature = temperature, WaterVolume = waterVolume }; return tankReading; }); } [UniversalApi(Description = "ONLY the deliveries read from ATG device side with timestamp > filterTimestamp will be returned.", InputParametersExampleJson = "[1,8,0,\"2020-04-01T18:25:43.511Z\"]")] public async Task> GetTankDeliveryAsync(int tankNumber, int pageRowCount = 10, int pageIndex = 0, DateTime? filterTimestamp = null) { this.logger.LogDebug($"VR ATG is GetTankDeliveryAsync for tankNumber: {tankNumber} with filterTimestamp: {(filterTimestamp?.ToString("yyyy-MM-dd HH:mm:ss fff") ?? "")}"); var response = await this.context.Outgoing.WriteAsync( new QueryInTankMostRecentDeliveryReportRequest(MessageBase.MessageFormat.Computer, tankNumber), (req, testResp) => testResp is QueryInTankMostRecentDeliveryReportResponse r && r.TankNumberInFunctionCode == tankNumber, 20000); var mostRecentDeliveryReportResponse = response as QueryInTankMostRecentDeliveryReportResponse; if (response == null) throw new TimeoutException($" QueryInTankMostRecentDeliveryReportRequest(tankNumber: {tankNumber}) timedout"); this.logger.LogDebug($" GetTankDeliveryAsync for tankNumber: {tankNumber} got mostRecentDeliveryReportResponse: {mostRecentDeliveryReportResponse.ToLogString()}"); var results = mostRecentDeliveryReportResponse.DeliveriesForTanks.SelectMany(d => d.Deliveries) .Select(queried => new Delivery() { TankNumber = (byte)(queried.TankNumber ?? -1), StartingDateTime = queried.StartingDateTime.Value, StartingFuelHeight = queried.Datas.FirstOrDefault(d => d.Key == DeliveryReadingDataName.StartingHeight).Value, StartingFuelVolume = queried.Datas.FirstOrDefault(d => d.Key == DeliveryReadingDataName.StartingVolume).Value, StartingFuelTCVolume = queried.Datas.FirstOrDefault(d => d.Key == DeliveryReadingDataName.StartingTcVolume).Value, StartingTemperture = queried.Datas.FirstOrDefault(d => d.Key == DeliveryReadingDataName.StartingTemp).Value, StartingWaterHeight = queried.Datas.FirstOrDefault(d => d.Key == DeliveryReadingDataName.StartingWater).Value, EndingDateTime = queried.EndingDateTime, EndingFuelHeight = queried.Datas.FirstOrDefault(d => d.Key == DeliveryReadingDataName.EndingHeight).Value, EndingFuelVolume = queried.Datas.FirstOrDefault(d => d.Key == DeliveryReadingDataName.EndingVolume).Value, EndingFuelTCVolume = queried.Datas.FirstOrDefault(d => d.Key == DeliveryReadingDataName.EndingTcVolume).Value, EndingTemperture = queried.Datas.FirstOrDefault(d => d.Key == DeliveryReadingDataName.EndingTemp).Value, EndingWaterHeight = queried.Datas.FirstOrDefault(d => d.Key == DeliveryReadingDataName.EndingWater).Value, }) .Where(d => d.StartingDateTime > (filterTimestamp ?? DateTime.MinValue)) .OrderByDescending(d => d.StartingDateTime).Skip(pageRowCount * pageIndex).Take(pageRowCount); this.logger.LogDebug($" TankNumber: {tankNumber} has {results.Count()} delivery records satisfy filter timestamp({(filterTimestamp?.ToString("yyyy-MM-dd HH:mm:ss fff") ?? "")}) condition."); return results; } //public async Task> GetTankActiveOrUnackedAlarmsAsync(int tankNumber, int count) //{ // var response = await this.context.Outgoing.WriteAsync( // new QueryInTankStatusReportRequest(MessageBase.MessageFormat.Computer, tankNumber), // (req, testResp) => testResp is MessageBaseGeneric, 3000); // if (response == null) throw new TimeoutException($"QueryInTankStatusReportRequest(tankNumber: {tankNumber}) timedout"); // var queryInTankStatusReportResponse = response as QueryInTankStatusReportResponse; // return queryInTankStatusReportResponse?.TanksOfAlarms; //} [UniversalApi(Description = "", InputParametersExampleJson = "[1,8,0,\"2020-04-01T18:25:43.511Z\"]")] public async Task> GetTankInventoryAsync(int tankNumber, int pageRowCount = 10, int pageIndex = 0, DateTime? filterTimestamp = null) { var reading = await this.GetTankReadingAsync(tankNumber); if (reading == null) return null; var inventory = new Inventory() { TankNumber = tankNumber, FuelHeight = reading.Height ?? -1, FuelVolume = reading.Volume ?? -1, FuelTCVolume = reading.TcVolume ?? -1, WaterHeight = reading.Water ?? -1, Temperture = reading.Temperature ?? int.MinValue, TimeStamp = DateTime.Now, }; return new[] { inventory }; } [UniversalApi(Description = "", InputParametersExampleJson = "[1,8,0,\"2020-04-01T18:25:43.511Z\"]")] public async Task> GetTankAlarmAsync(int tankNumber, int pageRowCount = 10, int pageIndex = 0, DateTime? filterTimestamp = null) { var response = await this.context.Outgoing.WriteAsync( new QueryInTankStatusReportRequest(MessageBase.MessageFormat.Computer, tankNumber), (req, testResp) => testResp is QueryInTankStatusReportResponse r && r.TankNumberInFunctionCode == tankNumber, 5000); if (response == null) throw new TimeoutException($"QueryInTankStatusReportRequest(tankNumber: {tankNumber}) timedout"); var queryInTankStatusReportResponse = response as QueryInTankStatusReportResponse; var alarmsForTanks = queryInTankStatusReportResponse?.TanksOfAlarms.Select(ta => { var alarms = new List(); alarms.AddRange(ta?.Alarms.Select(aType => new Edge.Core.IndustryStandardInterface.ATG.Alarm() { TankNumber = (byte)ta.TankNumber, Priority = AlarmPriority.Alarm, Type = aType, CreatedTimeStamp = DateTime.Now, })); return alarms; }); return alarmsForTanks?.SelectMany(al => al) .Where(al => al.CreatedTimeStamp > (filterTimestamp ?? DateTime.MinValue)) .OrderByDescending(d => d.CreatedTimeStamp).Skip(pageRowCount * pageIndex).Take(pageRowCount); } } }