using System.Net; using System.Text; using InnovEnergy.App.Collector.Influx; using InnovEnergy.App.Collector.Records; using InnovEnergy.App.Collector.Utils; using InnovEnergy.Lib.Utils; using InnovEnergy.Lib.Utils.Net; using Convert = System.Convert; namespace InnovEnergy.App.Collector; using Data = IReadOnlyList; public static class BatteryDataParser { public static IReadOnlyList ParseDatagram(UdpDatagram datagram) { return ParseV3Datagram(datagram); // if (IsV4Payload(buffer)) // return ParseV4Datagram(endPoint, buffer); // throw new Exception($"Wrong protocol header: Expected '{Settings.ProtocolV3}'"); } private static Boolean IsV4Payload(Byte[] buffer) { return buffer .ParseLengthValueEncoded() .First() .ToArray() .Apply(Encoding.UTF8.GetString) .Equals(Settings.ProtocolV4); } private static Boolean IsV3Payload(IEnumerable buffer) { return buffer .ToArray(Settings.ProtocolV3.Length) .Apply(Encoding.UTF8.GetString) .Equals(Settings.ProtocolV3); } // private static LineProtocolPayload ParseV4Datagram(IPEndPoint endPoint, Byte[] buffer) // { // var timeOfArrival = DateTime.UtcNow; // influx wants UTC // // BatteryDataParserV4.ParseV4Datagram(endPoint, buffer); // return new LineProtocolPayload(); // } private static IReadOnlyList ParseV3Datagram(UdpDatagram datagram) { var timeOfArrival = DateTime.UtcNow; var data = datagram .Payload .ToArray() .Apply(Encoding.UTF8.GetString) .Split('\n'); data.ParseProtocolVersion().Apply(CheckProtocolId); var installationName = data.ParseInstallationName(); return ParseBatteryRecords(data, installationName, timeOfArrival, datagram.EndPoint); } private static String ParseString(this Data data, Int32 i) => data[i].Trim(); private static UInt16 ParseUInt16(this Data data, Int32 i) => UInt16.Parse(ParseString(data, i)); private static UInt16 ParseUInt16Register(this Data data, Int32 register) { var i = register.RegToIndex(); return data.ParseUInt16(i); } private static UInt32 ParseUInt32Register(this Data data, Int32 register) { var lo = ParseUInt16Register(data, register); var hi = ParseUInt16Register(data, register + 1); return Convert.ToUInt32(((hi << 16) | lo) & UInt32.MaxValue); } private static UInt64 ParseUInt64Register(this Data data, Int32 register) { return Enumerable .Range(register, 4) .Reverse() .Select(data.ParseUInt16Register) .Aggregate(0ul, (a, b) => a << 16 | b); } private static Decimal ParseDecimalRegister(this Data data, Int32 register, Decimal scaleFactor = 1, Decimal offset = 0) { var i = register.RegToIndex(); Int32 n = data.ParseUInt16(i); if (n >= 0x8000) n -= 0x10000; // fiamm stores their integers signed AND with sign-offset @#%^&! return (Convert.ToDecimal(n) + offset) * scaleFactor; // according fiamm doc } private static Int32 RegToIndex(this Int32 register) => register - 992; private static Leds ParseLeds(this Data data, String installation, String batteryId) { var ledBitmap = ParseUInt16Register(data, 1004); LedState Led(Int32 n) => (LedState) (ledBitmap >> (n * 2) & 0b11); return new Leds { Installation = installation, BatteryId = batteryId, Green = Led(0), Amber = Led(1), Blue = Led(2), Red = Led(3), }; } private static IoStatus ParseIoStatus(this Data data, String installation, String batteryId) { var ioStatusBitmap = data.ParseUInt16Register(1013); Boolean IoStatus(Int32 b) => (ioStatusBitmap >> b & 1) > 0; return new IoStatus { Installation = installation, BatteryId = batteryId, MainSwitchClosed = IoStatus(0), AlarmOutActive = IoStatus(1), InternalFanActive = IoStatus(2), VoltMeasurementAllowed = IoStatus(3), AuxRelay = IoStatus(4), RemoteState = IoStatus(5), HeatingOn = IoStatus(6), }; } private static Warnings ParseWarnings(this Data data, String installation, String batteryId) { var warningsBitmap = data.ParseUInt64Register(1005); Boolean Warning(Int32 b) => (warningsBitmap >> b & 1ul) > 0; return new Warnings { Installation = installation, BatteryId = batteryId, TaM1 = Warning(1), TbM1 = Warning(4), VBm1 = Warning(6), VBM1 = Warning(8), IDM1 = Warning(10), vsM1 = Warning(24), iCM1 = Warning(26), iDM1 = Warning(28), MID1 = Warning(30), BLPW = Warning(32), Ah_W = Warning(35), MPMM = Warning(38), TCMM = Warning(39), TCdi = Warning(40), LMPW = Warning(44) }; } private static Alarms ParseAlarms(this Data data, String installation, String batteryId) { var alarmsBitmap = data.ParseUInt64Register(1009); Boolean Alarm(Int32 b) => (alarmsBitmap >> b & 1ul) > 0; return new Alarms { Installation = installation, BatteryId = batteryId, Tam = Alarm(0), TaM2 = Alarm(2), Tbm = Alarm(3), TbM2 = Alarm(5), VBm2 = Alarm(7), VBM2 = Alarm(9), IDM2 = Alarm(11), ISOB = Alarm(12), MSWE = Alarm(13), FUSE = Alarm(14), HTRE = Alarm(15), TCPE = Alarm(16), CME = Alarm(18), HWFL = Alarm(19), HWEM = Alarm(20), ThM = Alarm(21), vsm1 = Alarm(22), vsm2 = Alarm(23), vsM2 = Alarm(25), iCM2 = Alarm(27), iDM2 = Alarm(29), MID2 = Alarm(31), CCBF = Alarm(33), AhFL = Alarm(34), TbCM = Alarm(36), HTFS = Alarm(42), DATA = Alarm(43), LMPA = Alarm(45), HEBT = Alarm(46), }; } private static BatteryStatus ParseBatteryStatus(this Data data, String installation, String batteryId, Decimal temperature, Warnings warnings, Alarms alarms, DateTime lastSeen, IPEndPoint endPoint) { var activeWarnings = Active(warnings); var activeAlarms = Active(alarms); return new BatteryStatus { InstallationName = installation, BatteryId = batteryId, HardwareVersion = data.ParseString(3), FirmwareVersion = data.ParseString(4), BmsVersion = data.ParseString(5), AmpereHours = data.ParseUInt16(6), Soc = data.ParseDecimalRegister(1053, 0.1m), Voltage = data.ParseDecimalRegister(999, 0.01m), Current = data.ParseDecimalRegister(1000, 0.01m, -10000m), BusVoltage = data.ParseDecimalRegister(1001, 0.01m), Temperature = temperature, RtcCounter = data.ParseUInt32Register(1050), IpAddress = endPoint.Address.ToString(), Port = endPoint.Port, // stuff below really should be done by Grafana/InfluxDb, but not possible (yet) // aka hacks to get around limitations of Grafana/InfluxDb NumberOfWarnings = activeWarnings.Count, NumberOfAlarms = activeAlarms.Count, WarningsBitmap = data.ParseUInt64Register(1005), AlarmsBitmap = data.ParseUInt64Register(1009), LastSeen = lastSeen.ToInfluxTime() }; static IReadOnlyCollection Active(BatteryRecord record) => record .GetFields() .Where(f => f.value is true) .Select(f => f.key) .ToList(); } private static Temperatures ParseTemperatures(this Data data, String installation, String batteryId) { return new Temperatures { Installation = installation, BatteryId = batteryId, Battery = data.ParseDecimalRegister(1003, 0.1m, -400m), Board = data.ParseDecimalRegister(1014, 0.1m, -400m), Center = data.ParseDecimalRegister(1015, 0.1m, -400m), Lateral1 = data.ParseDecimalRegister(1016, 0.1m, -400m), Lateral2 = data.ParseDecimalRegister(1017, 0.1m, -400m), CenterHeaterPwm = data.ParseDecimalRegister(1018, 0.1m), LateralHeaterPwm = data.ParseDecimalRegister(1019, 0.1m), }; } private static String CheckProtocolId(String ascii) { var protocolId = ascii.Substring(0, Settings.ProtocolV3.Length); if (protocolId != Settings.ProtocolV3) throw new Exception($"Wrong protocol header: Expected '{Settings.ProtocolV3}' but got '{protocolId}'"); return protocolId; } private static IReadOnlyList ParseBatteryRecords(Data data, String installation, DateTime lastSeen, IPEndPoint endPoint) { var batteryId = data.ParseBatteryId(); var warnings = data.ParseWarnings (installation, batteryId); var alarms = data.ParseAlarms (installation, batteryId); var leds = data.ParseLeds (installation, batteryId); var temperatures = data.ParseTemperatures (installation, batteryId); var ioStatus = data.ParseIoStatus (installation, batteryId); var batteryStatus = data.ParseBatteryStatus(installation, batteryId, temperatures.Battery, warnings, alarms, lastSeen, endPoint); return new BatteryRecord[] { batteryStatus, temperatures, leds, ioStatus, warnings, alarms }; } private static String ParseProtocolVersion (this Data data) => data.ParseString(0); private static String ParseInstallationName(this Data data) => data.ParseString(1); private static String ParseBatteryId (this Data data) => data.ParseString(2); }