Innovenergy_trunk/csharp/App/SinexcelCommunication/Program.cs

922 lines
41 KiB
C#

using System.IO.Compression;
using System.IO.Ports;
using System.Text;
using System.Text.Json;
using Flurl.Http;
using System.Reactive.Linq;
using System.Reactive.Threading.Tasks;
using InnovEnergy.Lib.Devices.Sinexcel_12K_TL;
using InnovEnergy.App.SinexcelCommunication.DataLogging;
using InnovEnergy.App.SinexcelCommunication.ESS;
using InnovEnergy.App.SinexcelCommunication.MiddlewareClasses;
using InnovEnergy.App.SinexcelCommunication.SystemConfig;
using InnovEnergy.Lib.Protocols.Modbus.Channels;
using InnovEnergy.App.SinexcelCommunication.DataTypes;
using InnovEnergy.Lib.Units;
using InnovEnergy.Lib.Utils;
using Newtonsoft.Json;
using Formatting = Newtonsoft.Json.Formatting;
using JsonSerializer = System.Text.Json.JsonSerializer;
using static InnovEnergy.App.SinexcelCommunication.MiddlewareClasses.MiddlewareAgent;
using System.Diagnostics.CodeAnalysis;
using InnovEnergy.Lib.Devices.Sinexcel_12K_TL.DataType;
using InnovEnergy.Lib.Protocols.Modbus.Protocol;
using static InnovEnergy.Lib.Devices.Sinexcel_12K_TL.DataType.WorkingMode;
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
namespace InnovEnergy.App.SinexcelCommunication;
[SuppressMessage("Trimming", "IL2026:Members annotated with \'RequiresUnreferencedCodeAttribute\' require dynamic access otherwise can break functionality when trimming application code")]
internal static class Program
{
private static readonly TimeSpan UpdateInterval = TimeSpan.FromSeconds(5);
private const UInt16 NbrOfFileToConcatenate = 15; // add this to config file
private static UInt16 _fileCounter = 0;
private static Channel _sinexcelChannel1;
private static Channel _sinexcelChannel2;
private static readonly String SwVersionNumber = " V1.00." + DateTime.Today;
private const String VpnServerIp = "10.2.0.11";
private static Boolean _subscribedToQueue = false;
private static Boolean _subscribeToQueueForTheFirstTime = false;
private static SodistoreAlarmState _prevSodiohomeAlarmState = SodistoreAlarmState.Green;
private static SodistoreAlarmState _sodiohomeAlarmState = SodistoreAlarmState.Green;
// Tracking for error/warning content changes
private static List<String> _prevErrorCodes = new List<String>();
private static List<String> _prevWarningCodes = new List<String>();
// Heartbeat tracking
private static DateTime _lastHeartbeatTime = DateTime.MinValue;
private const Int32 HeartbeatIntervalSeconds = 60;
// move all this to config file
private const String Port1 = "/dev/ttyUSB0";
private const String Port2 = "/dev/ttyUSB1";
private const Byte SlaveId = 1;
private const Parity Parity = 0; //none
private const Int32 StopBits = 1;
private const Int32 BaudRate = 115200;
private const Int32 DataBits = 8;
public static async Task Main(String[] args)
{
_sinexcelChannel1 = new SerialPortChannel(Port1, BaudRate, Parity, DataBits, StopBits);
_sinexcelChannel2 = new SerialPortChannel(Port2, BaudRate, Parity, DataBits, StopBits);
InitializeCommunicationToMiddleware();
while (true)
{
try
{
await Run();
}
catch (Exception e)
{
e.WriteLine();
}
}
// ReSharper disable once FunctionNeverReturns
}
private static async Task Run()
{
Watchdog.NotifyReady();
Console.WriteLine("Starting Sinexcel Communication, SW Version : " + SwVersionNumber);
var sinexcelDevice1 = new SinexcelDevice(_sinexcelChannel1, SlaveId);
var sinexcelDevice2 = new SinexcelDevice(_sinexcelChannel2, SlaveId);
StatusRecord? ReadStatus()
{
var config = Config.Load();
var sinexcelRecord1 = sinexcelDevice1.Read();
var sinexcelRecord2 = sinexcelDevice2.Read();
return new StatusRecord
{
InverterRecord1 = sinexcelRecord1,
InverterRecord2 = sinexcelRecord2,
Config = config // load from disk every iteration, so config can be changed while running
};
}
while (true)
{
await Observable
.Interval(UpdateInterval)
.Select(_ => RunIteration())
.SelectMany(status =>
DataLogging(status, DateTime.Now.Round(UpdateInterval))
.ContinueWith(_ => status)) // back to StatusRecord
.SelectMany(SaveModbusTcpFile)
.SelectError()
.ToTask();
}
StatusRecord? RunIteration()
{
try
{
Watchdog.NotifyAlive();
var startTime = DateTime.Now;
Console.WriteLine("***************************** Reading Battery Data *********************************************");
Console.WriteLine(startTime.ToString("HH:mm:ss.fff ")+ "Start Reading");
// the order matter of the next three lines
var statusrecord = ReadStatus();
if (statusrecord == null)
return null;
Console.WriteLine(" ************************************************ Inverter 1 ************************************************ ");
Console.WriteLine( statusrecord.InverterRecord1.SystemDateTime + " SystemDateTime ");
Console.WriteLine( statusrecord.InverterRecord1.TotalPhotovoltaicPower + " TotalPhotovoltaicPower ");
Console.WriteLine( statusrecord.InverterRecord1.TotalBatteryPower + " TotalBatteryPower ");
Console.WriteLine( statusrecord.InverterRecord1.TotalLoadPower + " TotalLoadPower ");
Console.WriteLine( statusrecord.InverterRecord1.TotalGridPower + " TotalGridPower ");
Console.WriteLine( statusrecord.InverterRecord1.Battery1Power + " Battery1Power ");
Console.WriteLine( statusrecord.InverterRecord1.Battery1Soc + " Battery1Soc ");
Console.WriteLine( statusrecord.InverterRecord1.Battery1BackupSoc + " Battery1BackupSoc ");
Console.WriteLine( statusrecord.InverterRecord1.Battery1MinSoc + " Battery1MinSoc ");
Console.WriteLine( statusrecord.InverterRecord1.Battery2Power + " Battery2Power ");
Console.WriteLine( statusrecord.InverterRecord1.Battery2Soc + " Battery2Soc ");
Console.WriteLine( statusrecord.InverterRecord1.Battery2BackupSoc + " Battery2BackupSoc ");
Console.WriteLine( statusrecord.InverterRecord1.Battery2MinSoc + " Battery2MinSoc ");
Console.WriteLine( statusrecord.InverterRecord1.EnableGridExport + " EnableGridExport ");
Console.WriteLine( statusrecord.InverterRecord1.PowerGridExportLimit + " PowerGridExportLimit ");
Console.WriteLine( statusrecord.InverterRecord1.PowerOn + " PowerOn ");
Console.WriteLine( statusrecord.InverterRecord1.PowerOff + " PowerOff ");
Console.WriteLine( statusrecord.InverterRecord1.WorkingMode + " WorkingMode ");
Console.WriteLine( statusrecord.InverterRecord1.GridSwitchMethod + " GridSwitchMethod ");
Console.WriteLine( statusrecord.InverterRecord1.ThreePhaseWireSystem + " ThreePhaseWireSystem ");
Console.WriteLine(" ************************************************ Inverter 2 ************************************************ ");
Console.WriteLine( statusrecord.InverterRecord2.SystemDateTime + " SystemDateTime ");
Console.WriteLine( statusrecord.InverterRecord2.TotalPhotovoltaicPower + " TotalPhotovoltaicPower ");
Console.WriteLine( statusrecord.InverterRecord2.TotalBatteryPower + " TotalBatteryPower ");
Console.WriteLine( statusrecord.InverterRecord2.TotalLoadPower + " TotalLoadPower ");
Console.WriteLine( statusrecord.InverterRecord2.TotalGridPower + " TotalGridPower ");
Console.WriteLine( statusrecord.InverterRecord2.Battery1Power + " Battery1Power ");
Console.WriteLine( statusrecord.InverterRecord2.Battery1Soc + " Battery1Soc ");
Console.WriteLine( statusrecord.InverterRecord2.Battery1BackupSoc + " Battery1BackupSoc ");
Console.WriteLine( statusrecord.InverterRecord2.Battery1MinSoc + " Battery1MinSoc ");
Console.WriteLine( statusrecord.InverterRecord2.Battery2Power + " Battery2Power ");
Console.WriteLine( statusrecord.InverterRecord2.Battery2Soc + " Battery2Soc ");
Console.WriteLine( statusrecord.InverterRecord2.Battery2BackupSoc + " Battery2BackupSoc ");
Console.WriteLine( statusrecord.InverterRecord2.Battery2MinSoc + " Battery2MinSoc ");
Console.WriteLine( statusrecord.InverterRecord2.EnableGridExport + " EnableGridExport ");
Console.WriteLine( statusrecord.InverterRecord2.PowerGridExportLimit + " PowerGridExportLimit ");
Console.WriteLine( statusrecord.InverterRecord2.PowerOn + " PowerOn ");
Console.WriteLine( statusrecord.InverterRecord2.PowerOff + " PowerOff ");
Console.WriteLine( statusrecord.InverterRecord2.WorkingMode + " WorkingMode ");
Console.WriteLine( statusrecord.InverterRecord2.GridSwitchMethod + " GridSwitchMethod ");
Console.WriteLine( statusrecord.InverterRecord2.ThreePhaseWireSystem + " ThreePhaseWireSystem ");
/*
Console.WriteLine( statusrecord.InverterRecord1.RepetitiveWeeks + " RepetitiveWeeks ");
Console.WriteLine( statusrecord.InverterRecord1.EffectiveStartDate + " EffectiveStartDate ");
Console.WriteLine( statusrecord.InverterRecord1.EffectiveEndDate + " EffectiveEndDate ");
Console.WriteLine( statusrecord.InverterRecord1.ChargingPowerPeriod1 + " ChargingPowerPeriod1 ");
Console.WriteLine( statusrecord.InverterRecord1.DishargingPowerPeriod1 + " dischargingPowerPeriod1 ");
Console.WriteLine( statusrecord.InverterRecord1.ChargeStartTimePeriod1 + " ChargeStartTimePeriod1 ");
Console.WriteLine( statusrecord.InverterRecord1.ChargeEndTimePeriod1 + " ChargeEndTimePeriod1 ");
Console.WriteLine( statusrecord.InverterRecord1.DischargeStartTimePeriod1 + " DischargeStartTimePeriod1 ");
Console.WriteLine( statusrecord.InverterRecord1.DischargeEndTimePeriod1 + " DischargeEndTimePeriod1 ");*/
SendSalimaxStateAlarm(GetSodiHomeStateAlarm(statusrecord),statusrecord);
statusrecord.ControlConstants();
Console.WriteLine( " ************************************ We are writing ************************************");
var startWritingTime = DateTime.Now;
Console.WriteLine(startWritingTime.ToString("HH:mm:ss.fff ") +"start Writing");
statusrecord?.Config.Save(); // save the config file
if (statusrecord is { Config.ControlPermission: true })
{
Console.WriteLine(" We have the Right to Write");
sinexcelDevice1.Write(statusrecord.InverterRecord1);
sinexcelDevice2.Write(statusrecord.InverterRecord2);
}
else
{
Console.WriteLine(" Nooooooo We cant' have the Right to Write");
}
return statusrecord;
}
catch (CrcException e)
{
Console.WriteLine(e);
throw; // restart only on CRC
}
catch (Exception e)
{
Console.WriteLine(e);
return null; // keep running for non-critical errors
}
}
}
private static void ControlConstants(this StatusRecord? statusrecord)
{
if (statusrecord == null) return;
statusrecord.InverterRecord1.Battery1BackupSoc = (Single)statusrecord.Config.MinSoc ;
statusrecord.InverterRecord1.Battery2BackupSoc = (Single)statusrecord.Config.MinSoc ;
statusrecord.InverterRecord1.RepetitiveWeeks = SinexcelWeekDays.All;
var isChargePeriod = IsNowInsideDateAndTime(statusrecord.Config.StartTimeChargeandDischargeDayandTime, statusrecord.Config.StopTimeChargeandDischargeDayandTime);
Console.WriteLine("Are we inside the charge/Discharge time " + isChargePeriod);
if (statusrecord.Config.OperatingPriority != TimeChargeDischarge)
{
statusrecord.InverterRecord1.WorkingMode = statusrecord.Config.OperatingPriority;
}
else if (statusrecord.Config.OperatingPriority == TimeChargeDischarge && isChargePeriod)
{
statusrecord.InverterRecord1.WorkingMode = statusrecord.Config.OperatingPriority;
if (statusrecord.Config.TimeChargeandDischargePower > 0)
{
statusrecord.InverterRecord1.EffectiveStartDate = statusrecord.Config.StartTimeChargeandDischargeDayandTime.Date;
statusrecord.InverterRecord1.EffectiveEndDate = statusrecord.Config.StopTimeChargeandDischargeDayandTime.Date;
statusrecord.InverterRecord1.ChargingPowerPeriod1 = Math.Abs(statusrecord.Config.TimeChargeandDischargePower);
statusrecord.InverterRecord1.ChargeStartTimePeriod1 = statusrecord.Config.StartTimeChargeandDischargeDayandTime.TimeOfDay;
statusrecord.InverterRecord1.ChargeEndTimePeriod1 = statusrecord.Config.StopTimeChargeandDischargeDayandTime.TimeOfDay;
statusrecord.InverterRecord1.DischargeStartTimePeriod1 = TimeSpan.Zero;
statusrecord.InverterRecord1.DischargeEndTimePeriod1 = TimeSpan.Zero;
}
else
{
statusrecord.InverterRecord1.EffectiveStartDate = statusrecord.Config.StartTimeChargeandDischargeDayandTime.Date;
statusrecord.InverterRecord1.EffectiveEndDate = statusrecord.Config.StopTimeChargeandDischargeDayandTime.Date;
statusrecord.InverterRecord1.DishargingPowerPeriod1 = Math.Abs(statusrecord.Config.TimeChargeandDischargePower);
statusrecord.InverterRecord1.DischargeStartTimePeriod1 = statusrecord.Config.StartTimeChargeandDischargeDayandTime.TimeOfDay;
statusrecord.InverterRecord1.DischargeEndTimePeriod1 = statusrecord.Config.StopTimeChargeandDischargeDayandTime.TimeOfDay;
statusrecord.InverterRecord1.ChargeStartTimePeriod1 = TimeSpan.Zero;
statusrecord.InverterRecord1.ChargeEndTimePeriod1 = TimeSpan.Zero;
}
}
else
{
statusrecord.InverterRecord1.WorkingMode = SpontaneousSelfUse;
}
statusrecord.InverterRecord1.PowerOn = 1;
statusrecord.InverterRecord1.PowerOff = 0;
//statusrecord.InverterRecord.FaultClearing = 1;
}
private static bool IsNowInsideDateAndTime(DateTime effectiveStart, DateTime effectiveEnd)
{
DateTime now = DateTime.Now;
// Date check
if (now < effectiveStart || now > effectiveEnd)
return false;
return true;
}
private static StatusMessage GetSodiHomeStateAlarm(StatusRecord? record)
{
var s3Bucket = Config.Load().S3?.Bucket;
var alarmList = new List<AlarmOrWarning>();
var warningList = new List<AlarmOrWarning>();
/*
if (record.SinexcelRecord.WorkingMode == GrowattSystemStatus.Fault)
{
if (record.AcDcGrowatt.FaultMainCode != 0)
{
alarmList.Add(new AlarmOrWarning
{
Date = DateTime.Now.ToString("yyyy-MM-dd"),
Time = DateTime.Now.ToString("HH:mm:ss"),
CreatedBy = "Growatt Inverter",
Description = record.AcDcGrowatt.WarningMainCode.ToString(), // to add the sub code
});
}
if (record.AcDcGrowatt.WarningMainCode != 0)
{
warningList.Add(new AlarmOrWarning
{
Date = DateTime.Now.ToString("yyyy-MM-dd"),
Time = DateTime.Now.ToString("HH:mm:ss"),
CreatedBy = "Growatt inverter",
Description = record.AcDcGrowatt.FaultMainCode.ToString(), //to add the sub code
});
}
}*/
_sodiohomeAlarmState = warningList.Any()
? SodistoreAlarmState.Orange
: SodistoreAlarmState.Green; // this will be replaced by LedState
_sodiohomeAlarmState = alarmList.Any()
? SodistoreAlarmState.Red
: _sodiohomeAlarmState; // this will be replaced by LedState
var installationId = GetInstallationId(s3Bucket ?? string.Empty);
var returnedStatus = new StatusMessage
{
InstallationId = installationId,
Product = 2,
Status = _sodiohomeAlarmState,
Type = MessageType.AlarmOrWarning,
Alarms = alarmList,
Warnings = warningList
};
return returnedStatus;
}
/// <summary>
/// Checks if the error or warning content has changed compared to the previous state.
/// This allows detection of new/cleared errors even when the overall alarm state (Red/Orange/Green) remains the same.
/// </summary>
private static Boolean HasErrorsOrWarningsChanged(StatusMessage currentState)
{
// Get current error codes (descriptions)
var currentErrors = currentState.Alarms?
.Select(a => a.Description ?? String.Empty)
.OrderBy(d => d) // Sort for consistent comparison
.ToList() ?? new List<String>();
// Get current warning codes (descriptions)
var currentWarnings = currentState.Warnings?
.Select(w => w.Description ?? String.Empty)
.OrderBy(d => d) // Sort for consistent comparison
.ToList() ?? new List<String>();
// Check if lists have changed (new items added or existing items removed)
var errorsChanged = !currentErrors.SequenceEqual(_prevErrorCodes);
var warningsChanged = !currentWarnings.SequenceEqual(_prevWarningCodes);
// Update tracking if changes detected
if (errorsChanged || warningsChanged)
{
Console.WriteLine($"Error/Warning content changed:");
Console.WriteLine($" Errors: {String.Join(", ", currentErrors)} (was: {String.Join(", ", _prevErrorCodes)})");
Console.WriteLine($" Warnings: {String.Join(", ", currentWarnings)} (was: {String.Join(", ", _prevWarningCodes)})");
_prevErrorCodes = currentErrors;
_prevWarningCodes = currentWarnings;
return true;
}
return false;
}
private static Int32 GetInstallationId(String s3Bucket)
{
var part = s3Bucket.Split('-').FirstOrDefault();
return int.TryParse(part, out var id) ? id : 0; // is 0 a default safe value? check with Marios
}
private static void SendSalimaxStateAlarm(StatusMessage currentSalimaxState, StatusRecord? record)
{
var s3Bucket = Config.Load().S3?.Bucket;
var subscribedNow = false;
//When the controller boots, it tries to subscribe to the queue
if (_subscribeToQueueForTheFirstTime == false)
{
subscribedNow = true;
_subscribeToQueueForTheFirstTime = true;
_prevSodiohomeAlarmState = currentSalimaxState.Status;
_subscribedToQueue = RabbitMqManager.SubscribeToQueue(currentSalimaxState, s3Bucket, VpnServerIp);
_lastHeartbeatTime = DateTime.Now; // Initialize heartbeat timer
}
// Check if we should send a message
var stateChanged = currentSalimaxState.Status != _prevSodiohomeAlarmState;
var contentChanged = HasErrorsOrWarningsChanged(currentSalimaxState);
var needsHeartbeat = (DateTime.Now - _lastHeartbeatTime).TotalSeconds >= HeartbeatIntervalSeconds;
Console.WriteLine($"subscribedNow={subscribedNow}");
Console.WriteLine($"_subscribedToQueue={_subscribedToQueue}");
Console.WriteLine($"stateChanged={stateChanged}");
Console.WriteLine($"contentChanged={contentChanged}");
Console.WriteLine($"needsHeartbeat={needsHeartbeat}");
Console.WriteLine($"s3Bucket null? {s3Bucket == null}");
if (s3Bucket == null)
{
Console.WriteLine("⚠ S3 bucket not configured. Skipping middleware send.");
LogMiddlewareFailure(new Exception("S3 Bucket not configured"));
return;
}
// Ensure connection FIRST
if (!RabbitMqManager.EnsureConnected(currentSalimaxState, s3Bucket, VpnServerIp))
{
Console.WriteLine($"❌ RabbitMQ EnsureConnected FAILED at {DateTime.Now:HH:mm:ss.fff}");
LogMiddlewareFailure(new Exception("EnsureConnected returned false"));
return;
}
//If already subscribed to the queue and the status has been changed, update the queue
if (!subscribedNow && (stateChanged || contentChanged || needsHeartbeat))
{
_prevSodiohomeAlarmState = currentSalimaxState.Status;
// Set appropriate message type
if (stateChanged || contentChanged)
{
currentSalimaxState.Type = MessageType.AlarmOrWarning;
Console.WriteLine($"Sending AlarmOrWarning message - StateChanged: {stateChanged}, ContentChanged: {contentChanged}");
}
else if (needsHeartbeat)
{
currentSalimaxState.Type = MessageType.Heartbit;
Console.WriteLine($"Sending Heartbeat message - {HeartbeatIntervalSeconds}s interval reached");
_lastHeartbeatTime = DateTime.Now;
}
try
{
RabbitMqManager.InformMiddleware(currentSalimaxState);
LogMiddlewareFailure(new Exception($"✅ Middleware message sent at {DateTime.Now:HH:mm:ss.fff}"));
}
catch (Exception ex)
{
Console.WriteLine($"❌ Failed to send middleware message: {ex.Message}");
LogMiddlewareFailure(ex);
}
}
//If there is an available message from the RabbitMQ Broker, apply the configuration file
Configuration? config = SetConfigurationFile();
if (config != null)
{
record.ApplyConfigFile(config);
}
}
private static void LogMiddlewareFailure(Exception ex)
{
try
{
var logPath = "/home/inesco/SodiStoreHome/middleware_failures.log";
var logEntry =
$"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}]\n" +
$"Exception: {ex.GetType().FullName}\n" +
$"Message: {ex.Message}\n" +
$"StackTrace:\n{ex.StackTrace}\n" +
$"--------------------------------------------------\n";
File.AppendAllText(logPath, logEntry);
}
catch
{
// Never allow logging to crash the service
}
}
private static void ApplyConfigFile(this StatusRecord? status, Configuration? config)
{
if (config == null) return;
if (status == null) return;
status.Config.MinSoc = config.MinimumSoC;
status.Config.MaximumChargingCurrent = config.MaximumChargingCurrent;
status.Config.MaximumDischargingCurrent = config.MaximumDischargingCurrent;
status.Config.OperatingPriority = config.OperatingPriority;
status.Config.BatteriesCount = config.BatteriesCount;
status.Config.ClusterNumber = config.ClusterNumber;
status.Config.StartTimeChargeandDischargeDayandTime = config.StartTimeChargeandDischargeDayandTime;
status.Config.StopTimeChargeandDischargeDayandTime = config.StopTimeChargeandDischargeDayandTime;
status.Config.TimeChargeandDischargePower = config.TimeChargeandDischargePower;
status.Config.PvNumber = config.PvNumber;
status.Config.ControlPermission = config.ControlPermission;
}
private static async Task<Boolean> SaveModbusTcpFile(StatusRecord status)
{
var modbusData = new Dictionary<String, UInt16>();
// SYSTEM DATA
var result1 = ConvertToModbusRegisters((status.Config.ModbusProtcolNumber * 10), "UInt16", 30001); // this to be updated to modbusTCP version
var result2 = ConvertToModbusRegisters(status.InverterRecord1.SystemDateTime.ToUnixTime(), "UInt32", 30002);
// SYSTEM DATA
var result3 = ConvertToModbusRegisters(status.InverterRecord1.WorkingMode, "UInt16", 30004);
// BATTERY SUMMARY (assuming single battery [0]) // this to be improved
var result4 = ConvertToModbusRegisters((status.Config.BatteriesCount), "UInt16", 31000);
var result8 = ConvertToModbusRegisters((status.InverterRecord1.Battery1Voltage.Value * 10), "UInt16", 31001);
var result12 = ConvertToModbusRegisters((status.InverterRecord1.Battery2Voltage.Value * 10), "Int16", 31002);
var result13 = ConvertToModbusRegisters((status.InverterRecord1.Battery1Current.Value * 10), "Int32", 31003);
var result16 = ConvertToModbusRegisters((status.InverterRecord1.Battery2Current.Value * 10), "Int32", 31005);
var result9 = ConvertToModbusRegisters((status.InverterRecord1.Battery1Soc.Value * 100), "UInt16", 31007);
var result14 = ConvertToModbusRegisters((status.InverterRecord1.Battery2Soc.Value * 100), "UInt16", 31008);
var result5 = ConvertToModbusRegisters((status.InverterRecord1.TotalBatteryPower.Value * 10), "Int32", 31009);
var result7 = ConvertToModbusRegisters((status.InverterRecord1.Battery1BackupSoc * 100), "UInt16", 31011);
var result20 = ConvertToModbusRegisters((status.InverterRecord1.Battery2BackupSoc * 100), "UInt16", 31012);
var result15 = ConvertToModbusRegisters((status.InverterRecord1.Battery1Soh.Value * 100), "UInt16", 31013);
var result26 = ConvertToModbusRegisters((status.InverterRecord1.Battery2Soh.Value * 100), "UInt16", 31014);
var result21 = ConvertToModbusRegisters((status.InverterRecord1.Battery1MaxChargingCurrent * 10), "UInt16", 31016);
var result22 = ConvertToModbusRegisters((status.InverterRecord1.Battery1MaxDischargingCurrent * 10), "UInt16", 31017);
var result18 = ConvertToModbusRegisters((status.InverterRecord1.PvTotalPower * 10), "UInt32", 32000);
var result19 = ConvertToModbusRegisters((status.InverterRecord1.GridPower * 10), "Int32", 33000);
var result23 = ConvertToModbusRegisters((status.InverterRecord1.GridVoltageFrequency * 10), "UInt16", 33002);
var result24 = ConvertToModbusRegisters((status.InverterRecord1.WorkingMode), "UInt16", 34000);
var result25 = ConvertToModbusRegisters((status.InverterRecord1.InverterActivePower * 10), "Int32", 34001);
var result29 = ConvertToModbusRegisters((status.InverterRecord1.EnableGridExport ), "UInt16", 34003);
var result27 = ConvertToModbusRegisters((status.InverterRecord1.PowerGridExportLimit ), "Int16", 34004);
// Merge all results into one dictionary
var allResults = new[]
{
result1, result2, result3, result4, result5, result23, result24, result25, result29, result27, result26, result7, result8, result9, result16, result20, result12, result13, result14, result15, result18, result19, result21, result22
};
foreach (var result in allResults)
{
foreach (var entry in result)
{
modbusData[entry.Key] = entry.Value;
}
}
// Write to JSON
var json = JsonSerializer.Serialize(modbusData, new JsonSerializerOptions { WriteIndented = true });
await File.WriteAllTextAsync("/home/inesco/SodiStoreHome/ModbusTCP/modbus_tcp_data.json", json);
//Console.WriteLine("JSON file written successfully.");
//Console.WriteLine(json);
var stopTime = DateTime.Now;
Console.WriteLine(stopTime.ToString("HH:mm:ss.fff" )+ " Finish the loop");
return true;
}
private static Dictionary<string, ushort> ConvertToModbusRegisters(object value, string outputType, int startingAddress)
{
var regs = new Dictionary<string, ushort>(capacity: 2);
switch (outputType)
{
case "UInt16":
{
regs[$"{startingAddress}"] = Convert.ToUInt16(value);
break;
}
case "Int16":
{
short v = Convert.ToInt16(value);
regs[$"{startingAddress}"] = unchecked((ushort)v); // reinterpret
break;
}
case "UInt32":
{
uint v = Convert.ToUInt32(value);
ushort hi = (ushort)(v >> 16);
ushort lo = (ushort)(v & 0xFFFF);
regs[$"{startingAddress}"] = hi; // HIGH word first (Modbus standard)
regs[$"{startingAddress + 1}"] = lo; // then LOW word
break;
}
case "Int32":
{
int v = Convert.ToInt32(value);
uint raw = unchecked((uint)v); // bit-reinterpret
ushort hi = (ushort)(raw >> 16);
ushort lo = (ushort)(raw & 0xFFFF);
regs[$"{startingAddress}"] = hi; // HIGH word
regs[$"{startingAddress + 1}"] = lo; // LOW word
break;
}
case "Float": // IEEE-754 single
{
float f = Convert.ToSingle(value);
// Convert to bytes, then to two big-endian 16-bit words
var bytes = BitConverter.GetBytes(f); // little-endian on most platforms
Array.Reverse(bytes); // to big-endian byte order
ushort hi = (ushort)((bytes[0] << 8) | bytes[1]);
ushort lo = (ushort)((bytes[2] << 8) | bytes[3]);
regs[$"{startingAddress}"] = hi;
regs[$"{startingAddress + 1}"] = lo;
break;
}
default:
throw new ArgumentException($"Unsupported output type: {outputType}");
}
return regs;
}
private static async Task<Boolean> DataLogging(StatusRecord status, DateTime timeStamp)
{
var csv = status.ToCsv();
// for debug, only to be deleted.
//foreach (var item in csv.SplitLines())
//{
// Console.WriteLine(item + "");
//}
await SavingLocalCsvFile(timeStamp.ToUnixTime(), csv);
var jsonData = new Dictionary<String, Object>();
ConvertToJson(csv, jsonData).LogInfo();
var s3Config = status.Config.S3;
if (s3Config is null)
return false;
//Concatenating 15 files in one file
return await ConcatinatingAndCompressingFiles(timeStamp.ToUnixTime(), s3Config);
}
private static void InsertIntoJson(Dictionary<String, Object> jsonDict, String[] keys, String value)
{
var currentDict = jsonDict;
for (Int16 i = 1; i < keys.Length; i++) // Start at 1 to skip empty root
{
var key = keys[i];
if (!currentDict.ContainsKey(key))
{
currentDict[key] = new Dictionary<String, Object>();
}
if (i == keys.Length - 1) // Last key, store the value
{
if (!value.Contains(",") && double.TryParse(value, out Double doubleValue)) // Try to parse value as a number
{
currentDict[key] = Math.Round(doubleValue, 2); // Round to 2 decimal places
}
else
{
currentDict[key] = value; // Store as string if not a number
}
}
else
{
currentDict = (Dictionary<String, Object>)currentDict[key];
}
}
}
private static String ConvertToJson(String csv, Dictionary<String, Object> jsonData)
{
foreach (var line in csv.Split('\n'))
{
if (string.IsNullOrWhiteSpace(line)) continue;
var parts = line.Split(';');
var keyPath = parts[0];
var value = parts[1];
var unit = parts.Length > 2 ? parts[2].Trim() : "";
InsertIntoJson(jsonData, keyPath.Split('/'), value);
}
var jsonOutput = JsonConvert.SerializeObject(jsonData, Formatting.None);
return jsonOutput;
}
private static async Task SavingLocalCsvFile(Int64 timestamp, String csv)
{
const String directoryPath = "/home/inesco/SodiStoreHome/csvFile";
// Ensure directory exists
if (!Directory.Exists(directoryPath))
{
Directory.CreateDirectory(directoryPath);
}
// Get all .csv files ordered by creation time (oldest first)
var csvFiles = new DirectoryInfo(directoryPath)
.GetFiles("*.csv")
.OrderBy(f => f.CreationTimeUtc)
.ToList();
// If more than 5000 files, delete the oldest
if (csvFiles.Count >= 5000)
{
var oldestFile = csvFiles.First();
try
{
oldestFile.Delete();
}
catch (Exception ex)
{
Console.WriteLine($"Failed to delete file: {oldestFile.FullName}, Error: {ex.Message}");
}
}
// Prepare the filtered CSV content
var filteredCsv = csv
.SplitLines()
.Where(l => !l.Contains("Secret"))
.JoinLines();
// Save the new CSV file
var filePath = Path.Combine(directoryPath, timestamp + ".csv");
await File.WriteAllTextAsync(filePath, filteredCsv);
}
private static async Task<Boolean> ConcatinatingAndCompressingFiles(Int64 timeStamp, S3Config s3Config)
{
if (_fileCounter >= NbrOfFileToConcatenate)
{
_fileCounter = 0;
var logFileConcatenator = new LogFileConcatenator();
var jsontoSend = logFileConcatenator.ConcatenateFiles(NbrOfFileToConcatenate);
var fileNameWithoutExtension = timeStamp.ToString(); // used for both S3 and local
var s3Path = fileNameWithoutExtension + ".json";
var request = s3Config.CreatePutRequest(s3Path);
var compressedBytes = CompresseBytes(jsontoSend);
var base64String = Convert.ToBase64String(compressedBytes);
var stringContent = new StringContent(base64String, Encoding.UTF8, "application/base64");
var uploadSucceeded = false;
try
{
var response = await request.PutAsync(stringContent);
if (response.StatusCode != 200)
{
Console.WriteLine("ERROR: PUT");
var error = await response.GetStringAsync();
Console.WriteLine(error);
await SaveToLocalCompressedFallback(compressedBytes, fileNameWithoutExtension);
// Heartbit();
return false;
}
uploadSucceeded = true;
Console.WriteLine("✅ File uploaded to S3 successfully.");
Console.WriteLine("---------------------------------------- Resending FailedUploadedFiles----------------------------------------");
// Heartbit();
await ResendLocalFailedFilesAsync(s3Config); // retry any pending failed files
}
catch (Exception ex)
{
Console.WriteLine("Upload exception: " + ex.Message);
if (!uploadSucceeded)
{
await SaveToLocalCompressedFallback(compressedBytes, fileNameWithoutExtension);
}
//Heartbit();
return false;
}
}
_fileCounter++;
return true;
}
/* private static void Heartbit()
{
var s3Bucket = Config.Load().S3?.Bucket;
var tryParse = int.TryParse(s3Bucket?.Split("-")[0], out var installationId);
if (tryParse)
{
var returnedStatus = new StatusMessage
{
InstallationId = installationId,
Product = 2,
Status = _sodiohomeAlarmState,
Type = MessageType.Heartbit,
};
if (s3Bucket != null)
RabbitMqManager.InformMiddleware(returnedStatus);
}
}*/
private static async Task SaveToLocalCompressedFallback(Byte[] compressedData, String fileNameWithoutExtension)
{
try
{
var fallbackDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FailedUploads");
Directory.CreateDirectory(fallbackDir);
var fileName = fileNameWithoutExtension + ".json"; // Save as .json, but still compressed
var fullPath = Path.Combine(fallbackDir, fileName);
await File.WriteAllBytesAsync(fullPath, compressedData); // Compressed data
Console.WriteLine($"Saved compressed failed upload to: {fullPath}");
}
catch (Exception ex)
{
Console.WriteLine("Failed to save compressed file locally: " + ex.Message);
}
}
private static async Task ResendLocalFailedFilesAsync(S3Config s3Config)
{
var fallbackDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FailedUploads");
if (!Directory.Exists(fallbackDir))
return;
var files = Directory.GetFiles(fallbackDir, "*.json");
files.Length.WriteLine(" Number of failed files, to upload");
foreach (var filePath in files)
{
var fileName = Path.GetFileName(filePath); // e.g., "1720023600.json"
try
{
byte[] compressedBytes = await File.ReadAllBytesAsync(filePath);
var base64String = Convert.ToBase64String(compressedBytes);
var stringContent = new StringContent(base64String, Encoding.UTF8, "application/base64");
var request = s3Config.CreatePutRequest(fileName);
var response = await request.PutAsync(stringContent);
if (response.StatusCode == 200)
{
File.Delete(filePath);
Console.WriteLine($"✅ Successfully resent and deleted: {fileName}");
}
else
{
Console.WriteLine($"❌ Failed to resend {fileName}, status: {response.StatusCode}");
}
}
catch (Exception ex)
{
Console.WriteLine($"⚠️ Exception while resending {fileName}: {ex.Message}");
}
}
}
private static Byte[] CompresseBytes(String jsonToSend)
{
//Compress JSON data to a byte array
using var memoryStream = new MemoryStream();
//Create a zip directory and put the compressed file inside
using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true))
{
var entry = archive.CreateEntry("data.json", CompressionLevel.SmallestSize); // Add JSON data to the ZIP archive
using (var entryStream = entry.Open())
using (var writer = new StreamWriter(entryStream))
{
writer.Write(jsonToSend);
}
}
var compressedBytes = memoryStream.ToArray();
return compressedBytes;
}
}