App.cs 100 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679
  1. using Application.VaporRecoveryOnlineWatchHubApp.UnversalApiModels;
  2. using AutoMapper;
  3. using Edge.Core.Database;
  4. using Edge.Core.Processor;
  5. using Edge.Core.IndustryStandardInterface.Pump;
  6. using Edge.Core.UniversalApi;
  7. using Microsoft.Extensions.DependencyInjection;
  8. using Microsoft.Extensions.Logging;
  9. using Microsoft.Extensions.Logging.Abstractions;
  10. using Wayne_VaporRecoveryDataCollectorBoard;
  11. using Wayne_VaporRecoveryDataCollectorBoard.MessageEntity;
  12. using Wayne_VaporRecoveryDataCollectorBoard.MessageEntity.Incoming;
  13. using System;
  14. using System.Collections.Generic;
  15. using System.Linq;
  16. using System.Text.Json;
  17. using System.Threading;
  18. using System.Threading.Tasks;
  19. using VaporRecoveryOnlineWatchHubApp;
  20. using VaporRecoveryOnlineWatchHubApp.Config;
  21. using VaporRecoveryOnlineWatchHubApp.UnversalApiModels;
  22. using Edge.Core.Database.Models;
  23. using Edge.Core.Processor.Dispatcher.Attributes;
  24. using System.Collections.Concurrent;
  25. using Aliyun.Acs.Core.Profile;
  26. using Aliyun.Acs.Core;
  27. using Aliyun.Acs.Dm.Model.V20151123;
  28. using Aliyun.Acs.Core.Exceptions;
  29. using Edge.Core;
  30. using System.Collections.Specialized;
  31. using static Wayne_VaporRecoveryDataCollectorBoard.GroupHandler;
  32. namespace Application.VaporRecoveryOnlineWatchHubApp
  33. {
  34. [UniversalApi(Name = OnVaporRecoveryDataCollectorBoardStateChangeEventName, EventDataType = typeof(Board), Description = "Subscribe this event for High level state of the board, if it's get initialized or not.")]
  35. [UniversalApi(Name = OnVaporRecoveryDataCollectorBoardNozzleFlowDataReadEventName, EventDataType = typeof(VRBoardNozzleTrxFlowData),
  36. Description = "2 cases of this event will fire: " +
  37. "1. when VR board detect the nozzle is on fuelling, it'll periodically report the real time vapor and liquid values, but the property 'FuellingStartTime' and 'FuellingEndTime' are of course null." +
  38. "2. when VR board detect a trx has done on certain nozzle, it'll report a single flow data for it, with property 'FuellingStartTime' and 'FuellingEndTime' with concrete values.")]
  39. [UniversalApi(Name = OnVaporRecoveryDataCollectorBoardNozzleStateChangeEventName, EventDataType = typeof(VRBoardNozzle), Description = "Notify the nozzle fuelling state change.")]
  40. [UniversalApi(Name = OnVaporRecoveryDataCollectorBoardAlarmsEventName, EventDataType = typeof(VRBoardAlarmListEventArgs), Description = "Indeed the App alarms, with alarm details.")]
  41. //[UniversalApi(Name = GenericAlarm.UniversalApiEventName, EventDataType = typeof(GenericAlarm[]), Description = "Fire GenericAlarms to AlarmBar for attracting users.")]
  42. [MetaPartsDescriptor(
  43. "lang-zh-cn:油气回收在线监测系统lang-zh-tw:油氣回收在線監測系統lang-en-us:Vapor recovery online watch system",
  44. "lang-zh-cn:油气回收在线监测系统, 用于链接多个气液比收集板,以及多个压力表和多个气体浓度表。日志名称: DynamicPrivate_VaporRecoveryOnlineWatchHubApp" +
  45. "lang-zh-tw:油氣回收在線監測系統, 用於鏈接多個氣液比收集板,以及多個壓力表和多個氣體濃度表" +
  46. "lang-en-us:油气回收在线监测系统, 用于链接多个气液比收集板,以及多个压力表和多个气体浓度表。日志名称: DynamicPrivate_VaporRecoveryOnlineWatchHubApp",
  47. new[] { "lang-zh-cn:在线监测lang-zh-tw:在線監測lang-en-us:OnlineWatch" })]
  48. public class App : IAppProcessor
  49. {
  50. public const string OnVaporRecoveryDataCollectorBoardStateChangeEventName = "OnVaporRecoveryDataCollectorBoardStateChange";
  51. public const string OnVaporRecoveryDataCollectorBoardNozzleStateChangeEventName = "OnVaporRecoveryDataCollectorBoardNozzleStateChange";
  52. public const string OnVaporRecoveryDataCollectorBoardNozzleFlowDataReadEventName = "OnVaporRecoveryDataCollectorBoardNozzleFlowDataRead";
  53. public const string OnVaporRecoveryDataCollectorBoardAlarmsEventName = "OnVaporRecoveryDataCollectorBoardAlarms";
  54. public string MetaConfigName { get; set; }
  55. private IServiceProvider services { get; }
  56. private ILogger logger = NullLogger.Instance;
  57. /// <summary>
  58. /// device handlers for fuel air&liquid 气液比控制板 data collector boards.
  59. /// </summary>
  60. public IEnumerable<Wayne_VaporRecoveryDataCollectorBoard.GroupHandler> vrBoardDeviceHandlers { get; set; }
  61. private AppConfigV1 appConfig;
  62. private IMapper objMapper;
  63. //private FdcServerHostApp fdcServerHostApp;
  64. //private bool isStopped = false;
  65. //private UploadingConfig uploadingConfig;
  66. private Timer dailyVRBoardNotifyWarningTimer;
  67. private Timer dailyVRBoardCheckingStateTimer;
  68. private Timer minutelySaveDbTimer;
  69. /// <summary>
  70. /// VR Board will watch on each nozzels, and for user side, they care about the nozzles, thus here bring in a logical vr nozzle concept.
  71. /// </summary>
  72. private List<VRBoardNozzle> vrBoardNozzles = null;
  73. private VRBoardDbHelper dbHelperForVRBoard;
  74. private readonly ConcurrentDictionary<string, Dictionary<string, object>> jsonDataDic = new ConcurrentDictionary<string, Dictionary<string, object>>();
  75. private Timer dailyTankPressureWarningTimer;
  76. private DateTime lastTankPressureWarningTime;
  77. private DateTime lastTankPressureWarningDate;
  78. private DateTime lastLowTankPressureWarningTime;
  79. private DateTime lastHighTankPressureWarningTime;
  80. private int tankPressureWarningTotalDays = 0;
  81. private DateTime lastLiquidPressureWarningTime;
  82. private DateTime lastGasConcentrationWarningTime;
  83. private StringDictionary alarmEmailsDic = new StringDictionary();
  84. #region app config
  85. public class StationMiscConfigV1 //: BaseConfig
  86. {
  87. public string StationName { get; set; }
  88. public string StationAddress { get; set; }
  89. public NotificationMailConfigV1 NotificationMailConfig { get; set; }
  90. }
  91. public class PerNozzleVRConfigV1
  92. {
  93. public PerNozzleVRGroupQualificationDefinitionV1[] QualificationDefinitions { get; set; }
  94. }
  95. //public enum PerNozzleVRGroupQualificationTypeEnum
  96. //{
  97. // 汽車,
  98. // 摩托車
  99. //}
  100. public class PerNozzleVRGroupQualificationDefinitionV1
  101. {
  102. public string GroupName { get; set; }
  103. public string NozzleIdsString { get; set; }
  104. public double LiquidVolumeMinThreshold { get; set; }
  105. public double QualifiedAirLiquidRatioMin { get; set; }
  106. public double QualifiedAirLiquidRatioMax { get; set; }
  107. public double UnqualifiedAirLiquidRatioRecordsPecentageTurnToWarningStateThreshold { get; set; }
  108. }
  109. public class NotificationMailConfigV1
  110. {
  111. public string Host { get; set; }
  112. public string RegionId { get; set; }
  113. public string AccessKeyId { get; set; }
  114. public string AccessKeySecret { get; set; }
  115. public string ApiVersion { get; set; }
  116. public string[] ReceiverAddresses { get; set; }
  117. }
  118. public class VaporRecoveryConfigV1 //: BaseConfig
  119. {
  120. /// <summary>
  121. /// only liquid volume read from board has its value >= this value will be treat as a valid read, and will be persist to db.
  122. /// the value is with decimal points, by Litre.
  123. /// </summary>
  124. public double LiquidVolumeMinThreshold { get; set; }
  125. /// <summary>
  126. /// the decimal point value for liquid volume read from board.
  127. /// </summary>
  128. public byte LiquidVolumeDigits { get; set; }
  129. /// <summary>
  130. /// the decimal point value for air volume read from board.
  131. /// </summary>
  132. public byte AirVolumeDigits { get; set; }
  133. /// <summary>
  134. /// 位于此值范围内的气液比值则认为是工作正常
  135. /// 下限
  136. /// </summary>
  137. public double QualifiedAirLiquidRatioMin { get; set; } = 0.83;
  138. /// <summary>
  139. /// 位于此值范围内的气液比值则认为是工作正常
  140. /// 上限
  141. /// </summary>
  142. public double QualifiedAirLiquidRatioMax { get; set; } = 1.35;
  143. /// <summary>
  144. /// 在最近的时间段time_range_by_hour内,当“非正常”气液比值 占比超过e%,将触发状态由 正常 转换至 预警
  145. /// </summary>
  146. public double TurnToWarningStateInRecentHoursThreshold { get; set; } = 24;
  147. /// <summary>
  148. /// 当“非正常”气液比值 占比超过 WarningThresthold%,将触发状态由 正常 转换至 预警, 默认25%
  149. /// </summary>
  150. public double UnqualifiedAirLiquidRatioRecordsPecentageTurnToWarningStateThreshold { get; set; } = 25;
  151. /// <summary>
  152. /// 当某把处于“预警状态”warning的加油枪持续保持了此值表示的天数后,将使此把油枪的状态转换至”报警”alarm.
  153. /// </summary>
  154. public double WarningToAlarmStateLastingDaysThreshold { get; set; } = 5;
  155. /// <summary>
  156. /// 即在时间time_in_day_with_24hourStyle时,如果存在预、报警枪,则播放相应的语音或者音乐。
  157. /// 可输入值是24小时制中的小时以及分,比如:13.10即代表13点10进行播报。
  158. /// </summary>
  159. public string AlarmPlayingTimeInDayWith24HourStyle { get; set; } = "13.10";
  160. /// <summary>
  161. /// 预警:在 24h(自然日,即过0点,则重新从此0点开始读数,此0点前的数据不再计入分析)内,在线监控设备监测每条加油枪的有效气液比(每次连续加油量大 于 15L)小于 1.00 或大于 1.20 的次数超过总次数的 25%(此比例值应可以配置,后续章节将提到)时,在线监控设备应预警。
  162. /// 应注意的是,对某天进行预警、报警状态判断的时间应是每天的23:59:59秒
  163. /// 检测是每天定点检查一次,而不是实时的
  164. /// </summary>
  165. public string RetrospectiveWarningCheckingTime { get; set; } = "24.00";
  166. /// <summary>
  167. /// 气液比最多保存最近多少天的。
  168. /// </summary>
  169. public int RecordMaxKeepDays { get; set; } = 800;
  170. public PerNozzleVRConfigV1 PerNozzleVRConfig { get; set; }
  171. public override string ToString()
  172. {
  173. return GetType().GetProperties().Aggregate(string.Empty,
  174. (result, next) => result + $"{next.Name} : {(next.GetValue(this)?.ToString()) ?? string.Empty} ");
  175. }
  176. }
  177. public class TankPressureGaugeMeterConfigV1
  178. {
  179. /// <summary>
  180. /// link to the device handler as there could have multiple PressureGauge Meters in system,
  181. /// so the device handler must have a correlated data for linkage.
  182. /// </summary>
  183. public string MeterDeviceHandlerIdentity { get; set; }
  184. /// <summary>
  185. /// name of this meter, will displayed in UI for help the user know what's this meter for.
  186. /// </summary>
  187. public string MeterName { get; set; }
  188. /// <summary>
  189. /// detail purpose of this meter, will displayed in UI for help the user know what's this meter for.
  190. /// </summary>
  191. public string Description { get; set; }
  192. /// <summary>
  193. /// 低压报警阈值
  194. /// </summary>
  195. public double LowPressureWarningThreshold { get; set; }
  196. /// <summary>
  197. /// 高压报警阈值
  198. /// </summary>
  199. public double HighPressureWarningTheshold { get; set; }
  200. /// <summary>
  201. /// 0 压报警阈值
  202. /// </summary>
  203. public string ZeroPressureWarningThreshold { get; set; }
  204. /// <summary>
  205. /// 0 压转换至预警状态的连续时间窗口大小。
  206. /// </summary>
  207. public int TurnToWarningStateInRecentHoursThreshold { get; set; } = 12;
  208. /// <summary>
  209. /// 高压预警转报警天数。
  210. /// </summary>
  211. public int WarningToAlarmStateLastingDaysThreshold { get; set; } = 7;
  212. /// <summary>
  213. /// 油罐压力报警开关。
  214. /// </summary>
  215. public bool AlarmIsEnabled { get; set; } = true;
  216. /// <summary>
  217. /// 压力值最多保存最近多少天的。
  218. /// </summary>
  219. public int RecordMaxKeepDays { get; set; } = 800;
  220. }
  221. public class LiquidPressureGaugeMeterConfigV1
  222. {
  223. /// <summary>
  224. /// link to a device handler as there could have multiple LiquidPressureGauge Meters in system,
  225. /// so the device handler must have a correlated data for linkage.
  226. /// </summary>
  227. public string MeterDeviceHandlerIdentity { get; set; }
  228. /// <summary>
  229. /// name of this meter, will displayed in UI for help the user know what's this meter for.
  230. /// </summary>
  231. public string MeterName { get; set; }
  232. /// <summary>
  233. /// detail purpose of this meter, will displayed in UI for help the user know what's this meter for.
  234. /// </summary>
  235. public string Description { get; set; }
  236. /// <summary>
  237. /// 高压预警阈值
  238. /// </summary>
  239. public double HighPressureWarningTheshold { get; set; }
  240. /// <summary>
  241. /// 高压报警阈值
  242. /// </summary>
  243. public double HighPressureAlarmTheshold { get; set; }
  244. /// <summary>
  245. /// 高压预警转报警天数。
  246. /// </summary>
  247. public int WarningToAlarmStateLastingDaysThreshold { get; set; } = 5;
  248. }
  249. public class GasConcentrationGaugeMeterConfigV1
  250. {
  251. /// <summary>
  252. /// link to a device handler as there could have multiple GasConcentrationGauge Meters in system,
  253. /// so the device handler must have a correlated data for linkage.
  254. /// </summary>
  255. public string MeterDeviceHandlerIdentity { get; set; }
  256. /// <summary>
  257. /// name of this meter, will displayed in UI for help the user know what's this meter for.
  258. /// </summary>
  259. public string MeterName { get; set; }
  260. /// <summary>
  261. /// detail purpose of this meter, will displayed in UI for help the user know what's this meter for.
  262. /// </summary>
  263. public string Description { get; set; }
  264. /// <summary>
  265. /// 高浓度预警阈值
  266. /// </summary>
  267. public double HighConcentrationWarningTheshold { get; set; }
  268. /// <summary>
  269. /// 高浓度报警阈值
  270. /// </summary>
  271. public double HighConcentrationAlarmTheshold { get; set; }
  272. /// <summary>
  273. /// 高浓度预警转报警天数。
  274. /// </summary>
  275. public int WarningToAlarmStateLastingDaysThreshold { get; set; } = 7;
  276. }
  277. public class AppConfigV1
  278. {
  279. public StationMiscConfigV1 StationMiscConfig { get; set; }
  280. public VaporRecoveryConfigV1 VaporRecoveryConfig { get; set; }
  281. public TankPressureGaugeMeterConfigV1 TankPressureGaugeMeterConfig { get; set; }
  282. public LiquidPressureGaugeMeterConfigV1 LiquidPressureGaugeMeterConfig { get; set; }
  283. public List<GasConcentrationGaugeMeterConfigV1> GasConcentrationGaugeMeterConfigs { get; set; }
  284. }
  285. #endregion
  286. [ParamsJsonSchemas("appCtorParamsJsonSchema")]
  287. public App(AppConfigV1 appConfig, IServiceProvider services)
  288. {
  289. this.services = services;
  290. this.appConfig = appConfig;
  291. if (services != null)
  292. {
  293. var loggerFactory = services.GetRequiredService<ILoggerFactory>();
  294. logger = loggerFactory.CreateLogger("DynamicPrivate_VaporRecoveryOnlineWatchApp");
  295. objMapper = this.services.GetRequiredService<IMapper>();
  296. dbHelperForVRBoard = new VRBoardDbHelper(services);
  297. }
  298. }
  299. [UniversalApi(Description = "Get the Pressure data of the pressure gage.")]
  300. public async Task<ConcurrentDictionary<string, Dictionary<string, object>>> GetPressureAsync()
  301. {
  302. return await Task.FromResult(jsonDataDic);
  303. }
  304. private async Task FireOrCloseAlarm(IProcessor source, string title, string category, GenericAlarmSeverity severity, string detail, bool isClose = false)
  305. {
  306. var universalApiHub = this.services.GetRequiredService<UniversalApiHub>();
  307. if (isClose)
  308. {
  309. await universalApiHub?.ClosePersistGenericAlarms(source, detail, "问题已解决, 所以关闭。");
  310. }
  311. else
  312. {
  313. await universalApiHub?.FirePersistGenericAlarm(source, new GenericAlarm()
  314. {
  315. Title = title,
  316. Category = category,
  317. Severity = severity,
  318. Detail = detail
  319. }, ga => ga.Title);
  320. }
  321. }
  322. private async Task ProcessPressureDataAsync(object sender, string message)
  323. {
  324. var source = sender as IProcessor;
  325. var responseDic = JsonSerializer.Deserialize(message, typeof(Dictionary<string, object>)) as Dictionary<string, object>;
  326. if (responseDic.ContainsKey("Pressure"))
  327. {
  328. double pressure = double.Parse(responseDic["Pressure"].ToString());
  329. string id = responseDic["DeviceAddress"].ToString();
  330. string key = string.Empty;
  331. var tankPressureConfig = this.appConfig.TankPressureGaugeMeterConfig;
  332. var liquidPressureConfig = this.appConfig.LiquidPressureGaugeMeterConfig;
  333. //在 24 小时(自然天)内,在线监控系统监测到的系统压力与大气压差 值(表压)处于(-50~50)Pa 范围内的连续时间超过 12 小时,系统应预警
  334. if (tankPressureConfig != null && id == tankPressureConfig.MeterDeviceHandlerIdentity)
  335. {
  336. var zeros = tankPressureConfig.ZeroPressureWarningThreshold.Split('~');
  337. responseDic["CurrentPointer"] = "high";
  338. if (pressure >= double.Parse(zeros[0]) && pressure <= double.Parse(zeros[1]))
  339. {
  340. responseDic["CurrentPointer"] = "zero";
  341. if (lastTankPressureWarningTime.Year == 1)
  342. {
  343. lastTankPressureWarningTime = DateTime.Now;
  344. await SaveOrUpdateDbAsync("LastTankPressureWarningTime", new GenericData()
  345. {
  346. Type = "LastTankPressureWarningTime",
  347. CreatedTimeStamp = lastTankPressureWarningTime
  348. });
  349. }
  350. else if ((DateTime.Now - lastTankPressureWarningTime).TotalHours > tankPressureConfig.TurnToWarningStateInRecentHoursThreshold)
  351. {
  352. if (!lastTankPressureWarningDate.Equals(DateTime.Today))
  353. {
  354. tankPressureWarningTotalDays++;
  355. lastTankPressureWarningDate = DateTime.Today;
  356. await SaveOrUpdateDbAsync("LastTankPressureWarningDate", new GenericData()
  357. {
  358. Type = "LastTankPressureWarningDate",
  359. CreatedTimeStamp = lastTankPressureWarningDate,
  360. IntProperty0 = tankPressureWarningTotalDays
  361. });
  362. }
  363. if (tankPressureWarningTotalDays < tankPressureConfig.WarningToAlarmStateLastingDaysThreshold)
  364. {
  365. await FireOrCloseAlarm(source, "lang-zh-cn:油罐压力零压lang-zh-tw:油罐壓力零壓", "lang-zh-cn:油罐压力lang-zh-tw:油罐壓力",
  366. GenericAlarmSeverity.Warning, $"lang-zh-cn:油罐压力零压预警lang-zh-tw:油罐壓力零壓預警 {pressure:F2}");
  367. }
  368. else
  369. {
  370. if (tankPressureConfig.AlarmIsEnabled)
  371. {
  372. await FireOrCloseAlarm(source, "lang-zh-cn:油罐压力零压lang-zh-tw:油罐壓力零壓", "lang-zh-cn:油罐压力lang-zh-tw:油罐壓力",
  373. GenericAlarmSeverity.Error, $"lang-zh-cn:油罐压力零压报警lang-zh-tw:油罐壓力零壓報警 {pressure:F2}");
  374. if (!alarmEmailsDic.ContainsKey("油罐压力零压"))
  375. {
  376. alarmEmailsDic["油罐压力零压"] = "";
  377. await TrySendAlarmEmailsAsync(
  378. $"lang-zh-cn:在线监测系统异常情况报告 - {this.appConfig.StationMiscConfig?.StationName ?? ""}lang-zh-tw:在線監測系統異常情況報告 - {this.appConfig.StationMiscConfig?.StationName ?? ""}".LocalizedContent("zh-tw"),
  379. $"lang-zh-cn:油罐压力零压 {pressure:F2}<b>连续多日</b>不合格lang-zh-tw:油罐壓力零壓 {pressure:F2}<b>連續多日</b>不合格".LocalizedContent("zh-tw"));
  380. }
  381. }
  382. }
  383. }
  384. }
  385. else if (pressure < tankPressureConfig.LowPressureWarningThreshold)
  386. {
  387. if (lastLowTankPressureWarningTime.Year == 1)
  388. {
  389. lastLowTankPressureWarningTime = DateTime.Now;
  390. await SaveOrUpdateDbAsync("LastLowTankPressureWarningTime", new GenericData
  391. {
  392. Type = "LastLowTankPressureWarningTime",
  393. CreatedTimeStamp = lastLowTankPressureWarningTime
  394. });
  395. }
  396. else if ((DateTime.Now - lastLowTankPressureWarningTime).TotalMinutes > 5.0)
  397. {
  398. responseDic["CurrentPointer"] = "low";
  399. await FireOrCloseAlarm(source, "lang-zh-cn:油罐压力负压lang-zh-tw:油罐壓力負壓", "lang-zh-cn:油罐压力lang-zh-tw:油罐壓力",
  400. GenericAlarmSeverity.Error, $"lang-zh-cn:油罐压力负压报警lang-zh-tw:油罐壓力負壓報警 {pressure:F2}");
  401. if (!alarmEmailsDic.ContainsKey("油罐压力负压"))
  402. {
  403. alarmEmailsDic["油罐压力负压"] = "";
  404. await TrySendAlarmEmailsAsync(
  405. $"lang-zh-cn:在线监测系统异常情况报告 - {this.appConfig.StationMiscConfig?.StationName ?? ""}lang-zh-tw:在線監測系統異常情況報告 - {this.appConfig.StationMiscConfig?.StationName ?? ""}".LocalizedContent("zh-tw"),
  406. $"lang-zh-cn:油罐压力负压 {pressure:F2}<b>超过5分钟</b>不合格lang-zh-tw:油罐壓力負壓 {pressure:F2}<b>超過5分鐘</b>不合格".LocalizedContent("zh-tw"));
  407. }
  408. }
  409. else
  410. {
  411. responseDic["CurrentPointer"] = "zero";
  412. await FireOrCloseAlarm(source, "lang-zh-cn:油罐压力负压lang-zh-tw:油罐壓力負壓", "lang-zh-cn:油罐压力lang-zh-tw:油罐壓力",
  413. GenericAlarmSeverity.Warning, $"lang-zh-cn:油罐压力负压预警lang-zh-tw:油罐壓力負壓預警 {pressure:F2}");
  414. }
  415. }
  416. else if (pressure > tankPressureConfig.HighPressureWarningTheshold)
  417. {
  418. if (lastHighTankPressureWarningTime.Year == 1)
  419. {
  420. lastHighTankPressureWarningTime = DateTime.Now;
  421. await SaveOrUpdateDbAsync("LastHighTankPressureWarningTime", new GenericData
  422. {
  423. Type = "LastHighTankPressureWarningTime",
  424. CreatedTimeStamp = lastHighTankPressureWarningTime
  425. });
  426. }
  427. else if ((DateTime.Now - lastHighTankPressureWarningTime).TotalMinutes > 5.0)
  428. {
  429. responseDic["CurrentPointer"] = "alarm";
  430. await FireOrCloseAlarm(source, "lang-zh-cn:油罐压力过高lang-zh-tw:油罐壓力過高", "lang-zh-cn:油罐压力lang-zh-tw:油罐壓力",
  431. GenericAlarmSeverity.Error, $"lang-zh-cn:油罐压力过高报警lang-zh-tw:油罐壓力過高報警 {pressure:F2}");
  432. if (!alarmEmailsDic.ContainsKey("油罐压力过高"))
  433. {
  434. alarmEmailsDic["油罐压力过高"] = "";
  435. await TrySendAlarmEmailsAsync(
  436. $"lang-zh-cn:在线监测系统异常情况报告 - {this.appConfig.StationMiscConfig?.StationName ?? ""}lang-zh-tw:在線監測系統異常情況報告 - {this.appConfig.StationMiscConfig?.StationName ?? ""}".LocalizedContent("zh-tw"),
  437. $"lang-zh-cn:油罐压力过高 {pressure:F2}<b>超过5分钟</b>不合格lang-zh-tw:油罐壓力過高 {pressure:F2}<b>超過5分鐘</b>不合格".LocalizedContent("zh-tw"));
  438. }
  439. }
  440. else
  441. {
  442. responseDic["CurrentPointer"] = "zero";
  443. await FireOrCloseAlarm(source, "lang-zh-cn:油罐压力过高lang-zh-tw:油罐壓力過高", "lang-zh-cn:油罐压力lang-zh-tw:油罐壓力",
  444. GenericAlarmSeverity.Warning, $"lang-zh-cn:油罐压力过高预警lang-zh-tw:油罐壓力過高預警 {pressure:F2}");
  445. }
  446. }
  447. else
  448. {
  449. if (lastTankPressureWarningTime.Year != 1)
  450. {
  451. lastTankPressureWarningTime = new DateTime();
  452. await SaveOrUpdateDbAsync("LastTankPressureWarningTime", new GenericData()
  453. {
  454. Type = "LastTankPressureWarningTime",
  455. CreatedTimeStamp = lastTankPressureWarningTime
  456. });
  457. }
  458. if (lastLowTankPressureWarningTime.Year != 1)
  459. {
  460. lastLowTankPressureWarningTime = new DateTime();
  461. await SaveOrUpdateDbAsync("LastLowTankPressureWarningTime", new GenericData
  462. {
  463. Type = "LastLowTankPressureWarningTime",
  464. CreatedTimeStamp = lastLowTankPressureWarningTime
  465. });
  466. }
  467. if (lastHighTankPressureWarningTime.Year != 1)
  468. {
  469. lastHighTankPressureWarningTime = new DateTime();
  470. await SaveOrUpdateDbAsync("LastHighTankPressureWarningTime", new GenericData
  471. {
  472. Type = "LastHighTankPressureWarningTime",
  473. CreatedTimeStamp = lastHighTankPressureWarningTime
  474. });
  475. }
  476. }
  477. key = "TankPressure";
  478. if (alarmEmailsDic.ContainsKey("油罐压力零压") && !(pressure >= double.Parse(zeros[0]) && pressure <= double.Parse(zeros[1])))
  479. {
  480. alarmEmailsDic.Remove("油罐压力零压");
  481. }
  482. else if (alarmEmailsDic.ContainsKey("油罐压力负压") && !(pressure < tankPressureConfig.LowPressureWarningThreshold))
  483. {
  484. alarmEmailsDic.Remove("油罐压力负压");
  485. }
  486. else if (alarmEmailsDic.ContainsKey("油罐压力过高") && !(pressure > tankPressureConfig.HighPressureWarningTheshold))
  487. {
  488. alarmEmailsDic.Remove("油罐压力过高");
  489. }
  490. }
  491. if (liquidPressureConfig != null && id == liquidPressureConfig.MeterDeviceHandlerIdentity)
  492. {
  493. if (pressure < liquidPressureConfig.HighPressureWarningTheshold)
  494. {
  495. responseDic["CurrentPointer"] = "3low";
  496. if (lastLiquidPressureWarningTime.Year != 1)
  497. {
  498. lastLiquidPressureWarningTime = new DateTime();
  499. //await RemoveLastWarningTimeData("lastLiquidPressureWarningTime");
  500. }
  501. }
  502. else if (pressure < liquidPressureConfig.HighPressureAlarmTheshold)
  503. {
  504. responseDic["CurrentPointer"] = "3zero";
  505. if (lastLiquidPressureWarningTime.Year == 1)
  506. {
  507. lastLiquidPressureWarningTime = DateTime.Now;
  508. await SaveOrUpdateDbAsync("LastLiquidPressureWarningTime", new GenericData()
  509. {
  510. Type = "LastLiquidPressureWarningTime",
  511. CreatedTimeStamp = lastLiquidPressureWarningTime
  512. });
  513. }
  514. else if ((DateTime.Now - lastLiquidPressureWarningTime).TotalDays > liquidPressureConfig.WarningToAlarmStateLastingDaysThreshold)
  515. {
  516. await FireOrCloseAlarm(source, "lang-zh-cn:液阻压力过高预警lang-zh-tw:液阻壓力過高預警", "lang-zh-cn:液阻压力lang-zh-tw:液阻壓力",
  517. GenericAlarmSeverity.Error, $"lang-zh-cn:液阻压力lang-zh-tw:液阻壓力 {pressure:F2}");
  518. }
  519. else
  520. await FireOrCloseAlarm(source, "lang-zh-cn:液阻压力过高预警lang-zh-tw:液阻壓力過高預警", "lang-zh-cn:液阻压力lang-zh-tw:液阻壓力",
  521. GenericAlarmSeverity.Warning, $"lang-zh-cn:液阻压力lang-zh-tw:液阻壓力 {pressure:F2}");
  522. }
  523. else
  524. {
  525. responseDic["CurrentPointer"] = "3alarm";
  526. await FireOrCloseAlarm(source, "lang-zh-cn:液阻压力过高报警lang-zh-tw:液阻壓力過高報警", "lang-zh-cn:液阻压力lang-zh-tw:液阻壓力",
  527. GenericAlarmSeverity.Error, $"lang-zh-cn:液阻压力lang-zh-tw:液阻壓力 {pressure:F2}");
  528. }
  529. key = "LiquidPressure";
  530. }
  531. jsonDataDic.AddOrUpdate(key, responseDic, (existingKey, existingJsonData) => { return responseDic; });
  532. }
  533. else if (responseDic.ContainsKey("DeviceState"))
  534. {
  535. string deviceState = responseDic["DeviceState"].ToString();
  536. if (deviceState == "Disconnected")
  537. await FireOrCloseAlarm(source, "lang-zh-cn:压力监测设备连接断开lang-zh-tw:壓力監測設備連接斷開", "lang-zh-cn:压力监测设备lang-zh-tw:壓力監測設備",
  538. GenericAlarmSeverity.Warning, $"lang-zh-cn:压力监测设备连接断开lang-zh-tw:壓力監測設備連接斷開");
  539. else
  540. await FireOrCloseAlarm(source, "", "", GenericAlarmSeverity.Information, $"lang-zh-cn:压力监测设备连接断开lang-zh-tw:壓力監測設備連接斷開", true);
  541. }
  542. await Task.FromResult(true);
  543. }
  544. private async Task ProcessGasConcentrationsDataAsync(object sender, string message)
  545. {
  546. var source = sender as IProcessor;
  547. var responseDic = JsonSerializer.Deserialize(message, typeof(Dictionary<string, object>)) as Dictionary<string, object>;
  548. string id = responseDic["DeviceAddress"].ToString();
  549. if (responseDic.ContainsKey("Concentration"))
  550. {
  551. var concentrationConfig = this.appConfig.GasConcentrationGaugeMeterConfigs.Find(m => m.MeterDeviceHandlerIdentity == id);
  552. double concentr = double.Parse(responseDic["Concentration"].ToString());
  553. if (concentr < concentrationConfig.HighConcentrationWarningTheshold)
  554. {
  555. responseDic["CurrentPointer"] = "3low";
  556. if (lastGasConcentrationWarningTime.Year != 1)
  557. {
  558. lastGasConcentrationWarningTime = new DateTime();
  559. await SaveOrUpdateDbAsync("LastGasConcentrationWarningTime", new GenericData()
  560. {
  561. Type = "LastGasConcentrationWarningTime",
  562. CreatedTimeStamp = lastGasConcentrationWarningTime
  563. });
  564. }
  565. }
  566. else if (concentr < concentrationConfig.HighConcentrationAlarmTheshold)
  567. {
  568. responseDic["CurrentPointer"] = "3zero";
  569. if (lastGasConcentrationWarningTime.Year == 1)
  570. {
  571. lastGasConcentrationWarningTime = DateTime.Now;
  572. await SaveOrUpdateDbAsync("LastGasConcentrationWarningTime", new GenericData()
  573. {
  574. Type = "LastGasConcentrationWarningTime",
  575. CreatedTimeStamp = lastGasConcentrationWarningTime
  576. });
  577. }
  578. else if ((DateTime.Now - lastGasConcentrationWarningTime).TotalDays > concentrationConfig.WarningToAlarmStateLastingDaysThreshold)
  579. {
  580. responseDic["CurrentPointer"] = "3alarm";
  581. await FireOrCloseAlarm(source, "lang-zh-cn:回气口浓度报警lang-zh-tw:回氣口濃度報警", "lang-zh-cn:回气口浓度lang-zh-tw:回氣口濃度",
  582. GenericAlarmSeverity.Error, $"lang-zh-cn:回气口浓度 {concentr:F2}lang-zh-tw:回氣口濃度 {concentr:F2}");
  583. }
  584. else
  585. await FireOrCloseAlarm(source, "lang-zh-cn:回气口浓度报警lang-zh-tw:回氣口濃度報警", "lang-zh-cn:回气口浓度lang-zh-tw:回氣口濃度",
  586. GenericAlarmSeverity.Warning, $"lang-zh-cn:回气口浓度 {concentr:F2}lang-zh-tw:回氣口濃度 {concentr:F2}");
  587. }
  588. else // 大于等于 8000 umol/mol
  589. {
  590. responseDic["CurrentPointer"] = "3alarm";
  591. await FireOrCloseAlarm(source, "lang-zh-cn:回气口浓度报警lang-zh-tw:回氣口濃度報警", "lang-zh-cn:回气口浓度lang-zh-tw:回氣口濃度",
  592. GenericAlarmSeverity.Error, $"lang-zh-cn:回气口浓度 {concentr:F2}lang-zh-tw:回氣口濃度 {concentr:F2}");
  593. }
  594. string key = "GasConcentrations";
  595. jsonDataDic.AddOrUpdate(key, responseDic, (existingKey, existingJsonData) => { return responseDic; });
  596. }
  597. else if (responseDic.ContainsKey("DeviceState"))
  598. {
  599. string deviceState = responseDic["DeviceState"].ToString();
  600. if (deviceState == "Disconnected")
  601. await FireOrCloseAlarm(source, "lang-zh-cn:气体浓度设备连接断开lang-zh-tw:氣體濃度設備連接斷開", "lang-zh-cn:气体浓度设备lang-zh-tw:氣體濃度設備",
  602. GenericAlarmSeverity.Warning, $"lang-zh-cn:气体浓度设备连接断开lang-zh-tw:氣體濃度設備連接斷開");
  603. else
  604. await FireOrCloseAlarm(source, "", "", GenericAlarmSeverity.Information, $"lang-zh-cn:气体浓度设备连接断开lang-zh-tw:氣體濃度設備連接斷開", true);
  605. }
  606. await Task.FromResult(true);
  607. }
  608. public void Init(IEnumerable<IProcessor> processors)
  609. {
  610. var pressureProcessors = processors.Where(p => p.MetaConfigName.StartsWith("PressureGage_3051.SensorGroupHandler"));
  611. foreach (dynamic p in pressureProcessors)
  612. {
  613. var handlerList = p.Context.Handler.SensorHandlerList;
  614. foreach (dynamic handler in handlerList)
  615. {
  616. //logger.LogInformation($"SensorHandler with deviceAddress: {handler.DeviceAddress}");
  617. handler.OnJsonDataRecieved = new EventHandler<string>(async (s, d) =>
  618. {
  619. await ProcessPressureDataAsync(p, d);
  620. });
  621. }
  622. }
  623. var gasProcessors = processors.Where(p => p.MetaConfigName.StartsWith("GasConcentrations_Yt95h.SensorGroupHandl"));
  624. foreach (dynamic p in gasProcessors)
  625. {
  626. var handlerList = p.Context.Handler.SensorHandlerList;
  627. foreach (dynamic handler in handlerList)
  628. {
  629. //logger.LogInformation($"SensorHandler with deviceAddress: {handler.DeviceAddress}");
  630. handler.OnJsonDataRecieved = new EventHandler<string>(async (s, d) =>
  631. {
  632. await ProcessGasConcentrationsDataAsync(p, d);
  633. });
  634. }
  635. }
  636. this.vrBoardDeviceHandlers = processors.WithHandlerOrApp<Wayne_VaporRecoveryDataCollectorBoard.GroupHandler>()
  637. .SelectHandlerOrAppThenCast<Wayne_VaporRecoveryDataCollectorBoard.GroupHandler>();
  638. if (this.vrBoardDeviceHandlers == null || !this.vrBoardDeviceHandlers.Any())
  639. throw new InvalidOperationException("当前 APP 的运行必须要求系统中配置一个或者多个: '油枪气液比收集板' ,但当前没有获取到,请配置并启用相应 device handler 后重试");
  640. if (this.vrBoardDeviceHandlers.SelectMany(dh => dh.Boards).GroupBy(b => b.BoardPhysicalAddress).Any(g => g.Count() >= 2))
  641. throw new ArgumentException("发现多个 '油枪气液比收集板' 配置中有重复的硬件地址值,请检查相应的 device hanlder 以确保全站唯一硬件地址,再重试 ");
  642. var calibratedLatestPersistVrBoardNozzleInitParameters = this.ReadLatestPersistVrBoardNozzleInitParameters().Result;
  643. foreach (var dh in this.vrBoardDeviceHandlers)
  644. {
  645. foreach (var bc in dh.DeviceConfig.BoardConfigs)
  646. {
  647. foreach (var nc in bc.NozzleConfigs)
  648. {
  649. nc.ExternalInitParameter = calibratedLatestPersistVrBoardNozzleInitParameters
  650. .Where(p => p.SiteLevelNozzleId == nc.SiteLevelNozzleId)
  651. .Select(cal => new BoardInitParameterConfigV1()
  652. {
  653. 停止加油阀值 = cal.停止加油阀值,
  654. 加油脉冲当量 = cal.加油脉冲当量,
  655. 开始加油阀值 = cal.开始加油阀值,
  656. 最大未变化次数 = cal.最大未变化次数,
  657. 最小加油量 = cal.最小加油量,
  658. 气液比值 = cal.气液比值,
  659. 油气脉冲当量 = cal.油气脉冲当量
  660. }).FirstOrDefault();
  661. }
  662. }
  663. }
  664. var lastTankPressureWarningTimes = GetLastWarningTimeData("LastTankPressureWarningTime");
  665. if (lastTankPressureWarningTimes.Count() > 0)
  666. {
  667. lastTankPressureWarningTime = (DateTime)lastTankPressureWarningTimes.First().CreatedTimeStamp;
  668. }
  669. var lastTankPressureWarningDates = GetLastWarningTimeData("LastTankPressureWarningDate");
  670. if (lastTankPressureWarningDates.Count() > 0)
  671. {
  672. lastTankPressureWarningDate = (DateTime)lastTankPressureWarningDates.First().CreatedTimeStamp;
  673. tankPressureWarningTotalDays = (int)lastTankPressureWarningDates.First().IntProperty0;
  674. }
  675. var lastLowTankPressureWarningTimes = GetLastWarningTimeData("LastLowTankPressureWarningTime");
  676. if (lastLowTankPressureWarningTimes.Count() > 0)
  677. {
  678. lastLowTankPressureWarningTime = lastLowTankPressureWarningTimes.First().CreatedTimeStamp.Value;
  679. }
  680. var lastHighTankPressureWarningTimes = GetLastWarningTimeData("LastHighTankPressureWarningTime");
  681. if (lastHighTankPressureWarningTimes.Count() > 0)
  682. {
  683. lastHighTankPressureWarningTime = lastHighTankPressureWarningTimes.First().CreatedTimeStamp.Value;
  684. }
  685. var lastGasConcentrationWarningTimes = GetLastWarningTimeData("LastGasConcentrationWarningTime");
  686. if (lastGasConcentrationWarningTimes.Count() > 0)
  687. {
  688. lastGasConcentrationWarningTime = (DateTime)lastGasConcentrationWarningTimes.First().CreatedTimeStamp;
  689. }
  690. //fdc server app could be null as the pump control feature may not be required.
  691. //this.fdcServerHostApp = processors.OfType<FdcServerHostApp>().FirstOrDefault();
  692. //this.logger.LogInformation(this.fdcServerHostApp == null ? "Fdc server app is NOT present" : "Fdc Server app is present.");
  693. }
  694. public async Task<bool> Start()
  695. {
  696. this.vrBoardNozzles = this.vrBoardDeviceHandlers.SelectMany(g => g.Boards).SelectMany(b => b.Nozzles).Select(n => new VRBoardNozzle()
  697. {
  698. FuelingState = VRBoardNozzleFuelingStateEnum.IDLE,
  699. HealthState = VRBoardNozzleTrxHealthStateEnum.NOT_SET,
  700. SiteLevelNozzleId = n.SiteLevelNozzleId,
  701. PerNozzleVRGroupQualificationDefinitionGroupName = this.GetNozzleVRGroupQualificationConfig(n.SiteLevelNozzleId)?.GroupName,
  702. }).ToList();
  703. foreach (var vrBoardGroup in this.vrBoardDeviceHandlers)
  704. {
  705. vrBoardGroup.OnBoardStateChange += async (s, a) =>
  706. {
  707. logger.LogInformation($"Board with PhyAddr: {a.Board.BoardPhysicalAddress} state changed to: {a.Board.State}");
  708. if (a.Board.State == BoardStateEnum.Initializing || a.Board.State == BoardStateEnum.UnInit)
  709. {
  710. var vrNzls = a.Board.Nozzles.Join(this.vrBoardNozzles, outer => outer.SiteLevelNozzleId, inner => inner.SiteLevelNozzleId, (o, i) => i);
  711. vrNzls.ToList().ForEach(n => n.FuelingState = VRBoardNozzleFuelingStateEnum.IDLE);
  712. }
  713. else if (a.Board.State == BoardStateEnum.Initialized)
  714. {
  715. if (vrBoardGroup.Context == null) {/* indicates using internal in-memory simulator*/ }
  716. else
  717. {
  718. var historyIncoming = vrBoardGroup.Context.Incoming as HistoryKeepIncoming<DataCollectorMessageBase>;
  719. if (historyIncoming != null)
  720. historyIncoming.Due = 60 * 6;
  721. else
  722. throw new ArgumentException($"This App requires {nameof(GroupHandler)} to use HistoryKeepIncoming for caculate max and avg fueling air and liquid readings.");
  723. }
  724. }
  725. var universalApiHub = this.services.GetRequiredService<UniversalApiHub>();
  726. await universalApiHub.FireEvent(this, OnVaporRecoveryDataCollectorBoardStateChangeEventName, a.Board);
  727. };
  728. vrBoardGroup.OnDataRecieved += async (s, a) =>
  729. {
  730. await ProcessVRBoardDataAsync(s, a.Data);
  731. };
  732. vrBoardGroup.OnBoardNozzleStateChange += async (s, a) =>
  733. {
  734. if (this.logger.IsEnabled(LogLevel.Debug))
  735. logger.LogDebug($"Nozzle with sLNzlId: {a.BoardNozzle.SiteLevelNozzleId} state changed to: {a.BoardNozzle.NozzleState}");
  736. var vrNzl = this.vrBoardNozzles.FirstOrDefault(vrN => vrN.SiteLevelNozzleId == a.BoardNozzle.SiteLevelNozzleId);
  737. if (vrNzl != null)
  738. {
  739. vrNzl.FuelingState = a.BoardNozzle.NozzleState == NozzleStateEnum.Fuelling ?
  740. VRBoardNozzleFuelingStateEnum.FUELING : VRBoardNozzleFuelingStateEnum.IDLE;
  741. }
  742. var universalApiHub = this.services.GetRequiredService<UniversalApiHub>();
  743. await universalApiHub.FireEvent(this,
  744. OnVaporRecoveryDataCollectorBoardNozzleStateChangeEventName, vrNzl);
  745. };
  746. }
  747. #region retrieve latest nozzles' trx flowData from db and fill back to logical vr board nozzle.
  748. var latestNozzlesTrxFlowDatas = await dbHelperForVRBoard.GetLatestNozzlesTrxFlowDatas();
  749. if (latestNozzlesTrxFlowDatas != null)
  750. {
  751. foreach (var n in this.vrBoardNozzles)
  752. {
  753. n.LatestTrxFlowData = latestNozzlesTrxFlowDatas.FirstOrDefault(r => r.SiteLevelNozzleId == n.SiteLevelNozzleId);
  754. }
  755. }
  756. #endregion
  757. #region retrieve latest nozzles' alarms from db and fill back to logical vr board nozzle.
  758. var latestNozzlesAlarms = await dbHelperForVRBoard.GetLatestNozzlesAlarms();
  759. if (latestNozzlesAlarms == null)
  760. {
  761. this.vrBoardNozzles.ForEach(i =>
  762. {
  763. i.HealthState = VRBoardNozzleTrxHealthStateEnum.NORMAL;
  764. });
  765. }
  766. else
  767. {
  768. this.vrBoardNozzles.ForEach(i =>
  769. {
  770. var record = latestNozzlesAlarms.Where(r => i.SiteLevelNozzleId == r.SiteLevelNozzleId).FirstOrDefault();
  771. if (record != null)
  772. {
  773. switch (record.AlarmType)
  774. {
  775. case VRBoardAlarmType.NONE:
  776. i.HealthState = VRBoardNozzleTrxHealthStateEnum.NORMAL;
  777. break;
  778. case VRBoardAlarmType.WARNING:
  779. i.HealthState = VRBoardNozzleTrxHealthStateEnum.WARNING;
  780. break;
  781. case VRBoardAlarmType.ALARM:
  782. i.HealthState = VRBoardNozzleTrxHealthStateEnum.ALARM;
  783. break;
  784. default:
  785. break;
  786. }
  787. }
  788. });
  789. }
  790. #endregion
  791. ScheduleMinutelySaveDbTimer();
  792. //集中播报声光提醒的时间timer设置
  793. ScheduleDailyWarningPlayTimerForVRBoard();
  794. //集中检查,即回溯一天所有交易的气液比的整体状态,即是否不合格笔数超过某个限额
  795. ScheduleDailyWarningCheckTimerForVRBoard();
  796. ScheduleDailyWarningCheckTimerForTankPressure();
  797. #region Purge database data
  798. using (var scope = this.services.CreateScope())
  799. {
  800. try
  801. {
  802. /*VRBoardNozzleTrxFlowData*/
  803. var dbContext = scope.ServiceProvider.GetRequiredService<SqliteDbContext>();
  804. var vrBoardNozzleTrxFlowDatas = dbContext.GenericDatas.Where(gd =>
  805. gd.Owner == AutoMapperProfile.VaporRecoveryOnlineWatchHubApp_MapToDbEntity_Owner
  806. && gd.Type == AutoMapperProfile.VRBoardNozzleTrxFlowData_MapToDbEntity_Type);
  807. var vrBoardNozzleTrxFlowDatasPurgeTimePoint = DateTime.Now.Subtract(new TimeSpan(365 * 2, 0, 0, 0, 0));
  808. dbContext.GenericDatas.RemoveRange(
  809. vrBoardNozzleTrxFlowDatas.Where(d => d.CreatedTimeStamp <= vrBoardNozzleTrxFlowDatasPurgeTimePoint));
  810. /*VRBoardAlarmRecord*/
  811. var vrBoardAlarmRecordDatas = dbContext.GenericDatas.Where(gd =>
  812. gd.Owner == AutoMapperProfile.VaporRecoveryOnlineWatchHubApp_MapToDbEntity_Owner
  813. && gd.Type == AutoMapperProfile.VRBoardAlarmRecord_MapToDbEntity_Type);
  814. var vrBoardAlarmRecordDatasPurgeTimePoint = DateTime.Now.Subtract(new TimeSpan(30 * 4, 0, 0, 0, 0));
  815. dbContext.GenericDatas.RemoveRange(
  816. vrBoardAlarmRecordDatas.Where(d => d.TimeStampProperty0 <= vrBoardAlarmRecordDatasPurgeTimePoint));
  817. /*calibration data should not be cleared directly by timestamp as the hardware replacement may occur at anytime.
  818. the purge should group by nozzle id then timestamp, since the data should be very limited, so leave it for now.*/
  819. //var vrBoardNozzleInitParametersDatas = dbContext.GenericDatas.Where(gd =>
  820. // gd.Owner == AutoMapperProfile.VaporRecoveryOnlineWatchHubApp_MapToDbEntity_Owner
  821. // && gd.Type == AutoMapperProfile.VRBoardNozzleInitParametersData_MapToDbEntity_Type);
  822. //dbContext.GenericDatas.RemoveRange(
  823. // vrBoardNozzleInitParametersDatas.Where(d => d.CreatedTimeStamp <= DateTime.Now.Subtract(new TimeSpan(30 * 48, 0, 0, 0, 0))));
  824. var removedRowCount = await dbContext.SaveChangesAsync();
  825. logger.LogInformation($"Total purged data row count: {removedRowCount}");
  826. }
  827. catch (Exception ex)
  828. {
  829. logger.LogError($"Exception In Purge database data, exception: {ex}");
  830. }
  831. }
  832. #endregion
  833. return true;
  834. }
  835. public async Task<bool> Stop()
  836. {
  837. this.dailyVRBoardNotifyWarningTimer?.Dispose();
  838. this.dailyVRBoardCheckingStateTimer?.Dispose();
  839. this.minutelySaveDbTimer?.Dispose();
  840. return true;
  841. }
  842. #region UniversalApi for VR board related.
  843. [UniversalApi(Description = "Get the persist trx flow datas for nozzle.")]
  844. public Task<IEnumerable<VRBoardNozzleTrxFlowData>> GetVRBoardNozzleTrxFlowDatas(DateTime? startDate, DateTime? endDate, int? siteLevelNozzleId,
  845. int pageIndex, int singlePageRowCount,
  846. string tankPressure, string liquidPressure, string gasConcentrations)
  847. {
  848. return dbHelperForVRBoard.GetNozzlesTrxFlowData(startDate ?? DateTime.Now.AddDays(-31), endDate ?? DateTime.MaxValue,
  849. siteLevelNozzleId ?? 0, pageIndex, singlePageRowCount,
  850. tankPressure, liquidPressure, gasConcentrations);
  851. }
  852. //[UniversalApi(Description = "Get the persist alarms with details for nozzle, if no due time specified, the latest 90 days alarms will be returned.")]
  853. //public Task<IEnumerable<VRBoardAlarmRecord>> GetVRBoardAlarms(DateTime? due, int? siteLevelNozzleId)
  854. //{
  855. // if (due == null)//with no search condition specified, return latest 1000 records.
  856. // {
  857. // return dbHelperForVRBoard.GetAllAlarms();
  858. // }
  859. // else
  860. // {
  861. // //var nozzleId = input.Get("nozzleid", 0);
  862. // //var startTime = input.Get("starttime", DateTime.Now.AddDays(-1));
  863. // //var endTime = input.Get("endtime", DateTime.Now);
  864. // //Logger.LogInformation($"GetAlarmList, nozzleId: {nozzleId}, startTime: {startTime}, endTime: {endTime}");
  865. // return dbHelperForVRBoard.GetAlarms(due.Value, DateTime.Now, siteLevelNozzleId ?? 0);
  866. // }
  867. //}
  868. [UniversalApi(Description = "Get all VR nozzles")]
  869. public async Task<List<VRBoardNozzle>> GetVRBoardNozzles()
  870. {
  871. return this.vrBoardNozzles;
  872. }
  873. [UniversalApi(Description = "获取与油枪气液比相关的(ConfigUI)配置")]
  874. public async Task<VaporRecoveryConfigV1> GetVaporRecoveryConfig()
  875. {
  876. return this.appConfig.VaporRecoveryConfig;
  877. }
  878. [UniversalApi(Description = "获取上一次(保存于数据库中)成功写入所有气液比数据收集板中的初始化参数值")]
  879. public Task<IEnumerable<VRBoardNozzleInitParametersData>> ReadLatestPersistVrBoardNozzleInitParameters()
  880. {
  881. var datas = this.dbHelperForVRBoard.GetLatestPersistBoardNozzleInitParameters();
  882. return datas;
  883. }
  884. [UniversalApi(Description = "从气液比数据收集板中读取初始化参数值")]
  885. public Task<READ_PARA_Response> ReadVrBoardNozzleInitParameters(int siteLevelNozzleId)
  886. {
  887. var nozzleAndBoardPairs = this.vrBoardDeviceHandlers.SelectMany(h => h.Boards.Select(b => new { Board = b, deviceHandler = h }))
  888. .SelectMany(bd => bd.Board.Nozzles.Select(n => new { n, bd })).Where(cp => cp.n.SiteLevelNozzleId == siteLevelNozzleId);
  889. var targetPair = nozzleAndBoardPairs.FirstOrDefault(c => c.n.SiteLevelNozzleId == siteLevelNozzleId);
  890. if (targetPair == null) throw new ArgumentException($"Could not find VRBoard device handler with siteLevelNozzleId: {siteLevelNozzleId} configurated on it.");
  891. return targetPair.bd.deviceHandler.ReadBoardNozzleInitParameters(targetPair.bd.Board.BoardPhysicalAddress, targetPair.n.NozzleNumberOnBoard);
  892. }
  893. [UniversalApi(Description = "写入初始化参数值至气液比数据收集板,如果成功的话则同时保存至数据库中")]
  894. public async Task<bool> WriteAndPersistVrBoardNozzleInitParameters(int siteLevelNozzleId, BoardInitParameterConfigV1 parametersConfig)
  895. {
  896. var nozzleAndBoardPairs = this.vrBoardDeviceHandlers.SelectMany(h => h.Boards.Select(b => new { Board = b, deviceHandler = h }))
  897. .SelectMany(bd => bd.Board.Nozzles.Select(n => new { n, bd })).Where(cp => cp.n.SiteLevelNozzleId == siteLevelNozzleId);
  898. var targetPair = nozzleAndBoardPairs.FirstOrDefault(c => c.n.SiteLevelNozzleId == siteLevelNozzleId);
  899. if (targetPair == null) throw new ArgumentException($"Could not find VRBoard device handler with siteLevelNozzleId: {siteLevelNozzleId} configurated on it.");
  900. var writeResult = await targetPair.bd.deviceHandler.WriteBoardNozzleInitParameters(targetPair.bd.Board.BoardPhysicalAddress, targetPair.n.NozzleNumberOnBoard, parametersConfig);
  901. if (writeResult)
  902. await this.SaveDbAsync(new VRBoardNozzleInitParametersData()
  903. {
  904. SiteLevelNozzleId = siteLevelNozzleId,
  905. DataCollectorDeviceAddress = targetPair.bd.Board.BoardPhysicalAddress,
  906. TimeStamp = DateTime.Now,
  907. 停止加油阀值 = parametersConfig.停止加油阀值,
  908. 加油脉冲当量 = parametersConfig.加油脉冲当量,
  909. 开始加油阀值 = parametersConfig.开始加油阀值,
  910. 最大未变化次数 = parametersConfig.最大未变化次数,
  911. 最小加油量 = parametersConfig.最小加油量,
  912. 气液比值 = parametersConfig.气液比值,
  913. 油气脉冲当量 = parametersConfig.油气脉冲当量
  914. });
  915. return writeResult;
  916. }
  917. public enum VrBoardNozzleInitParameterCalibrationType
  918. {
  919. 油气脉冲当量
  920. }
  921. [UniversalApi(Description = "校准VR board油枪的 初始化参数值.</br> " +
  922. "<b>calibrateType</b>:待校准的参数名称</br>" +
  923. "<b>currentConefficientValue</b>:待校准的参数的当前值,一般它是一个静态系数,业务数据的读数与其相关,校准的过程即通过对此值进行调整而进行</br>" +
  924. "<b>currentReadingValue</b>:与ConefficientValue相关的业务数据读数值,即当前系统未校准前(认为其不准确)的读数值</br>" +
  925. "<b>standardReadingValue</b>:与currentReadingValue相对的,由专业校准设备所读出的业务数据值,一般它与当前未校准前的业务数据读数不一致,校准即指将它两者进行靠近的调整过程</br>" +
  926. "<i>返回值</i>:校准后的系数值")]
  927. public async Task<double> CaculateVrBoardNozzleInitParameterCalibrationValue(VrBoardNozzleInitParameterCalibrationType calibrateType,
  928. double currentConefficientValue, double currentReadingValue, double standardReadingValue)
  929. {
  930. if (calibrateType == VrBoardNozzleInitParameterCalibrationType.油气脉冲当量)
  931. return (standardReadingValue / currentReadingValue) * currentConefficientValue;
  932. throw new ArgumentException("Only Support calibrate for 油气脉冲当量");
  933. }
  934. #endregion
  935. #region UniversalApi for Meters related.
  936. [UniversalApi(Description = "获取与油罐压力表相关的配置")]
  937. public async Task<TankPressureGaugeMeterConfigV1> GetTankPressureGaugeMeterConfigs()
  938. {
  939. return this.appConfig.TankPressureGaugeMeterConfig;
  940. }
  941. [UniversalApi(Description = "获取与液阻压力表相关的配置")]
  942. public async Task<LiquidPressureGaugeMeterConfigV1> GetLiquidPressureGaugeMeterConfigs()
  943. {
  944. return this.appConfig.LiquidPressureGaugeMeterConfig;
  945. }
  946. [UniversalApi(Description = "获取与危险气体浓度表相关的配置")]
  947. public async Task<List<GasConcentrationGaugeMeterConfigV1>> GetGasConcentrationGaugeMeterConfigs()
  948. {
  949. return this.appConfig.GasConcentrationGaugeMeterConfigs;
  950. }
  951. #endregion
  952. private async Task<bool> TrySendAlarmEmailsAsync(string title, string htmlBody)
  953. {
  954. if (string.IsNullOrEmpty(this.appConfig.StationMiscConfig?.StationName))
  955. return false;
  956. if (string.IsNullOrEmpty(this.appConfig.StationMiscConfig?.NotificationMailConfig?.RegionId)
  957. || string.IsNullOrEmpty(this.appConfig.StationMiscConfig?.NotificationMailConfig?.AccessKeyId)
  958. || string.IsNullOrEmpty(this.appConfig.StationMiscConfig?.NotificationMailConfig?.AccessKeySecret))
  959. return false;
  960. if (!(this.appConfig.StationMiscConfig?.NotificationMailConfig.ReceiverAddresses?.Any() ?? false))
  961. return false;
  962. try
  963. {
  964. //Create a client used for initiating a request
  965. IClientProfile profile = DefaultProfile.GetProfile(
  966. this.appConfig.StationMiscConfig?.NotificationMailConfig?.RegionId,
  967. this.appConfig.StationMiscConfig?.NotificationMailConfig?.AccessKeyId,
  968. this.appConfig.StationMiscConfig?.NotificationMailConfig?.AccessKeySecret);
  969. IAcsClient client = new DefaultAcsClient(profile);
  970. foreach (var mailAddress in this.appConfig.StationMiscConfig.NotificationMailConfig.ReceiverAddresses)
  971. {
  972. SingleSendMailRequest request = new SingleSendMailRequest();
  973. //try
  974. //{
  975. /*Sample key from aliyun RAM user under wayne account.
  976. AccessKey ID: LTAI5tFEVuW3DXDkmEdweM21
  977. AccessKey Secret: B25GYOqPgg0y9D74y5C5IB4Nkc1uGK
  978. */
  979. //Version must set to "2017-06-22" when the regionId is not "cn-hangzhou"
  980. //request.Version = "2017-06-22";
  981. if (!string.IsNullOrEmpty(this.appConfig.StationMiscConfig?.NotificationMailConfig?.ApiVersion))
  982. request.Version = this.appConfig.StationMiscConfig?.NotificationMailConfig?.ApiVersion;
  983. request.AccountName = "alarm@emailnotify.ipos.biz";
  984. request.FromAlias = "";
  985. request.AddressType = 1;
  986. request.TagName = "";// "控制台创建的标签";
  987. request.ReplyToAddress = true;
  988. request.ToAddress = mailAddress;// "johnson.shao@doverfs.com";
  989. request.Subject = title;// "加油站油气回收在线监测系统 - 异常报告";
  990. request.HtmlBody = htmlBody;// "邮件正文 from arm";
  991. SingleSendMailResponse httpResponse = client.GetAcsResponse(request);
  992. //}
  993. //catch (ServerException e)
  994. //{
  995. // System.Console.WriteLine(e.ToString());
  996. //}
  997. //catch (ClientException e)
  998. //{
  999. // System.Console.WriteLine(e.ToString());
  1000. //}catch(Exception exxx)
  1001. }
  1002. return true;
  1003. }
  1004. catch (Exception exxx)
  1005. {
  1006. this.logger.LogInformation($"TrySendAlarmEmailAsync failed with error: {exxx}");
  1007. return false;
  1008. }
  1009. }
  1010. private async Task ProcessVRBoardDataAsync(object sender, DataCollectorMessageBase message)
  1011. {
  1012. /*气液比监测板的数据(from device handler)将在此进行处理*/
  1013. var baordGroupDeviceHandler = sender as GroupHandler;
  1014. int? operatingNozzleSiteLevelNozzleId = null;
  1015. switch (message)
  1016. {
  1017. case READ_ONE_HISTORY_DATA_Response trxFlowDataResponse:
  1018. try
  1019. {
  1020. /*一笔加油交易完成后的气液比*/
  1021. operatingNozzleSiteLevelNozzleId =
  1022. this.vrBoardDeviceHandlers.SelectMany(gb => gb.Boards)
  1023. .FirstOrDefault(b => b.BoardPhysicalAddress == trxFlowDataResponse.Address).Nozzles
  1024. .FirstOrDefault(n => n.NozzleNumberOnBoard == trxFlowDataResponse.NozzleNumber)?.SiteLevelNozzleId;
  1025. var vaporVolumeWithDecimal = Util.RoundToDouble(trxFlowDataResponse.AirVolume / Math.Pow(10, this.appConfig.VaporRecoveryConfig.AirVolumeDigits));
  1026. var liquidVolumeWithDecimal = Util.RoundToDouble(trxFlowDataResponse.LiquidVolume / Math.Pow(10, this.appConfig.VaporRecoveryConfig.LiquidVolumeDigits));
  1027. double vaporAndLiquidRatio = 0;
  1028. if (liquidVolumeWithDecimal != 0)
  1029. vaporAndLiquidRatio = Util.RoundToDouble(vaporVolumeWithDecimal / liquidVolumeWithDecimal);
  1030. logger.LogDebug($"VR trx data-> " +
  1031. $"sLNzlId: {operatingNozzleSiteLevelNozzleId ?? -1}, " +
  1032. $"liquid/vapor Vol(with decimal): {liquidVolumeWithDecimal}/{vaporVolumeWithDecimal}, " +
  1033. $"startTime/endTime: {trxFlowDataResponse.FuellingStartTime.ToString("yyyy-MM-dd HH:mm:ss")}/{trxFlowDataResponse.FuellingEndTime.ToString("yyyy-MM-dd HH:mm:ss")}, " +
  1034. $"duration: {trxFlowDataResponse.FuellingEndTime.Subtract(trxFlowDataResponse.FuellingStartTime).TotalSeconds}(s)");
  1035. var perNozzleRule = this.GetNozzleVRGroupQualificationConfig(operatingNozzleSiteLevelNozzleId ?? -1);
  1036. double liquidVolumeMinThreshold;
  1037. if (perNozzleRule == null)
  1038. liquidVolumeMinThreshold = this.appConfig.VaporRecoveryConfig.LiquidVolumeMinThreshold;
  1039. else
  1040. liquidVolumeMinThreshold = perNozzleRule.LiquidVolumeMinThreshold;
  1041. double? maxAirFlowRateWithDecimal = null;
  1042. double? maxLiquidFlowRateWithDecimal = null;
  1043. double? avgAirFlowRateWithDecimal = null;
  1044. double? avgLiquidFlowRateWithDecimal = null;
  1045. #region Caculate max and avg value for air and liquid from fueling progress
  1046. try
  1047. {
  1048. var historyIncoming = baordGroupDeviceHandler.Context?.Incoming as HistoryKeepIncoming<DataCollectorMessageBase>;
  1049. if (historyIncoming != null)
  1050. {
  1051. var allFuelInfos = historyIncoming.History.Where(d => d.Item1 is READ_ONE_HISTORY_DATA_Response t
  1052. && t.Address == trxFlowDataResponse.Address
  1053. && t.NozzleNumber == trxFlowDataResponse.NozzleNumber).OrderByDescending(d => d.Item2);
  1054. //logger.LogDebug($" ##debug siteLevelNozzleId: {operatingNozzleSiteLevelNozzleId ?? -1}, allFuelInfos: {allFuelInfos.Select(f => f.Item2 + " " + f.Item1.ToLogString()).Aggregate("", (acc, n) => acc + " || " + n)}");
  1055. DateTime lastFuelReportDateTime = DateTime.MinValue;
  1056. if (allFuelInfos.Count() > 1)
  1057. {
  1058. lastFuelReportDateTime = allFuelInfos.Skip(1).First().Item2;
  1059. //logger.LogDebug($" ##debug siteLevelNozzleId: {operatingNozzleSiteLevelNozzleId ?? -1}, lastFuelReportDateTime set to: {lastFuelReportDateTime.ToLongTimeString()}");
  1060. }
  1061. var currentFuelProgressInfos =
  1062. historyIncoming.History.Where(d => d.Item1 is READ_WORKING_AND_TYPE_Response p
  1063. && p.Address == trxFlowDataResponse.Address
  1064. && p.NozzleLiquidAndAirFlowData.Any(f => f.Item1 == trxFlowDataResponse.NozzleNumber)
  1065. && d.Item2 >= lastFuelReportDateTime)
  1066. .Select(h => h.Item1).Cast<READ_WORKING_AND_TYPE_Response>().SelectMany(d => d.NozzleLiquidAndAirFlowData).Where(f => f.Item1 == trxFlowDataResponse.NozzleNumber);
  1067. if (currentFuelProgressInfos?.Any() ?? false)
  1068. {
  1069. maxAirFlowRateWithDecimal = Util.RoundToDouble(currentFuelProgressInfos.Select(i => i.Item3)?.Max() / Math.Pow(10, this.appConfig.VaporRecoveryConfig.AirVolumeDigits));
  1070. maxLiquidFlowRateWithDecimal = Util.RoundToDouble(currentFuelProgressInfos.Select(i => i.Item2)?.Max() / Math.Pow(10, this.appConfig.VaporRecoveryConfig.LiquidVolumeDigits));
  1071. avgAirFlowRateWithDecimal = Util.RoundToDouble(currentFuelProgressInfos.Select(i => i.Item3)?.Average() / Math.Pow(10, this.appConfig.VaporRecoveryConfig.AirVolumeDigits));
  1072. avgLiquidFlowRateWithDecimal = Util.RoundToDouble(currentFuelProgressInfos.Select(i => i.Item2)?.Average() / Math.Pow(10, this.appConfig.VaporRecoveryConfig.LiquidVolumeDigits));
  1073. }
  1074. }
  1075. }
  1076. catch (Exception eee)
  1077. {
  1078. logger.LogInformation($"VR trx data-> siteLevelNozzleId: {operatingNozzleSiteLevelNozzleId ?? -1}, exceptioned for caculate max and avg rate related data: {eee}");
  1079. }
  1080. #endregion
  1081. logger.LogDebug($" sLNzlId: {operatingNozzleSiteLevelNozzleId ?? -1}-> maxAirFlowRateWithDecimal: {maxAirFlowRateWithDecimal ?? -1}, maxLiquidFlowRateWithDecimal: {maxLiquidFlowRateWithDecimal ?? -1}, avgAirFlowRateWithDecimal: {avgAirFlowRateWithDecimal ?? -1}, avgLiquidFlowRateWithDecimal: {avgLiquidFlowRateWithDecimal ?? -1}");
  1082. var trxData = new VRBoardNozzleTrxFlowData()
  1083. {
  1084. DataCollectorType = baordGroupDeviceHandler.CurrentDataCollectorType,
  1085. DataCollectorDeviceAddress = trxFlowDataResponse.Address,
  1086. VaporVolumeWithDecimal = vaporVolumeWithDecimal,
  1087. LiquidVolumeWithDecimal = liquidVolumeWithDecimal,
  1088. VaporLiquidRatio = vaporAndLiquidRatio,
  1089. SiteLevelNozzleId = operatingNozzleSiteLevelNozzleId ?? -1,
  1090. MaxAirFlowRateWithDecimal = maxAirFlowRateWithDecimal,
  1091. MaxLiquidFlowRateWithDecimal = maxLiquidFlowRateWithDecimal,
  1092. AvgAirFlowRateWithDecimal = avgAirFlowRateWithDecimal,
  1093. AvgLiquidFlowRateWithDecimal = avgLiquidFlowRateWithDecimal,
  1094. TimeStamp = DateTime.Now,
  1095. FuellingStartTime = trxFlowDataResponse.FuellingStartTime,
  1096. FuellingEndTime = trxFlowDataResponse.FuellingEndTime,
  1097. //SensorId = dataCollectorDeviceHandler.HardwareIdentity,
  1098. 气液比值是否正常 = Is气液比值是否正常(vaporAndLiquidRatio, operatingNozzleSiteLevelNozzleId),
  1099. //VrState = VRState.NOT_SET
  1100. TankPressure = jsonDataDic.ContainsKey("TankPressure") ? double.Parse(jsonDataDic["TankPressure"]["Pressure"].ToString()) : 0,
  1101. LiquidPressure = jsonDataDic.ContainsKey("LiquidPressure") ? double.Parse(jsonDataDic["LiquidPressure"]["Pressure"].ToString()) : 0,
  1102. GasConcentrations = jsonDataDic.ContainsKey("GasConcentrations") ? double.Parse(jsonDataDic["GasConcentrations"]["Concentration"].ToString()) : 0,
  1103. };
  1104. if (trxFlowDataResponse.LiquidVolume /
  1105. Math.Pow(10, this.appConfig.VaporRecoveryConfig.LiquidVolumeDigits) > liquidVolumeMinThreshold)
  1106. {
  1107. await SaveDbAsync(trxData);
  1108. }
  1109. else
  1110. {
  1111. logger.LogInformation($"SiteLevelNozzleId: {operatingNozzleSiteLevelNozzleId ?? -1} report a complete air and liquid data reading with LiquidVolume: {trxFlowDataResponse.LiquidVolume}, which is lower than threashold, will discard the record in db.");
  1112. }
  1113. if (this.vrBoardNozzles.FirstOrDefault(n => n.SiteLevelNozzleId == trxData.SiteLevelNozzleId) is VRBoardNozzle n)
  1114. n.LatestTrxFlowData = trxData;
  1115. var universalApiHub = this.services.GetRequiredService<UniversalApiHub>();
  1116. await universalApiHub.FireEvent(this, OnVaporRecoveryDataCollectorBoardNozzleFlowDataReadEventName, trxData);
  1117. }
  1118. catch (Exception exx)
  1119. {
  1120. logger.LogError($"SiteLevelNozzleId: {operatingNozzleSiteLevelNozzleId ?? -1} Prepare VRBoardNozzleTrxFlowData for saving to db exception, detail: {exx}");
  1121. }
  1122. break;
  1123. case READ_WORKING_AND_TYPE_Response realTimeFlowDataResponse:
  1124. try
  1125. {
  1126. foreach (var fuellingBoardNozzle in realTimeFlowDataResponse.NozzleStates.Where(s => s.Value == NozzleStateEnum.Fuelling))
  1127. {
  1128. /*加油过程中的实时气液比*/
  1129. operatingNozzleSiteLevelNozzleId =
  1130. this.vrBoardDeviceHandlers.SelectMany(gb => gb.Boards)
  1131. .FirstOrDefault(b => b.BoardPhysicalAddress == realTimeFlowDataResponse.Address).Nozzles
  1132. .FirstOrDefault(n => n.NozzleNumberOnBoard == fuellingBoardNozzle.Key)?.SiteLevelNozzleId;
  1133. var liquidAndAirFlowData =
  1134. realTimeFlowDataResponse.NozzleLiquidAndAirFlowData.FirstOrDefault(n => n.Item1 == fuellingBoardNozzle.Key);
  1135. if (liquidAndAirFlowData != null)
  1136. {
  1137. var vaporVolumeWithDecimal = Util.RoundToDouble(liquidAndAirFlowData.Item3 / Math.Pow(10, this.appConfig.VaporRecoveryConfig.AirVolumeDigits));
  1138. var liquidVolumeWithDecimal = Util.RoundToDouble(liquidAndAirFlowData.Item2 / Math.Pow(10, this.appConfig.VaporRecoveryConfig.LiquidVolumeDigits));
  1139. double vaporAndLiquidRatio = 0;
  1140. if (liquidVolumeWithDecimal != 0)
  1141. vaporAndLiquidRatio = Util.RoundToDouble(vaporVolumeWithDecimal / liquidVolumeWithDecimal);
  1142. if (this.logger.IsEnabled(LogLevel.Debug))
  1143. logger.LogDebug($"VR trx flow data-> " +
  1144. $"sLNzlId: {operatingNozzleSiteLevelNozzleId ?? -1}, " +
  1145. $"boardNzlNo.: {liquidAndAirFlowData.Item1}, " +
  1146. $"liquid/vapor Vol(with decimal): {liquidVolumeWithDecimal}/{vaporVolumeWithDecimal}");
  1147. var realTimeFlowData = new VRBoardNozzleTrxFlowData()
  1148. {
  1149. DataCollectorType = baordGroupDeviceHandler.CurrentDataCollectorType,
  1150. DataCollectorDeviceAddress = realTimeFlowDataResponse.Address,
  1151. //DataCollectorNozzleNumber = response.NozzleNumber,
  1152. TimeStamp = DateTime.Now,
  1153. VaporVolumeWithDecimal = vaporVolumeWithDecimal,
  1154. LiquidVolumeWithDecimal = liquidVolumeWithDecimal,
  1155. VaporLiquidRatio = vaporAndLiquidRatio,
  1156. 气液比值是否正常 = Is气液比值是否正常(vaporAndLiquidRatio, operatingNozzleSiteLevelNozzleId),
  1157. SiteLevelNozzleId = operatingNozzleSiteLevelNozzleId ?? -1,
  1158. };
  1159. var universalApiHub = this.services.GetRequiredService<UniversalApiHub>();
  1160. await universalApiHub.FireEvent(this, OnVaporRecoveryDataCollectorBoardNozzleFlowDataReadEventName, realTimeFlowData);
  1161. }
  1162. }
  1163. }
  1164. catch (Exception ex)
  1165. {
  1166. logger.LogError($"SiteLevelNozzleId: {operatingNozzleSiteLevelNozzleId ?? -1} Failed to parse READ_WORKING_AND_TYPE_Response, exception message: {ex.ToString()}");
  1167. }
  1168. break;
  1169. default:
  1170. throw new ArgumentException($"SiteLevelNozzleId: {operatingNozzleSiteLevelNozzleId ?? -1} Unexpected respone msg type: {message.GetType().FullName}");
  1171. }
  1172. }
  1173. private void ScheduleMinutelySaveDbTimer()
  1174. {
  1175. int minutes = 3;
  1176. if (minutelySaveDbTimer == null)
  1177. {
  1178. this.minutelySaveDbTimer = new Timer(new TimerCallback(async _ =>
  1179. {
  1180. if (jsonDataDic.ContainsKey("TankPressure"))
  1181. {
  1182. var newData = new GenericData() { Owner = "VaporRecoveryOnlineWatchHubApp", CreatedTimeStamp = DateTime.Now };
  1183. newData.DoubleProperty0 = double.Parse(jsonDataDic["TankPressure"]["Pressure"].ToString());
  1184. newData.Type = "TankPressure";
  1185. await SaveDbAsync(newData);
  1186. }
  1187. if (jsonDataDic.ContainsKey("LiquidPressure"))
  1188. {
  1189. var newData = new GenericData() { Owner = "VaporRecoveryOnlineWatchHubApp", CreatedTimeStamp = DateTime.Now };
  1190. newData.DoubleProperty1 = double.Parse(jsonDataDic["LiquidPressure"]["Pressure"].ToString());
  1191. newData.Type = "LiquidPressure";
  1192. await SaveDbAsync(newData);
  1193. }
  1194. if (jsonDataDic.ContainsKey("GasConcentrations"))
  1195. {
  1196. var newData = new GenericData() { Owner = "VaporRecoveryOnlineWatchHubApp", CreatedTimeStamp = DateTime.Now };
  1197. newData.DoubleProperty2 = double.Parse(jsonDataDic["GasConcentrations"]["Concentration"].ToString());
  1198. newData.Type = "GasConcentrations";
  1199. await SaveDbAsync(newData);
  1200. }
  1201. }), null, TimeSpan.FromMinutes(minutes), TimeSpan.FromMinutes(minutes));
  1202. }
  1203. else
  1204. {
  1205. this.minutelySaveDbTimer?.Change(TimeSpan.FromMinutes(minutes), TimeSpan.FromMinutes(minutes));
  1206. }
  1207. }
  1208. private void ScheduleDailyWarningPlayTimerForVRBoard()
  1209. {
  1210. var alarmPlayTime = this.appConfig.VaporRecoveryConfig.AlarmPlayingTimeInDayWith24HourStyle;
  1211. var time = alarmPlayTime.Split(".");
  1212. if (time.Length != 2)
  1213. throw new ArgumentException($"illegal VRDataCollectorBoard alarmPlayTime value: ${alarmPlayTime ?? ""}");
  1214. double.TryParse(time[0], out double hour);
  1215. double.TryParse(time[1], out double minute);
  1216. DateTime todayPlayTime = DateTime.Now.Date.AddHours(hour).AddMinutes(minute);
  1217. DateTime nextPlayTime = DateTime.Now <= todayPlayTime ? todayPlayTime : todayPlayTime.AddDays(1);
  1218. if (this.dailyVRBoardNotifyWarningTimer == null)
  1219. {
  1220. logger.LogInformation($"ScheduleDailyWarningPlayTimer next scheduled time: {nextPlayTime.ToString()}, " +
  1221. $"due in {(nextPlayTime - DateTime.Now).TotalSeconds} seconds");
  1222. this.dailyVRBoardNotifyWarningTimer = new Timer(async _ =>
  1223. {
  1224. logger.LogInformation($"Daily Warning Play Timer is kicking off...");
  1225. var universalApiHub = this.services.GetRequiredService<UniversalApiHub>();
  1226. var alarmRecords = await dbHelperForVRBoard.GetLatestNozzlesAlarms();//.GetAlarmsByTimeRange((DateTime.Now.Date, DateTime.Now));
  1227. logger.LogInformation($" Total {alarmRecords?.Count() ?? -1} data are read, they're(siteLevelNozzleId<->AlarmType): {string.Join(", ", alarmRecords?.Select(r => r.SiteLevelNozzleId + "<->" + r.AlarmType) ?? new string[] { })}");
  1228. foreach (var alarm in alarmRecords.Where(al => al.AlarmType != VRBoardAlarmType.NONE))
  1229. {
  1230. await universalApiHub.FirePersistGenericAlarm(this,
  1231. new GenericAlarm()
  1232. {
  1233. Title = $"lang-en-us:Nozzle {alarm.SiteLevelNozzleId} A/L daily ratio not qualifiedlang-zh-cn:{alarm.SiteLevelNozzleId}号油枪气液比数值当日不合格lang-zh-tw:{alarm.SiteLevelNozzleId}號油槍氣液比數值當日不合格",
  1234. Category = "lang-en-us:Nozzle A/L ratiolang-zh-cn:油枪气液比lang-zh-tw:油槍氣液比",
  1235. Severity = (alarm.AlarmType == VRBoardAlarmType.ALARM ? GenericAlarmSeverity.Error : GenericAlarmSeverity.Warning),
  1236. Detail = (alarm.AlarmDetails ?? "") + (alarm.AlarmDescription ?? "")
  1237. }, ga => ga.Title);
  1238. }
  1239. if (alarmRecords.Where(al => al.AlarmType == VRBoardAlarmType.ALARM).Any())
  1240. {
  1241. var nozzlesStr = string.Join(", ", alarmRecords.Where(al => al.AlarmType == VRBoardAlarmType.ALARM).Select(al => al.SiteLevelNozzleId.ToString()));
  1242. var sendEmailResult = await TrySendAlarmEmailsAsync(
  1243. $"lang-zh-cn:在线监测系统异常情况报告 - {this.appConfig.StationMiscConfig?.StationName ?? ""}lang-zh-tw:在線監測系統異常情況報告 - {this.appConfig.StationMiscConfig?.StationName ?? ""}".LocalizedContent("zh-tw"),
  1244. $"lang-zh-cn:油枪: {nozzlesStr} 气液比数值<b>连续多日</b>不合格,请即时处理.lang-zh-tw:油槍: {nozzlesStr} 氣液比數值<b>連續多日</b>不合格,請即時處理.".LocalizedContent("zh-tw"));
  1245. logger.LogInformation($"Firing alarm for VR, TrySendAlarmEmails with overall result: {sendEmailResult}");
  1246. }
  1247. await universalApiHub?.FireEvent(this, OnVaporRecoveryDataCollectorBoardAlarmsEventName, new VRBoardAlarmListEventArgs(alarmRecords));
  1248. }, null, nextPlayTime - DateTime.Now, TimeSpan.FromHours(24));
  1249. }
  1250. else
  1251. {
  1252. logger.LogInformation($"ScheduleDailyWarningPlayTimer next rescheduled time: {nextPlayTime.ToString()}, " +
  1253. $"due in {(nextPlayTime - DateTime.Now).TotalSeconds} seconds");
  1254. this.dailyVRBoardNotifyWarningTimer.Change(nextPlayTime - DateTime.Now, TimeSpan.FromHours(24));
  1255. }
  1256. }
  1257. private void ScheduleDailyWarningCheckTimerForTankPressure()
  1258. {
  1259. var nextDay = DateTime.Today.AddDays(1);
  1260. var untilNextCheck = nextDay - DateTime.Now;
  1261. if (dailyTankPressureWarningTimer == null)
  1262. {
  1263. logger.LogInformation($"ScheduleDailyWarningCheckTimerForTankPressure at : {nextDay.ToString()}, " +
  1264. $"due in {(nextDay - DateTime.Now).TotalSeconds} seconds");
  1265. this.dailyTankPressureWarningTimer = new Timer(async (_) =>
  1266. {
  1267. logger.LogInformation($"DailyTankPressureWarningTimer with lastTankPressureWarningTime: {lastTankPressureWarningTime}, " +
  1268. $"lastTankPressureWarningDate {lastTankPressureWarningDate}");
  1269. if (lastTankPressureWarningTime.Year != 1)
  1270. {
  1271. lastTankPressureWarningTime = new DateTime();
  1272. await SaveOrUpdateDbAsync("LastTankPressureWarningTime", new GenericData()
  1273. {
  1274. Type = "LastTankPressureWarningTime",
  1275. CreatedTimeStamp = lastTankPressureWarningTime
  1276. });
  1277. }
  1278. var yesterday = DateTime.Today.AddDays(-1);
  1279. if (lastTankPressureWarningDate.Year != 1 && !lastTankPressureWarningDate.Equals(yesterday))
  1280. {
  1281. tankPressureWarningTotalDays = 0;
  1282. lastTankPressureWarningDate = new DateTime();
  1283. await SaveOrUpdateDbAsync("LastTankPressureWarningDate", new GenericData()
  1284. {
  1285. Type = "LastTankPressureWarningDate",
  1286. CreatedTimeStamp = lastTankPressureWarningDate,
  1287. IntProperty0 = tankPressureWarningTotalDays
  1288. });
  1289. }
  1290. }, null, untilNextCheck, TimeSpan.FromHours(24));
  1291. }
  1292. else
  1293. {
  1294. logger.LogInformation($"Rescheduled next TankPressure check will be triggered at : {nextDay.ToString()}, " +
  1295. $"due in {(nextDay - DateTime.Now).TotalSeconds} seconds");
  1296. this.dailyTankPressureWarningTimer?.Change(untilNextCheck, TimeSpan.FromHours(24));
  1297. }
  1298. }
  1299. private void ScheduleDailyWarningCheckTimerForVRBoard()
  1300. {
  1301. var checkingTime = this.appConfig.VaporRecoveryConfig.RetrospectiveWarningCheckingTime;
  1302. var time = checkingTime.Split(".");
  1303. if (time.Length != 2)
  1304. throw new ArgumentException($"illegal VRDataCollectorBoard WarningCheckingTime value: ${checkingTime ?? ""}");
  1305. double.TryParse(time[0], out double hour);
  1306. double.TryParse(time[1], out double minute);
  1307. var now = DateTime.Now;
  1308. DateTime today = now.Date.AddHours(hour).AddMinutes(minute);
  1309. DateTime nextDay = now <= today ? today : today.AddDays(1);
  1310. var untilNextCheck = nextDay.AddSeconds(-1) - DateTime.Now;
  1311. if (dailyVRBoardCheckingStateTimer == null)
  1312. {
  1313. logger.LogInformation($"ScheduleDailyWarningCheckTimerForVRBoard at : {nextDay.ToString()}, " +
  1314. $"due in {(nextDay - DateTime.Now).TotalSeconds} seconds");
  1315. this.dailyVRBoardCheckingStateTimer = new Timer(async (_) =>
  1316. {
  1317. var startTime = DateTime.Now.Subtract(new TimeSpan(0, 0,
  1318. (int)(this.appConfig.VaporRecoveryConfig.TurnToWarningStateInRecentHoursThreshold * 3600)));
  1319. var endTime = DateTime.Now;
  1320. logger.LogInformation($"Daily Warning Check Timer is kicking off(target time range is from: {startTime.ToString("yyyy-MM-dd HH:mm:ss")} to: {endTime.ToString("yyyy-MM-dd HH:mm:ss")})...");
  1321. await CaculateAndPersistVRBoardHealthStateForNozzles(startTime, endTime);
  1322. }, null, untilNextCheck, TimeSpan.FromHours(24));
  1323. }
  1324. else
  1325. {
  1326. logger.LogInformation($"Rescheduled next vr state check will be triggered at : {nextDay.ToString()}, " +
  1327. $"due in {(nextDay - DateTime.Now).TotalSeconds} seconds");
  1328. this.dailyVRBoardCheckingStateTimer?.Change(untilNextCheck, TimeSpan.FromHours(24));
  1329. }
  1330. }
  1331. private async Task CaculateAndPersistVRBoardHealthStateForNozzles(DateTime startTime, DateTime endTime)
  1332. {
  1333. var nozzlesTrxFlowDatas = await dbHelperForVRBoard.GetNozzlesTrxFlowDatasByTimeRange((startTime, endTime));
  1334. // todo change WarningTimeRangeFromNow to WarningTimeRange
  1335. var nozzleWithTrxFlowDataRatiosGroup = nozzlesTrxFlowDatas.GroupBy(g => g.SiteLevelNozzleId).Select(g => new
  1336. {
  1337. SiteLevelNozzleId = g.Key,
  1338. TrxFlowDataRatios = g.Select(x => x.VaporLiquidRatio).ToList()
  1339. });
  1340. foreach (var nozzle in this.vrBoardNozzles)
  1341. {
  1342. var alarm = new VRBoardAlarmRecord();
  1343. var nozzleWithTrxFlowDataRatios = nozzleWithTrxFlowDataRatiosGroup.FirstOrDefault(g => g.SiteLevelNozzleId == nozzle.SiteLevelNozzleId);
  1344. if (nozzleWithTrxFlowDataRatios == null) // no fueling during last warning period, typically 24 hours
  1345. {
  1346. logger.LogInformation($"Nozzle VR Health Check, SiteLevelNozzle: {nozzle.SiteLevelNozzleId} has no data at all, set HealthState to NORMAL");
  1347. nozzle.HealthState = VRBoardNozzleTrxHealthStateEnum.NORMAL;
  1348. alarm = CreateNoAlarmRecord(nozzle);
  1349. }
  1350. else
  1351. {
  1352. var perNozzleRule = this.GetNozzleVRGroupQualificationConfig(nozzle.SiteLevelNozzleId);
  1353. double unqualifiedAirLiquidRatioRecordsPecentageTurnToWarningStateThreshold;
  1354. int unqualifiedTrxCount;
  1355. if (perNozzleRule == null)
  1356. {
  1357. unqualifiedTrxCount = nozzleWithTrxFlowDataRatios.TrxFlowDataRatios
  1358. .Where(i => i < this.appConfig.VaporRecoveryConfig.QualifiedAirLiquidRatioMin
  1359. || i > this.appConfig.VaporRecoveryConfig.QualifiedAirLiquidRatioMax).ToList().Count;
  1360. unqualifiedAirLiquidRatioRecordsPecentageTurnToWarningStateThreshold = this.appConfig.VaporRecoveryConfig.UnqualifiedAirLiquidRatioRecordsPecentageTurnToWarningStateThreshold;
  1361. }
  1362. else
  1363. {
  1364. unqualifiedTrxCount = nozzleWithTrxFlowDataRatios.TrxFlowDataRatios
  1365. .Where(i => i < perNozzleRule.QualifiedAirLiquidRatioMin
  1366. || i > perNozzleRule.QualifiedAirLiquidRatioMax).ToList().Count;
  1367. unqualifiedAirLiquidRatioRecordsPecentageTurnToWarningStateThreshold = perNozzleRule.UnqualifiedAirLiquidRatioRecordsPecentageTurnToWarningStateThreshold;
  1368. }
  1369. var totalTrxCount = nozzleWithTrxFlowDataRatios.TrxFlowDataRatios.ToList().Count;
  1370. logger.LogInformation($"Nozzle VR Health Check, SiteLevelNozzle: {nozzle.SiteLevelNozzleId} using {(perNozzleRule == null ? "global" : "perNozzle")} rule, has {unqualifiedTrxCount} unqualified trx data, and total trx data count is {totalTrxCount}");
  1371. if (totalTrxCount > 0)
  1372. {
  1373. var unqualifiedRatio = ((double)unqualifiedTrxCount) / totalTrxCount;
  1374. var threshold = (unqualifiedAirLiquidRatioRecordsPecentageTurnToWarningStateThreshold / 100.0);
  1375. if (unqualifiedRatio > threshold)
  1376. {
  1377. logger.LogInformation($"Nozzle VR Health Check, SiteLevelNozzle: {nozzle.SiteLevelNozzleId} will be set with HealthState to WARNING as unqualifiedRatio is: {unqualifiedRatio} which > threshold: {threshold}");
  1378. nozzle.HealthState = VRBoardNozzleTrxHealthStateEnum.WARNING;
  1379. alarm.AlarmTime = DateTime.Now.AddHours(-1);
  1380. alarm.SiteLevelNozzleId = nozzleWithTrxFlowDataRatios.SiteLevelNozzleId;
  1381. alarm.AlarmType = VRBoardAlarmType.WARNING;
  1382. alarm.AlarmDescription = $"lang-zh-cn:{nozzleWithTrxFlowDataRatios.SiteLevelNozzleId}号枪气液比报警lang-zh-tw:{nozzleWithTrxFlowDataRatios.SiteLevelNozzleId}號槍氣液比報警";
  1383. alarm.AlarmDetails =
  1384. $"lang-zh-cn:{nozzleWithTrxFlowDataRatios.SiteLevelNozzleId}号枪,使用{(perNozzleRule == null ? "全局" : "单枪")}规则," +
  1385. $"有效气液比{totalTrxCount}次,不合格{unqualifiedTrxCount}次,不合格占比{Util.Round(unqualifiedRatio) * 100}%," +
  1386. $"平均气液比{Util.Round(nozzleWithTrxFlowDataRatios.TrxFlowDataRatios.Average())}" +
  1387. $"lang-zh-tw:{nozzleWithTrxFlowDataRatios.SiteLevelNozzleId}號槍,使用{(perNozzleRule == null ? "全局" : "單槍")}規則," +
  1388. $"有效氣液比{totalTrxCount}次,不合格{unqualifiedTrxCount}次,不合格占比{Util.Round(unqualifiedRatio) * 100}%," +
  1389. $"平均氣液比{Util.Round(nozzleWithTrxFlowDataRatios.TrxFlowDataRatios.Average())}";
  1390. alarm.TimeInterval = unqualifiedAirLiquidRatioRecordsPecentageTurnToWarningStateThreshold;
  1391. alarm.RecordCount = totalTrxCount;
  1392. alarm.AlarmCount = unqualifiedTrxCount;
  1393. alarm.AlarmPercentage = Util.RoundToDouble(unqualifiedRatio);
  1394. alarm.AverageVaporLiquidRatio = Util.RoundToDouble(nozzleWithTrxFlowDataRatios.TrxFlowDataRatios.Average());
  1395. alarm.Acknowledged = 0;
  1396. var alarmConditionCheckFrom = endTime.Date.Subtract(TimeSpan.FromHours(24 * (this.appConfig.VaporRecoveryConfig.WarningToAlarmStateLastingDaysThreshold - 1)));
  1397. var alarmConditionCheckTo = endTime.Date;
  1398. logger.LogInformation($"Nozzle VR Health Check, SiteLevelNozzle: {nozzle.SiteLevelNozzleId} Warning to Alarm state is on caculating(target time range is from: {alarmConditionCheckFrom.ToString("yyyy-MM-dd HH:mm:ss")} to: {alarmConditionCheckTo.ToString("yyyy-MM-dd HH:mm:ss")})...");
  1399. var recentWarningOrAlarmRecords = await dbHelperForVRBoard.GetWarningOrAlarmStateRecordsByTimeRange(
  1400. (alarmConditionCheckFrom, alarmConditionCheckTo),
  1401. nozzleWithTrxFlowDataRatios.SiteLevelNozzleId);
  1402. logger.LogInformation($"Nozzle VR Health Check, SiteLevelNozzle: {nozzle.SiteLevelNozzleId} recent WarningOrAlarm Records count is {recentWarningOrAlarmRecords.Count()}");
  1403. if (recentWarningOrAlarmRecords.Count() >= (this.appConfig.VaporRecoveryConfig.WarningToAlarmStateLastingDaysThreshold - 1))
  1404. {
  1405. alarm.AlarmType = VRBoardAlarmType.ALARM;
  1406. nozzle.HealthState = VRBoardNozzleTrxHealthStateEnum.ALARM;
  1407. }
  1408. }
  1409. else
  1410. {
  1411. logger.LogInformation($"Nozzle VR Health Check, SiteLevelNozzle: {nozzle.SiteLevelNozzleId} will be set with HealthState to NORMAL as unqualifiedRatio is: {unqualifiedRatio} which <= threshold: {threshold}");
  1412. alarm.AlarmTime = DateTime.Now.AddHours(-1);
  1413. alarm.SiteLevelNozzleId = nozzleWithTrxFlowDataRatios.SiteLevelNozzleId;
  1414. alarm.AlarmType = VRBoardAlarmType.NONE;
  1415. alarm.AlarmDescription = $"{nozzleWithTrxFlowDataRatios.SiteLevelNozzleId}号枪气液比正常";
  1416. alarm.AlarmDetails = $"{nozzleWithTrxFlowDataRatios.SiteLevelNozzleId}号枪," +
  1417. $"有效气液比{totalTrxCount}次,不合格{unqualifiedTrxCount}次,不合格占比{Util.Round(unqualifiedRatio) * 100}%," +
  1418. $"平均气液比{Util.Round(nozzleWithTrxFlowDataRatios.TrxFlowDataRatios.Average())}";
  1419. alarm.TimeInterval = unqualifiedAirLiquidRatioRecordsPecentageTurnToWarningStateThreshold;
  1420. alarm.RecordCount = totalTrxCount;
  1421. alarm.AlarmCount = unqualifiedTrxCount;
  1422. alarm.AlarmPercentage = Util.RoundToDouble(unqualifiedRatio);
  1423. alarm.AverageVaporLiquidRatio = Util.RoundToDouble(nozzleWithTrxFlowDataRatios.TrxFlowDataRatios.Average());
  1424. alarm.Acknowledged = 0;
  1425. nozzle.HealthState = VRBoardNozzleTrxHealthStateEnum.NORMAL;
  1426. }
  1427. }
  1428. else
  1429. {
  1430. alarm = CreateNoAlarmRecord(nozzle);
  1431. nozzle.HealthState = VRBoardNozzleTrxHealthStateEnum.NORMAL;
  1432. }
  1433. }
  1434. await SaveDbAsync(alarm);
  1435. logger.LogInformation($"Nozzle VR Health Check, SiteLevelNozzle: {nozzle.SiteLevelNozzleId} done by save an Alarm with type: {alarm.AlarmType} into db.");
  1436. var universalApiHub = this.services.GetRequiredService<UniversalApiHub>();
  1437. await universalApiHub.FireEvent(this,
  1438. OnVaporRecoveryDataCollectorBoardNozzleStateChangeEventName, nozzle);
  1439. }
  1440. }
  1441. private VRBoardAlarmRecord CreateNoAlarmRecord(VRBoardNozzle nozzle)
  1442. {
  1443. var perNozzleRule = this.GetNozzleVRGroupQualificationConfig(nozzle.SiteLevelNozzleId);
  1444. double unqualifiedAirLiquidRatioRecordsPecentageTurnToWarningStateThreshold;
  1445. if (perNozzleRule == null)
  1446. unqualifiedAirLiquidRatioRecordsPecentageTurnToWarningStateThreshold = this.appConfig.VaporRecoveryConfig.UnqualifiedAirLiquidRatioRecordsPecentageTurnToWarningStateThreshold;
  1447. else
  1448. unqualifiedAirLiquidRatioRecordsPecentageTurnToWarningStateThreshold = perNozzleRule.UnqualifiedAirLiquidRatioRecordsPecentageTurnToWarningStateThreshold;
  1449. return new VRBoardAlarmRecord()
  1450. {
  1451. AlarmTime = DateTime.Now.AddHours(-1),
  1452. SiteLevelNozzleId = nozzle.SiteLevelNozzleId,
  1453. AlarmType = VRBoardAlarmType.NONE,
  1454. AlarmDescription = $"{nozzle.SiteLevelNozzleId}号枪气液比正常(没有交易)",
  1455. AlarmDetails = $"{nozzle.SiteLevelNozzleId}号枪, 有效气液比0次, 不合格0次, 不合格占比0%, 平均气液比0",
  1456. TimeInterval = unqualifiedAirLiquidRatioRecordsPecentageTurnToWarningStateThreshold,
  1457. RecordCount = 0,
  1458. AlarmCount = 0,
  1459. AlarmPercentage = 0,
  1460. AverageVaporLiquidRatio = 0,
  1461. Acknowledged = 0
  1462. };
  1463. }
  1464. private bool Is气液比值是否正常(double vaporLiquidRatio, int? sourceNozzleSiteLevelId)
  1465. {
  1466. if (sourceNozzleSiteLevelId != null)
  1467. {
  1468. var rule = this.GetNozzleVRGroupQualificationConfig(sourceNozzleSiteLevelId.Value);
  1469. if (rule != null)
  1470. return vaporLiquidRatio >= rule.QualifiedAirLiquidRatioMin && vaporLiquidRatio <= rule.QualifiedAirLiquidRatioMax;
  1471. }
  1472. return vaporLiquidRatio >= this.appConfig.VaporRecoveryConfig.QualifiedAirLiquidRatioMin && vaporLiquidRatio <= this.appConfig.VaporRecoveryConfig.QualifiedAirLiquidRatioMax;
  1473. }
  1474. private PerNozzleVRGroupQualificationDefinitionV1 GetNozzleVRGroupQualificationConfig(int siteLevelNozzleId)
  1475. {
  1476. var definition = this.appConfig.VaporRecoveryConfig.PerNozzleVRConfig?.QualificationDefinitions
  1477. ?.FirstOrDefault(m => m.NozzleIdsString.Split(';').Any(nId => nId == siteLevelNozzleId.ToString()));
  1478. return definition;
  1479. }
  1480. private async Task SaveDbAsync<T>(T data)
  1481. {
  1482. var dbModel = this.objMapper.Map<GenericData>(data);
  1483. using (var scope = this.services.CreateScope())
  1484. {
  1485. try
  1486. {
  1487. var dbContext = scope.ServiceProvider.GetRequiredService<SqliteDbContext>();
  1488. dbContext.GenericDatas.Add(dbModel);
  1489. await dbContext.SaveChangesAsync();
  1490. }
  1491. catch (Exception ex)
  1492. {
  1493. logger.LogError($"Exception In SaveDbAsync for data: {data}, exception: {ex}");
  1494. }
  1495. }
  1496. }
  1497. private IEnumerable<GenericData> GetLastWarningTimeData(string type)
  1498. {
  1499. using (var scope = this.services.CreateScope())
  1500. {
  1501. var dbContext = scope.ServiceProvider.GetRequiredService<SqliteDbContext>();
  1502. var genericDatas = dbContext.GenericDatas.Where(gd =>
  1503. gd.Type == type
  1504. && gd.Owner == "VaporRecoveryOnlineWatchHubApp");
  1505. return this.objMapper.Map<IEnumerable<GenericData>>(genericDatas.ToList());
  1506. }
  1507. }
  1508. private async Task SaveOrUpdateDbAsync(string type, GenericData data)
  1509. {
  1510. using (var scope = this.services.CreateScope())
  1511. {
  1512. try
  1513. {
  1514. var dbContext = scope.ServiceProvider.GetRequiredService<SqliteDbContext>();
  1515. var genericDatas = dbContext.GenericDatas.Where(gd =>
  1516. gd.Type == type
  1517. && gd.Owner == AutoMapperProfile.VaporRecoveryOnlineWatchHubApp_MapToDbEntity_Owner);
  1518. if (genericDatas.Count() > 0)
  1519. dbContext.GenericDatas.RemoveRange(genericDatas);
  1520. data.Owner = AutoMapperProfile.VaporRecoveryOnlineWatchHubApp_MapToDbEntity_Owner;
  1521. dbContext.GenericDatas.Add(data);
  1522. await dbContext.SaveChangesAsync();
  1523. }
  1524. catch (Exception ex)
  1525. {
  1526. logger.LogError($"In SaveOrUpdateDbAsync: {ex}");
  1527. }
  1528. }
  1529. }
  1530. }
  1531. }