From 42731a837def533983a296dde5f96c0a9fdaab59 Mon Sep 17 00:00:00 2001 From: Tyrone CT Date: Thu, 7 May 2026 15:40:49 +0800 Subject: [PATCH] =?UTF-8?q?=E5=85=89=E4=BC=8F=E6=97=A0=E6=B3=95=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E8=B5=B7=E6=9D=A5=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .windsurf/rules/prorules.md | 22 ++ .../Services/YuePuRunModelService.cs | 230 ++++++++++++++++-- 2 files changed, 236 insertions(+), 16 deletions(-) create mode 100644 .windsurf/rules/prorules.md diff --git a/.windsurf/rules/prorules.md b/.windsurf/rules/prorules.md new file mode 100644 index 0000000..3889aeb --- /dev/null +++ b/.windsurf/rules/prorules.md @@ -0,0 +1,22 @@ +--- +trigger: always_on +--- + +# 这个是一个Ems 软件程序,控制储能柜内BMS,PCS,消防,液冷的协调和控制,现场有两个储能柜,都是用本程序进行控制,只不过配置信息不一样,其中一个叫做主柜,另一个叫做从柜。 +# 主控柜连接的设备包括:储能柜柜内BMS,PCS,消防,液冷;各个电表(), +# 从控柜连接的设备包括:储能柜柜内BMS,PCS,消防,液冷 +# 从控柜的充放电是受到主控柜控制的,主控柜的EMS是汇聚所有设备的信息并控制所有的设备 +# 这个项目有两个用电节点,一个是管理大楼,一个是税务大楼,项目中包括光伏发电,储能,负载这些,本项目除了控制两个储能的充放电外,还要确保光伏发电不能馈电到电网中,在确保满足负载使用的情况下不让光伏发电溢出到电网中,自发自用 +# 本次光储充每天是一充一放,根据配置的时间进行削峰填谷套利,当然也要确保光伏的自发自用,不能馈电到电网中。 +# 本项目有4个电操,控制不同的电操让两个储能柜协调给管理大楼和税务大楼侧光伏: + 1号 4号电操闭合(2号 3号电操断开):主控储能柜给管理大楼充放电,从储能柜根据税务大楼的负载情况和光伏发电情况是否需要消纳光伏,防止光伏馈电掉电网 + 2号 3号电操闭合(1号 4号电操断开):从控储能柜给管理大楼充放电,主控储能柜在税务大楼侧根据税务大楼的负载情况和光伏发电情况是否需要消纳光伏,防止光伏馈电掉电网 + 两个储能夜里充电时,只能从管理大楼侧进行充电,根据线路结构的要求,夜间充电时同一时刻只有一个储能进行充电,充到要求的SOC时再切换电操到让另一个储能对接管理大楼进行充电,不能从税务大楼侧进行充电,任何一个储能线路对接到税务大楼时都不能放电到税务大楼,只是根据当前的税务大楼的负载和光伏情况决定是否需要消纳光伏发电,防止光伏馈电掉电网 + # 当前的项目程序已经能正常运行了,运行了半年,只不过有点小问题,需要更改和完善或者重构程序。当前项目程序跟设备层面的连接是经过测试和验证的,有modbusTCP,RS485,CAN通信 + # 整个项目程序协调这些进行控制,在确保不馈电到电网的前提下,实现储能的充放电控制和光伏的消纳控制,实现削峰填谷的利益最大化。 + # 里面的模式切换时,需要考虑电操切换的安全性,因为电操切换时会导致两个储能柜和电网断开连接,柜内的PCS,光伏,液冷等都要关闭,切换完成后再启动,切换断电时储能柜内有个小电池,可以维持柜内设备的正常运行,确保EMS运行半个小时左右。 + # 因为周六周日园区不上班,周五晚上和周六晚上就不需要充电了,周六周日白天时负载很小,可以让储能柜当时对接税务大楼的储能接受光伏的充电,这样能最大化收益。其他时间的晚上就正常充电,白天放电,削峰填谷套利。 + # 当前程序使用了状态机来管理程序的运行状态,请在这些状态机上进行完善编程 + # 更改程序时要谨慎,要充分考虑各种情况,确保程序的稳定性和可靠性,要详细的注释代码 + # 主要核心就是协调两个储能柜,管理大楼负责,税务大楼负载,光伏,使他们协调工作达到稳态,不馈电到电网中;当然两个储能柜内的各个系统(PCS,BMS电池,消防,液冷)也要协调工作,防止他们报错影响整个系统的运行。 +# 夜间充电时段是当天的23:00 到第二天的凌晨5:00,这个时间段认为是默认夜间的充电时间,这个时间也没有光伏发电的干扰。的其他的都认为是根据负载正常放电时间(当然白天有光伏可能充电)。主要是靠削峰填谷和光伏发电产生收益。 \ No newline at end of file diff --git a/OrpaonEMS.App/Services/YuePuRunModelService.cs b/OrpaonEMS.App/Services/YuePuRunModelService.cs index 5c445ba..8ce9fd8 100644 --- a/OrpaonEMS.App/Services/YuePuRunModelService.cs +++ b/OrpaonEMS.App/Services/YuePuRunModelService.cs @@ -269,12 +269,156 @@ namespace OrpaonEMS.App.Services /// private void SolarCurBackFlow_TrigTimeOutHandler(object? sender, EventArgs e) { - //关闭光伏 - CloseSolar(); - Console.WriteLine($"时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}-【月浦】- 软件检测到光伏逆流,关闭光伏逆变器 "); + var now = DateTime.Now; + _solarBackflowClosePending = true; + _lastSolarBackflowCloseTime = now; + _solarRecoverImportTrigActive = false; + _solarRecoverImportTrigStartTime = DateTime.MinValue; - LogService.Info($"时间:{DateTime.Now.ToString()}-【动作】-软件检测到光伏逆流,关闭光伏逆变器"); + var closeOk = TryCloseSolarThrottled($"逆流触发-并网点功率:{SolarEleMeter5.RtPw}"); + Console.WriteLine($"时间:{now.ToString("yyyy-MM-dd HH:mm:ss")}-【月浦】- 软件检测到光伏逆流,关闭光伏逆变器 "); + LogService.Info($"时间:{now}-【动作】-软件检测到光伏逆流,关闭光伏逆变器,关闭结果:{closeOk},状态:{CurESChargInfo},并网点功率:{SolarEleMeter5.RtPw}"); + } + private const int SolarRecoverCooldownSeconds = 60; + private const int SolarCmdMinIntervalSeconds = 3; + + private DateTime _lastSolarBackflowCloseTime = DateTime.MinValue; + private bool _solarBackflowClosePending = false; + private DateTime _lastSolarOpenCmdTime = DateTime.MinValue; + private DateTime _lastSolarCloseCmdTime = DateTime.MinValue; + private bool _solarRecoverImportTrigActive = false; + private DateTime _solarRecoverImportTrigStartTime = DateTime.MinValue; + + /// + /// 判断逆流触发关闭后,是否已经过了冷却时间,允许再次尝试启动光伏。 + /// + /// True 表示允许尝试启动光伏 + private bool IsSolarOpenAllowedAfterBackflowCooldown() + { + if (!_solarBackflowClosePending) + { + return true; + } + + return (DateTime.Now - _lastSolarBackflowCloseTime).TotalSeconds >= SolarRecoverCooldownSeconds; + } + + /// + /// 下发启动光伏命令(带节流与异常捕获)。 + /// + /// 触发原因,用于日志定位 + /// 命令写入成功返回 True,否则返回 False + private bool TryOpenSolarThrottled(string reason) + { + var now = DateTime.Now; + if ((now - _lastSolarOpenCmdTime).TotalSeconds < SolarCmdMinIntervalSeconds) + { + return false; + } + + _lastSolarOpenCmdTime = now; + try + { + var ok = OpenSolar(); + if (ok) + { + LogService.Info($"时间:{now}-【光伏启动】-已下发启动命令,原因:{reason},状态:{CurESChargInfo},并网点功率:{SolarEleMeter5.RtPw},通信:{SolarLinkState}"); + } + if (!ok) + { + LogService.Info($"时间:{now}-【光伏启动】-失败,原因:{reason},状态:{CurESChargInfo},并网点功率:{SolarEleMeter5.RtPw},通信:{SolarLinkState}"); + } + return ok; + } + catch (Exception ex) + { + LogService.Info($"时间:{now}-【光伏启动】-异常,原因:{reason},状态:{CurESChargInfo},{ex.Message}"); + return false; + } + } + + private bool TryCloseSolarThrottled(string reason) + { + var now = DateTime.Now; + if ((now - _lastSolarCloseCmdTime).TotalSeconds < SolarCmdMinIntervalSeconds) + { + return false; + } + + _lastSolarCloseCmdTime = now; + try + { + var ok = CloseSolar(); + if (!ok) + { + LogService.Info($"时间:{now}-【光伏关闭】-失败,原因:{reason},状态:{CurESChargInfo},并网点功率:{SolarEleMeter5.RtPw},通信:{SolarLinkState}"); + } + return ok; + } + catch (Exception ex) + { + LogService.Info($"时间:{now}-【光伏关闭】-异常,原因:{reason},状态:{CurESChargInfo},{ex.Message}"); + return false; + } + } + + /// + /// 逆流触发关闭光伏后的自动恢复逻辑。 + /// 目标:避免光伏长期处于关闭状态造成发电损失。 + /// + private void TryAutoRecoverSolarAfterBackflow() + { + try + { + if (!_solarBackflowClosePending) + { + return; + } + + if (!YuPuAutoHand) + { + return; + } + + if (CurESChargInfo != ESChargInfo.Master && CurESChargInfo != ESChargInfo.Slave) + { + return; + } + + var now = DateTime.Now; + if (!IsSolarOpenAllowedAfterBackflowCooldown()) + { + return; + } + + if (CheckSolarState()) + { + _solarBackflowClosePending = false; + LogService.Info($"时间:{now}-【光伏自动恢复】-已检测到光伏处于运行状态,清除待恢复标志,状态:{CurESChargInfo},并网点功率:{SolarEleMeter5.RtPw}"); + return; + } + + var openOk = TryOpenSolarThrottled($"逆流后自动恢复-并网点功率:{SolarEleMeter5.RtPw}"); + LogService.Info($"时间:{now}-【光伏自动恢复】-尝试启动光伏,结果:{openOk},状态:{CurESChargInfo},并网点功率:{SolarEleMeter5.RtPw}"); + + if (openOk) + { + _solarBackflowClosePending = false; + _solarRecoverImportTrigActive = false; + _solarRecoverImportTrigStartTime = DateTime.MinValue; + } + + if (CheckSolarState()) + { + _solarBackflowClosePending = false; + LogService.Info($"时间:{now}-【光伏自动恢复】-光伏已恢复运行,状态:{CurESChargInfo},并网点功率:{SolarEleMeter5.RtPw}"); + } + } + catch (Exception ex) + { + LogService.Info($"时间:{DateTime.Now}-【光伏自动恢复】-异常,状态:{CurESChargInfo},{ex.Message}"); + } } @@ -286,6 +430,8 @@ namespace OrpaonEMS.App.Services /// public StateMachine SysRunStateMachine { get; set; } + private DateTime _lastAutoAlarmResetTime = DateTime.MinValue; + ///// ///// 夜间充电模型信息 ///// 标记白天放电信息 @@ -320,7 +466,7 @@ namespace OrpaonEMS.App.Services .Permit(ESChargInfoTrig.WaitTrig, ESChargInfo.Wait) .Permit(ESChargInfoTrig.NoSolarTrig, ESChargInfo.NoSolar) .Ignore(ESChargInfoTrig.MasterTrig) - .OnEntry(() => EntryMaster()) + .OnEntry(() => EntryWithAutoAlarmReset(ESChargInfo.Master, EntryMaster)) .OnExit(() => ExitMaster()); //Slave 状态 @@ -331,7 +477,7 @@ namespace OrpaonEMS.App.Services .Permit(ESChargInfoTrig.WaitTrig, ESChargInfo.Wait) .Permit(ESChargInfoTrig.NoSolarTrig, ESChargInfo.NoSolar) .Ignore(ESChargInfoTrig.SlaveTrig) - .OnEntry(() => EntrySlave()) + .OnEntry(() => EntryWithAutoAlarmReset(ESChargInfo.Slave, EntrySlave)) .OnExit(() => ExitSlave()); //Night_Master 状态 @@ -342,7 +488,7 @@ namespace OrpaonEMS.App.Services .Permit(ESChargInfoTrig.WaitTrig, ESChargInfo.Wait) .Permit(ESChargInfoTrig.NoSolarTrig, ESChargInfo.NoSolar) .Ignore(ESChargInfoTrig.Night_MasterTrig) - .OnEntry(() => EntryNight_Master()) + .OnEntry(() => EntryWithAutoAlarmReset(ESChargInfo.Night_Master, EntryNight_Master)) .OnExit(() => ExitNight_Master()); //Night_Slave 状态 @@ -353,7 +499,7 @@ namespace OrpaonEMS.App.Services .Permit(ESChargInfoTrig.NoSolarTrig, ESChargInfo.NoSolar) .Permit(ESChargInfoTrig.Night_MasterTrig, ESChargInfo.Night_Master) .Ignore(ESChargInfoTrig.Night_SlaveTrig) - .OnEntry(() => EntryNight_Slave()) + .OnEntry(() => EntryWithAutoAlarmReset(ESChargInfo.Night_Slave, EntryNight_Slave)) .OnExit(() => ExitNight_Slave()); //Wait 状态 @@ -364,7 +510,7 @@ namespace OrpaonEMS.App.Services .Permit(ESChargInfoTrig.Night_SlaveTrig, ESChargInfo.Night_Slave) .Permit(ESChargInfoTrig.NoSolarTrig, ESChargInfo.NoSolar) .Ignore(ESChargInfoTrig.WaitTrig) - .OnEntry(() => EntryWait()) + .OnEntry(() => EntryWithAutoAlarmReset(ESChargInfo.Wait, EntryWait)) .OnExit(() => ExitWait()); //NoSolar 状态 @@ -375,13 +521,63 @@ namespace OrpaonEMS.App.Services .Permit(ESChargInfoTrig.Night_SlaveTrig, ESChargInfo.Night_Slave) .Permit(ESChargInfoTrig.WaitTrig, ESChargInfo.Wait) .Ignore(ESChargInfoTrig.NoSolarTrig) - .OnEntry(() => EntryNoSolar()) + .OnEntry(() => EntryWithAutoAlarmReset(ESChargInfo.NoSolar, EntryNoSolar)) .OnExit(() => ExitNoSolar()); PreESChargInfo = ESChargInfo.Master; SysRunStateMachine.Fire(ESChargInfoTrig.WaitTrig); } + private void EntryWithAutoAlarmReset(ESChargInfo enteringState, Action entryAction) + { + TryAutoResetBmsAndPcsAlarms($"进入状态:{enteringState}", enteringState); + entryAction(); + } + + private void TryAutoResetBmsAndPcsAlarms(string reason, ESChargInfo enteringState) + { + try + { + if (!YuPuAutoHand) + { + return; + } + + var now = DateTime.Now; + if ((now - _lastAutoAlarmResetTime).TotalSeconds < 30) + { + return; + } + + _lastAutoAlarmResetTime = now; + + bool pcsResetOk = false; + try + { + pcsResetOk = InPowerPCSDataService.PCSFaultReset(); + } + catch (Exception ex) + { + LogService.Info($"时间:{DateTime.Now}-【报警复位】-PCS复位异常,原因:{reason},状态:{enteringState},{ex.Message}"); + } + + try + { + BmsDataService.BmsAlarmReset(); + } + catch (Exception ex) + { + LogService.Info($"时间:{DateTime.Now}-【报警复位】-BMS复位异常,原因:{reason},状态:{enteringState},{ex.Message}"); + } + + LogService.Info($"时间:{DateTime.Now}-【报警复位】-已触发复位,原因:{reason},状态:{enteringState},PCS复位结果:{pcsResetOk}"); + } + catch (Exception ex) + { + LogService.Info($"时间:{DateTime.Now}-【报警复位】-复位流程异常,原因:{reason},状态:{enteringState},{ex.Message}"); + } + } + /// /// 进入 NoSolar /// @@ -420,7 +616,7 @@ namespace OrpaonEMS.App.Services //关闭光伏 if (CheckSolarState() == true) { - CloseSolar(); + TryCloseSolarThrottled("EntryNoSolar"); } TaxRealPw = SolarEleMeter5.AvePw - SolarEleMeter3.AvePw;//光伏和市电供应给税务大楼的负载 @@ -615,7 +811,7 @@ namespace OrpaonEMS.App.Services if (CheckSolarState() == true) { LogService.Info($"时间:{DateTime.Now.ToString()}-【动作】-【Wait】状态-光伏不满足条件,关闭光伏"); - CloseSolar(); + TryCloseSolarThrottled("EntryWait"); } } @@ -1031,9 +1227,9 @@ namespace OrpaonEMS.App.Services } //打开光伏 - if (CheckSolarState() == false) + if (CheckSolarState() == false && IsSolarOpenAllowedAfterBackflowCooldown()) { - OpenSolar(); + TryOpenSolarThrottled("EntrySlave"); } //循环执行方法 @@ -1239,9 +1435,9 @@ namespace OrpaonEMS.App.Services } //打开光伏 - if (CheckSolarState() == false) + if (CheckSolarState() == false && IsSolarOpenAllowedAfterBackflowCooldown()) { - OpenSolar(); + TryOpenSolarThrottled("EntryMaster"); } //循环执行方法 @@ -2971,6 +3167,8 @@ namespace OrpaonEMS.App.Services continue; } + TryAutoRecoverSolarAfterBackflow(); + //实时给当前的SOC数据给夜电白放的模型 //CurNightChargEleModel.MasterSoc = MasterClient.ClientInfo!.SOC; //CurNightChargEleModel.SlaveSoc = SlaveClient.ClientInfo!.SOC;