Merge remote-tracking branch 'origin/main'

This commit is contained in:
atef 2025-03-03 08:44:38 +01:00
commit 94829992aa
43 changed files with 4973 additions and 1465 deletions

View File

@ -233,7 +233,8 @@ public class Controller : ControllerBase
else else
{ {
// Define a regex pattern to match the filenames without .csv extension // Define a regex pattern to match the filenames without .csv extension
var pattern = @"/([^/]+)\.csv$";
var pattern = @"/([^/]+)\.(csv|json)$";
var regex = new Regex(pattern); var regex = new Regex(pattern);
// Process each line of the output // Process each line of the output

View File

@ -25,8 +25,8 @@ public static class Program
Db.Init(); Db.Init();
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
//RabbitMqManager.InitializeEnvironment(); RabbitMqManager.InitializeEnvironment();
//RabbitMqManager.StartRabbitMqConsumer().SupressAwaitWarning(); RabbitMqManager.StartRabbitMqConsumer().SupressAwaitWarning();
WebsocketManager.MonitorSalimaxInstallationTable().SupressAwaitWarning(); WebsocketManager.MonitorSalimaxInstallationTable().SupressAwaitWarning();
WebsocketManager.MonitorSalidomoInstallationTable().SupressAwaitWarning(); WebsocketManager.MonitorSalidomoInstallationTable().SupressAwaitWarning();

View File

@ -1,5 +1,5 @@
#To deploy to the monitor server, uncomment the following line #To deploy to the monitor server, uncomment the following line
#dotnet publish Backend.csproj -c Release -r linux-x64 --self-contained true -p:PublishTrimmed=false && rsync -av bin/Release/net6.0/linux-x64/publish/ ubuntu@194.182.190.208:~/backend && ssh ubuntu@194.182.190.208 'sudo systemctl restart backend' dotnet publish Backend.csproj -c Release -r linux-x64 --self-contained true -p:PublishTrimmed=false && rsync -av bin/Release/net6.0/linux-x64/publish/ ubuntu@194.182.190.208:~/backend && ssh ubuntu@194.182.190.208 'sudo systemctl restart backend'
#To deploy to the stage server, uncomment the following line #To deploy to the stage server, uncomment the following line
dotnet publish Backend.csproj -c Release -r linux-x64 --self-contained true -p:PublishTrimmed=false && rsync -av bin/Release/net6.0/linux-x64/publish/ ubuntu@91.92.154.141:~/backend && ssh ubuntu@91.92.154.141 'sudo systemctl restart backend' #dotnet publish Backend.csproj -c Release -r linux-x64 --self-contained true -p:PublishTrimmed=false && rsync -av bin/Release/net6.0/linux-x64/publish/ ubuntu@91.92.154.141:~/backend && ssh ubuntu@91.92.154.141 'sudo systemctl restart backend'

View File

@ -1,3 +1,4 @@
using System.Text.Json;
using InnovEnergy.App.SaliMax.Devices; using InnovEnergy.App.SaliMax.Devices;
using InnovEnergy.App.SaliMax.SaliMaxRelays; using InnovEnergy.App.SaliMax.SaliMaxRelays;
using InnovEnergy.App.SaliMax.System; using InnovEnergy.App.SaliMax.System;
@ -30,4 +31,25 @@ public record StatusRecord
public required EssControl EssControl { get; set; } // TODO: init only public required EssControl EssControl { get; set; } // TODO: init only
public required StateMachine StateMachine { get; init; } public required StateMachine StateMachine { get; init; }
public string ToJson()
{
// Try to get the "Battery" property via reflection
// var batteryProperty = thing.GetType().GetProperty("Battery");
// if (batteryProperty == null)
// throw new InvalidOperationException("The object does not have a 'Battery' property.");
//
// // Retrieve the value of the Battery property
// var batteryValue = Battery.GetValue(thing);
var jsonOptions = new JsonSerializerOptions { WriteIndented = true };
// Serialize the Battery property
Console.WriteLine("eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee");
string json = JsonSerializer.Serialize(this.Battery, jsonOptions);
Console.WriteLine(json);
return json;
}
} }

View File

