using System.Diagnostics.CodeAnalysis; using InnovEnergy.App.SaliMax.Devices; using InnovEnergy.App.SaliMax.Ess; using InnovEnergy.Lib.Devices.AMPT; using InnovEnergy.Lib.Devices.Battery48TL; using InnovEnergy.Lib.Devices.EmuMeter; using InnovEnergy.Lib.Devices.Trumpf.TruConvertAc; using InnovEnergy.Lib.Devices.Trumpf.TruConvertDc; using InnovEnergy.Lib.Units; using InnovEnergy.Lib.Units.Power; using InnovEnergy.Lib.Utils; using Ac3Bus = InnovEnergy.Lib.Units.Composite.Ac3Bus; namespace InnovEnergy.App.SaliMax; // ┌────┐ ┌────┐ // │ Pv │ │ Pv │ ┌────┐ // └────┘ └────┘ │ Pv │ // V V └────┘ // V V V // (b) 0 W (e) 0 W V // V V (i) 13.2 kW ┌────────────┐ // V V V │ Battery │ // ┌─────────┐ ┌──────────┐ ┌────────────┐ ┌─────────┐ V ├────────────┤ // │ Grid │ │ Grid Bus │ │ Island Bus │ │ AC/DC │ ┌────────┐ ┌───────┐ │ 52.3 V │ // ├─────────┤ -10.3 kW ├──────────┤ -11.7 kW ├────────────┤ -11.7 kW ├─────────┤ -11.7 kW │ Dc Bus │ 1008 W │ DC/DC │ 1008 W │ 99.1 % │ // │ -3205 W │<<<<<<<<<<│ 244 V │<<<<<<<<<<│ 244 V │<<<<<<<<<<│ -6646 W │<<<<<<<<<<├────────┤>>>>>>>>>>├───────┤>>>>>>>>>>│ 490 mA │ // │ -3507 W │ (a) │ 244 V │ (d) │ 244 V │ (g) │ -5071 W │ (h) │ 776 V │ (k) │ 56 V │ (l) │ 250 °C │ // │ -3605 W │ K1 │ 246 V │ K2 │ 246 V │ K3 └─────────┘ └────────┘ └───────┘ │ 445 A │ // └─────────┘ └──────────┘ └────────────┘ V │ 0 Warnings │ // V V V │ 0 Alarms │ // V V (j) 0 W └────────────┘ // (c) 1400 W (f) 0 W V // V V V // V V ┌──────┐ // ┌──────┐ ┌──────┐ │ Load │ // │ Load │ │ Load │ └──────┘ // └──────┘ └──────┘ // Calculated values: c,d & h // ========================== // // // AC side // a + b - c - d = 0 [eq1] // d + e - f - g = 0 [eq2] // // c & d are not measured! // // d = f + g - e [eq2] // c = a + b - d [eq1] // // DC side // h + i - j - k = 0 [eq3] // // g = h assuming no losses in ACDC // k = l assuming no losses in DCDC // j = h + i - k [eq3] public static class Topology { public static TextBlock CreateTopologyTextBlock(this StatusRecord status) { var a = status.GridMeter?.Ac.Power.Active; var b = status.PvOnAcGrid?.Dc.Power.Value; var e = status.PvOnAcIsland?.Dc.Power.Value; var f = status.LoadOnAcIsland?.Ac.Power.Active; var g = status.AcDc.Dc.Power.Value; var h = g; var i = status.PvOnDc?.Dc.Power.Value; var k = status.DcDc.Dc.Link.Power.Value; var l = status.Battery is not null ? status.Battery.Dc.Power.Value : 0; var j = status.LoadOnDc?.Power.Value; var d = status.AcGridToAcIsland?.Power.Active; var c = status.LoadOnAcGrid?.Power.Active; ///////////////////////////// var grid = status.CreateGridColumn(a); var gridBus = status.CreateGridBusColumn(b, c, d); var islandBus = status.CreateIslandBusColumn(e, f, g); var inverter = status.CreateInverterColumn(h); var dcBus = status.CreateDcBusColumn(i, j, k); var dcDc = status.CreateDcDcColumn(l); var batteries = status.CreateBatteryColumn(); return TextBlock.AlignCenterVertical ( grid, gridBus, islandBus, inverter, dcBus, dcDc, batteries ); } private static TextBlock CreateGridColumn(this StatusRecord status, ActivePower? a) { // ┌─────────┐ // │ Grid │ // ├─────────┤ -10.3 kW // │ -3205 W │<<<<<<<<<< // │ -3507 W │ (a) // │ -3605 W │ K1 // └─────────┘ var gridMeterAc = status.GridMeter?.Ac; var k1 = status.Relays?.K1GridBusIsConnectedToGrid; var gridBox = PhasePowersActive(gridMeterAc).TitleBox("Grid"); var gridFlow = SwitchedFlow(k1, a, "K1"); return TextBlock.AlignCenterVertical(gridBox, gridFlow); } private static TextBlock CreateGridBusColumn(this StatusRecord status, ActivePower? b, ActivePower? c, ActivePower? d) { // ┌────┐ // │ Pv │ // └────┘ // V // V // (b) 0 W // V // V // ┌──────────┐ // │ Grid Bus │ // ├──────────┤ -11.7 kW // │ 244 V │<<<<<<<<<< // │ 244 V │ (d) // │ 246 V │ K2 // └──────────┘ // V // V // (c) 1400 W // V // V // ┌──────┐ // │ Load │ // └──────┘ ////////////// top ////////////// var pvBox = TextBlock.FromString("PV").Box(); var pvFlow = Flow.Vertical(b); ////////////// center ////////////// // on IslandBus show voltages measured by inverter // on GridBus show voltages measured by grid meter // ought to be approx the same var gridMeterAc = status.GridMeter?.Ac; var k2 = status.Relays?.K2IslandBusIsConnectedToGridBus; var busBox = PhaseVoltages(gridMeterAc).TitleBox("Grid Bus"); var busFlow = SwitchedFlow(k2, d, "K2"); ////////////// bottom ////////////// var loadFlow = Flow.Vertical(c); var loadBox = TextBlock.FromString("Load").Box(); ////////////// assemble ////////////// return TextBlock.AlignCenterVertical ( TextBlock.AlignCenterHorizontal(pvBox, pvFlow, busBox, loadFlow, loadBox), busFlow ); } private static TextBlock CreateIslandBusColumn(this StatusRecord status, ActivePower? e, ActivePower? f, ActivePower? g) { // ┌────┐ // │ Pv │ // └────┘ // V // V // (e) 0 W // V // V // ┌────────────┐ // │ Island Bus │ // ├────────────┤ -11.7 kW // │ 244 V │<<<<<<<<<< // │ 244 V │ (g) // │ 246 V │ K3 // └────────────┘ // V // V // (f) 0 W // V // V // ┌──────┐ // │ Load │ // └──────┘ ////////////// top ////////////// var pvBox = TextBlock.FromString("PV").Box(); var pvFlow = Flow.Vertical(e); ////////////// center ////////////// // on IslandBus show voltages measured by inverter // on GridBus show voltages measured by grid meter // ought to be approx the same var inverterAc = status.AcDc.Ac; var busBox = PhaseVoltages(inverterAc).TitleBox("Island Bus"); var busFlow = status.IslandBusToInverterConnection(g); ////////////// bottom ////////////// var loadFlow = Flow.Vertical(f); var loadBox = TextBlock.FromString("Load").Box(); ////////////// assemble ////////////// return TextBlock.AlignCenterVertical ( TextBlock.AlignCenterHorizontal(pvBox, pvFlow, busBox, loadFlow, loadBox), busFlow ); } private static TextBlock CreateInverterColumn(this StatusRecord status, ActivePower? h) { // ┌─────────┐ // │ AC/DC │ // ├─────────┤ -11.7 kW // │ -6646 W │<<<<<<<<<< // │ -5071 W │ (h) // └─────────┘ var inverterBox = status .AcDc .Devices .Select(d => d.Status.Ac.Power.Active) .Apply(TextBlock.AlignLeft) .TitleBox("AC/DC"); var dcFlow = Flow.Horizontal(h); return TextBlock.AlignCenterVertical(inverterBox, dcFlow); } [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] private static TextBlock IslandBusToInverterConnection(this StatusRecord status, ActivePower? g) { if (status.Relays is null) return TextBlock.FromString("????????"); var nInverters = status.AcDc.Devices.Count; var k3S = status .Relays .K3InverterIsConnectedToIslandBus .Take(nInverters); if (k3S.Prepend(true).All(s => s)) // TODO: display when no ACDC present return Flow.Horizontal(g); return Switch.Open("K3"); } private static TextBlock CreateDcDcColumn(this StatusRecord status, ActivePower? p) { var dc48Voltage = status.DcDc.Dc.Battery.Voltage.ToDisplayString(); var busBox = TextBlock .AlignLeft(dc48Voltage) .TitleBox("DC/DC"); var busFlow = Flow.Horizontal(p); return TextBlock.AlignCenterVertical(busBox, busFlow); } private static TextBlock CreateDcBusColumn(this StatusRecord status, ActivePower? i, ActivePower? j, ActivePower? k) { // ┌────┐ // │ Pv │ // └────┘ // V // V // (i) 13.2 kW // V // V // ┌────────┐ // │ Dc Bus │ 1008 W // ├────────┤>>>>>>>>>> // │ 776 V │ (k) // └────────┘ // V // V // (j) 0 W // V // V // ┌──────┐ // │ Load │ // └──────┘ // /////////////////// top /////////////////// var mppt = status.PvOnDc; var nStrings = mppt is not null ? "x" + mppt.Strings.Count : "?"; var pvBox = TextBlock.FromString($"PV {nStrings}").Box(); var pvToBus = Flow.Vertical(i); /////////////////// center /////////////////// var dcBusVoltage = status.DcDc.Dc.Link.Voltage; var dcBusBox = dcBusVoltage .ToDisplayString() .Apply(TextBlock.FromString) .TitleBox("DC Bus "); var busFlow = Flow.Horizontal(k); /////////////////// bottom /////////////////// var busToLoad = Flow.Vertical(j); var loadBox = TextBlock.FromString("DC Load").Box(); ////////////// assemble ////////////// return TextBlock.AlignCenterVertical ( TextBlock.AlignCenterHorizontal(pvBox, pvToBus, dcBusBox, busToLoad, loadBox), busFlow ); } private static TextBlock CreateBatteryColumn(this StatusRecord status) { var bat = status.Battery; var batteryAvgBox = CreateAveragedBatteryBox(bat); return batteryAvgBox; // TODO: individualBatteries hidden atm #pragma warning disable CS0162 var batteryBoxes = bat .Devices .Select(CreateBatteryBox) .ToReadOnlyList(); var individualBatteries = batteryBoxes.Any() ? TextBlock.AlignLeft(batteryBoxes) : TextBlock.Empty; return TextBlock .AlignCenterVertical ( batteryAvgBox , individualBatteries ); #pragma warning enable CS0162 } private static TextBlock CreateAveragedBatteryBox(Battery48TlRecords? bat) { if (bat is null) return TextBlock.AlignLeft("no battery").Box(); var voltage = bat.Dc.Voltage.ToDisplayString(); var soc = bat.Devices.Any() ? bat.Devices.Average(b => b.Soc).Percent().ToDisplayString() : "0"; // TODO var current = bat.Dc.Current.ToDisplayString(); var busCurrent = bat.Devices.Any() ? bat.Devices.Sum(b => b.BusCurrent).A().ToDisplayString() : "0"; var temp = bat.Temperature.ToDisplayString(); var heatingPower = bat.HeatingPower.ToDisplayString(); var alarms = bat.Alarms.Count + " Alarms"; var warnings = bat.Warnings.Count + " Warnings"; var nBatteries = bat.Devices.Count; return TextBlock .AlignLeft ( voltage, soc, current, busCurrent, temp, heatingPower, warnings, alarms ) .TitleBox($"Battery x{nBatteries}"); } private static TextBlock PhaseVoltages(Ac3Bus? ac) { return TextBlock.AlignLeft ( ac?.L1.Voltage.ToDisplayString() ?? "???", ac?.L2.Voltage.ToDisplayString() ?? "???", ac?.L3.Voltage.ToDisplayString() ?? "???" ); } private static TextBlock PhasePowersActive(Ac3Bus? ac) { return TextBlock.AlignLeft ( ac?.L1.Power.Active.ToDisplayString() ?? "???", ac?.L2.Power.Active.ToDisplayString() ?? "???", ac?.L3.Power.Active.ToDisplayString() ?? "???" ); } private static TextBlock CreateBatteryBox(Battery48TlRecord battery, Int32 i) { var batteryWarnings = battery.Warnings.Any(); var batteryAlarms = battery.Alarms.Any(); var content = TextBlock.AlignLeft ( battery.Dc.Voltage.ToDisplayString(), battery.Soc.ToDisplayString(), battery.Dc.Current.ToDisplayString() + " C/D", battery.Temperatures.Cells.Average.ToDisplayString(), battery.BusCurrent.ToDisplayString() , batteryWarnings, batteryAlarms, battery.HeatingCurrent.ToDisplayString() + " H" ); var box = content.TitleBox($"Battery {i + 1}"); var flow = Flow.Horizontal(battery.Dc.Power); return TextBlock.AlignCenterVertical(flow, box); } private static TextBlock SwitchedFlow(Boolean? switchClosed, ActivePower? power, String kx) { return switchClosed is null ? TextBlock.FromString("??????????") : !switchClosed.Value ? Switch.Open(kx) : power is null ? TextBlock.FromString("??????????") : Flow.Horizontal(power); } //We are fake using the ampt instead of PvOnAc, We dont have a Pv on Ac at the moment and we don't have it classes :TODO public static AcPowerDevice? CalculateGridBusLoad(EmuMeterRegisters? gridMeter, AmptStatus? pvOnAcGrid, AcPowerDevice? gridBusToIslandBusPower) { var a = gridMeter ?.Ac.Power; var b = pvOnAcGrid is not null? pvOnAcGrid?.Dc.Power.Value: 0; var d = gridBusToIslandBusPower?.Power; if (a is null || b is null || d is null) return null; var c = a + b - d; // [eq1] return new AcPowerDevice { Power = c }; } //We are fake using the ampt instead of PvOnAc, We dont have a Pv on Ac at the moment and we don't have it classes :TODO public static AcPowerDevice? CalculateGridBusToIslandBusPower(AmptStatus? pvOnAcIsland, EmuMeterRegisters? loadOnAcIsland, AcDcDevicesRecord? acDc) { var e = pvOnAcIsland is not null? pvOnAcIsland?.Dc.Power.Value: 0; var f = loadOnAcIsland is not null? loadOnAcIsland?.Ac.Power : 0; var g = acDc ?.Ac.Power; // We dont check on the AcDc because this device is mandatory, if this does not exist the system will not start if (e is null || f is null || g is null) return null; var d = f + g - e; // [eq2] return new AcPowerDevice { Power = d }; } public static DcPowerDevice? CalculateDcLoad(AcDcDevicesRecord? acDc, AmptStatus? pvOnDc, DcDcDevicesRecord? dcDc) { var h = acDc?.Dc.Power; // We dont check on the AcDc because this device is mandatory var i = pvOnDc is not null? pvOnDc?.Dc.Power: 0; var k = dcDc?.Dc.Link.Power; // We dont check on the DcDc because this device is mandatory if (h is null || i is null || k is null) return null; var j = h + i - k; // [eq3] return new DcPowerDevice { Power = j}; } // public static (AcPowerDevice? acGridToAcIsland, AcPowerDevice? loadOnAcGrid, DcPowerDevice? dcPowerDevice) // // CalculateEnergyFlow(EmuMeterRegisters? gridMeter, // AcPowerDevice pvOnAcGrid, // AcPowerDevice pvOnAcIsland, // EmuMeterRegisters? loadOnAcIsland, // AcDcDevicesRecord acDc, // AmptStatus? pvOnDc, // DcDcDevicesRecord dcDc) // { // var gridPower = gridMeter?.Ac.Power.Active; // var islandLoadPower = loadOnAcIsland?.Ac.Power.Active; // var inverterAcPower = acDc.Ac.Power.Active; // var inverterDcPower = acDc.Dc.Power; // // var a = gridPower; // var b = pvOnAcGrid.Power.Active; // var e = pvOnAcIsland.Power.Active; // var f = islandLoadPower; // var g = inverterAcPower; // var h = inverterDcPower; // var i = pvOnDc?.Dc.Power; // var k = dcDc.Dc.Link.Power; // var j = Sum(h, i, -k); // var d = Sum(f, g, -e); // var c = Sum(a, b, -d); // // var acGridToAcIsland = d is null ? null : new AcPowerDevice { Power = d.Value }; // var loadOnAcGrid = c is null ? null : new AcPowerDevice { Power = c.Value }; // var dcPowerDevice = j is null ? null : new DcPowerDevice { Power = j }; // // return (acGridToAcIsland, loadOnAcGrid, dcPowerDevice); // } }