using AutoMapper; using Edge.Core.Database; using Edge.Core.Database.Models; using Edge.Core.IndustryStandardInterface.PhotoVoltaicInverter; using Edge.Core.Processor; using Edge.Core.Processor.Dispatcher.Attributes; using Edge.Core.UniversalApi; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; namespace SuZhou_SIPAC_Client { [MetaPartsDescriptor( "SIPAC 苏州工业园区碳达峰平台", "用于配置 SIPAC 苏州工业园区 光伏发电数据接入园区的碳达峰平台 接入app的参数", new[] { "lang-zh-cn:光伏lang-en-us:PhotoVoltaic" })] public class App : IAppProcessor { private bool isStarted = false; private AppConfigV1 appConfig; private IServiceProvider services; private IEnumerable inverterHandlers; private System.Timers.Timer httpPostTimer; private ILogger logger = NullLogger.Instance; public string MetaConfigName { get; set; } private HttpClient httpClient = new HttpClient(); public class AppConfigV1 { /// /// 1. 光伏接口(单个插入) /// public string HttpPostUrl_ForSingleRecord { get; set; } public string HttpPostTestUrl { get; set; } = "http://jjdn-yqfk.sipac.gov.cn/api/jjdn/photovoltaics/test"; public int HttpPostIntervalBySecond { get; set; } = 60 * 5; /// /// 2. 光伏接口(批量插入) /// //public string HttpPostUrl_ForBatchRecords { get; set; } public string Id_唯一标识符 { get; set; } public string gffdqyhh_光伏发电企业户号 { get; set; } public string qymc_企业名称 { get; set; } public string tyshxydm_统一社会信用代码 { get; set; } public string jsdd_建设地点 { get; set; } public string jsddjwd_建设地点经纬度 { get; set; } public string xmmc_项目名称 { get; set; } public string bwtysj_并网投运时间 { get; set; } } [ParamsJsonSchemas("appCtorParamsJsonSchema")] public App(AppConfigV1 appConfig, IServiceProvider services) { this.appConfig = appConfig; var loggerFactory = services.GetRequiredService(); this.logger = loggerFactory.CreateLogger("DynamicPrivate_SuZhou_SIPAC_Client"); this.services = services; this.httpPostTimer = new System.Timers.Timer(); this.httpPostTimer.Interval = this.appConfig.HttpPostIntervalBySecond * 1000; this.httpPostTimer.Elapsed += async (s, a) => { await ReadAndPersistDeviceRealTimeDataAsync(); }; } private async Task ReadAndPersistDeviceRealTimeDataAsync() { if (this.inverterHandlers == null || !this.inverterHandlers.Any()) { //var universalApiHub = this.services.GetRequiredService(); //await universalApiHub.FirePersistGenericAlarmIfNotExists(this, // new GenericAlarm() // { // Title = $"地址为{targetDevice.SlaveAddress}的光伏逆变器未配置", // Category = $"光伏逆变器", // Detail = $"地址为{targetDevice.SlaveAddress}的光伏逆变器:{targetDevice.Description ?? ""} 于 {DateTime.Now} 成功连接,但本地未对其进行配置,请打开FCC配置页面添加此设备", // Severity = GenericAlarmSeverity.Warning // }, ga => ga.Detail, // ga => ga.Detail); return; } var realTimeDataInfos = new List>(); foreach (var h in this.inverterHandlers) { var devices = h.GetDevices(); foreach (var d in devices) { try { this.logger.LogDebug($"Reading realtime data for inverter({d.Name ?? ""} - {(d.Description ?? "")} with slaveAddr: {d.SlaveAddress})..."); var data = await h.ReadRealTimeDataAsync(d.SlaveAddress); this.logger.LogDebug($" read realtime data for inverter(slaveAddr: {d.SlaveAddress}): {data.ToLogString()}"); realTimeDataInfos.Add(new Tuple(d, data)); } catch (Exception eee) { /*the inverter device always shutdown itself automatically at midnight, so ommit below to avoid produce too many alarms */ this.logger.LogInformation($" read realtime data for inverter(slaveAddr: {d.SlaveAddress}) got exception, will skip this reading loop: {eee}"); //var universalApiHub = this.services.GetRequiredService(); //await universalApiHub.FirePersistGenericAlarmIfNotExists(this, // new GenericAlarm() // { // Title = $"读取地址为{d.SlaveAddress}的光伏逆变器发电数据失败", // Category = $"光伏逆变器", // Detail = $"地址为{d.SlaveAddress}的光伏逆变器:{d.Description ?? ""} 于 {DateTime.Now} 进行实时发电数据读取时出错.", // Severity = GenericAlarmSeverity.Warning // }, ga => ga.Detail, // ga => ga.Detail); //return; } } } await this.PersistRealTimeDataAsync(realTimeDataInfos); } public void Init(IEnumerable processors) { var inverterHandlers = processors.WithHandlerOrApp().SelectHandlerOrAppThenCast(); this.inverterHandlers = inverterHandlers; this.logger.LogInformation($"Found {this.inverterHandlers?.Count() ?? -1} inverter group handlers, and total {this.inverterHandlers?.SelectMany(ih => ih.GetDevices())?.Count() ?? -1} devices."); } private async Task PersistRealTimeDataAsync(IEnumerable> realTimeDataBatchInfos) { //Guid readingBatchNo = Guid.NewGuid(); var realTimeDataModels = realTimeDataBatchInfos.Select(d => new InverterRealTimeDataModel() { ReadTimeStamp = DateTime.Now, DeviceName = d.Item1.Name, DeviceDescription = d.Item1.Description, DeviceSlaveAddress = d.Item1.SlaveAddress, Raw_当日有功发电量 = d.Item2.日有功发电量, Raw_当日无功发电量 = d.Item2.日无功发电量, }).ToList(); using (var dbContext = new AppDbContext()) { try { var lastAccumulatedDataModel = await dbContext.InverterAccumulatedDatas.Include(d => d.SourceRealTimeDatas).OrderByDescending(d => d.Id).FirstOrDefaultAsync(); decimal 当日有功发电量_incremental = 0; decimal 当日无功发电量_incremental = 0; if (lastAccumulatedDataModel == null) { 当日有功发电量_incremental = realTimeDataModels.Sum(d => d.Raw_当日有功发电量); 当日无功发电量_incremental = realTimeDataModels.Sum(d => d.Raw_当日无功发电量); } else { if (realTimeDataModels.Max(n => n.ReadTimeStamp).Year == lastAccumulatedDataModel.CreatedTimeStamp.Year && realTimeDataModels.Max(n => n.ReadTimeStamp).DayOfYear == lastAccumulatedDataModel.CreatedTimeStamp.DayOfYear) { /*当日有功发电量 and 当日无功发电量 are directly retrieved from device, and device already accumlated them, * so here need caculate the incremental and later add to 当月 and 当年 */ 当日有功发电量_incremental = realTimeDataModels.Sum(d => d.Raw_当日有功发电量) - lastAccumulatedDataModel.当日有功发电量; 当日无功发电量_incremental = realTimeDataModels.Sum(d => d.Raw_当日无功发电量) - lastAccumulatedDataModel.当日无功发电量; if (当日有功发电量_incremental < 0 || 当日无功发电量_incremental < 0) { this.logger.LogInformation($"PersistRealTimeDataAsync, see at least one negative value for 当日有功发电量_incremental: {当日有功发电量_incremental} OR 当日无功发电量_incremental: {当日无功发电量_incremental}, will reset today's accumulation as it likes the device side reset its accum."); /*this indicates the device side reset day accum, for what reason?*/ 当日有功发电量_incremental = realTimeDataModels.Sum(d => d.Raw_当日有功发电量); 当日无功发电量_incremental = realTimeDataModels.Sum(d => d.Raw_当日无功发电量); } } else { this.logger.LogDebug($"PersistRealTimeDataAsync, see a day flip, so set 当日 incremental to this round of realTime reading data."); /*this indicates a day flip, the device side should have cleared the day accumluation and start a new round from zero for day*/ 当日有功发电量_incremental = realTimeDataModels.Sum(d => d.Raw_当日有功发电量); 当日无功发电量_incremental = realTimeDataModels.Sum(d => d.Raw_当日无功发电量); } } this.logger.LogDebug($"PersistRealTimeDataAsync, 当日有功发电量_incremental: {当日有功发电量_incremental}, 当日无功发电量_incremental: {当日无功发电量_incremental}"); var newAccumulatedDataModel = new InverterAccumulatedDataModel(); newAccumulatedDataModel.CreatedTimeStamp = DateTime.Now; newAccumulatedDataModel.SourceRealTimeDatas = realTimeDataModels; newAccumulatedDataModel.当日有功发电量 = realTimeDataModels.Sum(d => d.Raw_当日有功发电量); newAccumulatedDataModel.当日无功发电量 = realTimeDataModels.Sum(d => d.Raw_当日无功发电量); if (lastAccumulatedDataModel == null) { newAccumulatedDataModel.当月有功发电量 = realTimeDataModels.Sum(d => d.Raw_当日有功发电量); newAccumulatedDataModel.当月无功发电量 = realTimeDataModels.Sum(d => d.Raw_当日无功发电量); newAccumulatedDataModel.当年有功发电量 = realTimeDataModels.Sum(d => d.Raw_当日有功发电量); newAccumulatedDataModel.当年无功发电量 = realTimeDataModels.Sum(d => d.Raw_当日无功发电量); } else { var lastSourceTimeStamp = lastAccumulatedDataModel.SourceRealTimeDatas.Max(d => d.ReadTimeStamp); if (lastSourceTimeStamp.Year != realTimeDataModels.First().ReadTimeStamp.Year) { this.logger.LogDebug($"PersistRealTimeDataAsync, see a year flip, so reset 当月 and 当年"); /*this indicates a year flip*/ newAccumulatedDataModel.当月有功发电量 = realTimeDataModels.Sum(d => d.Raw_当日有功发电量); newAccumulatedDataModel.当月无功发电量 = realTimeDataModels.Sum(d => d.Raw_当日无功发电量); newAccumulatedDataModel.当年有功发电量 = realTimeDataModels.Sum(d => d.Raw_当日有功发电量); newAccumulatedDataModel.当年无功发电量 = realTimeDataModels.Sum(d => d.Raw_当日无功发电量); } else { newAccumulatedDataModel.当年有功发电量 = lastAccumulatedDataModel.当年有功发电量 + 当日有功发电量_incremental; newAccumulatedDataModel.当年无功发电量 = lastAccumulatedDataModel.当年无功发电量 + 当日无功发电量_incremental; if (lastSourceTimeStamp.Month == realTimeDataModels.First().ReadTimeStamp.Month) { newAccumulatedDataModel.当月有功发电量 = lastAccumulatedDataModel.当月有功发电量 + 当日有功发电量_incremental; newAccumulatedDataModel.当月无功发电量 = lastAccumulatedDataModel.当月无功发电量 + 当日无功发电量_incremental; } else { this.logger.LogDebug($"PersistRealTimeDataAsync, see a month flip, so reset 当月"); /*this indicates a month flip*/ newAccumulatedDataModel.当月有功发电量 = realTimeDataModels.Sum(d => d.Raw_当日有功发电量); newAccumulatedDataModel.当月无功发电量 = realTimeDataModels.Sum(d => d.Raw_当日无功发电量); } } } await dbContext.AddRangeAsync(realTimeDataModels); await dbContext.AddAsync(newAccumulatedDataModel); await dbContext.SaveChangesAsync(); } catch (Exception ex) { logger.LogError($"Exception In db SaveChangesAsync for new realTimeDataModels and newAccumulatedDataModel, exception: {ex}"); var universalApiHub = this.services.GetRequiredService(); await universalApiHub.FirePersistGenericAlarmIfNotExists(this, new GenericAlarm() { Title = $"存储光伏逆变器待上传数据失败", Category = $"存储待上传数据", Detail = $"Exception In db SaveChangesAsync for new realTimeDataModels and newAccumulatedDataModel, exception: {ex}", Severity = GenericAlarmSeverity.Warning }, ga => ga.Detail, ga => ga.Detail); return; } } } public async Task Start() { #region Migrating database this.logger.LogInformation("Migrating database..."); var migrateDbContext = new AppDbContext(); try { migrateDbContext.Database.Migrate(); } catch (Exception exx) { string migrationsStr = ""; string pendingMigrationsStr = ""; string appliedMigrationsStr = ""; try { migrationsStr = migrateDbContext.Database?.GetMigrations()?.Aggregate("", (acc, n) => acc + ", " + n) ?? ""; pendingMigrationsStr = migrateDbContext.Database?.GetPendingMigrations()?.Aggregate("", (acc, n) => acc + ", " + n) ?? ""; appliedMigrationsStr = migrateDbContext.Database?.GetAppliedMigrations()?.Aggregate("", (acc, n) => acc + ", " + n) ?? ""; } catch { } this.logger.LogError("SuZhou_SIPAC_Client App Exceptioned when Migrating the database, detail: " + exx + System.Environment.NewLine + "migrations are: " + migrationsStr + System.Environment.NewLine + ", pendingMigrations are: " + pendingMigrationsStr + System.Environment.NewLine + ", appliedMigrations are: " + appliedMigrationsStr); throw new InvalidOperationException("failed for migrating the database"); } this.logger.LogInformation(" Migrate database finished."); #endregion this.httpPostTimer.Start(); this.isStarted = true; var __ = Task.Run(async () => { await this.StartHttpPostLoopAsync(); }); return true; } private async Task AddTestData() { var realTimeDatas = Enumerable.Range(0, 10).Select(i => new Tuple( new InverterDevice("device" + i, (byte)i) { Description = $"I'm the dummy device{i}" }, new InverterDeviceRealTimeData() { 日有功发电量 = 10 + i, 日无功发电量 = 5 + i, } )); await this.PersistRealTimeDataAsync(realTimeDatas); //var inverterAccumulatedDataModels = Enumerable.Range(0, 9).Select(i => new InverterAccumulatedDataModel() //{ // CreatedTimeStamp = DateTime.Now.Subtract(new TimeSpan(10 - i, 0, 0)), // TriggeredReadingBatchNo = Guid.NewGuid(), // 当日有功发电量 = 10 + i * 10, // 当日无功发电量 = 5 + i * 10, // 当月有功发电量 = 100 + i * 10, // 当月无功发电量 = 50 + i * 10, // 当年有功发电量 = 1000 + i * 10, // 当年无功发电量 = 500 + i * 10, //}); //using (var dbContext = new AppDbContext()) //{ // try // { // await dbContext.InverterAccumulatedDatas.AddRangeAsync(inverterAccumulatedDataModels); // await dbContext.SaveChangesAsync(); // } // catch (Exception ex) // { // logger.LogError($"Exception In AddTestData, exception: {ex}"); // return; // } //} } public Task Stop() { this.isStarted = false; this.httpPostTimer?.Stop(); return Task.FromResult(true); } public async Task Test(params object[] parameters) { if (string.IsNullOrEmpty(this.appConfig.HttpPostTestUrl)) throw new NotImplementedException("用于测试的 HTTP URL 未配置, 无法进行测试"); var postContent = JsonSerializer.Serialize(new { id = this.appConfig.Id_唯一标识符, gffdqyhh = this.appConfig.gffdqyhh_光伏发电企业户号, qymc = this.appConfig.qymc_企业名称, tyshxydm = this.appConfig.tyshxydm_统一社会信用代码, jsdd = this.appConfig.jsdd_建设地点, jsddjwd = this.appConfig.jsddjwd_建设地点经纬度, xmmc = this.appConfig.xmmc_项目名称, bwtysj = this.appConfig.bwtysj_并网投运时间, //当日有功发电量 KWh drygfdl = 150, //当日无功发电量 Kvarh drwgfdl = 10, //当月有功发电量 KWh dyygfdl = 1500, //当月无功发电量 KWh dywgfdl = 100, //当年有功发电量 KWh dnygfdl = 15000, //当年无功发电量 KWh dnwgfdl = 1000, cjrq = DateTime.Now, }, new JsonSerializerOptions() { WriteIndented = false, AllowTrailingCommas = false, Converters = { new DateTimeConverter() } }); var response = await this.httpClient.PostAsync( this.appConfig.HttpPostTestUrl, new StringContent(postContent, Encoding.UTF8, "application/json")).ConfigureAwait(false); if (response != null && response.IsSuccessStatusCode) { /* sample success response: { "success": true, "message": "成功", "code": 200, "data": null, "timestamp": 1653873404980 } */ try { var content = await response.Content.ReadAsStringAsync(); JsonDocument jd = JsonDocument.Parse(content); if (jd.RootElement.GetProperty("code").GetInt32() == 200) { return; } else { throw new InvalidOperationException($"post failed with response json data has an inner error code: {jd.RootElement.GetProperty("code").GetInt32()}, full json: {content}"); } } catch (Exception eee) { throw new InvalidOperationException($"post failed with parsing response to json failure: {eee}"); } } else { throw new InvalidOperationException($"post failed with http level failure response: {response?.ReasonPhrase ?? ""}"); } } public class DateTimeConverter : JsonConverter { public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return DateTime.Parse(reader.GetString()); } public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) { writer.WriteStringValue(value.ToString("yyyy-MM-dd HH:mm:ss")); } } private async Task StartHttpPostLoopAsync() { // there're may have many un-posted records, so here only look back in some days. var maxLookBackDays = 1; var lookBackStartDateTime = DateTime.Now.Subtract(new TimeSpan(maxLookBackDays, 0, 0, 0)); // by ms var loopIntervalTime = 5000; int currentLoopTimes = 0; while (this.isStarted) { try { currentLoopTimes++; using (var dbContext = new AppDbContext()) { var postingRecord = await dbContext.InverterAccumulatedDatas.Include(d => d.SourceRealTimeDatas).OrderBy(d => d.Id) .Where(d => d.HttpPostResponseReceivedTime == null && d.CreatedTimeStamp >= lookBackStartDateTime).FirstOrDefaultAsync(); if (postingRecord == null) { //this.logger.LogDebug($"Nothing for Posting, delay a while for re-check..."); await Task.Delay(loopIntervalTime); continue; } this.logger.LogDebug($"Posting Record is selected: {postingRecord.ToLogString()}"); var postContent = JsonSerializer.Serialize(new { id = this.appConfig.Id_唯一标识符, gffdqyhh = this.appConfig.gffdqyhh_光伏发电企业户号, qymc = this.appConfig.qymc_企业名称, tyshxydm = this.appConfig.tyshxydm_统一社会信用代码, jsdd = this.appConfig.jsdd_建设地点, jsddjwd = this.appConfig.jsddjwd_建设地点经纬度, xmmc = this.appConfig.xmmc_项目名称, bwtysj = this.appConfig.bwtysj_并网投运时间, //当日有功发电量 KWh drygfdl = postingRecord.当日有功发电量, //当日无功发电量 Kvarh drwgfdl = postingRecord.当日无功发电量, //当月有功发电量 KWh dyygfdl = postingRecord.当月有功发电量, //当月无功发电量 KWh dywgfdl = postingRecord.当月无功发电量, //当年有功发电量 KWh dnygfdl = postingRecord.当年有功发电量, //当年无功发电量 KWh dnwgfdl = postingRecord.当年无功发电量, cjrq = postingRecord.SourceRealTimeDatas.Max(d => d.ReadTimeStamp) }, new JsonSerializerOptions() { Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, WriteIndented = false, AllowTrailingCommas = false, Converters = { new DateTimeConverter() } }); this.logger.LogDebug($" posting content:{Environment.NewLine} {postContent}"); var response = await this.httpClient.PostAsync( this.appConfig.HttpPostUrl_ForSingleRecord, new StringContent(postContent, Encoding.UTF8, "application/json")).ConfigureAwait(false); if (response != null && response.IsSuccessStatusCode) { /* sample success response: { "success": true, "message": "成功", "code": 200, "data": null, "timestamp": 1653873404980 } */ try { var content = await response.Content.ReadAsStringAsync(); JsonDocument jd = JsonDocument.Parse(content); if (jd.RootElement.GetProperty("code").GetInt32() == 200) { this.logger.LogDebug($" post successfully with response: {content ?? ""}"); postingRecord.HttpPostResponseReceivedTime = DateTime.Now; postingRecord.HttpPostResponse = content ?? ""; await dbContext.SaveChangesAsync(); this.logger.LogDebug($" post progress saved into database"); } else { this.logger.LogInformation($" post failed with response json data has an inner error code: {jd.RootElement.GetProperty("code").GetInt32()}, full json: {content}"); var universalApiHub = this.services.GetRequiredService(); await universalApiHub.ClearAndFirePersistGenericAlarm(this, new GenericAlarm() { Title = $"上传光伏数据到SIPAC平台失败", Category = $"光伏逆变器", Detail = $"上传光伏数据到SIPAC平台失败, 原因: post failed with response json data has an inner error code: {jd.RootElement.GetProperty("code").GetInt32()}, full json: {content}", Severity = GenericAlarmSeverity.Warning }, ga => ga.Detail, ga => ga.Detail); } } catch (Exception eee) { this.logger.LogInformation($" post failed with parsing response to json failure: {eee}"); var universalApiHub = this.services.GetRequiredService(); await universalApiHub.ClearAndFirePersistGenericAlarm(this, new GenericAlarm() { Title = $"上传光伏数据到SIPAC平台失败", Category = $"光伏逆变器", Detail = $"上传光伏数据到SIPAC平台失败, 原因: post failed with parsing response to json failure: {eee}", Severity = GenericAlarmSeverity.Warning }, ga => ga.Detail, ga => ga.Detail); } } else { var failedReason = (response == null ? "HttpStatusCode: null" : ("HttpStatusCode: " + response.StatusCode.ToString())) + "-" + response?.ReasonPhrase ?? ""; this.logger.LogInformation($" post failed with http level failure response: {failedReason}"); var universalApiHub = this.services.GetRequiredService(); await universalApiHub.ClearAndFirePersistGenericAlarm(this, new GenericAlarm() { Title = $"上传光伏数据到SIPAC平台失败", Category = $"光伏逆变器", Detail = $"上传光伏数据到SIPAC平台失败, 原因: post failed with http level failure response: {failedReason}", Severity = GenericAlarmSeverity.Warning }, ga => ga.Detail, ga => ga.Detail); } } } catch (Exception exxx) { this.logger.LogInformation($"HttpPostLoop, see an exception: {exxx}"); } await Task.Delay(loopIntervalTime); } } } }