diff --git a/csharp/Lib/Devices/PLVario2Meter/PLVario2Meter.csproj b/csharp/Lib/Devices/PLVario2Meter/PLVario2Meter.csproj new file mode 100644 index 000000000..5a9ec8ba5 --- /dev/null +++ b/csharp/Lib/Devices/PLVario2Meter/PLVario2Meter.csproj @@ -0,0 +1,17 @@ + + + + net6.0 + enable + enable + InnovEnergy.Lib.Devices.PLVario2Meter + preview + + + + + + + + + diff --git a/csharp/Lib/Devices/PLVario2Meter/PlVarioMeterDevice.cs b/csharp/Lib/Devices/PLVario2Meter/PlVarioMeterDevice.cs new file mode 100644 index 000000000..385180012 --- /dev/null +++ b/csharp/Lib/Devices/PLVario2Meter/PlVarioMeterDevice.cs @@ -0,0 +1,47 @@ +using InnovEnergy.Lib.Protocols.Modbus.Channels; +using InnovEnergy.Lib.Protocols.Modbus.Clients; +using InnovEnergy.Lib.Protocols.Modbus.Slaves; +using InnovEnergy.Lib.Utils; + +namespace InnovEnergy.Lib.Devices.PLVario2Meter; + +public class PlVarioMeterDevice: ModbusDevice +{ + public PlVarioMeterDevice(String hostname, UInt16 port = 502, Byte slaveId = 2) : this(new TcpChannel(hostname, port), slaveId) + { + } + + public PlVarioMeterDevice(Channel channel, Byte slaveId = 2) : base(new ModbusTcpClient(channel, slaveId)) + { + } + + public PlVarioMeterDevice(ModbusClient client) : base(client) + { + } + + public new PlVarioMeterRegisters? Read() + { + try + { + return base.Read(); + } + catch + { + "Failed to read data from PLVario-II meter".WriteLine(); + return null; + } + } + + public new void Write(PlVarioMeterRegisters registers) + { + try + { + base.Write(registers); + } + catch (Exception e) + { + // TODO: Log + Console.WriteLine(e); + } + } +} \ No newline at end of file diff --git a/csharp/Lib/Devices/PLVario2Meter/PlVarioMeterRecord.cs b/csharp/Lib/Devices/PLVario2Meter/PlVarioMeterRecord.cs new file mode 100644 index 000000000..393948719 --- /dev/null +++ b/csharp/Lib/Devices/PLVario2Meter/PlVarioMeterRecord.cs @@ -0,0 +1,133 @@ +using InnovEnergy.Lib.Units; +using InnovEnergy.Lib.Units.Power; + +namespace InnovEnergy.Lib.Devices.PLVario2Meter; + +public class PlVarioMeterRecord +{ + private readonly PlVarioMeterRegisters _raw; + + public PlVarioMeterRecord(PlVarioMeterRegisters raw) + { + _raw = raw ?? throw new ArgumentNullException(nameof(raw)); + } + + + static ushort SwapBytes16(ushort x) + => (ushort)((x << 8) | (x >> 8)); + + static float DecodeFloat_BADC(ushort w0, ushort w1) + { + // Build bytes: [B A D C] = swap bytes in each word, and use W1 then W0 + ushort w0s = SwapBytes16(w0); // F1AA + ushort w1s = SwapBytes16(w1); // 4247 + uint raw = ((uint)w1s << 16) | w0s; // 0x4247F1AA + return BitConverter.Int32BitsToSingle(unchecked((int)raw)); + } + + + + // ------------------------------------------------------------------------- + // Decoders (DCBA) from raw UInt16 pairs + // ------------------------------------------------------------------------- + private static uint DecodeUInt32DCBA(ushort lowWord, ushort highWord) + => ((uint)highWord << 16) | (uint)lowWord; + + private static float DecodeFloatDCBA(ushort lowWord, ushort highWord) + { + uint raw = ((uint)highWord << 16) | (uint)lowWord; + return BitConverter.Int32BitsToSingle(unchecked((int)raw)); + } + + private static Double F(ushort w0, ushort w1) => DecodeFloat_BADC(w0, w1); + private static UInt32 U32(ushort w0, ushort w1) => DecodeUInt32DCBA(w0, w1); + + // ------------------------------------------------------------------------- + // Time + // ------------------------------------------------------------------------- + /// Timestamp in seconds (as provided by the device). + public uint TimestampSeconds => U32(_raw.Timestamp_W0, _raw.Timestamp_W1); + + /// Timestamp interpreted as Unix epoch seconds (UTC). + public DateTimeOffset TimestampUtc => DateTimeOffset.FromUnixTimeSeconds(TimestampSeconds); + + // ------------------------------------------------------------------------- + // Voltages (phase-neutral and line-line) + // ------------------------------------------------------------------------- + public VoltageRms VoltageU1 => new(F(_raw.VoltageU1_W0, _raw.VoltageU1_W1)); // U-1 + public VoltageRms VoltageU2 => new(F(_raw.VoltageU2_W0, _raw.VoltageU2_W1)); // U-2 + public VoltageRms VoltageU3 => new(F(_raw.VoltageU3_W0, _raw.VoltageU3_W1)); // U-3 + + // Friendly aliases (match your naming style) + public VoltageRms GridAbLineVoltage => (VoltageRms)new(F(_raw.VoltageUL1_W0, _raw.VoltageUL1_W1)); + public VoltageRms GridBcLineVoltage => (VoltageRms)new(F(_raw.VoltageUL2_W0, _raw.VoltageUL2_W1)); + public VoltageRms GridCaLineVoltage => (VoltageRms)new(F(_raw.VoltageUL3_W0, _raw.VoltageUL3_W1)); + + // ------------------------------------------------------------------------- + // Frequency + // ------------------------------------------------------------------------- + public Double Frequency => (F(_raw.Frequency_W0, _raw.Frequency_W1)); + + // ------------------------------------------------------------------------- + // Currents + // ------------------------------------------------------------------------- + public CurrentRms CurrentI1 => new(F(_raw.CurrentI1_W0, _raw.CurrentI1_W1)); + public CurrentRms CurrentI2 => new(F(_raw.CurrentI2_W0, _raw.CurrentI2_W1)); + public CurrentRms CurrentI3 => new(F(_raw.CurrentI3_W0, _raw.CurrentI3_W1)); + public CurrentRms CurrentI4 => new(F(_raw.CurrentI4_W0, _raw.CurrentI4_W1)); // optional channel + public CurrentRms CurrentTotal => new(F(_raw.CurrentTotal_W0, _raw.CurrentTotal_W1)); + + // ------------------------------------------------------------------------- + // Active Power (kW) + // ------------------------------------------------------------------------- + public ActivePower ActivePowerL1 => new(F(_raw.ActivePowerP1_W0, _raw.ActivePowerP1_W1) * 1000); + public ActivePower ActivePowerL2 => new(F(_raw.ActivePowerP2_W0, _raw.ActivePowerP2_W1) * 1000); + public ActivePower ActivePowerL3 => new(F(_raw.ActivePowerP3_W0, _raw.ActivePowerP3_W1) * 1000); + public ActivePower ActivePowerCh4 => new(F(_raw.ActivePowerP4_W0, _raw.ActivePowerP4_W1) * 1000); // optional channel + public ActivePower ActivePowerTotal => new(F(_raw.ActivePowerTotal_W0, _raw.ActivePowerTotal_W1) * 1000); + + // If you later map import/export separately, you can redefine these. + // For now, the table provides only total P (signed), so we expose it as "GridPower". + public ActivePower GridPower => ActivePowerTotal; + + // ------------------------------------------------------------------------- + // Reactive Power (kVAr) + // ------------------------------------------------------------------------- + public ReactivePower ReactivePowerL1 => new(F(_raw.ReactivePowerQ1_W0, _raw.ReactivePowerQ1_W1)); + public ReactivePower ReactivePowerL2 => new(F(_raw.ReactivePowerQ2_W0, _raw.ReactivePowerQ2_W1)); + public ReactivePower ReactivePowerL3 => new(F(_raw.ReactivePowerQ3_W0, _raw.ReactivePowerQ3_W1)); + public ReactivePower ReactivePowerCh4 => new(F(_raw.ReactivePowerQ4_W0, _raw.ReactivePowerQ4_W1)); + public ReactivePower ReactivePowerTotal => new(F(_raw.ReactivePowerTotal_W0, _raw.ReactivePowerTotal_W1)); + + // Friendly alias + public ReactivePower GridReactivePower => ReactivePowerTotal; + + // ------------------------------------------------------------------------- + // Apparent Power (kVA) + // ------------------------------------------------------------------------- + public ApparentPower ApparentPowerL1 => new(F(_raw.ApparentPowerS1_W0, _raw.ApparentPowerS1_W1)); + public ApparentPower ApparentPowerL2 => new(F(_raw.ApparentPowerS2_W0, _raw.ApparentPowerS2_W1)); + public ApparentPower ApparentPowerL3 => new(F(_raw.ApparentPowerS3_W0, _raw.ApparentPowerS3_W1)); + public ApparentPower ApparentPowerCh4 => new(F(_raw.ApparentPowerS4_W0, _raw.ApparentPowerS4_W1)); + public ApparentPower ApparentPowerTotal => new(F(_raw.ApparentPowerTotal_W0, _raw.ApparentPowerTotal_W1)); + + // ------------------------------------------------------------------------- + // Power Factor + // ------------------------------------------------------------------------- + public Double PowerFactorL1 => (F(_raw.PowerFactorPF1_W0, _raw.PowerFactorPF1_W1)); + public Double PowerFactorL2 => (F(_raw.PowerFactorPF2_W0, _raw.PowerFactorPF2_W1)); + public Double PowerFactorL3 => (F(_raw.PowerFactorPF3_W0, _raw.PowerFactorPF3_W1)); + public Double PowerFactorCh4 => (F(_raw.PowerFactorPF4_W0, _raw.PowerFactorPF4_W1)); + public Double PowerFactorTotal => (F(_raw.PowerFactorTotal_W0, _raw.PowerFactorTotal_W1)); + + // ------------------------------------------------------------------------- + // Repeated totals (table shows duplicates at offsets 66..74) + // Keep them exposed in case your installation populates them differently. + // ------------------------------------------------------------------------- + public CurrentRms CurrentTotalRepeat => new(F(_raw.CurrentTotalRepeat_W0, _raw.CurrentTotalRepeat_W1)); + public ActivePower ActivePowerTotalRepeat => new(F(_raw.ActivePowerTotalRepeat_W0, _raw.ActivePowerTotalRepeat_W1)); + public ReactivePower ReactivePowerTotalRepeat => new(F(_raw.ReactivePowerTotalRepeat_W0, _raw.ReactivePowerTotalRepeat_W1)); + public ApparentPower ApparentPowerTotalRepeat => new(F(_raw.ApparentPowerTotalRepeat_W0, _raw.ApparentPowerTotalRepeat_W1)); + public Double PowerFactorTotalRepeat => (F(_raw.PowerFactorTotalRepeat_W0, _raw.PowerFactorTotalRepeat_W1)); + +} \ No newline at end of file diff --git a/csharp/Lib/Devices/PLVario2Meter/PlVarioMeterRegisters.cs b/csharp/Lib/Devices/PLVario2Meter/PlVarioMeterRegisters.cs new file mode 100644 index 000000000..556ff1d71 --- /dev/null +++ b/csharp/Lib/Devices/PLVario2Meter/PlVarioMeterRegisters.cs @@ -0,0 +1,168 @@ +using InnovEnergy.Lib.Protocols.Modbus.Reflection.Attributes; +using InnovEnergy.Lib.StatusApi.DeviceTypes; +using InnovEnergy.Lib.Units.Composite; +// ReSharper disable InconsistentNaming + +namespace InnovEnergy.Lib.Devices.PLVario2Meter; + +using Float32 = Single; + +// If your project already defines Float32 as an alias/struct, keep that. +// Otherwise, you can just use float. Here we use float for simplicity. +public class PlVarioMeterRegisters : IAc3Meter +{ + // ------------------------------------------------------------------------- + // Timestamp (UINT32) -> 2 x UInt16 + // Offset 0: Timestamp [s] + // ------------------------------------------------------------------------- + [HoldingRegister(1000, writable: false)] public UInt16 Timestamp_W0; // low word + [HoldingRegister(1001, writable: false)] public UInt16 Timestamp_W1; // high word + + // ------------------------------------------------------------------------- + // Voltages phase-neutral (FLOAT32) -> 2 x UInt16 each + // Offsets 2,4,6 + // ------------------------------------------------------------------------- + [HoldingRegister(1002, writable: false)] public UInt16 VoltageU1_W0; + [HoldingRegister(1003, writable: false)] public UInt16 VoltageU1_W1; + + [HoldingRegister(1004, writable: false)] public UInt16 VoltageU2_W0; + [HoldingRegister(1005, writable: false)] public UInt16 VoltageU2_W1; + + [HoldingRegister(1006, writable: false)] public UInt16 VoltageU3_W0; + [HoldingRegister(1007, writable: false)] public UInt16 VoltageU3_W1; + + // ------------------------------------------------------------------------- + // Voltages line-line (FLOAT32) -> 2 x UInt16 each + // Offsets 8,10,12 + // ------------------------------------------------------------------------- + [HoldingRegister(1008, writable: false)] public UInt16 VoltageUL1_W0; + [HoldingRegister(1009, writable: false)] public UInt16 VoltageUL1_W1; + + [HoldingRegister(1010, writable: false)] public UInt16 VoltageUL2_W0; + [HoldingRegister(1011, writable: false)] public UInt16 VoltageUL2_W1; + + [HoldingRegister(1012, writable: false)] public UInt16 VoltageUL3_W0; + [HoldingRegister(1013, writable: false)] public UInt16 VoltageUL3_W1; + + // ------------------------------------------------------------------------- + // Frequency (FLOAT32) -> 2 x UInt16 + // Offset 14 + // ------------------------------------------------------------------------- + [HoldingRegister(1014, writable: false)] public UInt16 Frequency_W0; + [HoldingRegister(1015, writable: false)] public UInt16 Frequency_W1; + + // ------------------------------------------------------------------------- + // Currents (FLOAT32) -> 2 x UInt16 each + // Offsets 16..24 + // ------------------------------------------------------------------------- + [HoldingRegister(1016, writable: false)] public UInt16 CurrentI1_W0; + [HoldingRegister(1017, writable: false)] public UInt16 CurrentI1_W1; + + [HoldingRegister(1018, writable: false)] public UInt16 CurrentI2_W0; + [HoldingRegister(1019, writable: false)] public UInt16 CurrentI2_W1; + + [HoldingRegister(1020, writable: false)] public UInt16 CurrentI3_W0; + [HoldingRegister(1021, writable: false)] public UInt16 CurrentI3_W1; + + [HoldingRegister(1022, writable: false)] public UInt16 CurrentI4_W0; + [HoldingRegister(1023, writable: false)] public UInt16 CurrentI4_W1; + + [HoldingRegister(1024, writable: false)] public UInt16 CurrentTotal_W0; + [HoldingRegister(1025, writable: false)] public UInt16 CurrentTotal_W1; + + // ------------------------------------------------------------------------- + // Active power P (FLOAT32, kW) -> 2 x UInt16 each + // Offsets 26..34 + // ------------------------------------------------------------------------- + [HoldingRegister(1026, writable: false)] public UInt16 ActivePowerP1_W0; + [HoldingRegister(1027, writable: false)] public UInt16 ActivePowerP1_W1; + + [HoldingRegister(1028, writable: false)] public UInt16 ActivePowerP2_W0; + [HoldingRegister(1029, writable: false)] public UInt16 ActivePowerP2_W1; + + [HoldingRegister(1030, writable: false)] public UInt16 ActivePowerP3_W0; + [HoldingRegister(1031, writable: false)] public UInt16 ActivePowerP3_W1; + + [HoldingRegister(1032, writable: false)] public UInt16 ActivePowerP4_W0; + [HoldingRegister(1033, writable: false)] public UInt16 ActivePowerP4_W1; + + [HoldingRegister(1034, writable: false)] public UInt16 ActivePowerTotal_W0; + [HoldingRegister(1035, writable: false)] public UInt16 ActivePowerTotal_W1; + + // ------------------------------------------------------------------------- + // Reactive power Q (FLOAT32, kVAr) -> 2 x UInt16 each + // Offsets 36..44 + // ------------------------------------------------------------------------- + [HoldingRegister(1036, writable: false)] public UInt16 ReactivePowerQ1_W0; + [HoldingRegister(1037, writable: false)] public UInt16 ReactivePowerQ1_W1; + + [HoldingRegister(1038, writable: false)] public UInt16 ReactivePowerQ2_W0; + [HoldingRegister(1039, writable: false)] public UInt16 ReactivePowerQ2_W1; + + [HoldingRegister(1040, writable: false)] public UInt16 ReactivePowerQ3_W0; + [HoldingRegister(1041, writable: false)] public UInt16 ReactivePowerQ3_W1; + + [HoldingRegister(1042, writable: false)] public UInt16 ReactivePowerQ4_W0; + [HoldingRegister(1043, writable: false)] public UInt16 ReactivePowerQ4_W1; + + [HoldingRegister(1044, writable: false)] public UInt16 ReactivePowerTotal_W0; + [HoldingRegister(1045, writable: false)] public UInt16 ReactivePowerTotal_W1; + + // ------------------------------------------------------------------------- + // Apparent power S (FLOAT32, kVA) -> 2 x UInt16 each + // Offsets 46..54 + // ------------------------------------------------------------------------- + [HoldingRegister(1046, writable: false)] public UInt16 ApparentPowerS1_W0; + [HoldingRegister(1047, writable: false)] public UInt16 ApparentPowerS1_W1; + + [HoldingRegister(1048, writable: false)] public UInt16 ApparentPowerS2_W0; + [HoldingRegister(1049, writable: false)] public UInt16 ApparentPowerS2_W1; + + [HoldingRegister(1050, writable: false)] public UInt16 ApparentPowerS3_W0; + [HoldingRegister(1051, writable: false)] public UInt16 ApparentPowerS3_W1; + + [HoldingRegister(1052, writable: false)] public UInt16 ApparentPowerS4_W0; + [HoldingRegister(1053, writable: false)] public UInt16 ApparentPowerS4_W1; + + [HoldingRegister(1054, writable: false)] public UInt16 ApparentPowerTotal_W0; + [HoldingRegister(1055, writable: false)] public UInt16 ApparentPowerTotal_W1; + + // ------------------------------------------------------------------------- + // Power factor PF (FLOAT32) -> 2 x UInt16 each + // Offsets 56..64 + // ------------------------------------------------------------------------- + [HoldingRegister(1056, writable: false)] public UInt16 PowerFactorPF1_W0; + [HoldingRegister(1057, writable: false)] public UInt16 PowerFactorPF1_W1; + + [HoldingRegister(1058, writable: false)] public UInt16 PowerFactorPF2_W0; + [HoldingRegister(1059, writable: false)] public UInt16 PowerFactorPF2_W1; + + [HoldingRegister(1060, writable: false)] public UInt16 PowerFactorPF3_W0; + [HoldingRegister(1061, writable: false)] public UInt16 PowerFactorPF3_W1; + + [HoldingRegister(1062, writable: false)] public UInt16 PowerFactorPF4_W0; + [HoldingRegister(1063, writable: false)] public UInt16 PowerFactorPF4_W1; + + [HoldingRegister(1064, writable: false)] public UInt16 PowerFactorTotal_W0; + [HoldingRegister(1065, writable: false)] public UInt16 PowerFactorTotal_W1; + + // ------------------------------------------------------------------------- + // Repeated totals (as shown in the table) at offsets 66..74 + // ------------------------------------------------------------------------- + [HoldingRegister(1066, writable: false)] public UInt16 CurrentTotalRepeat_W0; + [HoldingRegister(1067, writable: false)] public UInt16 CurrentTotalRepeat_W1; + + [HoldingRegister(1068, writable: false)] public UInt16 ActivePowerTotalRepeat_W0; + [HoldingRegister(1069, writable: false)] public UInt16 ActivePowerTotalRepeat_W1; + + [HoldingRegister(1070, writable: false)] public UInt16 ReactivePowerTotalRepeat_W0; + [HoldingRegister(1071, writable: false)] public UInt16 ReactivePowerTotalRepeat_W1; + + [HoldingRegister(1072, writable: false)] public UInt16 ApparentPowerTotalRepeat_W0; + [HoldingRegister(1073, writable: false)] public UInt16 ApparentPowerTotalRepeat_W1; + + [HoldingRegister(1074, writable: false)] public UInt16 PowerFactorTotalRepeat_W0; + [HoldingRegister(1075, writable: false)] public UInt16 PowerFactorTotalRepeat_W1; + + public Ac3Bus Ac { get; } +}