@ -816,6 +816,7 @@ internal static class Program
private static async Task<Boolean> UploadCsv(StatusRecord status, DateTime timeStamp) private static async Task<Boolean> UploadCsv(StatusRecord status, DateTime timeStamp)
{ {
status.ToJson();
var csv = status.ToCsv().LogInfo(); var csv = status.ToCsv().LogInfo();
await RestApiSavingFile(csv); await RestApiSavingFile(csv);

View File

@ -1,211 +0,0 @@
using System.Reflection.Metadata;
namespace InnovEnergy.App.SaliMax.SaliMaxRelays;
#pragma warning disable CS8602 // Dereference of a possibly null reference.
public class CombinedAdamRelaysRecord : IRelaysRecord
{
private const UInt16 SlowFreq = 3000;
private const UInt16 HighFreq = 1000;
public CombinedAdamRelaysRecord(RelaysRecordAdam6060? relaysRecordAdam6060, RelaysRecordAdam6360D? relaysRecordAdam6360D)
{
_recordAdam6060 = relaysRecordAdam6060;
_recordAdam6360D = relaysRecordAdam6360D;
}
private static RelaysRecordAdam6060? _recordAdam6060;
private static RelaysRecordAdam6360D? _recordAdam6360D;
public static IRelaysRecord Instance { get; } = new CombinedAdamRelaysRecord(_recordAdam6060, _recordAdam6360D);
public Boolean K1GridBusIsConnectedToGrid => _recordAdam6360D.K1GridBusIsConnectedToGrid;
public Boolean K2IslandBusIsConnectedToGridBus => _recordAdam6360D.K2IslandBusIsConnectedToGridBus;
public Boolean FiWarning => _recordAdam6360D.FiWarning;
public Boolean FiError => _recordAdam6360D.FiError;
public Boolean K2ConnectIslandBusToGridBus
{
get => _recordAdam6360D.K2ConnectIslandBusToGridBus;
set => _recordAdam6360D.K2ConnectIslandBusToGridBus = value;
}
public Boolean Inverter1WagoStatus => _recordAdam6360D.Inverter1WagoStatus;
public Boolean Inverter2WagoStatus => _recordAdam6360D.Inverter2WagoStatus;
public Boolean Inverter3WagoStatus => _recordAdam6360D.Inverter3WagoStatus;
public Boolean Inverter4WagoStatus => _recordAdam6360D.Inverter4WagoStatus;
public Boolean Dc1WagoStatus => _recordAdam6060.Dc1WagoStatus;
public Boolean Dc2WagoStatus => _recordAdam6060.Dc2WagoStatus;
public Boolean Dc3WagoStatus => _recordAdam6060.Dc3WagoStatus;
public Boolean Dc4WagoStatus => _recordAdam6060.Dc4WagoStatus;
public Boolean DcSystemControlWagoStatus => _recordAdam6060.DcSystemControlWagoStatus;
public Boolean LedGreen { get => _recordAdam6360D.LedGreen; set => _recordAdam6360D.LedGreen = value;}
public Boolean LedRed { get => _recordAdam6360D.LedRed; set => _recordAdam6360D.LedRed = value;}
public Boolean Harvester1Step => _recordAdam6360D.Harvester1Step;
public Boolean Harvester2Step => _recordAdam6360D.Harvester2Step;
public Boolean Harvester3Step => _recordAdam6360D.Harvester3Step;
public Boolean Harvester4Step => _recordAdam6360D.Harvester4Step;
public UInt16 DigitalOutput0Mode { get => _recordAdam6360D.DigitalOutput0Mode; set => _recordAdam6360D.DigitalOutput0Mode = value; }
public UInt16 DigitalOutput1Mode
{
get => _recordAdam6360D.DigitalOutput1Mode;
set => _recordAdam6360D.DigitalOutput1Mode = value;
}
public UInt16 DigitalOutput2Mode
{
get => _recordAdam6360D.DigitalOutput2Mode;
set => _recordAdam6360D.DigitalOutput2Mode = value;
}
public UInt16 DigitalOutput3Mode
{
get => _recordAdam6360D.DigitalOutput3Mode;
set => _recordAdam6360D.DigitalOutput3Mode = value;
}
public UInt16 DigitalOutput4Mode
{
get => _recordAdam6360D.DigitalOutput4Mode;
set => _recordAdam6360D.DigitalOutput4Mode = value;
}
public UInt16 DigitalOutput5Mode
{
get => _recordAdam6360D.DigitalOutput5Mode;
set => _recordAdam6360D.DigitalOutput5Mode = value;
}
public Boolean Do0StartPulse { get => _recordAdam6360D.Do0Pulse; set => _recordAdam6360D.Do0Pulse = value; }
public Boolean Do1StartPulse { get => _recordAdam6360D.Do1Pulse; set => _recordAdam6360D.Do1Pulse = value; }
public Boolean Do2StartPulse { get => _recordAdam6360D.Do2Pulse; set => _recordAdam6360D.Do2Pulse = value; }
public Boolean Do3StartPulse { get => _recordAdam6360D.Do3Pulse; set => _recordAdam6360D.Do3Pulse = value; }
public Boolean Do4StartPulse { get => _recordAdam6360D.Do4Pulse; set => _recordAdam6360D.Do4Pulse = value; }
public Boolean Do5StartPulse { get => _recordAdam6360D.Do5Pulse; set => _recordAdam6360D.Do5Pulse = value; }
public UInt16 PulseOut0LowTime { get => _recordAdam6360D.PulseOut0LowTime; set => _recordAdam6360D.PulseOut0LowTime = value; }
public UInt16 PulseOut1LowTime { get => _recordAdam6360D.PulseOut1LowTime; set => _recordAdam6360D.PulseOut1LowTime = value; }
public UInt16 PulseOut2LowTime { get => _recordAdam6360D.PulseOut2LowTime; set => _recordAdam6360D.PulseOut2LowTime = value; }
public UInt16 PulseOut3LowTime { get => _recordAdam6360D.PulseOut3LowTime; set => _recordAdam6360D.PulseOut3LowTime = value; }
public UInt16 PulseOut4LowTime { get => _recordAdam6360D.PulseOut4LowTime; set => _recordAdam6360D.PulseOut4LowTime = value; }
public UInt16 PulseOut5LowTime { get => _recordAdam6360D.PulseOut5LowTime; set => _recordAdam6360D.PulseOut5LowTime = value; }
public UInt16 PulseOut0HighTime { get => _recordAdam6360D.PulseOut0HighTime; set => _recordAdam6360D.PulseOut0HighTime = value; }
public UInt16 PulseOut1HighTime { get => _recordAdam6360D.PulseOut1HighTime; set => _recordAdam6360D.PulseOut1HighTime = value; }
public UInt16 PulseOut2HighTime { get => _recordAdam6360D.PulseOut2HighTime; set => _recordAdam6360D.PulseOut2HighTime = value; }
public UInt16 PulseOut3HighTime { get => _recordAdam6360D.PulseOut3HighTime; set => _recordAdam6360D.PulseOut3HighTime = value; }
public UInt16 PulseOut4HighTime { get => _recordAdam6360D.PulseOut4HighTime; set => _recordAdam6360D.PulseOut4HighTime = value; }
public UInt16 PulseOut5HighTime { get => _recordAdam6360D.PulseOut5HighTime; set => _recordAdam6360D.PulseOut5HighTime = value; }
/**************************** Green LED *********************************/
public void PerformSolidGreenLed()
{
DigitalOutput0Mode = 0;
DigitalOutput1Mode = 0;
LedGreen = true;
LedRed = false;
}
public void PerformSlowFlashingGreenLed()
{
PulseOut0HighTime = SlowFreq;
PulseOut0LowTime = SlowFreq;
DigitalOutput0Mode = 2;
Do0StartPulse = true;
Do1StartPulse = false; // make sure the red LED is off
Console.WriteLine("Green Slow Flashing Starting");
}
public void PerformFastFlashingGreenLed()
{
PulseOut0HighTime = HighFreq;
PulseOut0LowTime = HighFreq;
DigitalOutput0Mode = 2;
Do0StartPulse = true;
Do1StartPulse = false;// make sure the red LED is off
Console.WriteLine("Green Slow Flashing Starting");
}
/**************************** Orange LED *********************************/
public void PerformSolidOrangeLed()
{
DigitalOutput0Mode = 0;
DigitalOutput1Mode = 0;
LedGreen = true;
LedRed = true;
}
public void PerformSlowFlashingOrangeLed()
{
PerformSlowFlashingGreenLed();
PerformSlowFlashingRedLed();
Do0StartPulse = true;
Do1StartPulse = true;
Console.WriteLine("Orange Slow Flashing Starting");
}
public void PerformFastFlashingOrangeLed()
{
PerformFastFlashingGreenLed();
PerformFastFlashingRedLed();
Do0StartPulse = true;
Do1StartPulse = true;
Console.WriteLine("Orange Fast Flashing Starting");
}
/**************************** RED LED *********************************/
public void PerformSolidRedLed()
{
DigitalOutput0Mode = 0;
DigitalOutput1Mode = 0;
LedGreen = false;
LedRed = true;
}
public void PerformSlowFlashingRedLed()
{
PulseOut1HighTime = SlowFreq;
PulseOut1LowTime = SlowFreq;
DigitalOutput1Mode = 2;
Do0StartPulse = false; // make sure the green LED is off
Do1StartPulse = true;
Console.WriteLine("Red Slow Flashing Starting");
}
public void PerformFastFlashingRedLed()
{
PulseOut1HighTime = HighFreq;
PulseOut1LowTime = HighFreq;
DigitalOutput1Mode = 2;
Do0StartPulse = false; // make sure the green LED is off
Do1StartPulse = true;
Console.WriteLine("Red Fast Flashing Starting");
}
public RelaysRecordAdam6360D? GetAdam6360DRecord()
{
return _recordAdam6360D;
}
public RelaysRecordAdam6060? GetAdam6060Record()
{
return _recordAdam6060;
}
public IEnumerable<Boolean> K3InverterIsConnectedToIslandBus => _recordAdam6360D.K3InverterIsConnectedToIslandBus;
}

View File

@ -1,78 +0,0 @@
namespace InnovEnergy.App.SaliMax.SaliMaxRelays;
public interface IRelaysRecord
{
Boolean K1GridBusIsConnectedToGrid { get; }
Boolean K2IslandBusIsConnectedToGridBus { get; }
IEnumerable<Boolean> K3InverterIsConnectedToIslandBus { get; }
Boolean FiWarning { get; }
Boolean FiError { get; }
Boolean K2ConnectIslandBusToGridBus { get; set; }
// Boolean Inverter1WagoRelay { get; set; } // to add in the future
// Boolean Inverter2WagoRelay { get; set; } // to add in the future
// Boolean Inverter3WagoRelay { get; set; } // to add in the future
// Boolean Inverter4WagoRelay { get; set; } // to add in the future
Boolean Inverter1WagoStatus { get; }
Boolean Inverter2WagoStatus { get; }
Boolean Inverter3WagoStatus { get; }
Boolean Inverter4WagoStatus { get; }
Boolean Dc1WagoStatus { get; } // to test
Boolean Dc2WagoStatus { get; } // to test
Boolean Dc3WagoStatus { get; } // to test
Boolean Dc4WagoStatus { get; } // to test
Boolean DcSystemControlWagoStatus { get; } // to test
Boolean LedGreen { get; set; }
Boolean LedRed { get; }
Boolean Harvester1Step { get; }
Boolean Harvester2Step { get; }
Boolean Harvester3Step { get; }
Boolean Harvester4Step { get; }
Boolean Do0StartPulse { get; set; }
Boolean Do1StartPulse { get; set; }
Boolean Do2StartPulse { get; set; }
Boolean Do3StartPulse { get; set; }
Boolean Do4StartPulse { get; set; }
Boolean Do5StartPulse { get; set; }
UInt16 DigitalOutput0Mode { get; set; }
UInt16 DigitalOutput1Mode { get; set; }
UInt16 DigitalOutput2Mode { get; set; }
UInt16 DigitalOutput3Mode { get; set; }
UInt16 DigitalOutput4Mode { get; set; }
UInt16 DigitalOutput5Mode { get; set; }
UInt16 PulseOut0LowTime { get; set; }
UInt16 PulseOut1LowTime { get; set; }
UInt16 PulseOut2LowTime { get; set; }
UInt16 PulseOut3LowTime { get; set; }
UInt16 PulseOut4LowTime { get; set; }
UInt16 PulseOut5LowTime { get; set; }
UInt16 PulseOut0HighTime { get; set; }
UInt16 PulseOut1HighTime { get; set; }
UInt16 PulseOut2HighTime { get; set; }
UInt16 PulseOut3HighTime { get; set; }
UInt16 PulseOut4HighTime { get; set; }
UInt16 PulseOut5HighTime { get; set; }
void PerformSolidGreenLed();
void PerformSlowFlashingGreenLed();
void PerformFastFlashingGreenLed();
void PerformSolidOrangeLed();
void PerformSlowFlashingOrangeLed();
void PerformFastFlashingOrangeLed();
void PerformSolidRedLed();
void PerformSlowFlashingRedLed();
void PerformFastFlashingRedLed();
}

View File

@ -1,40 +0,0 @@
using InnovEnergy.Lib.Devices.Adam6360D;
using InnovEnergy.Lib.Protocols.Modbus.Channels;
namespace InnovEnergy.App.SaliMax.SaliMaxRelays;
public class RelaysDeviceAdam6360
{
private Adam6360DDevice AdamDevice6360D { get; }
public RelaysDeviceAdam6360(String hostname) => AdamDevice6360D = new Adam6360DDevice(hostname, 2);
public RelaysDeviceAdam6360(Channel channel) => AdamDevice6360D = new Adam6360DDevice(channel, 2);
public RelaysRecordAdam6360D? Read()
{
try
{
return AdamDevice6360D.Read();
}
catch (Exception e)
{
$"Failed to read from {nameof(RelaysDeviceAdam6360)}\n{e}".LogError();
return null;
}
}
public void Write(RelaysRecordAdam6360D r)
{
try
{
AdamDevice6360D.Write(r);
}
catch (Exception e)
{
$"Failed to write to {nameof(RelaysDeviceAdam6360)}\n{e}".LogError();
}
}
}

View File

@ -1,38 +0,0 @@
using InnovEnergy.Lib.Devices.Adam6060;
using InnovEnergy.Lib.Protocols.Modbus.Channels;
namespace InnovEnergy.App.SaliMax.SaliMaxRelays;
public class RelaysDeviceAdam6060
{
private Adam6060Device AdamDevice6060 { get; }
public RelaysDeviceAdam6060(String hostname) => AdamDevice6060 = new Adam6060Device(hostname, 2);
public RelaysDeviceAdam6060(Channel channel) => AdamDevice6060 = new Adam6060Device(channel, 2);
public RelaysRecordAdam6060? Read()
{
try
{
return AdamDevice6060.Read();
}
catch (Exception e)
{
$"Failed to read from {nameof(RelaysDeviceAdam6060)}\n{e}".LogError();
return null;
}
}
public void Write(RelaysRecordAdam6060 r)
{
try
{
AdamDevice6060.Write(r);
}
catch (Exception e)
{
$"Failed to write to {nameof(RelaysDeviceAdam6060)}\n{e}".LogError();
}
}
}

View File

@ -1,37 +0,0 @@
using InnovEnergy.Lib.Devices.Amax5070;
using InnovEnergy.Lib.Protocols.Modbus.Channels;
namespace InnovEnergy.App.SaliMax.SaliMaxRelays;
public class RelaysDeviceAmax
{
private Amax5070Device AmaxDevice { get; }
public RelaysDeviceAmax(Channel channel) => AmaxDevice = new Amax5070Device(channel);
public RelaysRecordAmax? Read()
{
try
{
return AmaxDevice.Read();
}
catch (Exception e)
{
$"Failed to read from {nameof(RelaysDeviceAmax)}\n{e}".LogError();
return null;
}
}
public void Write(RelaysRecordAmax r)
{
try
{
AmaxDevice.Write(r);
}
catch (Exception e)
{
$"Failed to write to {nameof(RelaysDeviceAmax)}\n{e}".LogError();
}
}
}

View File

@ -1,24 +0,0 @@
using InnovEnergy.Lib.Devices.Adam6060;
namespace InnovEnergy.App.SaliMax.SaliMaxRelays;
public class RelaysRecordAdam6060
{
private readonly Adam6060Registers _Regs;
private RelaysRecordAdam6060(Adam6060Registers regs) => _Regs = regs;
public Boolean Dc1WagoStatus => _Regs.DigitalInput0; // to test
public Boolean Dc2WagoStatus => _Regs.DigitalInput1; // to test
public Boolean Dc3WagoStatus => _Regs.DigitalInput4; // to test
public Boolean Dc4WagoStatus => _Regs.DigitalInput5; // to test
public Boolean DcSystemControlWagoStatus => _Regs.DigitalInput3; // to test
public static implicit operator Adam6060Registers(RelaysRecordAdam6060 d) => d._Regs;
public static implicit operator RelaysRecordAdam6060(Adam6060Registers d) => new RelaysRecordAdam6060(d);
}

View File

@ -1,81 +0,0 @@
using InnovEnergy.Lib.Devices.Adam6360D;
namespace InnovEnergy.App.SaliMax.SaliMaxRelays;
public class RelaysRecordAdam6360D
{
private readonly Adam6360DRegisters _Regs;
private RelaysRecordAdam6360D(Adam6360DRegisters regs) => _Regs = regs;
public Boolean K1GridBusIsConnectedToGrid => _Regs.DigitalInput6;
public Boolean K2IslandBusIsConnectedToGridBus => !_Regs.DigitalInput4;
public Boolean Inverter1WagoStatus => _Regs.DigitalInput8;
public Boolean Inverter2WagoStatus => _Regs.DigitalInput9;
public Boolean Inverter3WagoStatus => _Regs.DigitalInput10;
public Boolean Inverter4WagoStatus => _Regs.DigitalInput11;
public IEnumerable<Boolean> K3InverterIsConnectedToIslandBus
{
get
{
yield return K3Inverter1IsConnectedToIslandBus;
yield return K3Inverter2IsConnectedToIslandBus;
yield return K3Inverter3IsConnectedToIslandBus;
yield return K3Inverter4IsConnectedToIslandBus;
}
}
private Boolean K3Inverter1IsConnectedToIslandBus => !_Regs.DigitalInput0; // change it to private should be ok
private Boolean K3Inverter2IsConnectedToIslandBus => !_Regs.DigitalInput1;
private Boolean K3Inverter3IsConnectedToIslandBus => !_Regs.DigitalInput2;
private Boolean K3Inverter4IsConnectedToIslandBus => !_Regs.DigitalInput3;
public Boolean FiWarning => !_Regs.DigitalInput5;
public Boolean FiError => !_Regs.DigitalInput7;
public Boolean Harvester1Step =>_Regs.DigitalOutput2;
public Boolean Harvester2Step =>_Regs.DigitalOutput3;
public Boolean Harvester3Step =>_Regs.DigitalOutput4;
public Boolean Harvester4Step =>_Regs.DigitalOutput5;
public Boolean LedGreen { get =>_Regs.DigitalOutput0; set => _Regs.DigitalOutput0 = value;}
public Boolean LedRed { get =>_Regs.DigitalOutput1; set => _Regs.DigitalOutput1 = value;}
public Boolean Do0Pulse { get => _Regs.Do0Pulse; set => _Regs.Do0Pulse = value;}
public Boolean Do1Pulse { get => _Regs.Do1Pulse; set => _Regs.Do1Pulse = value;}
public Boolean Do2Pulse { get => _Regs.Do2Pulse; set => _Regs.Do2Pulse = value;}
public Boolean Do3Pulse { get => _Regs.Do3Pulse; set => _Regs.Do3Pulse = value;}
public Boolean Do4Pulse { get => _Regs.Do4Pulse; set => _Regs.Do4Pulse = value;}
public Boolean Do5Pulse { get => _Regs.Do5Pulse; set => _Regs.Do5Pulse = value;}
public UInt16 PulseOut0LowTime { get => _Regs.PulseOut0LowTime; set => _Regs.PulseOut0LowTime = value;} //in milleseconds
public UInt16 PulseOut1LowTime { get => _Regs.PulseOut1LowTime; set => _Regs.PulseOut1LowTime = value;}
public UInt16 PulseOut2LowTime { get => _Regs.PulseOut2LowTime; set => _Regs.PulseOut2LowTime = value;}
public UInt16 PulseOut3LowTime { get => _Regs.PulseOut3LowTime; set => _Regs.PulseOut3LowTime = value;}
public UInt16 PulseOut4LowTime { get => _Regs.PulseOut4LowTime; set => _Regs.PulseOut4LowTime = value;}
public UInt16 PulseOut5LowTime { get => _Regs.PulseOut5LowTime; set => _Regs.PulseOut5LowTime = value;}
public UInt16 PulseOut0HighTime { get => _Regs.PulseOut0HighTime; set => _Regs.PulseOut0HighTime = value;} // in milleseconds
public UInt16 PulseOut1HighTime { get => _Regs.PulseOut1HighTime; set => _Regs.PulseOut1HighTime = value;}
public UInt16 PulseOut2HighTime { get => _Regs.PulseOut2HighTime; set => _Regs.PulseOut2HighTime = value;}
public UInt16 PulseOut3HighTime { get => _Regs.PulseOut3HighTime; set => _Regs.PulseOut3HighTime = value;}
public UInt16 PulseOut4HighTime { get => _Regs.PulseOut4HighTime; set => _Regs.PulseOut4HighTime = value;}
public UInt16 PulseOut5HighTime { get => _Regs.PulseOut5HighTime; set => _Regs.PulseOut5HighTime = value;}
public UInt16 DigitalOutput0Mode { get => _Regs.DigitalOutput0Mode; set => _Regs.DigitalOutput0Mode = value;} // To test: 0, 1 or 2
public UInt16 DigitalOutput1Mode { get => _Regs.DigitalOutput1Mode; set => _Regs.DigitalOutput1Mode = value;}
public UInt16 DigitalOutput2Mode { get => _Regs.DigitalOutput2Mode; set => _Regs.DigitalOutput2Mode = value;}
public UInt16 DigitalOutput3Mode { get => _Regs.DigitalOutput3Mode; set => _Regs.DigitalOutput3Mode = value;}
public UInt16 DigitalOutput4Mode { get => _Regs.DigitalOutput4Mode; set => _Regs.DigitalOutput4Mode = value;}
public UInt16 DigitalOutput5Mode { get => _Regs.DigitalOutput5Mode; set => _Regs.DigitalOutput5Mode = value;}
public Boolean K2ConnectIslandBusToGridBus { get => _Regs.Relay0; set => _Regs.Relay0 = value;}
public static implicit operator Adam6360DRegisters(RelaysRecordAdam6360D d) => d._Regs;
public static implicit operator RelaysRecordAdam6360D(Adam6360DRegisters d) => new RelaysRecordAdam6360D(d);
}

View File

@ -1,134 +0,0 @@
using InnovEnergy.Lib.Devices.Amax5070;
namespace InnovEnergy.App.SaliMax.SaliMaxRelays;
public class RelaysRecordAmax : IRelaysRecord
{
private readonly Amax5070Registers _Regs;
private RelaysRecordAmax(Amax5070Registers regs) => _Regs = regs;
public Boolean K1GridBusIsConnectedToGrid => _Regs.DigitalInput22;
public Boolean K2IslandBusIsConnectedToGridBus => !_Regs.DigitalInput20;
public Boolean Inverter1WagoStatus => _Regs.DigitalInput0;
public Boolean Inverter2WagoStatus => _Regs.DigitalInput1;
public Boolean Inverter3WagoStatus => _Regs.DigitalInput2;
public Boolean Inverter4WagoStatus => _Regs.DigitalInput3;
public Boolean Dc1WagoStatus => _Regs.DigitalInput6;
public Boolean Dc2WagoStatus => _Regs.DigitalInput7;
public Boolean Dc3WagoStatus => _Regs.DigitalInput10;
public Boolean Dc4WagoStatus => _Regs.DigitalInput11;
public Boolean DcSystemControlWagoStatus => _Regs.DigitalInput9;
public Boolean LedGreen
{
get => _Regs.DigitalOutput0;
set => _Regs.DigitalOutput0 = value;
}
public Boolean LedRed => _Regs.DigitalOutput1;
public Boolean Harvester1Step => _Regs.DigitalOutput2;
public Boolean Harvester2Step => _Regs.DigitalOutput3;
public Boolean Harvester3Step => _Regs.DigitalOutput4;
public Boolean Harvester4Step => _Regs.DigitalOutput5;
public Boolean Do0StartPulse { get; set; }
public Boolean Do1StartPulse { get; set; }
public Boolean Do2StartPulse { get; set; }
public Boolean Do3StartPulse { get; set; }
public Boolean Do4StartPulse { get; set; }
public Boolean Do5StartPulse { get; set; }
public UInt16 DigitalOutput0Mode { get; set; }
public UInt16 DigitalOutput1Mode { get; set; }
public UInt16 DigitalOutput2Mode { get; set; }
public UInt16 DigitalOutput3Mode { get; set; }
public UInt16 DigitalOutput4Mode { get; set; }
public UInt16 DigitalOutput5Mode { get; set; }
public UInt16 PulseOut0LowTime { get; set; }
public UInt16 PulseOut1LowTime { get; set; }
public UInt16 PulseOut2LowTime { get; set; }
public UInt16 PulseOut3LowTime { get; set; }
public UInt16 PulseOut4LowTime { get; set; }
public UInt16 PulseOut5LowTime { get; set; }
public UInt16 PulseOut0HighTime { get; set; }
public UInt16 PulseOut1HighTime { get; set; }
public UInt16 PulseOut2HighTime { get; set; }
public UInt16 PulseOut3HighTime { get; set; }
public UInt16 PulseOut4HighTime { get; set; }
public UInt16 PulseOut5HighTime { get; set; }
public void PerformSolidGreenLed()
{
Console.WriteLine("Solid Green: This is not yet implemented ");
}
public void PerformSlowFlashingGreenLed()
{
Console.WriteLine("Slow Flashing Green: This is not yet implemented ");
}
public void PerformFastFlashingGreenLed()
{
Console.WriteLine("Fast Flashing Green: This is not yet implemented ");
}
public void PerformSolidOrangeLed()
{
Console.WriteLine("Solid Orange: This is not yet implemented ");
}
public void PerformSlowFlashingOrangeLed()
{
Console.WriteLine("Slow Flashing Orange: This is not yet implemented ");
}
public void PerformFastFlashingOrangeLed()
{
Console.WriteLine("Fast Flashing Orange: This is not yet implemented ");
}
public void PerformSolidRedLed()
{
Console.WriteLine("Solid Red: This is not yet implemented ");
}
public void PerformSlowFlashingRedLed()
{
Console.WriteLine("Slow Flashing Red: This is not yet implemented ");
}
public void PerformFastFlashingRedLed()
{
Console.WriteLine("Fast Flashing Red: This is not yet implemented ");
}
public IEnumerable<Boolean> K3InverterIsConnectedToIslandBus
{
get
{
yield return K3Inverter1IsConnectedToIslandBus;
yield return K3Inverter2IsConnectedToIslandBus;
yield return K3Inverter3IsConnectedToIslandBus;
yield return K3Inverter4IsConnectedToIslandBus;
}
}
private Boolean K3Inverter1IsConnectedToIslandBus => !_Regs.DigitalInput16;
private Boolean K3Inverter2IsConnectedToIslandBus => !_Regs.DigitalInput17;
private Boolean K3Inverter3IsConnectedToIslandBus => !_Regs.DigitalInput18;
private Boolean K3Inverter4IsConnectedToIslandBus => !_Regs.DigitalInput19;
public Boolean FiWarning => !_Regs.DigitalInput21;
public Boolean FiError => !_Regs.DigitalInput23;
public Boolean K2ConnectIslandBusToGridBus
{
get => _Regs.Relay23;
set => _Regs.Relay23 = value;
}
public static implicit operator Amax5070Registers(RelaysRecordAmax d) => d._Regs;
public static implicit operator RelaysRecordAmax(Amax5070Registers d) => new RelaysRecordAmax(d);
}

View File

@ -13,10 +13,11 @@ import hmac
import hashlib import hashlib
from threading import Thread, Event from threading import Thread, Event
import config as cfg import config as cfg
import json
CSV_DIR = "/data/csv_files/" JSON_DIR = "/data/json_files/"
HOURLY_DIR = "/data/csv_files/HourlyData" HOURLY_DIR = "/data/json_files/HourlyData"
DAILY_DIR = "/data/csv_files/DailyData" DAILY_DIR = "/data/json_files/DailyData"
# S3 Credentials # S3 Credentials
print("Start with the correct credentials") print("Start with the correct credentials")
@ -38,23 +39,24 @@ class AggregatedData:
self.charging_battery_power = charging_battery_power self.charging_battery_power = charging_battery_power
self.heating_power = heating_power self.heating_power = heating_power
def to_csv(self): def to_json(self):
return ("/MinSoc;{};\n" return json.dumps({
"/MaxSoc;{};\n" "MinSoc": self.min_soc,
"/DischargingBatteryPower;{};\n" "MaxSoc": self.max_soc,
"/ChargingBatteryPower;{};\n" "DischargingBatteryPower": self.discharging_battery_power,
"/HeatingPower;{};").format( "ChargingBatteryPower": self.charging_battery_power,
self.min_soc, self.max_soc, self.discharging_battery_power, self.charging_battery_power, self.heating_power) "HeatingPower": self.heating_power
}, separators=(',', ':'))
def save(self, directory): def save(self, directory):
timestamp = int(time.time()) timestamp = int(time.time())
if not os.path.exists(directory): if not os.path.exists(directory):
os.makedirs(directory) os.makedirs(directory)
csv_path = os.path.join(directory, "{}.csv".format(timestamp)) json_path = os.path.join(directory, "{}.json".format(timestamp))
with open(csv_path, 'w') as file: with open(json_path, 'w') as file:
file.write(self.to_csv()) file.write(self.to_json())
print("Saved file to:", csv_path) print("Saved file to:", json_path)
print("File content:\n", self.to_csv()) print("File content:\n", self.to_json())
@staticmethod @staticmethod
def delete_data(directory): def delete_data(directory):
@ -67,16 +69,16 @@ class AggregatedData:
print("Deleted file: {}".format(file_path)) print("Deleted file: {}".format(file_path))
def push_to_s3(self, s3_config): def push_to_s3(self, s3_config):
csv_data = self.to_csv() json_data = self.to_json()
compressed_csv = self.compress_csv_data(csv_data) compressed_json = self.compress_json_data(json_data)
now = datetime.now() now = datetime.now()
if now.hour == 0 and now.minute < 30: if now.hour == 0 and now.minute < 30:
adjusted_date = now - timedelta(days=1) adjusted_date = now - timedelta(days=1)
else: else:
adjusted_date = now adjusted_date = now
s3_path = adjusted_date.strftime("%Y-%m-%d") + ".csv" s3_path = adjusted_date.strftime("%Y-%m-%d") + ".json"
response = s3_config.create_put_request(s3_path, compressed_csv) response = s3_config.create_put_request(s3_path, compressed_json)
if response.status_code != 200: if response.status_code != 200:
print("ERROR: PUT", response.text) print("ERROR: PUT", response.text)
return False return False
@ -84,10 +86,10 @@ class AggregatedData:
return True return True
@staticmethod @staticmethod
def compress_csv_data(csv_data): def compress_json_data(json_data):
memory_stream = io.BytesIO() memory_stream = io.BytesIO()
with zipfile.ZipFile(memory_stream, 'w', zipfile.ZIP_DEFLATED) as archive: with zipfile.ZipFile(memory_stream, 'w', zipfile.ZIP_DEFLATED) as archive:
archive.writestr("data.csv", csv_data.encode('utf-8')) archive.writestr("data.json", json_data.encode('utf-8'))
compressed_bytes = memory_stream.getvalue() compressed_bytes = memory_stream.getvalue()
return base64.b64encode(compressed_bytes).decode('utf-8') return base64.b64encode(compressed_bytes).decode('utf-8')
@ -150,7 +152,7 @@ class Aggregator:
current_time = datetime.now() current_time = datetime.now()
after_timestamp = datetime_to_timestamp(current_time - timedelta(hours=1)) after_timestamp = datetime_to_timestamp(current_time - timedelta(hours=1))
before_timestamp = datetime_to_timestamp(current_time) before_timestamp = datetime_to_timestamp(current_time)
aggregated_data = Aggregator.create_hourly_data(CSV_DIR, after_timestamp, before_timestamp) aggregated_data = Aggregator.create_hourly_data(JSON_DIR, after_timestamp, before_timestamp)
print("Saving in hourly directory") print("Saving in hourly directory")
aggregated_data.save(HOURLY_DIR) aggregated_data.save(HOURLY_DIR)
except Exception as e: except Exception as e:
@ -195,31 +197,55 @@ class Aggregator:
@staticmethod @staticmethod
def create_hourly_data(directory, after_timestamp, before_timestamp): def create_hourly_data(directory, after_timestamp, before_timestamp):
node_data = {} node_data = {}
print("INSIDE HOURLY MANAGER")
for filename in os.listdir(directory): for filename in os.listdir(directory):
file_path = os.path.join(directory, filename) file_path = os.path.join(directory, filename)
if os.path.isfile(file_path) and Aggregator.is_file_within_time_range(filename, after_timestamp, before_timestamp): if os.path.isfile(file_path) and Aggregator.is_file_within_time_range(filename, after_timestamp, before_timestamp):
with open(file_path, 'r') as file: with open(file_path, 'r') as file:
reader = csv.reader(file, delimiter=';')
for row in reader: data = json.load(file)
if len(row) >= 2: devices = data.get("Battery", {}).get("Devices", {})
variable_name, value = row[0], row[1]
try: for node_number, device_data in devices.items():
value = float(value)
node_number = Aggregator.extract_node_number(variable_name)
if node_number not in node_data: if node_number not in node_data:
node_data[node_number] = {'soc': [], 'discharge': [], 'charge': [], 'heating': []} node_data[node_number] = {'soc': [], 'discharge': [], 'charge': [], 'heating': []}
if "Soc" in variable_name:
node_data[node_number]['soc'].append(value) value = device_data.get("Soc", {}).get("value", "N/A")
elif "/Dc/Power" in variable_name or "/DischargingBatteryPower" in variable_name or "/ChargingBatteryPower" in variable_name: node_data[node_number]['soc'].append(float(value))
value = device_data.get("Dc", {}).get("Power", "N/A").get("value", "N/A")
value = float(value)
if value < 0: if value < 0:
node_data[node_number]['discharge'].append(value) node_data[node_number]['discharge'].append(value)
else: else:
node_data[node_number]['charge'].append(value) node_data[node_number]['charge'].append(value)
elif "/HeatingPower" in variable_name: value = device_data.get("HeatingPower", "N/A").get("value", "N/A")
value = float(value)
node_data[node_number]['heating'].append(value) node_data[node_number]['heating'].append(value)
except ValueError:
pass
# reader = csv.reader(file, delimiter=';')
# for row in reader:
# if len(row) >= 2:
# variable_name, value = row[0], row[1]
# try:
# value = float(value)
# node_number = Aggregator.extract_node_number(variable_name)
# if node_number not in node_data:
# node_data[node_number] = {'soc': [], 'discharge': [], 'charge': [], 'heating': []}
# if "Soc" in variable_name:
# node_data[node_number]['soc'].append(value)
# elif "/Dc/Power" in variable_name or "/DischargingBatteryPower" in variable_name or "/ChargingBatteryPower" in variable_name:
# if value < 0:
# node_data[node_number]['discharge'].append(value)
# else:
# node_data[node_number]['charge'].append(value)
# elif "/HeatingPower" in variable_name:
# node_data[node_number]['heating'].append(value)
# except ValueError:
# pass
if len(node_data) == 0: if len(node_data) == 0:
# No data collected, return default AggregatedData with zeros # No data collected, return default AggregatedData with zeros
@ -249,7 +275,45 @@ class Aggregator:
@staticmethod @staticmethod
def create_daily_data(directory, after_timestamp, before_timestamp): def create_daily_data(directory, after_timestamp, before_timestamp):
return Aggregator.create_hourly_data(directory, after_timestamp, before_timestamp)
node_data = {'MinSoc': [], 'MaxSoc': [], 'ChargingBatteryPower': [], 'DischargingBatteryPower': [],
'HeatingPower': []}
for filename in os.listdir(directory):
file_path = os.path.join(directory, filename)
if os.path.isfile(file_path) and Aggregator.is_file_within_time_range(filename, after_timestamp,
before_timestamp):
with open(file_path, 'r') as file:
data = json.load(file)
value = data.get("MinSoc", "N/A")
node_data['MinSoc'].append(float(value))
value = data.get("MaxSoc", "N/A")
node_data['MaxSoc'].append(float(value))
value = data.get("ChargingBatteryPower", "N/A")
node_data['ChargingBatteryPower'].append(float(value))
value = data.get("DischargingBatteryPower", "N/A")
node_data['DischargingBatteryPower'].append(float(value))
value = data.get("HeatingPower", "N/A")
node_data['HeatingPower'].append(float(value))
print(node_data)
min_soc = min(node_data['MinSoc']) if node_data else 0.0
max_soc = max(node_data['MaxSoc']) if node_data else 0.0
total_discharging_power = sum(node_data['DischargingBatteryPower']) if node_data else 0.0
total_charging_power = sum(node_data['ChargingBatteryPower']) if node_data else 0.0
total_heating_power = sum(node_data['HeatingPower']) if node_data else 0.0
avg_discharging_power = total_discharging_power / len(node_data['DischargingBatteryPower'])
avg_charging_power = total_charging_power / len(node_data['ChargingBatteryPower'])
avg_heating_power = total_heating_power / len(node_data['HeatingPower'])
return AggregatedData(min_soc, max_soc, avg_discharging_power, avg_charging_power, avg_heating_power)
@staticmethod @staticmethod
def is_file_within_time_range(filename, start_time, end_time): def is_file_within_time_range(filename, start_time, end_time):

View File

@ -23,21 +23,10 @@ from os import path
app_dir = path.dirname(path.realpath(__file__)) app_dir = path.dirname(path.realpath(__file__))
sys.path.insert(1, path.join(app_dir, 'ext', 'velib_python')) sys.path.insert(1, path.join(app_dir, 'ext', 'velib_python'))
from vedbus import VeDbusService as DBus from vedbus import VeDbusService as DBus
import time
import os import os
import csv import csv
import requests
import hmac
import hashlib
import base64
from datetime import datetime
import io
import json import json
import requests import requests
import hmac import hmac
import hashlib import hashlib
@ -45,22 +34,18 @@ import base64
from datetime import datetime from datetime import datetime
import pika import pika
import time import time
# zip-comp additions # zip-comp additions
import zipfile import zipfile
import io import io
import shutil
def compress_csv_data(csv_data, file_name="data.csv"): def compress_json_data(json_data, file_name="data.json"):
memory_stream = io.BytesIO() memory_stream = io.BytesIO()
# Create a zip archive in the memory buffer # Create a zip archive in the memory buffer
with zipfile.ZipFile(memory_stream, 'w', zipfile.ZIP_DEFLATED) as archive: with zipfile.ZipFile(memory_stream, 'w', zipfile.ZIP_DEFLATED) as archive:
# Add CSV data to the ZIP archive # Add JSON data to the ZIP archive
with archive.open('data.csv', 'w') as entry_stream: with archive.open('data.json', 'w') as entry_stream:
entry_stream.write(csv_data.encode('utf-8')) entry_stream.write(json_data.encode('utf-8'))
# Get the compressed byte array from the memory buffer # Get the compressed byte array from the memory buffer
compressed_bytes = memory_stream.getvalue() compressed_bytes = memory_stream.getvalue()
@ -112,9 +97,9 @@ class S3config:
).decode() ).decode()
return f"AWS {s3_key}:{signature}" return f"AWS {s3_key}:{signature}"
def read_csv_as_string(file_path): def read_json_as_string(file_path):
""" """
Reads a CSV file from the given path and returns its content as a single string. Reads a JSON file from the given path and returns its content as a single string.
""" """
try: try:
with open(file_path, 'r', encoding='utf-8') as file: with open(file_path, 'r', encoding='utf-8') as file:
@ -126,8 +111,7 @@ def read_csv_as_string(file_path):
print(f"IO error occurred: {str(e)}") print(f"IO error occurred: {str(e)}")
return None return None
CSV_DIR = "/data/csv_files/" JSON_DIR = "/data/json_files/"
#CSV_DIR = "csv_files/"
# Define the path to the file containing the installation name # Define the path to the file containing the installation name
INSTALLATION_NAME_FILE = '/data/innovenergy/openvpn/installation-name' INSTALLATION_NAME_FILE = '/data/innovenergy/openvpn/installation-name'
@ -645,7 +629,7 @@ def read_battery_status(modbus, battery):
return BatteryStatus(battery, data.registers) return BatteryStatus(battery, data.registers)
except Exception as e: except Exception as e:
logging.error(f"An error occurred: {e}") logging.error(f"An error occurred: {e}")
create_batch_of_csv_files() # Call this only if there's an error create_batch_of_json_files() # Call this only if there's an error
raise raise
finally: finally:
modbus.close() # close in any case modbus.close() # close in any case
@ -659,7 +643,7 @@ def publish_values(dbus, signals, statuses):
previous_warnings = {} previous_warnings = {}
previous_alarms = {} previous_alarms = {}
num_of_csv_files_saved=0 num_of_json_files_saved=0
class MessageType: class MessageType:
ALARM_OR_WARNING = "AlarmOrWarning" ALARM_OR_WARNING = "AlarmOrWarning"
@ -878,7 +862,8 @@ def update(modbus, batteries, dbus, signals, csv_signals):
status_message, alarms_number_list, warnings_number_list = update_state_from_dictionaries(current_warnings, current_alarms, node_numbers) status_message, alarms_number_list, warnings_number_list = update_state_from_dictionaries(current_warnings, current_alarms, node_numbers)
publish_values(dbus, signals, statuses) publish_values(dbus, signals, statuses)
create_csv_files(csv_signals, statuses, node_numbers, alarms_number_list, warnings_number_list)
create_json_files(csv_signals, statuses, node_numbers, alarms_number_list, warnings_number_list)
logging.debug('finished update cycle\n') logging.debug('finished update cycle\n')
return True return True
@ -907,59 +892,59 @@ def count_files_in_folder(folder_path):
except Exception as e: except Exception as e:
return str(e) return str(e)
def create_batch_of_csv_files(): def create_batch_of_json_files():
global prev_status,INSTALLATION_ID, PRODUCT_ID, num_of_csv_files_saved global prev_status,INSTALLATION_ID, PRODUCT_ID, num_of_json_files_saved
# list all files in the directory # list all files in the directory
files = os.listdir(CSV_DIR) files = os.listdir(JSON_DIR)
# filter out only csv files # filter out only json files
csv_files = [file for file in files if file.endswith('.csv')] json_files = [file for file in files if file.endswith('.json')]
# sort csv files by creation time # sort json files by creation time
csv_files.sort(key=lambda x: os.path.getctime(os.path.join(CSV_DIR, x))) json_files.sort(key=lambda x: os.path.getctime(os.path.join(JSON_DIR, x)))
# keep the 600 MOST RECENT FILES # keep the 600 MOST RECENT FILES
recent_csv_files = csv_files[-num_of_csv_files_saved:] recent_json_files = json_files[-num_of_json_files_saved:]
print("num_of_csv_files_saved is " + str(num_of_csv_files_saved)) print("num_of_json_files_saved is " + str(num_of_json_files_saved))
# get the name of the first csv file # get the name of the first json file
if not csv_files: if not json_files:
print("No csv files found in the directory.") print("No json files found in the directory.")
exit(0) exit(0)
first_csv_file = os.path.join(CSV_DIR, recent_csv_files.pop(0)) first_json_file = os.path.join(JSON_DIR, recent_json_files.pop(0))
first_csv_filename = os.path.basename(first_csv_file) first_json_filename = os.path.basename(first_json_file)
temp_file_path = os.path.join(CSV_DIR, 'temp_batch_file.csv') temp_file_path = os.path.join(JSON_DIR, 'temp_batch_file.json')
# create a temporary file and write the timestamp and the original content of the first file # create a temporary file and write the timestamp and the original content of the first file
with open(temp_file_path, 'wb') as temp_file: with open(temp_file_path, 'wb') as temp_file:
# Write the timestamp (filename) at the beginning # Write the timestamp (filename) at the beginning
numeric_part = first_csv_filename.split('.')[0] numeric_part = first_json_filename.split('.')[0]
temp_file.write(f'Timestamp;{numeric_part}\n'.encode('utf-8')) temp_file.write(f'Timestamp;{numeric_part}\n'.encode('utf-8'))
# write the original content of the first csv file # write the original content of the first csv file
with open(first_csv_file, 'rb') as f: with open(first_json_file, 'rb') as f:
temp_file.write(f.read()) temp_file.write(f.read())
for csv_file in recent_csv_files: for json_file in recent_json_files:
file_path = os.path.join(CSV_DIR, csv_file) file_path = os.path.join(JSON_DIR, json_file)
# write an empty line # write an empty line
temp_file.write(b'\n') temp_file.write(b'\n')
# write the timestamp (filename) # write the timestamp (filename)
numeric_part = csv_file.split('.')[0] numeric_part = json_file.split('.')[0]
temp_file.write(f'Timestamp;{numeric_part}\n'.encode('utf-8')) temp_file.write(f'Timestamp;{numeric_part}\n'.encode('utf-8'))
# write the content of the file # write the content of the file
with open(file_path, 'rb') as f: with open(file_path, 'rb') as f:
temp_file.write(f.read()) temp_file.write(f.read())
# replace the original first csv file with the temporary file # replace the original first json file with the temporary file
os.remove(first_csv_file) os.remove(first_json_file)
os.rename(temp_file_path, first_csv_file) os.rename(temp_file_path, first_json_file)
num_of_csv_files_saved = 0 num_of_json_files_saved = 0
# create a loggin directory that contains at max 20 batch files for logging info # create a loggin directory that contains at max 20 batch files for logging info
# logging_dir = os.path.join(CSV_DIR, 'logging_batch_files') # logging_dir = os.path.join(JSON_DIR, 'logging_batch_files')
# if not os.path.exists(logging_dir): # if not os.path.exists(logging_dir):
# os.makedirs(logging_dir) # os.makedirs(logging_dir)
# #
@ -967,23 +952,23 @@ def create_batch_of_csv_files():
# manage_csv_files(logging_dir) # manage_csv_files(logging_dir)
# prepare for compression # prepare for compression
csv_data = read_csv_as_string(first_csv_file) json_data = read_json_as_string(first_json_file)
if csv_data is None: if json_data is None:
print("error while reading csv as string") print("error while reading json as string")
return return
# zip-comp additions # zip-comp additions
compressed_csv = compress_csv_data(csv_data) compressed_json = compress_json_data(json_data)
# Use the name of the last (most recent) CSV file in sorted csv_files as the name for the compressed file # Use the name of the last (most recent) JSON file in sorted json_files as the name for the compressed file
last_csv_file_name = os.path.basename(recent_csv_files[-1]) if recent_csv_files else first_csv_filename last_json_file_name = os.path.basename(recent_json_files[-1]) if recent_json_files else first_json_filename
numeric_part = int(last_csv_file_name.split('.')[0][:-2]) numeric_part = int(last_json_file_name.split('.')[0][:-2])
compressed_filename = "{}.csv".format(numeric_part) compressed_filename = "{}.json".format(numeric_part)
response = s3_config.create_put_request(compressed_filename, compressed_csv) response = s3_config.create_put_request(compressed_filename, compressed_json)
if response.status_code == 200: if response.status_code == 200:
os.remove(first_csv_file) os.remove(first_json_file)
print("Successfully uploaded the compresseed batch of files in s3") print("Successfully uploaded the compresseed batch of files in s3")
status_message = { status_message = {
"InstallationId": INSTALLATION_ID, "InstallationId": INSTALLATION_ID,
@ -1001,14 +986,14 @@ def create_batch_of_csv_files():
channel.basic_publish(exchange="", routing_key="statusQueue", body=status_message) channel.basic_publish(exchange="", routing_key="statusQueue", body=status_message)
print("Successfully sent the heartbit with timestamp") print("Successfully sent the heartbit with timestamp")
else: else:
# we save data that were not successfully uploaded in s3 in a failed directory inside the CSV_DIR for logging # we save data that were not successfully uploaded in s3 in a failed directory inside the JSON_DIR for logging
failed_dir = os.path.join(CSV_DIR, "failed") failed_dir = os.path.join(JSON_DIR, "failed")
if not os.path.exists(failed_dir): if not os.path.exists(failed_dir):
os.makedirs(failed_dir) os.makedirs(failed_dir)
failed_path = os.path.join(failed_dir, first_csv_filename) failed_path = os.path.join(failed_dir, first_json_filename)
os.rename(first_csv_file, failed_path) os.rename(first_json_file, failed_path)
print("Uploading failed") print("Uploading failed")
manage_csv_files(failed_dir, 100) manage_json_files(failed_dir, 100)
alive = True # global alive flag, watchdog_task clears it, update_task sets it alive = True # global alive flag, watchdog_task clears it, update_task sets it
@ -1033,11 +1018,11 @@ def create_update_task(modbus, dbus, batteries, signals, csv_signals, main_loop)
elapsed_time = time.time() - start_time elapsed_time = time.time() - start_time
print("11111111111111111111111111111111111111111111 elapsed time is ", elapsed_time) print("11111111111111111111111111111111111111111111 elapsed time is ", elapsed_time)
# keep at most 1900 files at CSV_DIR for logging and aggregation # keep at most 1900 files at JSON_DIR for logging and aggregation
manage_csv_files(CSV_DIR, 1900) manage_json_files(JSON_DIR, 1900)
if elapsed_time >= 1200: if elapsed_time >= 1200:
print("CREATE BATCH ======================================>") print("CREATE BATCH ======================================>")
create_batch_of_csv_files() create_batch_of_json_files()
start_time = time.time() start_time = time.time()
#alive = update_for_testing(modbus, batteries, dbus, signals, csv_signals) #alive = update_for_testing(modbus, batteries, dbus, signals, csv_signals)
if not alive: if not alive:
@ -1070,12 +1055,12 @@ def get_installation_name(file_path):
with open(file_path, 'r') as file: with open(file_path, 'r') as file:
return file.read().strip() return file.read().strip()
def manage_csv_files(directory_path, max_files=20): def manage_json_files(directory_path, max_files=20):
csv_files = [f for f in os.listdir(directory_path) if os.path.isfile(os.path.join(directory_path, f))] json_files = [f for f in os.listdir(directory_path) if os.path.isfile(os.path.join(directory_path, f))]
csv_files.sort(key=lambda x: os.path.getctime(os.path.join(directory_path, x))) json_files.sort(key=lambda x: os.path.getctime(os.path.join(directory_path, x)))
# Remove oldest files if exceeds maximum # Remove oldest files if exceeds maximum
while len(csv_files) > max_files: while len(json_files) > max_files:
file_to_delete = os.path.join(directory_path, csv_files.pop(0)) file_to_delete = os.path.join(directory_path, json_files.pop(0))
os.remove(file_to_delete) os.remove(file_to_delete)
def insert_id(path, id_number): def insert_id(path, id_number):
@ -1084,36 +1069,102 @@ def insert_id(path, id_number):
parts.insert(insert_position, str(id_number)) parts.insert(insert_position, str(id_number))
return "/".join(parts) return "/".join(parts)
def create_csv_files(signals, statuses, node_numbers, alarms_number_list, warnings_number_list): def insert_nested_data(data, split_list, value, symbol):
global s3_config, num_of_csv_files_saved key = split_list[0] # Get the first key in the list
if len(split_list) == 1:
data[key] = {
"value": round(value, 2) if isinstance(value, float) else value,
#"symbol": str(symbol)
}
else:
if key not in data:
data[key] = {} # Create a new dictionary if key doesn't exist
insert_nested_data(data[key], split_list[1:], value, symbol)
def create_json_files(signals, statuses, node_numbers, alarms_number_list, warnings_number_list):
global num_of_json_files_saved
timestamp = int(time.time()) timestamp = int(time.time())
if timestamp % 2 != 0: if timestamp % 2 != 0:
timestamp -= 1 timestamp -= 1
# Create CSV directory if it doesn't exist if not os.path.exists(JSON_DIR):
if not os.path.exists(CSV_DIR): os.makedirs(JSON_DIR)
os.makedirs(CSV_DIR) json_filename = "{}.json".format(timestamp)
csv_filename = f"{timestamp}.csv" json_path = os.path.join(JSON_DIR, json_filename)
csv_path = os.path.join(CSV_DIR, csv_filename) num_of_json_files_saved += 1
num_of_csv_files_saved+=1
# Append values to the CSV file data = {
if not os.path.exists(csv_path): "Battery": {
with open(csv_path, 'a', newline='') as csvfile: "Devices": {}
csv_writer = csv.writer(csvfile, delimiter=';') }
# Add a special row for the nodes configuration }
nodes_config_path = "/Config/Devices/BatteryNodes"
nodes_list = ",".join(str(node) for node in node_numbers) # Iterate over each node and construct the data structure
config_row = [nodes_config_path, nodes_list, ""]
csv_writer.writerow(config_row)
# Iterate over each node and signal to create rows in the new format
for i, node in enumerate(node_numbers): for i, node in enumerate(node_numbers):
csv_writer.writerow([f"/Battery/Devices/{str(i+1)}/Alarms", alarms_number_list[i], ""]) print("Inside json generation file, node num is", i, " and node is ", node)
csv_writer.writerow([f"/Battery/Devices/{str(i+1)}/Warnings", warnings_number_list[i], ""]) device_data = {} # This dictionary will hold the data for a specific device
# Add Alarms and Warnings for this device
device_data["Alarms"] = alarms_number_list[i]
device_data["Warnings"] = warnings_number_list[i]
# Iterate over the signals and add their values
for s in signals: for s in signals:
signal_name = insert_id(s.name, i+1) split_list = s.name.split("/")[3:]
# print(split_list)
value = s.get_value(statuses[i]) value = s.get_value(statuses[i])
row_values = [signal_name, value, s.get_text] symbol = s.get_text
csv_writer.writerow(row_values) insert_nested_data(device_data, split_list, value, symbol)
# print(device_data)
# Add this device's data to the "Devices" section
data["Battery"]["Devices"][str(i + 1)] = device_data
# Add the node configuration row (optional)
nodes_config_path = "/Config/Devices/BatteryNodes"
nodes_list = [str(node) for node in node_numbers]
insert_nested_data(data, nodes_config_path.split("/")[1:], nodes_list, "")
# data[nodes_config_path] = nodes_list
# print(json.dumps(data, indent=4))
# Write the JSON data to the file
with open(json_path, 'w') as jsonfile:
# json.dump(data, jsonfile, indent=4)
json.dump(data, jsonfile, separators=(',', ':'))
#
# def create_csv_files(signals, statuses, node_numbers, alarms_number_list, warnings_number_list):
# global s3_config, num_of_csv_files_saved
# timestamp = int(time.time())
# if timestamp % 2 != 0:
# timestamp -= 1
# # Create CSV directory if it doesn't exist
# if not os.path.exists(JSON_DIR):
# os.makedirs(JSON_DIR)
# csv_filename = f"{timestamp}.csv"
# csv_path = os.path.join(JSON_DIR, csv_filename)
# num_of_csv_files_saved+=1
#
# # Append values to the CSV file
# if not os.path.exists(csv_path):
# with open(csv_path, 'a', newline='') as csvfile:
# csv_writer = csv.writer(csvfile, delimiter=';')
# # Add a special row for the nodes configuration
# nodes_config_path = "/Config/Devices/BatteryNodes"
# nodes_list = ",".join(str(node) for node in node_numbers)
# config_row = [nodes_config_path, nodes_list, ""]
# csv_writer.writerow(config_row)
# # Iterate over each node and signal to create rows in the new format
# for i, node in enumerate(node_numbers):
# csv_writer.writerow([f"/Battery/Devices/{str(i+1)}/Alarms", alarms_number_list[i], ""])
# csv_writer.writerow([f"/Battery/Devices/{str(i+1)}/Warnings", warnings_number_list[i], ""])
# for s in signals:
# signal_name = insert_id(s.name, i+1)
# value = s.get_value(statuses[i])
# row_values = [signal_name, value, s.get_text]
# csv_writer.writerow(row_values)
BATTERY_COUNTS_FILE = '/data/battery_count.csv' BATTERY_COUNTS_FILE = '/data/battery_count.csv'
def load_battery_counts(): def load_battery_counts():

View File

@ -13,10 +13,11 @@ import hmac
import hashlib import hashlib
from threading import Thread, Event from threading import Thread, Event
import config as cfg import config as cfg
import json
CSV_DIR = "/data/csv_files/" JSON_DIR = "/data/json_files/"
HOURLY_DIR = "/data/csv_files/HourlyData" HOURLY_DIR = "/data/json_files/HourlyData"
DAILY_DIR = "/data/csv_files/DailyData" DAILY_DIR = "/data/json_files/DailyData"
print("start with the correct credentials") print("start with the correct credentials")
@ -37,23 +38,24 @@ class AggregatedData:
self.charging_battery_power = charging_battery_power self.charging_battery_power = charging_battery_power
self.heating_power = heating_power self.heating_power = heating_power
def to_csv(self): def to_json(self):
return ("/MinSoc;{};\n" return json.dumps({
"/MaxSoc;{};\n" "MinSoc": self.min_soc,
"/DischargingBatteryPower;{};\n" "MaxSoc": self.max_soc,
"/ChargingBatteryPower;{};\n" "DischargingBatteryPower": self.discharging_battery_power,
"/HeatingPower;{};").format( "ChargingBatteryPower": self.charging_battery_power,
self.min_soc, self.max_soc, self.discharging_battery_power, self.charging_battery_power, self.heating_power) "HeatingPower": self.heating_power
}, separators=(',', ':'))
def save(self, directory): def save(self, directory):
timestamp = int(time.time()) timestamp = int(time.time())
if not os.path.exists(directory): if not os.path.exists(directory):
os.makedirs(directory) os.makedirs(directory)
csv_path = os.path.join(directory, "{}.csv".format(timestamp)) json_path = os.path.join(directory, "{}.json".format(timestamp))
with open(csv_path, 'w') as file: with open(json_path, 'w') as file:
file.write(self.to_csv()) file.write(self.to_json())
print("Saved file to:", csv_path) print("Saved file to:", json_path)
print("File content:\n", self.to_csv()) print("File content:\n", self.to_json())
@staticmethod @staticmethod
def delete_data(directory): def delete_data(directory):
@ -66,16 +68,16 @@ class AggregatedData:
print("Deleted file: {}".format(file_path)) print("Deleted file: {}".format(file_path))
def push_to_s3(self, s3_config): def push_to_s3(self, s3_config):
csv_data = self.to_csv() json_data = self.to_json()
compressed_csv = self.compress_csv_data(csv_data) compressed_json = self.compress_json_data(json_data)
now = datetime.now() now = datetime.now()
if now.hour == 0 and now.minute < 30: if now.hour == 0 and now.minute < 30:
adjusted_date = now - timedelta(days=1) adjusted_date = now - timedelta(days=1)
else: else:
adjusted_date = now adjusted_date = now
s3_path = adjusted_date.strftime("%Y-%m-%d") + ".csv" s3_path = adjusted_date.strftime("%Y-%m-%d") + ".json"
response = s3_config.create_put_request(s3_path, compressed_csv) response = s3_config.create_put_request(s3_path, compressed_json)
if response.status_code != 200: if response.status_code != 200:
print("ERROR: PUT", response.text) print("ERROR: PUT", response.text)
return False return False
@ -83,10 +85,10 @@ class AggregatedData:
return True return True
@staticmethod @staticmethod
def compress_csv_data(csv_data): def compress_json_data(json_data):
memory_stream = io.BytesIO() memory_stream = io.BytesIO()
with zipfile.ZipFile(memory_stream, 'w', zipfile.ZIP_DEFLATED) as archive: with zipfile.ZipFile(memory_stream, 'w', zipfile.ZIP_DEFLATED) as archive:
archive.writestr("data.csv", csv_data.encode('utf-8')) archive.writestr("data.json", json_data.encode('utf-8'))
compressed_bytes = memory_stream.getvalue() compressed_bytes = memory_stream.getvalue()
return base64.b64encode(compressed_bytes).decode('utf-8') return base64.b64encode(compressed_bytes).decode('utf-8')
@ -152,7 +154,7 @@ class Aggregator:
current_time = datetime.now() current_time = datetime.now()
after_timestamp = datetime_to_timestamp(current_time - timedelta(hours=1)) after_timestamp = datetime_to_timestamp(current_time - timedelta(hours=1))
before_timestamp = datetime_to_timestamp(current_time) before_timestamp = datetime_to_timestamp(current_time)
aggregated_data = Aggregator.create_hourly_data(CSV_DIR, after_timestamp, before_timestamp) aggregated_data = Aggregator.create_hourly_data(JSON_DIR, after_timestamp, before_timestamp)
print("save in hourly dir") print("save in hourly dir")
aggregated_data.save(HOURLY_DIR) aggregated_data.save(HOURLY_DIR)
except Exception as e: except Exception as e:
@ -205,26 +207,49 @@ class Aggregator:
file_path = os.path.join(directory, filename) file_path = os.path.join(directory, filename)
if os.path.isfile(file_path) and Aggregator.is_file_within_time_range(filename, after_timestamp, before_timestamp): if os.path.isfile(file_path) and Aggregator.is_file_within_time_range(filename, after_timestamp, before_timestamp):
with open(file_path, 'r') as file: with open(file_path, 'r') as file:
reader = csv.reader(file, delimiter=';') data = json.load(file)
for row in reader: devices = data.get("Battery", {}).get("Devices", {})
if len(row) >= 2:
variable_name, value = row[0], row[1] for node_number, device_data in devices.items():
try:
value = float(value)
node_number = Aggregator.extract_node_number(variable_name)
if node_number not in node_data: if node_number not in node_data:
node_data[node_number] = {'soc': [], 'discharge': [], 'charge': [], 'heating': []} node_data[node_number] = {'soc': [], 'discharge': [], 'charge': [], 'heating': []}
if "Soc" in variable_name:
node_data[node_number]['soc'].append(value) value = device_data.get("Soc", {}).get("value", "N/A")
elif "/Dc/Power" in variable_name or "/DischargingBatteryPower" in variable_name or "/ChargingBatteryPower" in variable_name: node_data[node_number]['soc'].append(float(value))
value = device_data.get("Dc", {}).get("Power", "N/A").get("value", "N/A")
value=float(value)
if value < 0: if value < 0:
node_data[node_number]['discharge'].append(value) node_data[node_number]['discharge'].append(value)
else: else:
node_data[node_number]['charge'].append(value) node_data[node_number]['charge'].append(value)
elif "/HeatingPower" in variable_name: value = device_data.get("HeatingPower", "N/A").get("value", "N/A")
value = float(value)
node_data[node_number]['heating'].append(value) node_data[node_number]['heating'].append(value)
except ValueError:
pass
#
# reader = csv.reader(file, delimiter=';')
# for row in reader:
# if len(row) >= 2:
# variable_name, value = row[0], row[1]
# try:
# value = float(value)
# node_number = Aggregator.extract_node_number(variable_name)
# if node_number not in node_data:
# node_data[node_number] = {'soc': [], 'discharge': [], 'charge': [], 'heating': []}
# if "Soc" in variable_name:
# node_data[node_number]['soc'].append(value)
# elif "/Dc/Power" in variable_name or "/DischargingBatteryPower" in variable_name or "/ChargingBatteryPower" in variable_name:
# if value < 0:
# node_data[node_number]['discharge'].append(value)
# else:
# node_data[node_number]['charge'].append(value)
# elif "/HeatingPower" in variable_name:
# node_data[node_number]['heating'].append(value)
# except ValueError:
# pass
if len(node_data) == 0: if len(node_data) == 0:
# No data collected, return default AggregatedData with zeros # No data collected, return default AggregatedData with zeros
@ -254,7 +279,42 @@ class Aggregator:
@staticmethod @staticmethod
def create_daily_data(directory, after_timestamp, before_timestamp): def create_daily_data(directory, after_timestamp, before_timestamp):
return Aggregator.create_hourly_data(directory, after_timestamp, before_timestamp) node_data = {'MinSoc': [], 'MaxSoc': [], 'ChargingBatteryPower': [], 'DischargingBatteryPower': [], 'HeatingPower': []}
for filename in os.listdir(directory):
file_path = os.path.join(directory, filename)
if os.path.isfile(file_path) and Aggregator.is_file_within_time_range(filename, after_timestamp,before_timestamp):
with open(file_path, 'r') as file:
data = json.load(file)
value = data.get("MinSoc", "N/A")
node_data['MinSoc'].append(float(value))
value = data.get("MaxSoc", "N/A")
node_data['MaxSoc'].append(float(value))
value = data.get("ChargingBatteryPower", "N/A")
node_data['ChargingBatteryPower'].append(float(value))
value = data.get("DischargingBatteryPower", "N/A")
node_data['DischargingBatteryPower'].append(float(value))
value = data.get("HeatingPower", "N/A")
node_data['HeatingPower'].append(float(value))
print(node_data)
min_soc = min (node_data['MinSoc']) if node_data else 0.0
max_soc = max(node_data['MaxSoc']) if node_data else 0.0
total_discharging_power = sum(node_data['DischargingBatteryPower']) if node_data else 0.0
total_charging_power = sum(node_data['ChargingBatteryPower']) if node_data else 0.0
total_heating_power = sum(node_data['HeatingPower']) if node_data else 0.0
avg_discharging_power = total_discharging_power / len(node_data['DischargingBatteryPower'])
avg_charging_power = total_charging_power / len(node_data['ChargingBatteryPower'])
avg_heating_power = total_heating_power / len(node_data['HeatingPower'])
return AggregatedData(min_soc, max_soc, avg_discharging_power, avg_charging_power, avg_heating_power)
@staticmethod @staticmethod
def is_file_within_time_range(filename, start_time, end_time): def is_file_within_time_range(filename, start_time, end_time):

View File

@ -54,6 +54,6 @@ INNOVENERGY_PROTOCOL_VERSION = '48TL200V3'
# S3 Credentials # S3 Credentials
S3BUCKET = "436-c0436b6a-d276-4cd8-9c44-1eae86cf5d0e" S3BUCKET = "357-c0436b6a-d276-4cd8-9c44-1eae86cf5d0e"
S3KEY = "EXO6bb2b06f3cebfdbbc8a9b240" S3KEY = "EXOd9cc93bec729d1bb9ad337d0"
S3SECRET = "m6bEzM8z9t2lCQ13OptMcZcNf80p_TSjaMDtZTNdEjo" S3SECRET = "Sgah7AmC7vUvnYqR_JmqZMOHpnUX3ERJZJynDUD3QdI"

View File

@ -34,7 +34,7 @@ import json
from convert import first from convert import first
import shutil import shutil
CSV_DIR = "/data/csv_files/" JSON_DIR = "/data/json_files/"
INSTALLATION_NAME_FILE = '/data/innovenergy/openvpn/installation-name' INSTALLATION_NAME_FILE = '/data/innovenergy/openvpn/installation-name'
# trick the pycharm type-checker into thinking Callable is in scope, not used at runtime # trick the pycharm type-checker into thinking Callable is in scope, not used at runtime
@ -45,13 +45,13 @@ if False:
RESET_REGISTER = 0x2087 RESET_REGISTER = 0x2087
def compress_csv_data(csv_data, file_name="data.csv"): def compress_json_data(json_data, file_name="data.json"):
memory_stream = io.BytesIO() memory_stream = io.BytesIO()
# Create a zip archive in the memory buffer # Create a zip archive in the memory buffer
with zipfile.ZipFile(memory_stream, 'w', zipfile.ZIP_DEFLATED) as archive: with zipfile.ZipFile(memory_stream, 'w', zipfile.ZIP_DEFLATED) as archive:
# Add CSV data to the ZIP archive using writestr # Add JSON data to the ZIP archive using writestr
archive.writestr(file_name, csv_data.encode('utf-8')) archive.writestr(file_name, json_data.encode('utf-8'))
# Get the compressed byte array from the memory buffer # Get the compressed byte array from the memory buffer
compressed_bytes = memory_stream.getvalue() compressed_bytes = memory_stream.getvalue()
@ -155,7 +155,7 @@ INSTALLATION_ID = int(s3_config.bucket.split('-')[0])
PRODUCT_ID = 1 PRODUCT_ID = 1
is_first_update = True is_first_update = True
prev_status = 0 prev_status = 0
num_of_csv_files_saved = 0 num_of_json_files_saved = 0
def update_state_from_dictionaries(current_warnings, current_alarms, node_numbers): def update_state_from_dictionaries(current_warnings, current_alarms, node_numbers):
@ -251,9 +251,9 @@ def update_state_from_dictionaries(current_warnings, current_alarms, node_number
return status_message, alarms_number_list, warnings_number_list return status_message, alarms_number_list, warnings_number_list
def read_csv_as_string(file_path): def read_json_as_string(file_path):
""" """
Reads a CSV file from the given path and returns its content as a single string. Reads a JSON file from the given path and returns its content as a single string.
""" """
try: try:
# Note: 'encoding' is not available in open() in Python 2.7, so we'll use 'codecs' module. # Note: 'encoding' is not available in open() in Python 2.7, so we'll use 'codecs' module.
@ -555,16 +555,17 @@ def create_update_task(modbus, service, batteries):
publish_values_on_dbus(service, _signals, statuses) publish_values_on_dbus(service, _signals, statuses)
elapsed_time = time.time() - start_time elapsed_time = time.time() - start_time
create_csv_files(csv_signals, statuses, node_numbers, alarms_number_list, warnings_number_list) #create_csv_files(csv_signals, statuses, node_numbers, alarms_number_list, warnings_number_list)
create_json_files(csv_signals, statuses, node_numbers, alarms_number_list, warnings_number_list)
print("11111111111111111111111111111111111111111111 elapsed time is ", elapsed_time) print("11111111111111111111111111111111111111111111 elapsed time is ", elapsed_time)
# keep at most 1900 files at CSV_DIR for logging and aggregation # keep at most 1900 files at JSON_DIR for logging and aggregation
manage_csv_files(CSV_DIR, 1900) manage_json_files(JSON_DIR, 1900)
num_files_in_csv_dir = count_files_in_folder(CSV_DIR) num_files_in_json_dir = count_files_in_folder(JSON_DIR)
if elapsed_time >= 1200: if elapsed_time >= 1200:
print("CREATE BATCH ======================================>") print("CREATE BATCH ======================================>")
create_batch_of_csv_files() create_batch_of_json_files()
start_time = time.time() start_time = time.time()
upload_status_to_innovenergy(_socket, statuses) upload_status_to_innovenergy(_socket, statuses)
@ -574,10 +575,10 @@ def create_update_task(modbus, service, batteries):
alive = True alive = True
except pika.exceptions.AMQPConnectionError: except pika.exceptions.AMQPConnectionError:
logging.error("AMQPConnectionError encountered. Subscribing to queue.") logging.error("AMQPConnectionError encountered. Subscribing to queue.")
create_batch_of_csv_files() create_batch_of_json_files()
except Exception as e: except Exception as e:
create_batch_of_csv_files() create_batch_of_json_files()
logging.error("Unexpected error") logging.error("Unexpected error")
raise raise
@ -587,14 +588,14 @@ def create_update_task(modbus, service, batteries):
return update_task return update_task
def manage_csv_files(directory_path, max_files=20): def manage_json_files(directory_path, max_files=20):
csv_files = [f for f in os.listdir(directory_path) if os.path.isfile(os.path.join(directory_path, f))] json_files = [f for f in os.listdir(directory_path) if os.path.isfile(os.path.join(directory_path, f))]
csv_files.sort(key=lambda x: os.path.getctime(os.path.join(directory_path, x))) json_files.sort(key=lambda x: os.path.getctime(os.path.join(directory_path, x)))
print("len of csv files is " + str(len(csv_files))) print("len of json files is " + str(len(json_files)))
# Remove oldest files if exceeds maximum # Remove oldest files if exceeds maximum
while len(csv_files) > max_files: while len(json_files) > max_files:
file_to_delete = os.path.join(directory_path, csv_files.pop(0)) file_to_delete = os.path.join(directory_path, json_files.pop(0))
os.remove(file_to_delete) os.remove(file_to_delete)
@ -605,89 +606,78 @@ def insert_id(path, id_number):
return "/".join(parts) return "/".join(parts)
def create_batch_of_csv_files(): def create_batch_of_json_files():
global prev_status, channel, INSTALLATION_ID, PRODUCT_ID, num_of_csv_files_saved global prev_status, channel, INSTALLATION_ID, PRODUCT_ID, num_of_json_files_saved
# list all files in the directory # list all files in the directory
files = os.listdir(CSV_DIR) files = os.listdir(JSON_DIR)
# filter out only csv files # filter out only json files
csv_files = [file for file in files if file.endswith('.csv')] json_files = [file for file in files if file.endswith('.json')]
# sort csv files by creation time # sort json files by creation time
csv_files.sort(key=lambda x: os.path.getctime(os.path.join(CSV_DIR, x))) json_files.sort(key=lambda x: os.path.getctime(os.path.join(JSON_DIR, x)))
# keep the num_of_csv_files_saved MOST RECENT FILES # keep the recent_json_files MOST RECENT FILES
recent_csv_files = csv_files[-num_of_csv_files_saved:] recent_json_files = json_files[-num_of_json_files_saved:]
print("num_of_csv_files_saved is " + str(num_of_csv_files_saved)) print("num_of_json_files_saved is " + str(recent_json_files))
# get the name of the first csv file # get the name of the first json file
if not csv_files: if not json_files:
print("No csv files found in the directory.") print("No json files found in the directory.")
exit(0) exit(0)
first_csv_file = os.path.join(CSV_DIR, recent_csv_files.pop(0)) first_json_file = os.path.join(JSON_DIR, recent_json_files.pop(0))
first_csv_filename = os.path.basename(first_csv_file) first_json_filename = os.path.basename(first_json_file)
temp_file_path = os.path.join(CSV_DIR, 'temp_batch_file.csv') temp_file_path = os.path.join(JSON_DIR, 'temp_batch_file.json')
# create a temporary file and write the timestamp and the original content of the first file # create a temporary file and write the timestamp and the original content of the first file
with open(temp_file_path, 'wb') as temp_file: with open(temp_file_path, 'wb') as temp_file:
# Write the timestamp (filename) at the beginning # Write the timestamp (filename) at the beginning
temp_file.write('Timestamp;{}\n'.format(first_csv_filename.split('.')[0])) temp_file.write('Timestamp;{}\n'.format(first_json_filename.split('.')[0]))
# write the original content of the first csv file # write the original content of the first json file
with open(first_csv_file, 'rb') as f: with open(first_json_file, 'rb') as f:
temp_file.write(f.read()) temp_file.write(f.read())
for csv_file in recent_csv_files: for json_file in recent_json_files:
file_path = os.path.join(CSV_DIR, csv_file) file_path = os.path.join(JSON_DIR, json_file)
# write an empty line # write an empty line
temp_file.write('\n') temp_file.write('\n')
# write the timestamp (filename) # write the timestamp (filename)
temp_file.write('Timestamp;{}\n'.format(csv_file.split('.')[0])) temp_file.write('Timestamp;{}\n'.format(json_file.split('.')[0]))
# write the content of the file # write the content of the file
with open(file_path, 'rb') as f: with open(file_path, 'rb') as f:
temp_file.write(f.read()) temp_file.write(f.read())
# replace the original first csv file with the temporary file
os.remove(first_csv_file)
os.rename(temp_file_path, first_csv_file)
num_of_csv_files_saved = 0
# create a loggin directory that contains at max 20 batch files for logging info # replace the original first json file with the temporary file
# logging_dir = os.path.join(CSV_DIR, 'logging_batch_files') os.remove(first_json_file)
# if not os.path.exists(logging_dir): os.rename(temp_file_path, first_json_file)
# os.makedirs(logging_dir) num_of_json_files_saved = 0
#
# shutil.copy(first_csv_file, logging_dir)
# manage_csv_files(logging_dir)
# print("The batch csv file is: {}".format(recent_csv_files[-1]))
# prepare for compression # prepare for compression
csv_data = read_csv_as_string(first_csv_file) json_data = read_json_as_string(first_json_file)
if csv_data is None: if json_data is None:
print("error while reading csv as string") print("error while reading json as string")
return return
# zip-comp additions # zip-comp additions
compressed_csv = compress_csv_data(csv_data) compressed_json = compress_json_data(json_data)
# Use the name of the last (most recent) CSV file in sorted csv_files as the name for the compressed file # Use the name of the last (most recent) JSON file in sorted json_files as the name for the compressed file
last_csv_file_name = os.path.basename(recent_csv_files[-1]) if recent_csv_files else first_csv_filename last_json_file_name = os.path.basename(recent_json_files[-1]) if recent_json_files else first_json_filename
# we send the csv files every 30 seconds and the timestamp is adjusted to be a multiple of 30 # we send the json files every 30 seconds and the timestamp is adjusted to be a multiple of 30
numeric_part = int(last_csv_file_name.split('.')[0][:-2]) numeric_part = int(last_json_file_name.split('.')[0][:-2])
# compressed_filename = "{}.csv".format(new_numeric_part) compressed_filename = "{}.json".format(numeric_part)
compressed_filename = "{}.csv".format(numeric_part)
print("FILE NAME =========================================================> ", compressed_filename) print("FILE NAME =========================================================> ", compressed_filename)
response = s3_config.create_put_request(compressed_filename, compressed_csv) response = s3_config.create_put_request(compressed_filename, compressed_json)
# response = s3_config.create_put_request(first_csv_filename, csv_data)
print(response) print(response)
if response.status_code == 200: if response.status_code == 200:
os.remove(first_csv_file) os.remove(first_json_file)
print("Successfully uploaded the compresseed batch of files in s3") print("Successfully uploaded the compresseed batch of files in s3")
status_message = { status_message = {
"InstallationId": INSTALLATION_ID, "InstallationId": INSTALLATION_ID,
@ -709,42 +699,80 @@ def create_batch_of_csv_files():
print("Successfully sent the heartbit with timestamp") print("Successfully sent the heartbit with timestamp")
else: else:
# we save data that were not successfully uploaded in s3 in a failed directory inside the CSV_DIR for logging #we save data that were not successfully uploaded in s3 in a failed directory inside the JSON_DIR for logging
failed_dir = os.path.join(CSV_DIR, "failed") failed_dir = os.path.join(JSON_DIR, "failed")
if not os.path.exists(failed_dir): if not os.path.exists(failed_dir):
os.makedirs(failed_dir) os.makedirs(failed_dir)
failed_path = os.path.join(failed_dir, first_csv_filename) failed_path = os.path.join(failed_dir, first_json_filename)
os.rename(first_csv_file, failed_path) os.rename(first_json_file, failed_path)
print("Uploading failed") print("Uploading failed")
manage_csv_files(failed_dir, 100) manage_json_files(failed_dir, 100)
def insert_nested_data(data, split_list, value, symbol):
key = split_list[0] # Get the first key in the list
def create_csv_files(signals, statuses, node_numbers, alarms_number_list, warnings_number_list): if len(split_list) == 1:
global num_of_csv_files_saved data[key] = {
"value": round(value, 2) if isinstance(value, float) else value,
#"symbol": str(symbol)
}
else:
if key not in data:
data[key] = {} # Create a new dictionary if key doesn't exist
insert_nested_data(data[key], split_list[1:], value, symbol)
def create_json_files(signals, statuses, node_numbers, alarms_number_list, warnings_number_list):
global num_of_json_files_saved
timestamp = int(time.time()) timestamp = int(time.time())
if timestamp % 2 != 0: if timestamp % 2 != 0:
timestamp -= 1 timestamp -= 1
if not os.path.exists(CSV_DIR): if not os.path.exists(JSON_DIR):
os.makedirs(CSV_DIR) os.makedirs(JSON_DIR)
csv_filename = "{}.csv".format(timestamp) json_filename = "{}.json".format(timestamp)
csv_path = os.path.join(CSV_DIR, csv_filename) json_path = os.path.join(JSON_DIR, json_filename)
num_of_csv_files_saved += 1 num_of_json_files_saved += 1
if not os.path.exists(csv_path): data = {
with open(csv_path, 'ab') as csvfile: "Battery": {
csv_writer = csv.writer(csvfile, delimiter=';') "Devices": {}
nodes_config_path = "/Config/Devices/BatteryNodes" }
nodes_list = ",".join(str(node) for node in node_numbers) }
config_row = [nodes_config_path, nodes_list, ""]
csv_writer.writerow(config_row) # Iterate over each node and construct the data structure
for i, node in enumerate(node_numbers): for i, node in enumerate(node_numbers):
csv_writer.writerow(["/Battery/Devices/{}/Alarms".format(str(i + 1)), alarms_number_list[i], ""]) print("Inside json generation file, node num is" , i," and node is ", node)
csv_writer.writerow(["/Battery/Devices/{}/Warnings".format(str(i + 1)), warnings_number_list[i], ""]) device_data = {} # This dictionary will hold the data for a specific device
# Add Alarms and Warnings for this device
device_data["Alarms"] = alarms_number_list[i]
device_data["Warnings"] = warnings_number_list[i]
# Iterate over the signals and add their values
for s in signals: for s in signals:
signal_name = insert_id(s.name, i + 1) split_list = s.name.split("/")[3:]
#print(split_list)
value = s.get_value(statuses[i]) value = s.get_value(statuses[i])
row_values = [signal_name, value, s.get_text] symbol=s.get_text
csv_writer.writerow(row_values) insert_nested_data(device_data, split_list, value, symbol)
#print(device_data)
# Add this device's data to the "Devices" section
data["Battery"]["Devices"][str(i + 1)] = device_data
# Add the node configuration row (optional)
nodes_config_path = "/Config/Devices/BatteryNodes"
nodes_list = [str(node) for node in node_numbers]
insert_nested_data(data,nodes_config_path.split("/")[1:], nodes_list, "")
#data[nodes_config_path] = nodes_list
# print(json.dumps(data, indent=4))
# Write the JSON data to the file
with open(json_path, 'w') as jsonfile:
#json.dump(data, jsonfile, indent=4)
json.dump(data, jsonfile, separators=(',', ':'))
def create_watchdog_task(main_loop): def create_watchdog_task(main_loop):
@ -796,14 +824,6 @@ def main(argv):
logging.basicConfig(level=cfg.LOG_LEVEL) logging.basicConfig(level=cfg.LOG_LEVEL)
logging.info('starting ' + __file__) logging.info('starting ' + __file__)
# tty = parse_cmdline_args(argv)
# modbus = init_modbus(tty)
# batteries = identify_batteries(modbus)
# if len(batteries) <= 0:
# sys.exit(2)
tty = parse_cmdline_args(argv) tty = parse_cmdline_args(argv)
battery_counts = load_battery_counts() battery_counts = load_battery_counts()
max_retry_attempts = 3 # Stop retrying in case it's a real battery loss case max_retry_attempts = 3 # Stop retrying in case it's a real battery loss case

View File

@ -0,0 +1,123 @@
#!/bin/bash
# Handle Ctrl+C to ensure a clean exit
trap "echo -e '\nScript interrupted by user. Exiting...'; kill 0; exit 1" SIGINT
username='root'
root_password='salidomo'
set -e
venus_release_file_path="./Venus_Release/VenusReleaseFiles"
cerbo_release_file_path="./Cerbo_Release/CerboReleaseFiles"
venus_ip_addresses=("10.2.0.191" "10.2.1.36" "10.2.1.108")
cerbo_ip_addresses=("10.2.2.212" "10.2.4.181" "10.2.3.198")
deploy() {
local device_type=$1
local ip_list=("${!2}")
local release_file_path=$3
echo -e "\n============================ Deploying to $device_type ============================\n"
for ip_address in "${ip_list[@]}"; do
echo "Processing $ip_address for $device_type..."
# Check if SSH is reachable within 60 seconds
if ! timeout 60 ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no "$username@$ip_address" "echo 'SSH connection successful'" &>/dev/null; then
echo "Skipping $ip_address: SSH connection failed or timed out."
continue
fi
echo "SSH connection successful: $ip_address"
# Stop battery service if changing battery-related files
if ssh -o StrictHostKeyChecking=no "$username@$ip_address" "svc -d /service/dbus-fzsonick-48tl.*"; then
echo "Stopped battery service on $ip_address"
else
echo "Warning: Failed to stop battery service on $ip_address"
fi
echo "SSH connection successful: $ip_address"
# Stop aggregator service if changing aggregator-related files
if ssh -o StrictHostKeyChecking=no "$username@$ip_address" "svc -d /service/aggregator"; then
echo "Stopped aggregator service on $ip_address"
else
echo "Warning: Failed to stop aggregator service on $ip_address"
fi
# Copy files
if scp -o ConnectTimeout=10 "$release_file_path/dbus-fzsonick-48tl/dbus-fzsonick-48tl.py" "root@$ip_address:/opt/victronenergy/dbus-fzsonick-48tl"; then
echo "Copied file to /opt on $ip_address"
else
echo "Warning: Failed to copy file to /opt on $ip_address"
fi
if scp -o ConnectTimeout=10 "$release_file_path/dbus-fzsonick-48tl/dbus-fzsonick-48tl.py" "root@$ip_address:/data/dbus-fzsonick-48tl"; then
echo "Copied file to /data on $ip_address"
else
echo "Warning: Failed to copy file to /data on $ip_address"
fi
# Copy files
if scp -o ConnectTimeout=10 "$release_file_path/dbus-fzsonick-48tl/aggregator.py" "root@$ip_address:/opt/victronenergy/dbus-fzsonick-48tl"; then
echo "Copied file to /opt on $ip_address"
else
echo "Warning: Failed to copy file to /opt on $ip_address"
fi
if scp -o ConnectTimeout=10 "$release_file_path/dbus-fzsonick-48tl/aggregator.py" "root@$ip_address:/data/dbus-fzsonick-48tl"; then
echo "Copied file to /data on $ip_address"
else
echo "Warning: Failed to copy file to /data on $ip_address"
fi
# Start battery service
if ssh -o StrictHostKeyChecking=no "$username@$ip_address" "svc -u /service/dbus-fzsonick-48tl.*"; then
echo "Started battery service on $ip_address"
else
echo "Warning: Failed to start battery service on $ip_address"
fi
# Start aggregator service
if ssh -o StrictHostKeyChecking=no "$username@$ip_address" "svc -u /service/aggregator"; then
echo "Started aggregator service on $ip_address"
else
echo "Warning: Failed to start aggregator service on $ip_address"
fi
echo "Deployment completed for $ip_address ($device_type)"
done
echo -e "\n============================ Finished deploying to $device_type ============================\n"
}
# Prompt user for deployment type
echo "Select deployment type:"
echo "1) Deploy to Venus devices"
echo "2) Deploy to Cerbo devices"
echo "3) Deploy to both Venus and Cerbo devices"
read -p "Enter your choice (1/2/3): " choice
case $choice in
1)
deploy "Venus" venus_ip_addresses[@] "$venus_release_file_path"
;;
2)
deploy "Cerbo" cerbo_ip_addresses[@] "$cerbo_release_file_path"
;;
3)
deploy "Venus" venus_ip_addresses[@] "$venus_release_file_path"
deploy "Cerbo" cerbo_ip_addresses[@] "$cerbo_release_file_path"
;;
*)
echo "Invalid choice. Exiting..."
exit 1
;;
esac
echo -e "\n============================ All Deployments Completed ============================\n"

View File

@ -1 +1,4 @@
npm run build && rsync -rv .* ubuntu@194.182.190.208:~/frontend/ && ssh ubuntu@194.182.190.208 'sudo cp -rf ~/frontend/build/* /var/www/html/monitor.innov.energy/html/' && ssh ubuntu@194.182.190.208 'sudo npm install -g serve' #npm run build && rsync -rv .* ubuntu@194.182.190.208:~/frontend/ && ssh ubuntu@194.182.190.208 'sudo cp -rf ~/frontend/build/* /var/www/html/monitor.innov.energy/html/' && ssh ubuntu@194.182.190.208 'sudo npm install -g serve'
npm run build && rsync -rv .* ubuntu@91.92.154.141:~/frontend/ && ssh ubuntu@91.92.154.141 'sudo cp -rf ~/frontend/build/* /var/www/html/monitor.innov.energy/html/' && ssh ubuntu@91.92.154.141 'sudo npm install -g serve'

View File

@ -20,6 +20,7 @@
"axios": "^1.5.0", "axios": "^1.5.0",
"chart.js": "^4.4.0", "chart.js": "^4.4.0",
"clsx": "1.1.1", "clsx": "1.1.1",
"crypto-js": "^4.2.0",
"cytoscape": "^3.26.0", "cytoscape": "^3.26.0",
"date-fns": "^2.28.0", "date-fns": "^2.28.0",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
@ -7318,6 +7319,11 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
},
"node_modules/crypto-random-string": { "node_modules/crypto-random-string": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
@ -26136,6 +26142,11 @@
"which": "^2.0.1" "which": "^2.0.1"
} }
}, },
"crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
},
"crypto-random-string": { "crypto-random-string": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",

View File

@ -1,12 +1,12 @@
import axios from 'axios'; import axios from 'axios';
export const axiosConfigWithoutToken = axios.create({ export const axiosConfigWithoutToken = axios.create({
baseURL: 'https://stage.innov.energy/api' baseURL: 'https://monitor.innov.energy/api'
//baseURL: 'http://127.0.0.1:7087/api' //baseURL: 'http://127.0.0.1:7087/api'
}); });
const axiosConfig = axios.create({ const axiosConfig = axios.create({
baseURL: 'https://stage.innov.energy/api' baseURL: 'https://monitor.innov.energy/api'
//baseURL: 'http://127.0.0.1:7087/api' //baseURL: 'http://127.0.0.1:7087/api'
}); });

View File

@ -23,9 +23,9 @@ import Button from '@mui/material/Button';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { I_S3Credentials } from '../../../interfaces/S3Types'; import { I_S3Credentials } from '../../../interfaces/S3Types';
import routes from '../../../Resources/routes.json'; import routes from '../../../Resources/routes.json';
import MainStats from './MainStats';
import DetailedBatteryView from './DetailedBatteryView';
import CircularProgress from '@mui/material/CircularProgress'; import CircularProgress from '@mui/material/CircularProgress';
import DetailedBatteryView from './DetailedBatteryView';
import MainStats from './MainStats';
interface BatteryViewProps { interface BatteryViewProps {
values: TopologyValues; values: TopologyValues;
@ -305,29 +305,23 @@ function BatteryView(props: BatteryViewProps) {
battery.AverageTemperature.unit} battery.AverageTemperature.unit}
</TableCell> </TableCell>
{props.productNum === 0 && (
<>
<TableCell <TableCell
style={{ style={{
width: '20%', width: '20%',
textAlign: 'center', textAlign: 'center',
padding: '8px', padding: '8px',
fontWeight: fontWeight:
battery.Warnings.value !== '' battery.Warnings.value !== '' ? 'bold' : 'inherit',
? 'bold'
: 'inherit',
backgroundColor: backgroundColor:
battery.Warnings.value === '' battery.Warnings.value === '' ? 'inherit' : '#ff9900',
? 'inherit'
: '#ff9900',
color: color:
battery.Warnings.value != '' ? 'black' : 'inherit' battery.Warnings.value != '' ? 'black' : 'inherit'
}} }}
> >
{battery.Warnings.value === '' ? ( {battery.Warnings.value === '' ? (
'None' 'None'
) : battery.Warnings.value.toString().split('-') ) : battery.Warnings.value.toString().split('-').length >
.length > 1 ? ( 1 ? (
<Link <Link
style={{ color: 'black' }} style={{ color: 'black' }}
to={ to={
@ -352,17 +346,14 @@ function BatteryView(props: BatteryViewProps) {
fontWeight: fontWeight:
battery.Alarms.value !== '' ? 'bold' : 'inherit', battery.Alarms.value !== '' ? 'bold' : 'inherit',
backgroundColor: backgroundColor:
battery.Alarms.value === '' battery.Alarms.value === '' ? 'inherit' : '#FF033E',
? 'inherit' color: battery.Alarms.value != '' ? 'black' : 'inherit'
: '#FF033E',
color:
battery.Alarms.value != '' ? 'black' : 'inherit'
}} }}
> >
{battery.Alarms.value === '' ? ( {battery.Alarms.value === '' ? (
'None' 'None'
) : battery.Alarms.value.toString().split('-') ) : battery.Alarms.value.toString().split('-').length >
.length > 1 ? ( 1 ? (
<Link <Link
style={{ color: 'black' }} style={{ color: 'black' }}
to={ to={
@ -380,114 +371,6 @@ function BatteryView(props: BatteryViewProps) {
battery.Alarms.value battery.Alarms.value
)} )}
</TableCell> </TableCell>
</>
)}
{props.productNum === 1 && (
<>
<TableCell
style={{
width: '20%',
textAlign: 'center',
padding: '8px',
fontWeight:
Number(battery.Warnings.value) !== 0
? 'bold'
: 'inherit',
backgroundColor:
Number(battery.Warnings.value) === 0
? 'inherit'
: '#ff9900',
color:
Number(battery.Warnings.value) != 0
? 'black'
: 'inherit'
}}
>
{Number(battery.Warnings.value) === 0 ? (
'None'
) : Number(battery.Warnings.value) === 1 ? (
<Link
style={{ color: 'black' }}
to={
currentLocation.pathname.substring(
0,
currentLocation.pathname.lastIndexOf('/') + 1
) +
routes.log +
'?open=warning'
}
>
New Warning
</Link>
) : (
<Link
style={{ color: 'black' }}
to={
currentLocation.pathname.substring(
0,
currentLocation.pathname.lastIndexOf('/') + 1
) +
routes.log +
'?open=warning'
}
>
Multiple Warnings
</Link>
)}
</TableCell>
<TableCell
sx={{
width: '20%',
textAlign: 'center',
fontWeight:
Number(battery.Alarms.value) !== 0
? 'bold'
: 'inherit',
backgroundColor:
Number(battery.Alarms.value) === 0
? 'inherit'
: '#FF033E',
color:
Number(battery.Alarms.value) != 0
? 'black'
: 'inherit'
}}
>
{Number(battery.Alarms.value) === 0 ? (
'None'
) : Number(battery.Alarms.value) === 1 ? (
<Link
style={{ color: 'black' }}
to={
currentLocation.pathname.substring(
0,
currentLocation.pathname.lastIndexOf('/') + 1
) +
routes.log +
'?open=error'
}
>
New Alarm
</Link>
) : (
<Link
style={{ color: 'black' }}
to={
currentLocation.pathname.substring(
0,
currentLocation.pathname.lastIndexOf('/') + 1
) +
routes.log +
'?open=error'
}
>
Multiple Alarms
</Link>
)}
</TableCell>
</>
)}
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>

View File

@ -0,0 +1,493 @@
import React, { useEffect, useState } from 'react';
import {
Container,
Grid,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Typography
} from '@mui/material';
import {
Link,
Route,
Routes,
useLocation,
useNavigate
} from 'react-router-dom';
import { I_S3Credentials } from '../../../interfaces/S3Types';
import routes from '../../../Resources/routes.json';
import CircularProgress from '@mui/material/CircularProgress';
import { JSONRecordData } from '../Log/graph.util';
import Button from '@mui/material/Button';
import { FormattedMessage } from 'react-intl';
import MainStatsSalidomo from './MainStatsSalidomo';
import DetailedBatteryViewSalidomo from './DetailedBatteryViewSalidomo';
interface BatteryViewProps {
values: JSONRecordData;
s3Credentials: I_S3Credentials;
installationId: number;
productNum: number;
connected: boolean;
}
function BatteryViewSalidomo(props: BatteryViewProps) {
if (props.values === null && props.connected == true) {
return null;
}
const currentLocation = useLocation();
const navigate = useNavigate();
const sortedBatteryView = Object.entries(props.values.Battery.Devices)
.map(([BatteryId, battery]) => {
return { BatteryId, battery }; // Here we return an object with the id and device
})
.sort((a, b) => parseInt(b.BatteryId) - parseInt(a.BatteryId));
const [loading, setLoading] = useState(sortedBatteryView.length == 0);
const handleMainStatsButton = () => {
navigate(routes.mainstats);
};
useEffect(() => {
if (sortedBatteryView.length == 0) {
setLoading(true);
} else {
setLoading(false);
}
}, [sortedBatteryView]);
return (
<>
{!props.connected && (
<Container
maxWidth="xl"
sx={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
height: '70vh'
}}
>
<CircularProgress size={60} style={{ color: '#ffc04d' }} />
<Typography
variant="body2"
style={{ color: 'black', fontWeight: 'bold' }}
mt={2}
>
Unable to communicate with the installation
</Typography>
<Typography variant="body2" style={{ color: 'black' }}>
Please wait or refresh the page
</Typography>
</Container>
)}
{loading && props.connected && (
<Container
maxWidth="xl"
sx={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
height: '70vh'
}}
>
<CircularProgress size={60} style={{ color: '#ffc04d' }} />
<Typography
variant="body2"
style={{ color: 'black', fontWeight: 'bold' }}
mt={2}
>
Battery service is not available at the moment
</Typography>
<Typography variant="body2" style={{ color: 'black' }}>
Please wait or refresh the page
</Typography>
</Container>
)}
{!loading && props.connected && (
<Container maxWidth="xl">
<Grid container>
<Grid
item
xs={6}
md={6}
sx={{
display:
!currentLocation.pathname.includes('detailed_view') &&
!currentLocation.pathname.includes('mainstats')
? 'block'
: 'none'
}}
>
<Button
variant="contained"
sx={{
marginTop: '20px',
backgroundColor: '#808080',
color: '#000000',
'&:hover': { bgcolor: '#f7b34d' }
}}
>
<FormattedMessage
id="battery_view"
defaultMessage="Battery View"
/>
</Button>
<Button
variant="contained"
onClick={handleMainStatsButton}
sx={{
marginTop: '20px',
marginLeft: '20px',
backgroundColor: '#ffc04d',
color: '#000000',
'&:hover': { bgcolor: '#f7b34d' }
}}
>
<FormattedMessage id="main_stats" defaultMessage="Main Stats" />
</Button>
</Grid>
</Grid>
<Grid container>
<Routes>
<Route
path={routes.mainstats + '*'}
element={
<MainStatsSalidomo
s3Credentials={props.s3Credentials}
id={props.installationId}
></MainStatsSalidomo>
}
/>
{Object.entries(props.values.Battery.Devices).map(
([BatteryId, battery]) => (
<Route
key={routes.detailed_view + BatteryId}
path={routes.detailed_view + BatteryId}
element={
<DetailedBatteryViewSalidomo
batteryId={Number(BatteryId)}
s3Credentials={props.s3Credentials}
batteryData={battery}
installationId={props.installationId}
productNum={props.productNum}
></DetailedBatteryViewSalidomo>
}
/>
)
)}
</Routes>
</Grid>
<TableContainer
component={Paper}
sx={{
marginTop: '20px',
marginBottom: '20px',
display:
!currentLocation.pathname.includes('detailed_view') &&
!currentLocation.pathname.includes('mainstats')
? 'block'
: 'none'
}}
>
<Table sx={{ minWidth: 650 }} aria-label="simple table">
<TableHead>
<TableRow>
<TableCell align="center">Battery</TableCell>
<TableCell align="center">Firmware</TableCell>
<TableCell align="center">Power</TableCell>
<TableCell align="center">Voltage</TableCell>
<TableCell align="center">SoC</TableCell>
<TableCell align="center">Temperature</TableCell>
<TableCell align="center">Warnings</TableCell>
<TableCell align="center">Alarms</TableCell>
</TableRow>
</TableHead>
<TableBody>
{sortedBatteryView.map(({ BatteryId, battery }) => (
<TableRow
key={BatteryId}
style={{
height: '10px'
}}
>
<TableCell
component="th"
scope="row"
align="center"
sx={{ fontWeight: 'bold' }}
>
<Link
style={{ color: 'black' }}
to={routes.detailed_view + BatteryId}
>
{'Node ' + BatteryId}
</Link>
</TableCell>
<TableCell
sx={{
width: '10%',
textAlign: 'center'
}}
>
{battery.FwVersion.value}
</TableCell>
<TableCell
sx={{
width: '10%',
textAlign: 'center'
}}
>
{battery.Dc.Power.value + ' W'}
</TableCell>
<TableCell
sx={{
width: '10%',
textAlign: 'center',
backgroundColor:
battery.Dc.Voltage.value < 44 ||
battery.Dc.Voltage.value > 57
? '#FF033E'
: '#32CD32',
color: battery.Dc.Voltage.value ? 'inherit' : 'white'
}}
>
{battery.Dc.Voltage.value + ' V'}
</TableCell>
<TableCell
sx={{
width: '10%',
textAlign: 'center',
backgroundColor:
battery.Soc.value < 20
? '#FF033E'
: battery.Soc.value < 50
? '#ffbf00'
: '#32CD32',
color: battery.Soc.value ? 'inherit' : 'white'
}}
>
{battery.Soc.value + ' %'}
</TableCell>
<TableCell
sx={{
width: '10%',
textAlign: 'center',
backgroundColor:
battery.Temperatures.Cells.Average.value > 300
? '#FF033E'
: battery.Temperatures.Cells.Average.value > 280
? '#ffbf00'
: battery.Temperatures.Cells.Average.value < 245
? '#008FFB'
: '#32CD32'
}}
>
{battery.Temperatures.Cells.Average.value + ' C'}
</TableCell>
{/*{props.productNum === 0 && (*/}
{/* <>*/}
{/* <TableCell*/}
{/* style={{*/}
{/* width: '20%',*/}
{/* textAlign: 'center',*/}
{/* padding: '8px',*/}
{/* fontWeight:*/}
{/* battery.Warnings.value !== ''*/}
{/* ? 'bold'*/}
{/* : 'inherit',*/}
{/* backgroundColor:*/}
{/* battery.Warnings.value === ''*/}
{/* ? 'inherit'*/}
{/* : '#ff9900',*/}
{/* color:*/}
{/* battery.Warnings.value != '' ? 'black' : 'inherit'*/}
{/* }}*/}
{/* >*/}
{/* {battery.Warnings.value === '' ? (*/}
{/* 'None'*/}
{/* ) : battery.Warnings.value.toString().split('-')*/}
{/* .length > 1 ? (*/}
{/* <Link*/}
{/* style={{ color: 'black' }}*/}
{/* to={*/}
{/* currentLocation.pathname.substring(*/}
{/* 0,*/}
{/* currentLocation.pathname.lastIndexOf('/') + 1*/}
{/* ) +*/}
{/* routes.log +*/}
{/* '?open=warning'*/}
{/* }*/}
{/* >*/}
{/* Multiple Warnings*/}
{/* </Link>*/}
{/* ) : (*/}
{/* battery.Warnings.value*/}
{/* )}*/}
{/* </TableCell>*/}
{/* <TableCell*/}
{/* sx={{*/}
{/* width: '20%',*/}
{/* textAlign: 'center',*/}
{/* fontWeight:*/}
{/* battery.Alarms.value !== '' ? 'bold' : 'inherit',*/}
{/* backgroundColor:*/}
{/* battery.Alarms.value === ''*/}
{/* ? 'inherit'*/}
{/* : '#FF033E',*/}
{/* color:*/}
{/* battery.Alarms.value != '' ? 'black' : 'inherit'*/}
{/* }}*/}
{/* >*/}
{/* {battery.Alarms.value === '' ? (*/}
{/* 'None'*/}
{/* ) : battery.Alarms.value.toString().split('-')*/}
{/* .length > 1 ? (*/}
{/* <Link*/}
{/* style={{ color: 'black' }}*/}
{/* to={*/}
{/* currentLocation.pathname.substring(*/}
{/* 0,*/}
{/* currentLocation.pathname.lastIndexOf('/') + 1*/}
{/* ) +*/}
{/* routes.log +*/}
{/* '?open=error'*/}
{/* }*/}
{/* >*/}
{/* Multiple Alarms*/}
{/* </Link>*/}
{/* ) : (*/}
{/* battery.Alarms.value*/}
{/* )}*/}
{/* </TableCell>*/}
{/* </>*/}
{/*)}*/}
{props.productNum === 1 && (
<>
<TableCell
style={{
width: '20%',
textAlign: 'center',
padding: '8px',
fontWeight:
Number(battery.Warnings) !== 0
? 'bold'
: 'inherit',
backgroundColor:
Number(battery.Warnings) === 0
? 'inherit'
: '#ff9900',
color:
Number(battery.Warnings) != 0
? 'black'
: 'inherit'
}}
>
{Number(battery.Warnings) === 0 ? (
'None'
) : Number(battery.Warnings) === 1 ? (
<Link
style={{ color: 'black' }}
to={
currentLocation.pathname.substring(
0,
currentLocation.pathname.lastIndexOf('/') + 1
) +
routes.log +
'?open=warning'
}
>
New Warning
</Link>
) : (
<Link
style={{ color: 'black' }}
to={
currentLocation.pathname.substring(
0,
currentLocation.pathname.lastIndexOf('/') + 1
) +
routes.log +
'?open=warning'
}
>
Multiple Warnings
</Link>
)}
</TableCell>
<TableCell
sx={{
width: '20%',
textAlign: 'center',
fontWeight:
Number(battery.Alarms) !== 0 ? 'bold' : 'inherit',
backgroundColor:
Number(battery.Alarms) === 0
? 'inherit'
: '#FF033E',
color:
Number(battery.Alarms) != 0 ? 'black' : 'inherit'
}}
>
{Number(battery.Alarms) === 0 ? (
'None'
) : Number(battery.Alarms) === 1 ? (
<Link
style={{ color: 'black' }}
to={
currentLocation.pathname.substring(
0,
currentLocation.pathname.lastIndexOf('/') + 1
) +
routes.log +
'?open=error'
}
>
New Alarm
</Link>
) : (
<Link
style={{ color: 'black' }}
to={
currentLocation.pathname.substring(
0,
currentLocation.pathname.lastIndexOf('/') + 1
) +
routes.log +
'?open=error'
}
>
Multiple Alarms
</Link>
)}
</TableCell>
</>
)}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Container>
)}
</>
);
}
export default BatteryViewSalidomo;

View File

@ -1531,111 +1531,6 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
</TableContainer> </TableContainer>
</Card> </Card>
</Grid> </Grid>
{/*----------------------------------------------------------------------------------------------------------------------------------*/}
{/*<Grid item md={1.5} xs={1.5}>*/}
{/* <Card*/}
{/* sx={{*/}
{/* overflow: 'visible',*/}
{/* marginTop: '30px',*/}
{/* marginLeft: '20px',*/}
{/* marginBottom: '30px',*/}
{/* backgroundColor: 'red'*/}
{/* }}*/}
{/* >*/}
{/* <TableContainer*/}
{/* component={Paper}*/}
{/* sx={{ marginTop: '20px', marginBottom: '20px', width: '100%' }}*/}
{/* >*/}
{/* <Table size="medium" aria-label="a dense table">*/}
{/* <TableBody>*/}
{/* <TableRow>*/}
{/* <TableCell*/}
{/* component="th"*/}
{/* scope="row"*/}
{/* align="left"*/}
{/* sx={{ fontWeight: 'bold' }}*/}
{/* >*/}
{/* Green Led*/}
{/* </TableCell>*/}
{/* <TableCell*/}
{/* align="right"*/}
{/* sx={{*/}
{/* width: '6ch',*/}
{/* whiteSpace: 'nowrap',*/}
{/* paddingRight: '12px'*/}
{/* }}*/}
{/* >*/}
{/* {props.batteryData.GreenLeds.value}*/}
{/* </TableCell>*/}
{/* </TableRow>*/}
{/* <TableRow>*/}
{/* <TableCell*/}
{/* component="th"*/}
{/* scope="row"*/}
{/* align="left"*/}
{/* sx={{ fontWeight: 'bold' }}*/}
{/* >*/}
{/* Amber Led*/}
{/* </TableCell>*/}
{/* <TableCell*/}
{/* align="right"*/}
{/* sx={{*/}
{/* width: '6ch',*/}
{/* whiteSpace: 'nowrap',*/}
{/* paddingRight: '12px'*/}
{/* }}*/}
{/* >*/}
{/* {props.batteryData.AmberLeds.value}*/}
{/* </TableCell>*/}
{/* </TableRow>*/}
{/* <TableRow>*/}
{/* <TableCell*/}
{/* component="th"*/}
{/* scope="row"*/}
{/* align="left"*/}
{/* sx={{ fontWeight: 'bold' }}*/}
{/* >*/}
{/* Blue Led*/}
{/* </TableCell>*/}
{/* <TableCell*/}
{/* align="right"*/}
{/* sx={{*/}
{/* width: '6ch',*/}
{/* whiteSpace: 'nowrap',*/}
{/* paddingRight: '12px'*/}
{/* }}*/}
{/* >*/}
{/* {props.batteryData.BlueLeds.value}*/}
{/* </TableCell>*/}
{/* </TableRow>*/}
{/* <TableRow>*/}
{/* <TableCell*/}
{/* component="th"*/}
{/* scope="row"*/}
{/* align="left"*/}
{/* sx={{ fontWeight: 'bold' }}*/}
{/* >*/}
{/* Red Led*/}
{/* </TableCell>*/}
{/* <TableCell*/}
{/* align="right"*/}
{/* sx={{*/}
{/* width: '6ch',*/}
{/* whiteSpace: 'nowrap',*/}
{/* paddingRight: '12px'*/}
{/* }}*/}
{/* >*/}
{/* {props.batteryData.RedLeds.value}*/}
{/* </TableCell>*/}
{/* </TableRow>*/}
{/* </TableBody>*/}
{/* </Table>*/}
{/* </TableContainer>*/}
{/* </Card>*/}
{/*</Grid>*/}
</> </>
); );
} }

View File

@ -5,6 +5,7 @@ import {
Grid, Grid,
IconButton, IconButton,
Modal, Modal,
TextField,
Typography Typography
} from '@mui/material'; } from '@mui/material';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
@ -17,7 +18,7 @@ import {
BatteryOverviewInterface, BatteryOverviewInterface,
transformInputToBatteryViewData transformInputToBatteryViewData
} from '../../../interfaces/Chart'; } from '../../../interfaces/Chart';
import dayjs from 'dayjs'; import dayjs, { Dayjs } from 'dayjs';
import { TimeSpan, UnixTime } from '../../../dataCache/time'; import { TimeSpan, UnixTime } from '../../../dataCache/time';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import { DateTimePicker, LocalizationProvider } from '@mui/x-date-pickers'; import { DateTimePicker, LocalizationProvider } from '@mui/x-date-pickers';
@ -360,19 +361,25 @@ function MainStats(props: MainStatsProps) {
<DateTimePicker <DateTimePicker
label="Select Start Date" label="Select Start Date"
value={startDate} value={startDate}
onChange={(newDate) => setStartDate(newDate)} onChange={(newDate: Dayjs | null) => {
sx={{ // Type assertion to Dayjs
marginTop: 2 if (newDate) {
setStartDate(newDate);
}
}} }}
renderInput={(props) => <TextField {...props} />}
/> />
<DateTimePicker <DateTimePicker
label="Select End Date" label="Select End Date"
value={endDate} value={endDate}
onChange={(newDate) => setEndDate(newDate)} onChange={(newDate: Dayjs | null) => {
sx={{ // Type assertion to Dayjs
marginTop: 2 if (newDate) {
setEndDate(newDate);
}
}} }}
renderInput={(props) => <TextField {...props} />}
/> />
<div <div

View File

@ -0,0 +1,812 @@
import {
Box,
Card,
Container,
Grid,
IconButton,
Modal,
TextField,
Typography
} from '@mui/material';
import { FormattedMessage } from 'react-intl';
import React, { useContext, useEffect, useState } from 'react';
import { I_S3Credentials } from '../../../interfaces/S3Types';
import ReactApexChart from 'react-apexcharts';
import { getChartOptions } from '../Overview/chartOptions';
import {
BatteryDataInterface,
BatteryOverviewInterface,
transformInputToBatteryViewDataJson
} from '../../../interfaces/Chart';
import dayjs, { Dayjs } from 'dayjs';
import { TimeSpan, UnixTime } from '../../../dataCache/time';
import Button from '@mui/material/Button';
import { DateTimePicker, LocalizationProvider } from '@mui/x-date-pickers';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import CircularProgress from '@mui/material/CircularProgress';
import { useLocation, useNavigate } from 'react-router-dom';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import { ProductIdContext } from '../../../contexts/ProductIdContextProvider';
interface MainStatsProps {
s3Credentials: I_S3Credentials;
id: number;
}
function MainStatsSalidomo(props: MainStatsProps) {
const [chartState, setChartState] = useState(0);
const [batteryViewDataArray, setBatteryViewDataArray] = useState<
{
chartData: BatteryDataInterface;
chartOverview: BatteryOverviewInterface;
}[]
>([]);
const [isDateModalOpen, setIsDateModalOpen] = useState(false);
const [dateOpen, setDateOpen] = useState(false);
const navigate = useNavigate();
const [startDate, setStartDate] = useState(dayjs().add(-1, 'day'));
const [endDate, setEndDate] = useState(dayjs());
const [isErrorDateModalOpen, setErrorDateModalOpen] = useState(false);
const [dateSelectionError, setDateSelectionError] = useState('');
const [loading, setLoading] = useState(true);
const location = useLocation();
const { product, setProduct } = useContext(ProductIdContext);
const blueColors = [
'#99CCFF',
'#80BFFF',
'#6699CC',
'#4D99FF',
'#2670E6',
'#3366CC',
'#1A4D99',
'#133366',
'#0D274D',
'#081A33'
];
const redColors = [
'#ff9090',
'#ff7070',
'#ff3f3f',
'#ff1e1e',
'#ff0606',
'#fc0000',
'#f40000',
'#d40000',
'#a30000',
'#7a0000'
];
const orangeColors = [
'#ffdb99',
'#ffc968',
'#ffb837',
'#ffac16',
'#ffa706',
'#FF8C00',
'#d48900',
'#CC7A00',
'#a36900',
'#993D00'
];
useEffect(() => {
setLoading(true);
const resultPromise: Promise<{
chartData: BatteryDataInterface;
chartOverview: BatteryOverviewInterface;
}> = transformInputToBatteryViewDataJson(
props.s3Credentials,
props.id,
product,
UnixTime.fromTicks(new Date().getTime() / 1000).earlier(
TimeSpan.fromDays(1)
),
UnixTime.fromTicks(new Date().getTime() / 1000)
);
resultPromise
.then((result) => {
setBatteryViewDataArray((prevData) =>
prevData.concat({
chartData: result.chartData,
chartOverview: result.chartOverview
})
);
setLoading(false);
})
.catch((error) => {
console.error('Error:', error);
});
}, []);
const [isZooming, setIsZooming] = useState(false);
useEffect(() => {
if (isZooming) {
setLoading(true);
} else if (!isZooming && batteryViewDataArray.length > 0) {
setLoading(false);
}
}, [isZooming, batteryViewDataArray]);
function generateSeries(chartData, category, color) {
const series = [];
const pathsToSearch = [
'Node2',
'Node3',
'Node4',
'Node5',
'Node6',
'Node7',
'Node8',
'Node9',
'Node10',
'Node11'
];
let i = 0;
pathsToSearch.forEach((devicePath) => {
if (
Object.hasOwnProperty.call(chartData[category].data, devicePath) &&
chartData[category].data[devicePath].data.length != 0
) {
series.push({
...chartData[category].data[devicePath],
color:
color === 'blue'
? blueColors[i]
: color === 'red'
? redColors[i]
: orangeColors[i]
});
}
i++;
});
return series;
}
const handleCancel = () => {
setIsDateModalOpen(false);
setDateOpen(false);
};
const handleConfirm = () => {
setIsDateModalOpen(false);
setDateOpen(false);
if (endDate.isAfter(dayjs())) {
setDateSelectionError('You cannot ask for future data');
setErrorDateModalOpen(true);
return;
} else if (startDate.isAfter(endDate)) {
setDateSelectionError('Εnd date must precede start date');
setErrorDateModalOpen(true);
return;
}
setLoading(true);
const resultPromise: Promise<{
chartData: BatteryDataInterface;
chartOverview: BatteryOverviewInterface;
}> = transformInputToBatteryViewDataJson(
props.s3Credentials,
props.id,
product,
UnixTime.fromTicks(startDate.unix()),
UnixTime.fromTicks(endDate.unix())
);
resultPromise
.then((result) => {
setBatteryViewDataArray((prevData) =>
prevData.concat({
chartData: result.chartData,
chartOverview: result.chartOverview
})
);
setLoading(false);
setChartState(batteryViewDataArray.length);
})
.catch((error) => {
console.error('Error:', error);
});
};
const handleSetDate = () => {
setDateOpen(true);
setIsDateModalOpen(true);
};
const handleBatteryViewButton = () => {
navigate(
location.pathname.split('/').slice(0, -2).join('/') + '/batteryview'
);
};
const handleGoBack = () => {
if (chartState > 0) {
setChartState(chartState - 1);
}
};
const handleGoForward = () => {
if (chartState + 1 < batteryViewDataArray.length) {
setChartState(chartState + 1);
}
};
const handleOkOnErrorDateModal = () => {
setErrorDateModalOpen(false);
};
const startZoom = () => {
setIsZooming(true);
};
const handleBeforeZoom = (chartContext, { xaxis }) => {
const startX = parseInt(xaxis.min) / 1000;
const endX = parseInt(xaxis.max) / 1000;
const resultPromise: Promise<{
chartData: BatteryDataInterface;
chartOverview: BatteryOverviewInterface;
}> = transformInputToBatteryViewDataJson(
props.s3Credentials,
props.id,
product,
UnixTime.fromTicks(startX).earlier(TimeSpan.fromHours(2)),
UnixTime.fromTicks(endX).earlier(TimeSpan.fromHours(2))
);
resultPromise
.then((result) => {
setBatteryViewDataArray((prevData) =>
prevData.concat({
chartData: result.chartData,
chartOverview: result.chartOverview
})
);
setIsZooming(false);
setChartState(batteryViewDataArray.length);
})
.catch((error) => {
console.error('Error:', error);
});
};
return (
<>
{loading && (
<Container
maxWidth="xl"
sx={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
height: '100vh'
}}
>
<CircularProgress size={60} style={{ color: '#ffc04d' }} />
<Typography variant="body2" style={{ color: 'black' }} mt={2}>
Fetching data...
</Typography>
</Container>
)}
{isErrorDateModalOpen && (
<Modal open={isErrorDateModalOpen} onClose={() => {}}>
<Box
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 450,
bgcolor: 'background.paper',
borderRadius: 4,
boxShadow: 24,
p: 4,
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
}}
>
<Typography
variant="body1"
gutterBottom
sx={{ fontWeight: 'bold' }}
>
{dateSelectionError}
</Typography>
<Button
sx={{
marginTop: 2,
textTransform: 'none',
bgcolor: '#ffc04d',
color: '#111111',
'&:hover': { bgcolor: '#f7b34d' }
}}
onClick={handleOkOnErrorDateModal}
>
Ok
</Button>
</Box>
</Modal>
)}
{isDateModalOpen && (
<LocalizationProvider dateAdapter={AdapterDayjs}>
<Modal open={isDateModalOpen} onClose={() => {}}>
<Box
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 450,
bgcolor: 'background.paper',
borderRadius: 4,
boxShadow: 24,
p: 4,
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
}}
>
<DateTimePicker
label="Select Start Date"
value={startDate}
onChange={(newDate: Dayjs | null) => {
// Type assertion to Dayjs
if (newDate) {
setStartDate(newDate);
}
}}
renderInput={(props) => <TextField {...props} />}
/>
<DateTimePicker
label="Select End Date"
value={endDate}
onChange={(newDate: Dayjs | null) => {
// Type assertion to Dayjs
if (newDate) {
setEndDate(newDate);
}
}}
renderInput={(props) => <TextField {...props} />}
/>
<div
style={{
display: 'flex',
alignItems: 'center',
marginTop: 10
}}
>
<Button
sx={{
marginTop: 2,
textTransform: 'none',
bgcolor: '#ffc04d',
color: '#111111',
'&:hover': { bgcolor: '#f7b34d' }
}}
onClick={handleConfirm}
>
Confirm
</Button>
<Button
sx={{
marginTop: 2,
marginLeft: 2,
textTransform: 'none',
bgcolor: '#ffc04d',
color: '#111111',
'&:hover': { bgcolor: '#f7b34d' }
}}
onClick={handleCancel}
>
Cancel
</Button>
</div>
</Box>
</Modal>
</LocalizationProvider>
)}
{!loading && (
<>
<Grid item xs={6} md={6}>
<IconButton
aria-label="go back"
sx={{
marginTop: '20px',
backgroundColor: 'grey',
color: '#000000',
'&:hover': { bgcolor: '#f7b34d' }
}}
onClick={handleBatteryViewButton}
>
<ArrowBackIcon />
</IconButton>
<Button
variant="contained"
onClick={handleSetDate}
disabled={loading}
sx={{
marginTop: '20px',
marginLeft: '20px',
backgroundColor: dateOpen ? '#808080' : '#ffc04d',
color: '#000000',
'&:hover': { bgcolor: '#f7b34d' }
}}
>
<FormattedMessage id="set_date" defaultMessage="Set Date" />
</Button>
</Grid>
<Grid
container
justifyContent="flex-end"
alignItems="center"
item
xs={6}
md={6}
>
<Button
variant="contained"
disabled={!(chartState > 0)}
onClick={handleGoBack}
sx={{
marginTop: '20px',
marginLeft: '10px',
backgroundColor: '#ffc04d',
color: '#000000',
'&:hover': { bgcolor: '#f7b34d' }
}}
>
<FormattedMessage id="goback" defaultMessage="Zoom out" />
</Button>
<Button
variant="contained"
disabled={!(chartState < batteryViewDataArray.length - 1)}
onClick={handleGoForward}
sx={{
marginTop: '20px',
marginLeft: '10px',
backgroundColor: '#ffc04d',
color: '#000000',
'&:hover': { bgcolor: '#f7b34d' }
}}
>
<FormattedMessage id="goback" defaultMessage="Zoom in" />
</Button>
</Grid>
<Grid
container
direction="row"
justifyContent="center"
alignItems="stretch"
spacing={3}
>
<Grid item md={12} xs={12}>
<Card
sx={{
overflow: 'visible',
marginTop: '30px',
marginBottom: '30px'
}}
>
<Box
sx={{
marginLeft: '20px'
}}
>
<Box display="flex" alignItems="center">
<Box>
<Typography variant="subtitle1" noWrap>
<FormattedMessage
id="battery_soc"
defaultMessage="Battery SOC (State Of Charge)"
/>
</Typography>
</Box>
</Box>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
pt: 3
}}
></Box>
</Box>
<ReactApexChart
options={{
...getChartOptions(
batteryViewDataArray[chartState].chartOverview.Soc,
'daily',
[],
true
),
chart: {
events: {
beforeZoom: (chartContext, options) => {
startZoom();
handleBeforeZoom(chartContext, options);
}
}
}
}}
series={generateSeries(
batteryViewDataArray[chartState].chartData,
'Soc',
'blue'
)}
type="line"
height={420}
/>
</Card>
</Grid>
<Grid item md={12} xs={12}>
<Card
sx={{
overflow: 'visible',
marginTop: '10px',
marginBottom: '30px'
}}
>
<Box
sx={{
marginLeft: '20px'
}}
>
<Box display="flex" alignItems="center">
<Box>
<Typography variant="subtitle1" noWrap>
<FormattedMessage
id="battery_soc"
defaultMessage="Battery Temperature"
/>
</Typography>
</Box>
</Box>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
pt: 3
}}
></Box>
</Box>
<ReactApexChart
options={{
...getChartOptions(
batteryViewDataArray[chartState].chartOverview
.Temperature,
'daily',
[],
true
),
chart: {
events: {
beforeZoom: (chartContext, options) => {
startZoom();
handleBeforeZoom(chartContext, options);
}
}
}
}}
series={generateSeries(
batteryViewDataArray[chartState].chartData,
'Temperature',
'blue'
)}
type="line"
height={420}
/>
</Card>
</Grid>
<Grid item md={12} xs={12}>
<Card
sx={{
overflow: 'visible',
marginTop: '10px',
marginBottom: '30px'
}}
>
<Box
sx={{
marginLeft: '20px'
}}
>
<Box display="flex" alignItems="center">
<Box>
<Typography variant="subtitle1" noWrap>
<FormattedMessage
id="battery_power"
defaultMessage="Battery Power"
/>
</Typography>
</Box>
</Box>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
pt: 3
}}
></Box>
</Box>
<ReactApexChart
options={{
...getChartOptions(
batteryViewDataArray[chartState].chartOverview.Power,
'daily',
[],
true
),
chart: {
events: {
beforeZoom: (chartContext, options) => {
startZoom();
handleBeforeZoom(chartContext, options);
}
}
}
}}
series={generateSeries(
batteryViewDataArray[chartState].chartData,
'Power',
'red'
)}
type="line"
height={420}
/>
</Card>
</Grid>
<Grid item md={12} xs={12}>
<Card
sx={{
overflow: 'visible',
marginTop: '10px',
marginBottom: '30px'
}}
>
<Box
sx={{
marginLeft: '20px'
}}
>
<Box display="flex" alignItems="center">
<Box>
<Typography variant="subtitle1" noWrap>
<FormattedMessage
id="battery_voltage"
defaultMessage="Battery Voltage"
/>
</Typography>
</Box>
</Box>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
pt: 3
}}
></Box>
</Box>
<ReactApexChart
options={{
...getChartOptions(
batteryViewDataArray[chartState].chartOverview.Voltage,
'daily',
[],
true
),
chart: {
events: {
beforeZoom: (chartContext, options) => {
startZoom();
handleBeforeZoom(chartContext, options);
}
}
}
}}
series={generateSeries(
batteryViewDataArray[chartState].chartData,
'Voltage',
'orange'
)}
type="line"
height={420}
/>
</Card>
</Grid>
<Grid item md={12} xs={12}>
<Card
sx={{
overflow: 'visible',
marginTop: '10px',
marginBottom: '30px'
}}
>
<Box
sx={{
marginLeft: '20px'
}}
>
<Box display="flex" alignItems="center">
<Box>
<Typography variant="subtitle1" noWrap>
<FormattedMessage
id="battery_current"
defaultMessage="Battery Current"
/>
</Typography>
</Box>
</Box>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
pt: 3
}}
></Box>
</Box>
<ReactApexChart
options={{
...getChartOptions(
batteryViewDataArray[chartState].chartOverview.Current,
'daily',
[],
true
),
chart: {
events: {
beforeZoom: (chartContext, options) => {
startZoom();
handleBeforeZoom(chartContext, options);
}
}
}
}}
series={generateSeries(
batteryViewDataArray[chartState].chartData,
'Current',
'orange'
)}
type="line"
height={420}
/>
</Card>
</Grid>
</Grid>
</>
)}
</>
);
}
export default MainStatsSalidomo;

View File

@ -20,17 +20,16 @@ import { FormattedMessage } from 'react-intl';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import { Close as CloseIcon } from '@mui/icons-material'; import { Close as CloseIcon } from '@mui/icons-material';
import MenuItem from '@mui/material/MenuItem'; import MenuItem from '@mui/material/MenuItem';
import { import { LocalizationProvider } from '@mui/x-date-pickers';
DateTimePicker,
LocalizationProvider,
TimePicker
} from '@mui/x-date-pickers';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import axiosConfig from '../../../Resources/axiosConfig'; import axiosConfig from '../../../Resources/axiosConfig';
import utc from 'dayjs/plugin/utc'; import utc from 'dayjs/plugin/utc';
import { UserContext } from '../../../contexts/userContext'; import { UserContext } from '../../../contexts/userContext';
import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker';
import { TimePicker } from '@mui/lab';
interface ConfigurationProps { interface ConfigurationProps {
values: TopologyValues; values: TopologyValues;
id: number; id: number;
@ -352,15 +351,30 @@ function Configuration(props: ConfigurationProps) {
<div> <div>
<LocalizationProvider dateAdapter={AdapterDayjs}> <LocalizationProvider dateAdapter={AdapterDayjs}>
<DateTimePicker <DateTimePicker
format="DD/MM/YYYY HH:mm"
ampm={false}
label="Select Next Calibration Charge Date" label="Select Next Calibration Charge Date"
value={dayjs(formValues.calibrationChargeDate)} value={dayjs(formValues.calibrationChargeDate)}
onChange={handleConfirm} onChange={handleConfirm}
renderInput={(params) => (
<TextField
{...params}
sx={{ sx={{
marginTop: 2 marginTop: 2, // Apply styles here
width: '100%' // Optional: You can adjust the width or other styling here
}} }}
/> />
)}
/>
{/*<DateTimePicker*/}
{/* format="DD/MM/YYYY HH:mm"*/}
{/* ampm={false}*/}
{/* label="Select Next Calibration Charge Date"*/}
{/* value={dayjs(formValues.calibrationChargeDate)}*/}
{/* onChange={handleConfirm}*/}
{/* sx={{*/}
{/* marginTop: 2*/}
{/* }} // This should work with the correct imports*/}
{/*/>*/}
</LocalizationProvider> </LocalizationProvider>
</div> </div>
)} )}
@ -405,6 +419,15 @@ function Configuration(props: ConfigurationProps) {
label="Calibration Charge Hour" label="Calibration Charge Hour"
value={dayjs(formValues.calibrationChargeDate)} value={dayjs(formValues.calibrationChargeDate)}
onChange={(newTime) => handleConfirm(dayjs(newTime))} onChange={(newTime) => handleConfirm(dayjs(newTime))}
renderInput={(params) => (
<TextField
{...params}
sx={{
marginTop: 2, // Apply styles here
width: '100%' // Optional: You can adjust the width or other styling here
}}
/>
)}
/> />
</LocalizationProvider> </LocalizationProvider>
</div> </div>

View File

@ -25,10 +25,15 @@ import Button from '@mui/material/Button';
import { DateTimePicker, LocalizationProvider } from '@mui/x-date-pickers'; import { DateTimePicker, LocalizationProvider } from '@mui/x-date-pickers';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
import EditIcon from '@mui/icons-material/Edit'; import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete'; import DeleteIcon from '@mui/icons-material/Delete';
import { UserContext } from '../../../contexts/userContext'; import { UserContext } from '../../../contexts/userContext';
dayjs.extend(utc);
dayjs.extend(timezone);
interface HistoryProps { interface HistoryProps {
errorLoadingS3Data: boolean; errorLoadingS3Data: boolean;
id: number; id: number;
@ -180,28 +185,30 @@ function HistoryOfActions(props: HistoryProps) {
}} }}
> >
<div> <div>
<DateTimePicker
label="Select Action Date"
name="timestamp"
value={editMode ? dayjs(newAction.timestamp) : actionDate}
onChange={(newDate) => handleDateChange(newDate)}
sx={{
width: 450,
marginTop: 2
}}
/>
{/*<DateTimePicker*/} {/*<DateTimePicker*/}
{/* label="Select Action Date"*/} {/* label="Select Action Date"*/}
{/* name="timestamp"*/}
{/* value={actionDate}*/} {/* value={actionDate}*/}
{/* onChange={handleDateChange}*/} {/* onChange={(newDate: Dayjs | null) =>*/}
{/* renderInput={(params) => (*/} {/* handleDateChange(newDate)*/}
{/* <TextField*/} {/* }*/}
{/* {...params}*/} {/* InputProps={{*/}
{/* sx={{ width: 450, marginTop: 2 }}*/} {/* sx: {*/}
{/* />*/} {/* width: 450,*/}
{/* )}*/} {/* marginTop: 2*/}
{/* }*/}
{/* }}*/}
{/*/>*/} {/*/>*/}
<DateTimePicker
label="Select Action Date"
value={actionDate}
onChange={handleDateChange}
renderInput={(params) => (
<TextField {...params} sx={{ width: 450, marginTop: 2 }} />
)}
/>
<TextField <TextField
label="Description" label="Description"
variant="outlined" variant="outlined"
@ -411,14 +418,18 @@ function HistoryOfActions(props: HistoryProps) {
<Divider /> <Divider />
<div style={{ maxHeight: '600px', overflowY: 'auto' }}> <div style={{ maxHeight: '600px', overflowY: 'auto' }}>
{history.map((action, index) => { {history.map((action, index) => {
// Parse the timestamp string to a Date object // // Parse the timestamp string to a Date object
const date = new Date(action.timestamp); // const date = new Date(action.timestamp);
//
// // Extract the date part (e.g., "2023-05-31")
// const datePart = date.toLocaleDateString();
//
// // Extract the time part (e.g., "12:34:56")
// const timePart = date.toLocaleTimeString();
const dateCET = dayjs.utc(action.timestamp).tz("Europe/Paris");
const datePart = dateCET.format("YYYY-MM-DD");
const timePart = dateCET.format("HH:mm:ss");
// Extract the date part (e.g., "2023-05-31")
const datePart = date.toLocaleDateString();
// Extract the time part (e.g., "12:34:56")
const timePart = date.toLocaleTimeString();
const iconStyle = const iconStyle =
action.userName === currentUser.name action.userName === currentUser.name
? {} ? {}

View File

@ -27,10 +27,10 @@ import BuildIcon from '@mui/icons-material/Build';
import { Navigate, Route, Routes, useLocation } from 'react-router-dom'; import { Navigate, Route, Routes, useLocation } from 'react-router-dom';
import routes from '../../../Resources/routes.json'; import routes from '../../../Resources/routes.json';
import Information from '../Information/Information'; import Information from '../Information/Information';
import BatteryView from '../BatteryView/BatteryView';
import { UserType } from '../../../interfaces/UserTypes'; import { UserType } from '../../../interfaces/UserTypes';
import HistoryOfActions from '../History/History'; import HistoryOfActions from '../History/History';
import PvView from '../PvView/PvView'; import PvView from '../PvView/PvView';
import BatteryView from '../BatteryView/BatteryView';
interface singleInstallationProps { interface singleInstallationProps {
current_installation?: I_Installation; current_installation?: I_Installation;
@ -38,6 +38,9 @@ interface singleInstallationProps {
} }
function Installation(props: singleInstallationProps) { function Installation(props: singleInstallationProps) {
if (props.current_installation == undefined) {
return null;
}
const context = useContext(UserContext); const context = useContext(UserContext);
const { currentUser } = context; const { currentUser } = context;
const location = useLocation().pathname; const location = useLocation().pathname;
@ -49,10 +52,6 @@ function Installation(props: singleInstallationProps) {
const [connected, setConnected] = useState(true); const [connected, setConnected] = useState(true);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
if (props.current_installation == undefined) {
return null;
}
const S3data = { const S3data = {
s3Region: props.current_installation.s3Region, s3Region: props.current_installation.s3Region,
s3Provider: props.current_installation.s3Provider, s3Provider: props.current_installation.s3Provider,
@ -62,11 +61,8 @@ function Installation(props: singleInstallationProps) {
}; };
const s3Bucket = const s3Bucket =
props.current_installation.product === 0 props.current_installation.s3BucketId.toString() +
? props.current_installation.s3BucketId.toString() + '-3e5b3069-214a-43ee-8d85-57d72000c19d';
'-3e5b3069-214a-43ee-8d85-57d72000c19d'
: props.current_installation.s3BucketId.toString() +
'-c0436b6a-d276-4cd8-9c44-1eae86cf5d0e';
const s3Credentials = { s3Bucket, ...S3data }; const s3Credentials = { s3Bucket, ...S3data };
@ -74,6 +70,8 @@ function Installation(props: singleInstallationProps) {
return new Promise((res) => setTimeout(res, delay)); return new Promise((res) => setTimeout(res, delay));
} }
//In React, useRef creates a mutable object that persists across renders without triggering re-renders when its value changes.
//While fetching, we check the value of continueFetching, if its false, we break. This means that either the user changed tab or the object has been finished its execution (return)
const continueFetching = useRef(false); const continueFetching = useRef(false);
const fetchDataForOneTime = async () => { const fetchDataForOneTime = async () => {
@ -193,6 +191,7 @@ function Installation(props: singleInstallationProps) {
setCurrentTab(path[path.length - 1]); setCurrentTab(path[path.length - 1]);
}, [location]); }, [location]);
//If status is -1, the topology will be empty and "Unable to communicate with the installation" should be rendered from the Topology component
useEffect(() => { useEffect(() => {
if (status === -1) { if (status === -1) {
setConnected(false); setConnected(false);
@ -206,19 +205,22 @@ function Installation(props: singleInstallationProps) {
currentTab == 'configuration' || currentTab == 'configuration' ||
location.includes('batteryview') location.includes('batteryview')
) { ) {
//Fetch periodically if the tab is live, pvview or batteryview
if ( if (
currentTab == 'live' || currentTab == 'live' ||
(location.includes('batteryview') && !location.includes('mainstats')) || currentTab == 'pvview' ||
currentTab == 'pvview' (location.includes('batteryview') && !location.includes('mainstats'))
) { ) {
if (!continueFetching.current) { if (!continueFetching.current) {
continueFetching.current = true; continueFetching.current = true;
//Call the function only one time. When the location and the currentTab change, this useEffect will be called 2 times
if (!fetchFunctionCalled) { if (!fetchFunctionCalled) {
setFetchFunctionCalled(true); setFetchFunctionCalled(true);
fetchDataPeriodically(); fetchDataPeriodically();
} }
} }
} }
//Fetch only one time in configuration tab
if (currentTab == 'configuration') { if (currentTab == 'configuration') {
fetchDataForOneTime(); fetchDataForOneTime();
} }
@ -227,6 +229,7 @@ function Installation(props: singleInstallationProps) {
continueFetching.current = false; continueFetching.current = false;
}; };
} else { } else {
//If the tab is not live, pvview, batteryview or configuration, then stop fetching.
continueFetching.current = false; continueFetching.current = false;
} }
}, [currentTab, location]); }, [currentTab, location]);

View File

@ -3,9 +3,171 @@ import { I_S3Credentials } from 'src/interfaces/S3Types';
import { FetchResult } from 'src/dataCache/dataCache'; import { FetchResult } from 'src/dataCache/dataCache';
import { DataRecord } from 'src/dataCache/data'; import { DataRecord } from 'src/dataCache/data';
import { S3Access } from 'src/dataCache/S3/S3Access'; import { S3Access } from 'src/dataCache/S3/S3Access';
import { parseChunk, parseCsv } from '../Log/graph.util'; import {
JSONRecordData,
parseChunk,
parseChunkJson,
parseCsv
} from '../Log/graph.util';
import JSZip from 'jszip'; import JSZip from 'jszip';
export const fetchDataJson = (
timestamp: UnixTime,
s3Credentials?: I_S3Credentials,
cutdigits?: boolean
): Promise<FetchResult<Record<string, JSONRecordData>>> => {
const s3Path = cutdigits
? `${timestamp.ticks.toString().slice(0, -2)}.json`
: `${timestamp.ticks}.json`;
if (s3Credentials && s3Credentials.s3Bucket) {
const s3Access = new S3Access(
s3Credentials.s3Bucket,
s3Credentials.s3Region,
s3Credentials.s3Provider,
s3Credentials.s3Key,
s3Credentials.s3Secret
);
return s3Access
.get(s3Path)
.then(async (r) => {
if (r.status === 404) {
return Promise.resolve(FetchResult.notAvailable);
} else if (r.status === 200) {
console.log('FOUND ITTTTTTTTTTTT');
const jsontext = await r.text(); // Assuming the server returns the Base64 encoded ZIP file as text
const byteArray = Uint8Array.from(atob(jsontext), (c) =>
c.charCodeAt(0)
);
//Decompress the byte array using JSZip
const zip = await JSZip.loadAsync(byteArray);
// Assuming the Json file is named "data.json" inside the ZIP archive
const jsonContent = await zip.file('data.json').async('text');
//console.log(jsonContent);
return parseChunkJson(jsonContent);
} else {
return Promise.resolve(FetchResult.notAvailable);
}
})
.catch((e) => {
return Promise.resolve(FetchResult.tryLater);
});
}
};
export const fetchAggregatedDataJson = (
date: string,
s3Credentials?: I_S3Credentials
): Promise<FetchResult<any>> => {
const s3Path = `${date}.json`;
if (s3Credentials && s3Credentials.s3Bucket) {
const s3Access = new S3Access(
s3Credentials.s3Bucket,
s3Credentials.s3Region,
s3Credentials.s3Provider,
s3Credentials.s3Key,
s3Credentials.s3Secret
);
return s3Access
.get(s3Path)
.then(async (r) => {
if (r.status === 404) {
return Promise.resolve(FetchResult.notAvailable);
} else if (r.status === 200) {
const jsontext = await r.text(); // Assuming the server returns the Base64 encoded ZIP file as text
const contentEncoding = r.headers.get('content-type');
if (contentEncoding != 'application/base64; charset=utf-8') {
return JSON.parse(jsontext);
}
const byteArray = Uint8Array.from(atob(jsontext), (c) =>
c.charCodeAt(0)
);
//Decompress the byte array using JSZip
const zip = await JSZip.loadAsync(byteArray);
// Assuming the CSV file is named "data.csv" inside the ZIP archive
const jsonContent = await zip.file('data.json').async('text');
// console.log(jsonContent);
return JSON.parse(jsonContent);
} else {
return Promise.resolve(FetchResult.notAvailable);
}
})
.catch((e) => {
return Promise.resolve(FetchResult.tryLater);
});
}
};
//--------------------------------------------------------------------------------------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------------------------------------------------------------------------------------
// For CSV manipulation, check the following functions
//--------------------------------------------------------------------------------------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------------------------------------------------------------------------------------
export const fetchData = (
timestamp: UnixTime,
s3Credentials?: I_S3Credentials,
cutdigits?: boolean
): Promise<FetchResult<Record<string, DataRecord>>> => {
const s3Path = cutdigits
? `${timestamp.ticks.toString().slice(0, -2)}.csv`
: `${timestamp.ticks}.csv`;
if (s3Credentials && s3Credentials.s3Bucket) {
const s3Access = new S3Access(
s3Credentials.s3Bucket,
s3Credentials.s3Region,
s3Credentials.s3Provider,
s3Credentials.s3Key,
s3Credentials.s3Secret
);
return s3Access
.get(s3Path)
.then(async (r) => {
if (r.status === 404) {
return Promise.resolve(FetchResult.notAvailable);
} else if (r.status === 200) {
console.log('FOUND ITTTTTTTTTTTT');
const csvtext = await r.text(); // Assuming the server returns the Base64 encoded ZIP file as text
const contentEncoding = r.headers.get('content-type');
//console.log(contentEncoding);
if (contentEncoding != 'application/base64; charset=utf-8') {
// console.log('uncompressed');
return parseChunk(csvtext);
}
const byteArray = Uint8Array.from(atob(csvtext), (c) =>
c.charCodeAt(0)
);
//Decompress the byte array using JSZip
const zip = await JSZip.loadAsync(byteArray);
// Assuming the CSV file is named "data.csv" inside the ZIP archive
const csvContent = await zip.file('data.csv').async('text');
//console.log(csvContent);
return parseChunk(csvContent);
} else {
return Promise.resolve(FetchResult.notAvailable);
}
})
.catch((e) => {
return Promise.resolve(FetchResult.tryLater);
});
}
};
export const fetchAggregatedData = ( export const fetchAggregatedData = (
date: string, date: string,
s3Credentials?: I_S3Credentials s3Credentials?: I_S3Credentials
@ -50,59 +212,3 @@ export const fetchAggregatedData = (
}); });
} }
}; };
export const fetchData = (
timestamp: UnixTime,
s3Credentials?: I_S3Credentials,
cutdigits?: boolean
): Promise<FetchResult<Record<string, DataRecord>>> => {
const s3Path = cutdigits
? `${timestamp.ticks.toString().slice(0, -2)}.csv`
: `${timestamp.ticks}.csv`;
if (s3Credentials && s3Credentials.s3Bucket) {
const s3Access = new S3Access(
s3Credentials.s3Bucket,
s3Credentials.s3Region,
s3Credentials.s3Provider,
s3Credentials.s3Key,
s3Credentials.s3Secret
);
return s3Access
.get(s3Path)
.then(async (r) => {
if (r.status === 404) {
return Promise.resolve(FetchResult.notAvailable);
} else if (r.status === 200) {
//console.log('FOUND ITTTTTTTTTTTT');
const csvtext = await r.text(); // Assuming the server returns the Base64 encoded ZIP file as text
const contentEncoding = r.headers.get('content-type');
//console.log(contentEncoding);
if (contentEncoding != 'application/base64; charset=utf-8') {
// console.log('uncompressed');
return parseChunk(csvtext);
}
const byteArray = Uint8Array.from(atob(csvtext), (c) =>
c.charCodeAt(0)
);
//Decompress the byte array using JSZip
const zip = await JSZip.loadAsync(byteArray);
// Assuming the CSV file is named "data.csv" inside the ZIP archive
const csvContent = await zip.file('data.csv').async('text');
//console.log(csvContent);
return parseChunk(csvContent);
} else {
return Promise.resolve(FetchResult.notAvailable);
}
})
.catch((e) => {
return Promise.resolve(FetchResult.tryLater);
});
}
};

View File

@ -50,21 +50,11 @@ function InstallationTabs() {
} else if (path[path.length - 2] === 'tree') { } else if (path[path.length - 2] === 'tree') {
setCurrentTab('tree'); setCurrentTab('tree');
} else { } else {
//Even if we are located at path: /batteryview/mainstats, we want the BatteryView tab to be bold //Even if we are located at path: /batteryview/mainstats, we want the BatteryViewSalidomo tab to be bold
setCurrentTab(path.find((pathElement) => tabList.includes(pathElement))); setCurrentTab(path.find((pathElement) => tabList.includes(pathElement)));
} }
}, [location]); }, [location]);
// useEffect(() => {
// if (salimaxInstallations && salimaxInstallations.length > 0) {
// if (socket) {
// closeSocket();
// }
//
// openSocket(salimaxInstallations);
// }
// }, [salimaxInstallations]);
useEffect(() => { useEffect(() => {
if (salimaxInstallations.length === 0) { if (salimaxInstallations.length === 0) {
fetchAllInstallations(); fetchAllInstallations();
@ -75,7 +65,7 @@ function InstallationTabs() {
if (salimaxInstallations && salimaxInstallations.length > 0) { if (salimaxInstallations && salimaxInstallations.length > 0) {
if (!socket) { if (!socket) {
openSocket(0); openSocket(0);
} else if (currentProduct == 1) { } else if (currentProduct != 0) {
closeSocket(); closeSocket();
openSocket(0); openSocket(0);
} }

View File

@ -1,3 +1,112 @@
// The interface for each device in the Battery object
interface Leds {
Blue: { value: string };
Amber: { value: string };
Green: { value: string };
Red: { value: string };
}
interface Dc {
Current: { value: number };
Voltage: { value: number };
Power: { value: number };
}
interface Temperatures {
Cells: {
Average: { value: number };
};
}
interface IoStatus {
ConnectedToDcBus: { value: boolean };
AuxRelayBus: { value: boolean };
AlarmOutActive: { value: boolean };
InternalFanActive: { value: boolean };
RemoteStateActive: { value: boolean };
VoltMeasurementAllowed: { value: boolean };
RiscActive: { value: boolean };
}
interface BatteryStrings {
String1Active: { value: string };
String2Active: { value: string };
String3Active: { value: string };
String4Active: { value: string };
String5Active: { value: string };
}
export interface Device {
Leds: Leds;
Eoc: { value: boolean };
Soc: { value: number };
SerialNumber: { value: string };
TimeSinceTOC: { value: string };
MaxChargePower: { value: number };
CellsCurrent: { value: number };
SOCAh: { value: number };
Dc: Dc;
FwVersion: { value: string };
HeatingCurrent: { value: number };
MaxDischargePower: { value: number };
Temperatures: Temperatures;
BusCurrent: { value: number };
HeatingPower: { value: number };
IoStatus: IoStatus;
BatteryStrings: BatteryStrings;
Alarms: number;
Warnings: number;
}
// The interface for the Battery structure, with dynamic keys (Device IDs)
export interface JSONRecordData {
Battery: {
Devices: {
[deviceId: string]: Device; // Device ID as the key
};
};
Config: {
Devices: {
BatteryNodes: {
value: string[];
};
};
};
}
export const parseChunkJson = (
text: string
): Record<string, JSONRecordData> => {
const lines = text.split(/\r?\n/).filter((line) => line.length > 0);
let result: Record<string, any> = {};
let currentTimestamp = null;
lines.forEach((line) => {
//console.log(line);
const fields = line.split(';');
if (fields[0] === 'Timestamp') {
currentTimestamp = fields[1];
result[currentTimestamp] = {};
} else if (currentTimestamp) {
result[currentTimestamp] = JSON.parse(line);
}
});
return result;
};
//--------------------------------------------------------------------------------------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------------------------------------------------------------------------------------
// For CSV manipulation, check the following functions
//--------------------------------------------------------------------------------------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------------------------------------------------------------------------------------
import { DataPoint, DataRecord } from 'src/dataCache/data'; import { DataPoint, DataRecord } from 'src/dataCache/data';
export interface I_CsvEntry { export interface I_CsvEntry {

View File

@ -1,4 +1,12 @@
import { Box, Card, Container, Grid, Modal, Typography } from '@mui/material'; import {
Box,
Card,
Container,
Grid,
Modal,
TextField,
Typography
} from '@mui/material';
import ReactApexChart from 'react-apexcharts'; import ReactApexChart from 'react-apexcharts';
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { I_S3Credentials } from 'src/interfaces/S3Types'; import { I_S3Credentials } from 'src/interfaces/S3Types';
@ -13,12 +21,13 @@ import {
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import CircularProgress from '@mui/material/CircularProgress'; import CircularProgress from '@mui/material/CircularProgress';
import { DateTimePicker, LocalizationProvider } from '@mui/x-date-pickers'; import { LocalizationProvider } from '@mui/x-date-pickers';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { UserContext } from '../../../contexts/userContext'; import { UserContext } from '../../../contexts/userContext';
import { UserType } from '../../../interfaces/UserTypes'; import { UserType } from '../../../interfaces/UserTypes';
import { TimeSpan, UnixTime } from '../../../dataCache/time'; import { TimeSpan, UnixTime } from '../../../dataCache/time';
import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker';
interface OverviewProps { interface OverviewProps {
s3Credentials: I_S3Credentials; s3Credentials: I_S3Credentials;
@ -73,6 +82,13 @@ function Overview(props: OverviewProps) {
const [endDate, setEndDate] = useState(dayjs()); const [endDate, setEndDate] = useState(dayjs());
const [isZooming, setIsZooming] = useState(false); const [isZooming, setIsZooming] = useState(false);
console.log(
UnixTime.fromTicks(new Date().getTime() / 1000).earlier(
TimeSpan.fromDays(1)
)
);
console.log(UnixTime.fromTicks(new Date().getTime() / 1000));
useEffect(() => { useEffect(() => {
if (isZooming) { if (isZooming) {
setLoading(true); setLoading(true);
@ -408,19 +424,31 @@ function Overview(props: OverviewProps) {
label="Select Start Date" label="Select Start Date"
value={startDate} value={startDate}
onChange={(newDate) => setStartDate(newDate)} onChange={(newDate) => setStartDate(newDate)}
renderInput={(params) => (
<TextField
{...params}
sx={{ sx={{
marginTop: 2 marginTop: 2, // Apply styles here
width: '100%' // Optional: You can adjust the width or other styling here
}} }}
/> />
)}
/>
<DateTimePicker <DateTimePicker
label="Select End Date" label="Select End Date"
value={endDate} value={endDate}
onChange={(newDate) => setEndDate(newDate)} onChange={(newDate) => setEndDate(newDate)}
renderInput={(params) => (
<TextField
{...params}
sx={{ sx={{
marginTop: 2 marginTop: 2, // Apply styles here
width: '100%' // Optional: You can adjust the width or other styling here
}} }}
/> />
)}
/>
<div <div
style={{ style={{

View File

@ -1,4 +1,12 @@
import { Box, Card, Container, Grid, Modal, Typography } from '@mui/material'; import {
Box,
Card,
Container,
Grid,
Modal,
TextField,
Typography
} from '@mui/material';
import ReactApexChart from 'react-apexcharts'; import ReactApexChart from 'react-apexcharts';
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { I_S3Credentials } from 'src/interfaces/S3Types'; import { I_S3Credentials } from 'src/interfaces/S3Types';
@ -6,14 +14,14 @@ import { getChartOptions } from './chartOptions';
import { import {
chartAggregatedDataInterface, chartAggregatedDataInterface,
overviewInterface, overviewInterface,
transformInputToAggregatedData transformInputToAggregatedDataJson
} from 'src/interfaces/Chart'; } from 'src/interfaces/Chart';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import CircularProgress from '@mui/material/CircularProgress'; import CircularProgress from '@mui/material/CircularProgress';
import { DateTimePicker, LocalizationProvider } from '@mui/x-date-pickers'; import { DateTimePicker, LocalizationProvider } from '@mui/x-date-pickers';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import dayjs from 'dayjs'; import dayjs, { Dayjs } from 'dayjs';
import { UserContext } from '../../../contexts/userContext'; import { UserContext } from '../../../contexts/userContext';
import { UserType } from '../../../interfaces/UserTypes'; import { UserType } from '../../../interfaces/UserTypes';
@ -37,6 +45,546 @@ const computeLast7Days = (): string[] => {
} }
return last7Days; return last7Days;
}; };
//
// function SalidomoOverview(props: salidomoOverviewProps) {
// const context = useContext(UserContext);
// const { currentUser } = context;
// const [loading, setLoading] = useState(true);
// const [aggregatedChartState, setAggregatedChartState] = useState(0);
// const [isDateModalOpen, setIsDateModalOpen] = useState(false);
// const [isErrorDateModalOpen, setErrorDateModalOpen] = useState(false);
// const [dateSelectionError, setDateSelectionError] = useState('');
// const [dateOpen, setDateOpen] = useState(false);
//
// const [aggregatedDataArray, setAggregatedDataArray] = useState<
// {
// chartData: chartAggregatedDataInterface;
// chartOverview: overviewInterface;
// datelist: any[];
// netbalance: any[];
// }[]
// >([]);
//
// const [startDate, setStartDate] = useState(dayjs().add(-1, 'day'));
// const [endDate, setEndDate] = useState(dayjs());
//
// useEffect(() => {
// handleWeekData();
// }, []);
//
// const handleWeekData = () => {
// setAggregatedChartState(0);
//
// if (
// aggregatedDataArray[aggregatedChartState] &&
// aggregatedDataArray[aggregatedChartState].chartData != null
// ) {
// return;
// }
// setLoading(true);
//
// const resultPromise: Promise<{
// chartAggregatedData: chartAggregatedDataInterface;
// chartOverview: overviewInterface;
// dateList: string[];
// }> = transformInputToAggregatedData(
// props.s3Credentials,
// dayjs().subtract(1, 'week'),
// dayjs()
// );
//
// resultPromise
// .then((result) => {
// const powerDifference = [];
// for (
// let i = 0;
// i < result.chartAggregatedData.gridImportPower.data.length;
// i++
// ) {
// powerDifference.push(
// result.chartAggregatedData.gridImportPower.data[i] -
// Math.abs(result.chartAggregatedData.gridExportPower.data[i])
// );
// }
//
// setAggregatedDataArray((prevData) =>
// prevData.concat({
// chartData: result.chartAggregatedData,
// chartOverview: result.chartOverview,
// datelist: result.dateList,
// netbalance: powerDifference
// })
// );
//
// setAggregatedChartState(aggregatedDataArray.length);
// setLoading(false);
// })
// .catch((error) => {
// console.error('Error:', error);
// });
// };
//
// const handleSetDate = () => {
// setDateOpen(true);
// setIsDateModalOpen(true);
// };
//
// const handleOkOnErrorDateModal = () => {
// setErrorDateModalOpen(false);
// };
//
// const handleCancel = () => {
// setIsDateModalOpen(false);
// setDateOpen(false);
// };
//
// const handleConfirm = () => {
// setIsDateModalOpen(false);
// setDateOpen(false);
//
// if (endDate.isAfter(dayjs())) {
// setDateSelectionError('You cannot ask for future data');
// setErrorDateModalOpen(true);
// return;
// } else if (startDate.isAfter(endDate)) {
// setDateSelectionError('Εnd date must precede start date');
// setErrorDateModalOpen(true);
// return;
// }
// setLoading(true);
//
// const resultPromise: Promise<{
// chartAggregatedData: chartAggregatedDataInterface;
// chartOverview: overviewInterface;
// dateList: string[];
// }> = transformInputToAggregatedData(
// props.s3Credentials,
// startDate,
// endDate
// );
//
// resultPromise
// .then((result) => {
// const powerDifference = [];
//
// for (
// let i = 0;
// i < result.chartAggregatedData.gridImportPower.data.length;
// i++
// ) {
// powerDifference.push(
// result.chartAggregatedData.gridImportPower.data[i] -
// Math.abs(result.chartAggregatedData.gridExportPower.data[i])
// );
// }
//
// setAggregatedDataArray((prevData) =>
// prevData.concat({
// chartData: result.chartAggregatedData,
// chartOverview: result.chartOverview,
// datelist: result.dateList,
// netbalance: powerDifference
// })
// );
//
// setAggregatedChartState(aggregatedDataArray.length);
// setLoading(false);
// })
// .catch((error) => {
// console.error('Error:', error);
// });
// };
//
// const handleGoBack = () => {
// if (aggregatedChartState > 0) {
// setAggregatedChartState(aggregatedChartState - 1);
// }
// };
//
// const handleGoForward = () => {
// if (aggregatedChartState + 1 < aggregatedDataArray.length) {
// setAggregatedChartState(aggregatedChartState + 1);
// }
// };
//
// const renderGraphs = () => {
// return (
// <Container maxWidth="xl">
// {isErrorDateModalOpen && (
// <Modal open={isErrorDateModalOpen} onClose={() => {}}>
// <Box
// sx={{
// position: 'absolute',
// top: '50%',
// left: '50%',
// transform: 'translate(-50%, -50%)',
// width: 450,
// bgcolor: 'background.paper',
// borderRadius: 4,
// boxShadow: 24,
// p: 4,
// display: 'flex',
// flexDirection: 'column',
// alignItems: 'center'
// }}
// >
// <Typography
// variant="body1"
// gutterBottom
// sx={{ fontWeight: 'bold' }}
// >
// {dateSelectionError}
// </Typography>
//
// <Button
// sx={{
// marginTop: 2,
// textTransform: 'none',
// bgcolor: '#ffc04d',
// color: '#111111',
// '&:hover': { bgcolor: '#f7b34d' }
// }}
// onClick={handleOkOnErrorDateModal}
// >
// Ok
// </Button>
// </Box>
// </Modal>
// )}
//
// {isDateModalOpen && (
// <LocalizationProvider dateAdapter={AdapterDayjs}>
// <Modal open={isDateModalOpen} onClose={() => {}}>
// <Box
// sx={{
// position: 'absolute',
// top: '50%',
// left: '50%',
// transform: 'translate(-50%, -50%)',
// width: 450,
// bgcolor: 'background.paper',
// borderRadius: 4,
// boxShadow: 24,
// p: 4,
// display: 'flex',
// flexDirection: 'column',
// alignItems: 'center'
// }}
// >
// <DateTimePicker
// label="Select Start Date"
// value={startDate}
// onChange={(newDate: Dayjs | null) => {
// // Type assertion to Dayjs
// if (newDate) {
// setStartDate(newDate);
// }
// }}
// renderInput={(props) => <TextField {...props} />}
// />
//
// <DateTimePicker
// label="Select End Date"
// value={endDate}
// onChange={(newDate: Dayjs | null) => {
// // Type assertion to Dayjs
// if (newDate) {
// setEndDate(newDate);
// }
// }}
// renderInput={(props) => <TextField {...props} />}
// />
//
// <div
// style={{
// display: 'flex',
// alignItems: 'center',
// marginTop: 10
// }}
// >
// <Button
// sx={{
// marginTop: 2,
// textTransform: 'none',
// bgcolor: '#ffc04d',
// color: '#111111',
// '&:hover': { bgcolor: '#f7b34d' }
// }}
// onClick={handleConfirm}
// >
// Confirm
// </Button>
//
// <Button
// sx={{
// marginTop: 2,
// marginLeft: 2,
// textTransform: 'none',
// bgcolor: '#ffc04d',
// color: '#111111',
// '&:hover': { bgcolor: '#f7b34d' }
// }}
// onClick={handleCancel}
// >
// Cancel
// </Button>
// </div>
// </Box>
// </Modal>
// </LocalizationProvider>
// )}
// <Grid container>
// <Grid item xs={6} md={6}>
// <Button
// variant="contained"
// onClick={handleSetDate}
// disabled={loading}
// sx={{
// marginTop: '20px',
// marginLeft: '10px',
// backgroundColor: dateOpen ? '#808080' : '#ffc04d',
// color: '#000000',
// '&:hover': { bgcolor: '#f7b34d' }
// }}
// >
// <FormattedMessage id="set_date" defaultMessage="Set Date" />
// </Button>
// </Grid>
//
// <Grid
// container
// justifyContent="flex-end"
// alignItems="center"
// item
// xs={6}
// md={6}
// >
// <Button
// variant="contained"
// disabled={!(aggregatedChartState > 0)}
// onClick={handleGoBack}
// sx={{
// marginTop: '20px',
// marginLeft: '10px',
// backgroundColor: '#ffc04d',
// color: '#000000',
// '&:hover': { bgcolor: '#f7b34d' }
// }}
// >
// <FormattedMessage id="goback" defaultMessage="Zoom out" />
// </Button>
//
// <Button
// variant="contained"
// disabled={
// !(aggregatedChartState < aggregatedDataArray.length - 1)
// }
// onClick={handleGoForward}
// sx={{
// marginTop: '20px',
// marginLeft: '10px',
// backgroundColor: '#ffc04d',
// color: '#000000',
// '&:hover': { bgcolor: '#f7b34d' }
// }}
// >
// <FormattedMessage id="goback" defaultMessage="Zoom in" />
// </Button>
// </Grid>
//
// {loading && (
// <Container
// maxWidth="xl"
// sx={{
// display: 'flex',
// flexDirection: 'column',
// justifyContent: 'center',
// alignItems: 'center',
// height: '100vh'
// }}
// >
// <CircularProgress size={60} style={{ color: '#ffc04d' }} />
// <Typography variant="body2" style={{ color: 'black' }} mt={2}>
// Fetching data...
// </Typography>
// </Container>
// )}
//
// {!loading && (
// <Grid item xs={12} md={12}>
// <Grid
// container
// direction="row"
// justifyContent="center"
// alignItems="stretch"
// spacing={3}
// >
// <Grid item md={6} xs={12}>
// <Card
// sx={{
// overflow: 'visible',
// marginTop: '30px',
// marginBottom: '30px'
// }}
// >
// <Box
// sx={{
// marginLeft: '20px'
// }}
// >
// <Box display="flex" alignItems="center">
// <Box>
// <Typography variant="subtitle1" noWrap>
// <FormattedMessage
// id="battery_soc"
// defaultMessage="Battery SOC (State Of Charge)"
// />
// </Typography>
// </Box>
// </Box>
// <Box
// sx={{
// display: 'flex',
// alignItems: 'center',
// justifyContent: 'flex-start',
// pt: 3
// }}
// ></Box>
// </Box>
//
// <ReactApexChart
// options={{
// ...getChartOptions(
// aggregatedDataArray[aggregatedChartState]
// .chartOverview.soc,
// 'weekly',
// aggregatedDataArray[aggregatedChartState].datelist,
// true
// )
// }}
// series={[
// {
// ...aggregatedDataArray[aggregatedChartState].chartData
// .minsoc,
// color: '#69d2e7'
// },
// {
// ...aggregatedDataArray[aggregatedChartState].chartData
// .maxsoc,
// color: '#008FFB'
// }
// ]}
// type="bar"
// height={400}
// />
// </Card>
// </Grid>
// <Grid item md={6} xs={12}>
// <Card
// sx={{
// overflow: 'visible',
// marginTop: '30px',
// marginBottom: '30px'
// }}
// >
// <Box
// sx={{
// marginLeft: '20px'
// }}
// >
// <Box display="flex" alignItems="center">
// <Box>
// <Typography variant="subtitle1" noWrap>
// <FormattedMessage
// id="battery_power"
// defaultMessage={'Battery Energy'}
// />
// </Typography>
// </Box>
// </Box>
// <Box
// sx={{
// display: 'flex',
// alignItems: 'center',
// justifyContent: 'flex-start',
// pt: 3
// }}
// ></Box>
// </Box>
//
// {currentUser.userType == UserType.admin && (
// <ReactApexChart
// options={{
// ...getChartOptions(
// aggregatedDataArray[aggregatedChartState]
// .chartOverview.dcPower,
// 'weekly',
// aggregatedDataArray[aggregatedChartState].datelist,
// false
// )
// }}
// series={[
// {
// ...aggregatedDataArray[aggregatedChartState]
// .chartData.dcChargingPower,
// color: '#008FFB'
// },
// {
// ...aggregatedDataArray[aggregatedChartState]
// .chartData.heatingPower,
// color: '#ff9900'
// },
// {
// ...aggregatedDataArray[aggregatedChartState]
// .chartData.dcDischargingPower,
// color: '#69d2e7'
// }
// ]}
// type="bar"
// height={400}
// />
// )}
//
// {currentUser.userType == UserType.client && (
// <ReactApexChart
// options={{
// ...getChartOptions(
// aggregatedDataArray[aggregatedChartState]
// .chartOverview.dcPowerWithoutHeating,
// 'weekly',
// aggregatedDataArray[aggregatedChartState].datelist,
// true
// )
// }}
// series={[
// {
// ...aggregatedDataArray[aggregatedChartState]
// .chartData.dcChargingPower,
// color: '#008FFB'
// },
//
// {
// ...aggregatedDataArray[aggregatedChartState]
// .chartData.dcDischargingPower,
// color: '#69d2e7'
// }
// ]}
// type="bar"
// height={400}
// />
// )}
// </Card>
// </Grid>
// </Grid>
// </Grid>
// )}
// </Grid>
// </Container>
// );
// };
//
// return <>{renderGraphs()}</>;
// }
function SalidomoOverview(props: salidomoOverviewProps) { function SalidomoOverview(props: salidomoOverviewProps) {
const context = useContext(UserContext); const context = useContext(UserContext);
@ -79,7 +627,7 @@ function SalidomoOverview(props: salidomoOverviewProps) {
chartAggregatedData: chartAggregatedDataInterface; chartAggregatedData: chartAggregatedDataInterface;
chartOverview: overviewInterface; chartOverview: overviewInterface;
dateList: string[]; dateList: string[];
}> = transformInputToAggregatedData( }> = transformInputToAggregatedDataJson(
props.s3Credentials, props.s3Credentials,
dayjs().subtract(1, 'week'), dayjs().subtract(1, 'week'),
dayjs() dayjs()
@ -149,7 +697,7 @@ function SalidomoOverview(props: salidomoOverviewProps) {
chartAggregatedData: chartAggregatedDataInterface; chartAggregatedData: chartAggregatedDataInterface;
chartOverview: overviewInterface; chartOverview: overviewInterface;
dateList: string[]; dateList: string[];
}> = transformInputToAggregatedData( }> = transformInputToAggregatedDataJson(
props.s3Credentials, props.s3Credentials,
startDate, startDate,
endDate endDate
@ -266,19 +814,25 @@ function SalidomoOverview(props: salidomoOverviewProps) {
<DateTimePicker <DateTimePicker
label="Select Start Date" label="Select Start Date"
value={startDate} value={startDate}
onChange={(newDate) => setStartDate(newDate)} onChange={(newDate: Dayjs | null) => {
sx={{ // Type assertion to Dayjs
marginTop: 2 if (newDate) {
setStartDate(newDate);
}
}} }}
renderInput={(props) => <TextField {...props} />}
/> />
<DateTimePicker <DateTimePicker
label="Select End Date" label="Select End Date"
value={endDate} value={endDate}
onChange={(newDate) => setEndDate(newDate)} onChange={(newDate: Dayjs | null) => {
sx={{ // Type assertion to Dayjs
marginTop: 2 if (newDate) {
setEndDate(newDate);
}
}} }}
renderInput={(props) => <TextField {...props} />}
/> />
<div <div

View File

@ -10,16 +10,13 @@ import { I_Installation } from 'src/interfaces/InstallationTypes';
import { UserContext } from 'src/contexts/userContext'; import { UserContext } from 'src/contexts/userContext';
import { TimeSpan, UnixTime } from 'src/dataCache/time'; import { TimeSpan, UnixTime } from 'src/dataCache/time';
import { FetchResult } from 'src/dataCache/dataCache'; import { FetchResult } from 'src/dataCache/dataCache';
import { import { JSONRecordData } from 'src/content/dashboards/Log/graph.util';
extractValues,
TopologyValues
} from 'src/content/dashboards/Log/graph.util';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { fetchData } from 'src/content/dashboards/Installations/fetchData'; import { fetchDataJson } from 'src/content/dashboards/Installations/fetchData';
import { Navigate, Route, Routes, useLocation } from 'react-router-dom'; import { Navigate, Route, Routes, useLocation } from 'react-router-dom';
import routes from '../../../Resources/routes.json'; import routes from '../../../Resources/routes.json';
import InformationSalidomo from '../Information/InformationSalidomo'; import InformationSalidomo from '../Information/InformationSalidomo';
import BatteryView from '../BatteryView/BatteryView'; import BatteryViewSalidomo from '../BatteryView/BatteryViewSalidomo';
import Log from '../Log/Log'; import Log from '../Log/Log';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
import SalidomoOverview from '../Overview/salidomoOverview'; import SalidomoOverview from '../Overview/salidomoOverview';
@ -40,7 +37,7 @@ function SalidomoInstallation(props: singleInstallationProps) {
const location = useLocation().pathname; const location = useLocation().pathname;
const [errorLoadingS3Data, setErrorLoadingS3Data] = useState(false); const [errorLoadingS3Data, setErrorLoadingS3Data] = useState(false);
const [currentTab, setCurrentTab] = useState<string>(undefined); const [currentTab, setCurrentTab] = useState<string>(undefined);
const [values, setValues] = useState<TopologyValues | null>(null); const [values, setValues] = useState<JSONRecordData | null>(null);
const status = props.current_installation.status; const status = props.current_installation.status;
const [ const [
failedToCommunicateWithInstallation, failedToCommunicateWithInstallation,
@ -77,7 +74,7 @@ function SalidomoInstallation(props: singleInstallationProps) {
const fetchDataPeriodically = async () => { const fetchDataPeriodically = async () => {
var timeperiodToSearch = 30; var timeperiodToSearch = 30;
let res; let res: FetchResult<Record<string, JSONRecordData>> | undefined;
let timestampToFetch; let timestampToFetch;
for (var i = 0; i < timeperiodToSearch; i += 1) { for (var i = 0; i < timeperiodToSearch; i += 1) {
@ -88,7 +85,7 @@ function SalidomoInstallation(props: singleInstallationProps) {
console.log('timestamp to fetch is ' + timestampToFetch); console.log('timestamp to fetch is ' + timestampToFetch);
try { try {
res = await fetchData(timestampToFetch, s3Credentials, true); res = await fetchDataJson(timestampToFetch, s3Credentials, true);
if (res !== FetchResult.notAvailable && res !== FetchResult.tryLater) { if (res !== FetchResult.notAvailable && res !== FetchResult.tryLater) {
break; break;
} }
@ -114,15 +111,16 @@ function SalidomoInstallation(props: singleInstallationProps) {
return false; return false;
} }
console.log(`Timestamp: ${timestamp}`); console.log(`Timestamp: ${timestamp}`);
console.log(res[timestamp]); //console.log('object is' + res[timestamp]);
// Set values asynchronously with delay // Set values asynchronously with delay
setValues( setValues(res[timestamp]);
extractValues({ // setValues(
time: UnixTime.fromTicks(parseInt(timestamp, 10)), // extractValues({
value: res[timestamp] // time: UnixTime.fromTicks(parseInt(timestamp, 10)),
}) // value: res[timestamp]
); // })
// );
// Wait for 2 seconds before processing next timestamp // Wait for 2 seconds before processing next timestamp
await timeout(2000); await timeout(2000);
} }
@ -137,7 +135,7 @@ function SalidomoInstallation(props: singleInstallationProps) {
try { try {
console.log('Trying to fetch timestamp ' + timestampToFetch); console.log('Trying to fetch timestamp ' + timestampToFetch);
res = await fetchData(timestampToFetch, s3Credentials, true); res = await fetchDataJson(timestampToFetch, s3Credentials, true);
if ( if (
res !== FetchResult.notAvailable && res !== FetchResult.notAvailable &&
res !== FetchResult.tryLater res !== FetchResult.tryLater
@ -363,13 +361,13 @@ function SalidomoInstallation(props: singleInstallationProps) {
<Route <Route
path={routes.batteryview + '*'} path={routes.batteryview + '*'}
element={ element={
<BatteryView <BatteryViewSalidomo
values={values} values={values}
s3Credentials={s3Credentials} s3Credentials={s3Credentials}
installationId={props.current_installation.id} installationId={props.current_installation.id}
productNum={props.current_installation.product} productNum={props.current_installation.product}
connected={connected} connected={connected}
></BatteryView> ></BatteryViewSalidomo>
} }
></Route> ></Route>

View File

@ -40,9 +40,7 @@ function SalidomoInstallationTabs() {
} = useContext(InstallationsContext); } = useContext(InstallationsContext);
const { product, setProduct } = useContext(ProductIdContext); const { product, setProduct } = useContext(ProductIdContext);
// const webSocketsContext = useContext(WebSocketContext); //The following useEffect is for the current tab to be bold. Based on the location, we set the corresponding tab to be bold
// const { socket, openSocket, closeSocket } = webSocketsContext;
useEffect(() => { useEffect(() => {
let path = location.pathname.split('/'); let path = location.pathname.split('/');
@ -51,12 +49,13 @@ function SalidomoInstallationTabs() {
} else if (path[path.length - 2] === 'tree') { } else if (path[path.length - 2] === 'tree') {
setCurrentTab('tree'); setCurrentTab('tree');
} else { } else {
//Even if we are located at path: /batteryview/mainstats, we want the BatteryView tab to be bold //Even if we are located at path: /batteryview/mainstats, we want the BatteryViewSalidomo tab to be bold
setCurrentTab(path.find((pathElement) => tabList.includes(pathElement))); setCurrentTab(path.find((pathElement) => tabList.includes(pathElement)));
} }
}, [location]); }, [location]);
useEffect(() => { useEffect(() => {
//The first time this component will be loaded, it needs to call the fetchAllSalidomoInstallations function from the InstallationsContextProvider
if (salidomoInstallations.length === 0 && fetchedInstallations === false) { if (salidomoInstallations.length === 0 && fetchedInstallations === false) {
fetchAllSalidomoInstallations(); fetchAllSalidomoInstallations();
setFetchedInstallations(true); setFetchedInstallations(true);
@ -64,10 +63,12 @@ function SalidomoInstallationTabs() {
}, [salidomoInstallations]); }, [salidomoInstallations]);
useEffect(() => { useEffect(() => {
//Since we know the ids of the installations we have access to, we need to open a web socket with the backend.
if (salidomoInstallations && salidomoInstallations.length > 0) { if (salidomoInstallations && salidomoInstallations.length > 0) {
if (!socket) { if (!socket) {
openSocket(1); openSocket(1);
} else if (currentProduct == 0) { } else if (currentProduct != 1) {
//If there is any other open websocket for another product, close it.
closeSocket(); closeSocket();
openSocket(1); openSocket(1);
} }
@ -82,6 +83,7 @@ function SalidomoInstallationTabs() {
setCurrentTab(value); setCurrentTab(value);
}; };
//Manually compute the path when the user clicks a tab
const navigateToTabPath = (pathname: string, tab_value: string): string => { const navigateToTabPath = (pathname: string, tab_value: string): string => {
let pathlist = pathname.split('/'); let pathlist = pathname.split('/');
let ret_path = ''; let ret_path = '';

View File

@ -25,8 +25,6 @@ interface FlatInstallationViewProps {
} }
const FlatInstallationView = (props: FlatInstallationViewProps) => { const FlatInstallationView = (props: FlatInstallationViewProps) => {
// const webSocketContext = useContext(WebSocketContext);
// const { getSortedInstallations } = webSocketContext;
const navigate = useNavigate(); const navigate = useNavigate();
const [selectedInstallation, setSelectedInstallation] = useState<number>(-1); const [selectedInstallation, setSelectedInstallation] = useState<number>(-1);
const currentLocation = useLocation(); const currentLocation = useLocation();
@ -57,7 +55,7 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
routes.installation + routes.installation +
`${installationID}` + `${installationID}` +
'/' + '/' +
routes.information, routes.live,
{ {
replace: true replace: true
} }

View File

@ -1,4 +1,4 @@
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useRef, useState } from 'react';
import { import {
Card, Card,
CircularProgress, CircularProgress,
@ -20,6 +20,7 @@ import BuildIcon from '@mui/icons-material/Build';
import AccessContextProvider from '../../../contexts/AccessContextProvider'; import AccessContextProvider from '../../../contexts/AccessContextProvider';
import Access from '../ManageAccess/Access'; import Access from '../ManageAccess/Access';
import InformationSodioHome from '../Information/InformationSodioHome'; import InformationSodioHome from '../Information/InformationSodioHome';
import CryptoJS from 'crypto-js';
interface singleInstallationProps { interface singleInstallationProps {
current_installation?: I_Installation; current_installation?: I_Installation;
@ -27,6 +28,9 @@ interface singleInstallationProps {
} }
function SodioHomeInstallation(props: singleInstallationProps) { function SodioHomeInstallation(props: singleInstallationProps) {
if (props.current_installation == undefined) {
return null;
}
const context = useContext(UserContext); const context = useContext(UserContext);
const { currentUser } = context; const { currentUser } = context;
const location = useLocation().pathname; const location = useLocation().pathname;
@ -40,17 +44,97 @@ function SodioHomeInstallation(props: singleInstallationProps) {
] = useState(0); ] = useState(0);
const [connected, setConnected] = useState(true); const [connected, setConnected] = useState(true);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
if (props.current_installation == undefined) {
return null;
}
const [fetchFunctionCalled, setFetchFunctionCalled] = useState(false); const [fetchFunctionCalled, setFetchFunctionCalled] = useState(false);
//In React, useRef creates a mutable object that persists across renders without triggering re-renders when its value changes.
//While fetching, we check the value of continueFetching, if its false, we break. This means that either the user changed tab or the object has been finished its execution (return)
const continueFetching = useRef(false);
function timeout(delay: number) { function timeout(delay: number) {
return new Promise((res) => setTimeout(res, delay)); return new Promise((res) => setTimeout(res, delay));
} }
const API_BASE_URL =
'https://www.biwattpower.com/gateway/admin/open/device/currentEnergyFlowData/';
async function encryptAES_CBC(data: string, secretKey: string) {
const encoder = new TextEncoder();
const keyBuffer = encoder.encode(secretKey.padEnd(32, ' ')); // Ensure 256-bit key
const cryptoKey = await crypto.subtle.importKey(
'raw',
keyBuffer,
{ name: 'AES-CBC' },
false,
['encrypt']
);
// Generate a random IV (initialization vector)
const iv = crypto.getRandomValues(new Uint8Array(16));
const dataBuffer = encoder.encode(
data.padEnd(Math.ceil(data.length / 16) * 16, '\0')
);
const encryptedBuffer = await crypto.subtle.encrypt(
{ name: 'AES-CBC', iv },
cryptoKey,
dataBuffer
);
// Convert encrypted data + IV to Base64
const encryptedArray = new Uint8Array(encryptedBuffer);
const combined = new Uint8Array(iv.length + encryptedArray.length);
combined.set(iv);
combined.set(encryptedArray, iv.length);
return btoa(String.fromCharCode(...combined));
}
const fetchDataPeriodically = async () => {
while (continueFetching.current) {
//Fetch data from Bitwatt cloud
console.log('Fetching from Bitwatt cloud');
console.log(props.current_installation.serialNumber);
console.log(props.current_installation.s3WriteKey);
console.log(props.current_installation.s3WriteSecret);
const timeStamp = Date.now().toString();
// Encrypt timestamp using AES-ECB with PKCS7 padding
const key = CryptoJS.enc.Utf8.parse(
props.current_installation.s3WriteSecret
);
const encrypted = CryptoJS.AES.encrypt(timeStamp, key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
}).toString();
// Set headers
const headers = {
'X-Signature': encrypted,
'X-AccessKey': props.current_installation.s3WriteKey,
'Content-Type': 'application/json'
};
// API URL
const url = `https://www.biwattpower.com/gateway/admin/open/device/currentEnergyFlowData/${props.current_installation.serialNumber}`;
try {
const response = await fetch(url, { method: 'GET', headers });
const result = await response.json();
console.log('API Response:', result);
} catch (error) {
console.error('Request failed:', error);
}
// Wait for 2 seconds before fetching again
await timeout(200000);
console.log('ssssssssssssssssssssssssssssssssssssss');
}
setFetchFunctionCalled(false);
};
useEffect(() => { useEffect(() => {
let path = location.split('/'); let path = location.split('/');
setCurrentTab(path[path.length - 1]); setCurrentTab(path[path.length - 1]);
@ -62,6 +146,33 @@ function SodioHomeInstallation(props: singleInstallationProps) {
} }
}, [status]); }, [status]);
useEffect(() => {
console.log(currentTab);
if (currentTab == 'live' || location.includes('batteryview')) {
//Fetch periodically if the tab is live or batteryview
if (
currentTab == 'live' ||
(location.includes('batteryview') && !location.includes('mainstats'))
) {
if (!continueFetching.current) {
continueFetching.current = true;
//Call the function only one time. When the location and the currentTab change, this useEffect will be called 2 times
if (!fetchFunctionCalled) {
setFetchFunctionCalled(true);
fetchDataPeriodically();
}
}
}
return () => {
continueFetching.current = false;
};
} else {
//If the tab is not live, pvview, batteryview or configuration, then stop fetching.
continueFetching.current = false;
}
}, [currentTab, location]);
return ( return (
<> <>
<Grid item xs={12} md={12}> <Grid item xs={12} md={12}>
@ -228,6 +339,18 @@ function SodioHomeInstallation(props: singleInstallationProps) {
} }
/> />
<Route
path={routes.live}
element={
<div></div>
// <Topology
// values={values}
// connected={connected}
// loading={loading}
// ></Topology>
}
/>
{/*<Route*/} {/*<Route*/}
{/* path={routes.overview}*/} {/* path={routes.overview}*/}
{/* element={*/} {/* element={*/}
@ -241,13 +364,13 @@ function SodioHomeInstallation(props: singleInstallationProps) {
{/*<Route*/} {/*<Route*/}
{/* path={routes.batteryview + '*'}*/} {/* path={routes.batteryview + '*'}*/}
{/* element={*/} {/* element={*/}
{/* <BatteryView*/} {/* <BatteryViewSalidomo*/}
{/* values={values}*/} {/* values={values}*/}
{/* s3Credentials={s3Credentials}*/} {/* s3Credentials={s3Credentials}*/}
{/* installationId={props.current_installation.id}*/} {/* installationId={props.current_installation.id}*/}
{/* productNum={props.current_installation.product}*/} {/* productNum={props.current_installation.product}*/}
{/* connected={connected}*/} {/* connected={connected}*/}
{/* ></BatteryView>*/} {/* ></BatteryViewSalidomo>*/}
{/* }*/} {/* }*/}
{/*></Route>*/} {/*></Route>*/}
@ -279,7 +402,7 @@ function SodioHomeInstallation(props: singleInstallationProps) {
<Route <Route
path={'*'} path={'*'}
element={<Navigate to={routes.information}></Navigate>} element={<Navigate to={routes.live}></Navigate>}
/> />
</Routes> </Routes>
</Grid> </Grid>

View File

@ -19,7 +19,7 @@ function SodioHomeInstallationTabs() {
const context = useContext(UserContext); const context = useContext(UserContext);
const { currentUser } = context; const { currentUser } = context;
const tabList = [ const tabList = [
'batteryview', 'live',
'information', 'information',
'manage', 'manage',
'overview', 'overview',
@ -40,9 +40,6 @@ function SodioHomeInstallationTabs() {
} = useContext(InstallationsContext); } = useContext(InstallationsContext);
const { product, setProduct } = useContext(ProductIdContext); const { product, setProduct } = useContext(ProductIdContext);
// const webSocketsContext = useContext(WebSocketContext);
// const { socket, openSocket, closeSocket } = webSocketsContext;
useEffect(() => { useEffect(() => {
let path = location.pathname.split('/'); let path = location.pathname.split('/');
@ -51,7 +48,7 @@ function SodioHomeInstallationTabs() {
} else if (path[path.length - 2] === 'tree') { } else if (path[path.length - 2] === 'tree') {
setCurrentTab('tree'); setCurrentTab('tree');
} else { } else {
//Even if we are located at path: /batteryview/mainstats, we want the BatteryView tab to be bold //Even if we are located at path: /batteryview/mainstats, we want the BatteryViewSalidomo tab to be bold
setCurrentTab(path.find((pathElement) => tabList.includes(pathElement))); setCurrentTab(path.find((pathElement) => tabList.includes(pathElement)));
} }
}, [location]); }, [location]);
@ -90,6 +87,7 @@ function SodioHomeInstallationTabs() {
ret_path += '/'; ret_path += '/';
ret_path += pathlist[i]; ret_path += pathlist[i];
} else { } else {
//When finding the installation id (number), break and then add the tab_value
ret_path += '/'; ret_path += '/';
ret_path += pathlist[i]; ret_path += pathlist[i];
ret_path += '/'; ret_path += '/';
@ -105,13 +103,8 @@ function SodioHomeInstallationTabs() {
currentUser.userType == UserType.admin currentUser.userType == UserType.admin
? [ ? [
{ {
value: 'batteryview', value: 'live',
label: ( label: <FormattedMessage id="live" defaultMessage="Live" />
<FormattedMessage
id="batteryview"
defaultMessage="Battery View"
/>
)
}, },
{ {
value: 'overview', value: 'overview',
@ -150,13 +143,8 @@ function SodioHomeInstallationTabs() {
] ]
: [ : [
{ {
value: 'batteryview', value: 'live',
label: ( label: <FormattedMessage id="live" defaultMessage="Live" />
<FormattedMessage
id="batteryview"
defaultMessage="Battery View"
/>
)
}, },
{ {
value: 'overview', value: 'overview',
@ -186,13 +174,8 @@ function SodioHomeInstallationTabs() {
icon: <AccountTreeIcon id="mode-toggle-button-tree-icon" /> icon: <AccountTreeIcon id="mode-toggle-button-tree-icon" />
}, },
{ {
value: 'batteryview', value: 'live',
label: ( label: <FormattedMessage id="live" defaultMessage="Live" />
<FormattedMessage
id="batteryview"
defaultMessage="Battery View"
/>
)
}, },
{ {
value: 'overview', value: 'overview',
@ -243,13 +226,8 @@ function SodioHomeInstallationTabs() {
icon: <AccountTreeIcon id="mode-toggle-button-tree-icon" /> icon: <AccountTreeIcon id="mode-toggle-button-tree-icon" />
}, },
{ {
value: 'batteryview', value: 'live',
label: ( label: <FormattedMessage id="live" defaultMessage="Live" />
<FormattedMessage
id="batteryview"
defaultMessage="Battery View"
/>
)
}, },
{ {
value: 'overview', value: 'overview',

View File

@ -41,18 +41,17 @@ const InstallationsContextProvider = ({
const [socket, setSocket] = useState<WebSocket>(null); const [socket, setSocket] = useState<WebSocket>(null);
const [currentProduct, setcurrentProduct] = useState<number>(0); const [currentProduct, setcurrentProduct] = useState<number>(0);
//Store pending updates and apply them in batches
const pendingUpdates = useRef< const pendingUpdates = useRef<
Record<number, { status: number; testingMode: boolean }> Record<number, { status: number; testingMode: boolean }>
>({}); // To store pending updates >({});
const updateInstallationStatus = useCallback( const updateInstallationStatus = useCallback((id, status, testingMode) => {
(product, id, status, testingMode) => { //Insert the incoming message to the pendingUpdates map
// Buffer updates instead of applying them immediately
pendingUpdates.current[id] = { status: Number(status), testingMode }; // Ensure status is a number pendingUpdates.current[id] = { status: Number(status), testingMode }; // Ensure status is a number
}, }, []);
[]
);
//This function will be called every 1 minute and it will update only the installations for which the status and/or the testingMOde has been changed
const applyBatchUpdates = useCallback(() => { const applyBatchUpdates = useCallback(() => {
if (Object.keys(pendingUpdates.current).length === 0) return; // No updates to apply if (Object.keys(pendingUpdates.current).length === 0) return; // No updates to apply
@ -109,7 +108,7 @@ const InstallationsContextProvider = ({
setcurrentProduct(product); setcurrentProduct(product);
const tokenString = localStorage.getItem('token'); const tokenString = localStorage.getItem('token');
const token = tokenString !== null ? tokenString : ''; const token = tokenString !== null ? tokenString : '';
const urlWithToken = `wss://stage.innov.energy/api/CreateWebSocket?authToken=${token}`; const urlWithToken = `wss://monitor.innov.energy/api/CreateWebSocket?authToken=${token}`;
const socket = new WebSocket(urlWithToken); const socket = new WebSocket(urlWithToken);
@ -124,7 +123,7 @@ const InstallationsContextProvider = ({
installationsToSend = sodiohomeInstallations; installationsToSend = sodiohomeInstallations;
} }
// Send the corresponding installation IDs // Send the corresponding installation IDs to the backend
socket.send( socket.send(
JSON.stringify( JSON.stringify(
installationsToSend.map((installation) => installation.id) installationsToSend.map((installation) => installation.id)
@ -132,16 +131,6 @@ const InstallationsContextProvider = ({
); );
}); });
// socket.addEventListener('open', () => {
// socket.send(
// JSON.stringify(
// product === 1
// ? salidomoInstallations.map((installation) => installation.id)
// : salimaxInstallations.map((installation) => installation.id)
// )
// );
// });
// Periodically send ping messages to keep the connection alive // Periodically send ping messages to keep the connection alive
const pingInterval = setInterval(() => { const pingInterval = setInterval(() => {
if (socket.readyState === WebSocket.OPEN) { if (socket.readyState === WebSocket.OPEN) {
@ -152,8 +141,8 @@ const InstallationsContextProvider = ({
socket.addEventListener('message', (event) => { socket.addEventListener('message', (event) => {
const message = JSON.parse(event.data); // Parse the JSON data const message = JSON.parse(event.data); // Parse the JSON data
if (message.id !== -1) { if (message.id !== -1) {
//For each received message (except the first one which is a batch, call the updateInstallationStatus function in order to import the message to the pendingUpdates list
updateInstallationStatus( updateInstallationStatus(
product,
message.id, message.id,
message.status, message.status,
message.testingMode message.testingMode

View File

@ -1,7 +1,9 @@
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { import {
fetchAggregatedData, fetchAggregatedData,
fetchData fetchAggregatedDataJson,
fetchData,
fetchDataJson
} from '../content/dashboards/Installations/fetchData'; } from '../content/dashboards/Installations/fetchData';
import { FetchResult } from '../dataCache/dataCache'; import { FetchResult } from '../dataCache/dataCache';
import { I_S3Credentials } from './S3Types'; import { I_S3Credentials } from './S3Types';
@ -9,6 +11,7 @@ import { TimeSpan, UnixTime } from '../dataCache/time';
import { DataRecord } from '../dataCache/data'; import { DataRecord } from '../dataCache/data';
import axiosConfig from '../Resources/axiosConfig'; import axiosConfig from '../Resources/axiosConfig';
import { AxiosError, AxiosResponse } from 'axios'; import { AxiosError, AxiosResponse } from 'axios';
import { JSONRecordData } from '../content/dashboards/Log/graph.util';
export interface chartInfoInterface { export interface chartInfoInterface {
magnitude: number; magnitude: number;
@ -68,6 +71,473 @@ export interface BatteryOverviewInterface {
Current: chartInfoInterface; Current: chartInfoInterface;
} }
export const transformInputToBatteryViewDataJson = async (
s3Credentials: I_S3Credentials,
id: number,
product: number,
start_time?: UnixTime,
end_time?: UnixTime
): Promise<{
chartData: BatteryDataInterface;
chartOverview: BatteryOverviewInterface;
}> => {
const prefixes = ['', 'k', 'M', 'G', 'T'];
const MAX_NUMBER = 9999999;
const categories = ['Soc', 'Temperature', 'Power', 'Voltage', 'Current'];
const pathCategories = [
'.Soc',
'.Temperatures.Cells.Average',
'.Dc.Power',
'.Dc.Voltage',
'.Dc.Current'
];
const pathsToSearch = [
'Battery.Devices.1',
'Battery.Devices.2',
'Battery.Devices.3',
'Battery.Devices.4',
'Battery.Devices.5',
'Battery.Devices.6',
'Battery.Devices.7',
'Battery.Devices.8',
'Battery.Devices.9',
'Battery.Devices.10'
];
const pathsToSave = [];
const chartData: BatteryDataInterface = {
Soc: { name: 'State Of Charge', data: [] },
Temperature: { name: 'Temperature', data: [] },
Power: { name: 'Power', data: [] },
Voltage: { name: 'Voltage', data: [] },
Current: { name: 'Current', data: [] }
};
const chartOverview: BatteryOverviewInterface = {
Soc: { magnitude: 0, unit: '', min: 0, max: 0 },
Temperature: { magnitude: 0, unit: '', min: 0, max: 0 },
Power: { magnitude: 0, unit: '', min: 0, max: 0 },
Voltage: { magnitude: 0, unit: '', min: 0, max: 0 },
Current: { magnitude: 0, unit: '', min: 0, max: 0 }
};
let initialiation = true;
let timestampArray: number[] = [];
let adjustedTimestampArray = [];
const timestampPromises = [];
await axiosConfig
.get(
`/GetCsvTimestampsForInstallation?id=${id}&start=${start_time.ticks}&end=${end_time.ticks}`
)
.then((res: AxiosResponse<number[]>) => {
timestampArray = res.data;
})
.catch((err: AxiosError) => {
if (err.response && err.response.status == 401) {
//removeToken();
//navigate(routes.login);
}
});
for (var i = 0; i < timestampArray.length; i++) {
timestampPromises.push(
fetchJsonDataForOneTime(
UnixTime.fromTicks(timestampArray[i], true),
s3Credentials
)
);
const adjustedTimestamp =
product == 0
? new Date(timestampArray[i] * 1000)
: new Date(timestampArray[i] * 100000);
//Timezone offset is negative, so we convert the timestamp to the current zone by subtracting the corresponding offset
adjustedTimestamp.setHours(
adjustedTimestamp.getHours() - adjustedTimestamp.getTimezoneOffset() / 60
);
adjustedTimestampArray.push(adjustedTimestamp);
}
const results: Promise<FetchResult<Record<string, JSONRecordData>>>[] =
await Promise.all(timestampPromises);
for (let i = 0; i < results.length; i++) {
if (results[i] == null) {
// Handle not available or try later case
} else {
const timestamp = Object.keys(results[i])[
Object.keys(results[i]).length - 1
];
const result = results[i][timestamp];
const battery_nodes = result.Config.Devices.BatteryNodes.value
.toString()
.split(',');
//Initialize the chartData structure based on the node names extracted from the first result
let old_length = pathsToSave.length;
if (battery_nodes.length > old_length) {
battery_nodes.forEach((node) => {
if (!pathsToSave.includes('Node' + node)) {
pathsToSave.push('Node' + node);
}
});
}
if (initialiation) {
initialiation = false;
categories.forEach((category) => {
chartData[category].data = [];
chartOverview[category] = {
magnitude: 0,
unit: '',
min: MAX_NUMBER,
max: -MAX_NUMBER
};
});
}
if (battery_nodes.length > old_length) {
categories.forEach((category) => {
pathsToSave.forEach((path) => {
if (pathsToSave.indexOf(path) >= old_length) {
chartData[category].data[path] = { name: path, data: [] };
}
});
});
}
for (
let category_index = 0;
category_index < pathCategories.length;
category_index++
) {
let category = categories[category_index];
for (let j = 0; j < pathsToSave.length; j++) {
let path =
pathsToSearch[j] + pathCategories[category_index] + '.value';
//if (result[path]) {
const value = path
.split('.')
.reduce((o, key) => (o ? o[key] : undefined), result);
if (value < chartOverview[category].min) {
chartOverview[category].min = value;
}
if (value > chartOverview[category].max) {
chartOverview[category].max = value;
}
chartData[category].data[pathsToSave[j]].data.push([
adjustedTimestampArray[i],
value
]);
// } else {
// // chartData[category].data[pathsToSave[j]].data.push([
// // adjustedTimestampArray[i],
// // null
// // ]);
// }
}
}
}
}
categories.forEach((category) => {
let value = Math.max(
Math.abs(chartOverview[category].max),
Math.abs(chartOverview[category].min)
);
let magnitude = 0;
if (value < 0) {
value = -value;
}
while (value >= 1000) {
value /= 1000;
magnitude++;
}
chartOverview[category].magnitude = magnitude;
});
chartOverview.Soc.unit = '(%)';
chartOverview.Soc.min = 0;
chartOverview.Soc.max = 100;
chartOverview.Temperature.unit = '(°C)';
chartOverview.Power.unit =
'(' + prefixes[chartOverview['Power'].magnitude] + 'W' + ')';
chartOverview.Voltage.unit =
'(' + prefixes[chartOverview['Voltage'].magnitude] + 'V' + ')';
chartOverview.Current.unit =
'(' + prefixes[chartOverview['Current'].magnitude] + 'A' + ')';
return {
chartData: chartData,
chartOverview: chartOverview
};
};
const fetchJsonDataForOneTime = async (
startUnixTime: UnixTime,
s3Credentials: I_S3Credentials
): Promise<FetchResult<Record<string, DataRecord>>> => {
var timeperiodToSearch = 2;
let res;
let timestampToFetch;
for (var i = 0; i < timeperiodToSearch; i++) {
timestampToFetch = startUnixTime.later(TimeSpan.fromSeconds(i));
try {
res = await fetchDataJson(timestampToFetch, s3Credentials);
if (res !== FetchResult.notAvailable && res !== FetchResult.tryLater) {
//console.log('Successfully fetched ' + timestampToFetch);
return res;
}
} catch (err) {
console.error('Error fetching data:', err);
}
}
return null;
};
export const transformInputToAggregatedDataJson = async (
s3Credentials: I_S3Credentials,
start_date: dayjs.Dayjs,
end_date: dayjs.Dayjs
): Promise<{
chartAggregatedData: chartAggregatedDataInterface;
chartOverview: overviewInterface;
dateList: string[];
}> => {
const data = {};
const overviewData = {};
const MAX_NUMBER = 9999999;
const dateList = [];
let currentDay = start_date;
const pathsToSearch = [
'MinSoc',
'MaxSoc',
'PvPower',
'DischargingBatteryPower',
'ChargingBatteryPower',
'GridImportPower',
'GridExportPower',
'HeatingPower'
];
const categories = [
'minsoc',
'maxsoc',
'pvProduction',
'dcChargingPower',
'heatingPower',
'dcDischargingPower',
'gridImportPower',
'gridExportPower'
];
const chartAggregatedData: chartAggregatedDataInterface = {
minsoc: { name: 'min SOC', data: [] },
maxsoc: { name: 'max SOC', data: [] },
pvProduction: { name: 'Pv Energy', data: [] },
dcChargingPower: { name: 'Charging Battery Energy', data: [] },
heatingPower: { name: 'Heating Energy', data: [] },
dcDischargingPower: { name: 'Discharging Battery Energy', data: [] },
gridImportPower: { name: 'Grid Import Energy', data: [] },
gridExportPower: { name: 'Grid Export Energy', data: [] }
};
const chartOverview: overviewInterface = {
soc: { magnitude: 0, unit: '', min: 0, max: 0 },
temperature: { magnitude: 0, unit: '', min: 0, max: 0 },
dcPower: { magnitude: 0, unit: '', min: 0, max: 0 },
dcPowerWithoutHeating: { magnitude: 0, unit: '', min: 0, max: 0 },
gridPower: { magnitude: 0, unit: '', min: 0, max: 0 },
pvProduction: { magnitude: 0, unit: '', min: 0, max: 0 },
dcBusVoltage: { magnitude: 0, unit: '', min: 0, max: 0 },
overview: { magnitude: 0, unit: '', min: 0, max: 0 },
ACLoad: { magnitude: 0, unit: '', min: 0, max: 0 },
DCLoad: { magnitude: 0, unit: '', min: 0, max: 0 }
};
pathsToSearch.forEach((path) => {
data[path] = [];
overviewData[path] = {
magnitude: 0,
unit: '',
min: MAX_NUMBER,
max: -MAX_NUMBER
};
});
const timestampPromises = [];
while (currentDay.isBefore(end_date)) {
timestampPromises.push(
fetchAggregatedDataJson(currentDay.format('YYYY-MM-DD'), s3Credentials)
);
currentDay = currentDay.add(1, 'day');
}
const results = await Promise.all(timestampPromises);
currentDay = start_date;
for (let i = 0; i < results.length; i++) {
const result = results[i];
if (
result === FetchResult.notAvailable ||
result === FetchResult.tryLater
) {
// Handle not available or try later case
} else {
console.log(result);
dateList.push(currentDay.format('DD-MM'));
pathsToSearch.forEach((path) => {
const value = path
.split('.')
.reduce((o, key) => (o ? o[key] : undefined), result);
if (value !== undefined) {
if (path === '.GridExportPower') {
result.GridExportPower = -value;
}
if (value < overviewData[path].min) {
overviewData[path].min = value;
}
if (value > overviewData[path].max) {
overviewData[path].max = value;
}
data[path].push(value as number);
}
});
}
currentDay = currentDay.add(1, 'day');
}
pathsToSearch.forEach((path) => {
let value = Math.max(
Math.abs(overviewData[path].max),
Math.abs(overviewData[path].min)
);
let magnitude = 0;
if (value < 0) {
value = -value;
}
while (value >= 1000) {
value /= 1000;
magnitude++;
}
overviewData[path].magnitude = magnitude;
});
let path = 'MinSoc';
chartAggregatedData.minsoc.data = data[path];
path = 'MaxSoc';
chartAggregatedData.maxsoc.data = data[path];
chartOverview.soc = {
unit: '(%)',
magnitude: overviewData[path].magnitude,
min: 0,
max: 100
};
path = 'PvPower';
chartAggregatedData.pvProduction.data = data[path];
chartOverview.pvProduction = {
magnitude: overviewData[path].magnitude,
unit: '(kWh)',
min: overviewData[path].min,
max: overviewData[path].max
};
path = 'ChargingBatteryPower';
chartAggregatedData.dcChargingPower.data = data[path];
path = 'DischargingBatteryPower';
chartAggregatedData.dcDischargingPower.data = data[path];
path = 'HeatingPower';
chartAggregatedData.heatingPower.data = data[path];
chartOverview.dcPowerWithoutHeating = {
magnitude: Math.max(
overviewData['ChargingBatteryPower'].magnitude,
overviewData['DischargingBatteryPower'].magnitude
),
unit: '(kWh)',
min: overviewData['DischargingBatteryPower'].min,
max: overviewData['ChargingBatteryPower'].max
};
chartOverview.dcPower = {
magnitude: Math.max(
overviewData['ChargingBatteryPower'].magnitude,
overviewData['HeatingPower'].magnitude,
overviewData['DischargingBatteryPower'].magnitude
),
unit: '(kWh)',
min: overviewData['DischargingBatteryPower'].min,
max:
overviewData['ChargingBatteryPower'].max +
overviewData['HeatingPower'].max
};
path = 'GridImportPower';
chartAggregatedData.gridImportPower.data = data[path];
path = 'GridExportPower';
chartAggregatedData.gridExportPower.data = data[path];
chartOverview.gridPower = {
magnitude: Math.max(
overviewData['GridImportPower'].magnitude,
overviewData['GridExportPower'].magnitude
),
unit: '(kWh)',
min: overviewData['GridExportPower'].min,
max: overviewData['GridImportPower'].max
};
chartOverview.overview = {
magnitude: 0,
unit: '(kWh)',
min: Math.min(
overviewData['GridImportPower'].min,
overviewData['GridExportPower'].min,
overviewData['PvPower'].min
),
max: Math.max(
overviewData['GridImportPower'].max,
overviewData['GridExportPower'].max,
overviewData['PvPower'].max
)
};
// console.log(chartAggregatedData);
return {
chartAggregatedData: chartAggregatedData,
chartOverview: chartOverview,
dateList: dateList
};
};
//UNCOMMENT THE FOLLOWING FOR CSV
// We use this function in order to retrieve data for main stats. // We use this function in order to retrieve data for main stats.
//The data is of the following form: //The data is of the following form:
//'Soc' : {name:'Soc',data:[ //'Soc' : {name:'Soc',data:[
@ -288,6 +758,17 @@ export const transformInputToBatteryViewData = async (
}; };
}; };
// We use this function in order to retrieve data for main stats.
// The data is of the following form:
// 'Soc' : {name:'Soc',data:[
// 'Node1': {name:'Node1', data: [[timestamp,value],[timestamp,value]]},
// 'Node2': {name:'Node2', data: [[timestamp,value],[timestamp,value]]},
// ]},
// 'Temperature' : {name:'Temperature',data:[
// 'Node1': {name:'Node1', data: [[timestamp,value],[timestamp,value]]},
// 'Node2': {name:'Node2', data: [[timestamp,value],[timestamp,value]]},
// ]}
const fetchDataForOneTime = async ( const fetchDataForOneTime = async (
startUnixTime: UnixTime, startUnixTime: UnixTime,
s3Credentials: I_S3Credentials s3Credentials: I_S3Credentials