Frontend supports json for salidomo

This commit is contained in:
Noe 2025-02-21 15:25:40 +01:00
parent 57b5eb7e95
commit d6267952e9
37 changed files with 2432 additions and 1586 deletions

View File

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

View File

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

View File

@ -1,5 +1,5 @@
#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
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.SaliMaxRelays;
using InnovEnergy.App.SaliMax.System;
@ -29,5 +30,26 @@ public record StatusRecord
public required SystemLog Log { get; init; } // 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

@ -815,7 +815,8 @@ internal static class Program
private static async Task<Boolean> UploadCsv(StatusRecord status, DateTime timeStamp)
{
status.ToJson();
var csv = status.ToCsv().LogInfo();
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

@ -23,21 +23,10 @@ from os import path
app_dir = path.dirname(path.realpath(__file__))
sys.path.insert(1, path.join(app_dir, 'ext', 'velib_python'))
from vedbus import VeDbusService as DBus
import time
import os
import csv
import requests
import hmac
import hashlib
import base64
from datetime import datetime
import io
import json
import requests
import hmac
import hashlib
@ -45,22 +34,18 @@ import base64
from datetime import datetime
import pika
import time
# zip-comp additions
import zipfile
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()
# Create a zip archive in the memory buffer
with zipfile.ZipFile(memory_stream, 'w', zipfile.ZIP_DEFLATED) as archive:
# Add CSV data to the ZIP archive
with archive.open('data.csv', 'w') as entry_stream:
entry_stream.write(csv_data.encode('utf-8'))
# Add JSON data to the ZIP archive
with archive.open('data.json', 'w') as entry_stream:
entry_stream.write(json_data.encode('utf-8'))
# Get the compressed byte array from the memory buffer
compressed_bytes = memory_stream.getvalue()
@ -112,9 +97,9 @@ class S3config:
).decode()
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:
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)}")
return None
CSV_DIR = "/data/csv_files/"
#CSV_DIR = "csv_files/"
JSON_DIR = "/data/json_files/"
# Define the path to the file containing the 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)
except Exception as 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
finally:
modbus.close() # close in any case
@ -659,7 +643,7 @@ def publish_values(dbus, signals, statuses):
previous_warnings = {}
previous_alarms = {}
num_of_csv_files_saved=0
num_of_json_files_saved=0
class MessageType:
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)
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')
return True
@ -907,59 +892,59 @@ def count_files_in_folder(folder_path):
except Exception as 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
files = os.listdir(CSV_DIR)
files = os.listdir(JSON_DIR)
# filter out only csv files
csv_files = [file for file in files if file.endswith('.csv')]
# filter out only json files
json_files = [file for file in files if file.endswith('.json')]
# sort csv files by creation time
csv_files.sort(key=lambda x: os.path.getctime(os.path.join(CSV_DIR, x)))
# sort json files by creation time
json_files.sort(key=lambda x: os.path.getctime(os.path.join(JSON_DIR, x)))
# keep the 600 MOST RECENT FILES
recent_csv_files = csv_files[-num_of_csv_files_saved:]
print("num_of_csv_files_saved is " + str(num_of_csv_files_saved))
recent_json_files = json_files[-num_of_json_files_saved:]
print("num_of_json_files_saved is " + str(num_of_json_files_saved))
# get the name of the first csv file
if not csv_files:
print("No csv files found in the directory.")
# get the name of the first json file
if not json_files:
print("No json files found in the directory.")
exit(0)
first_csv_file = os.path.join(CSV_DIR, recent_csv_files.pop(0))
first_csv_filename = os.path.basename(first_csv_file)
first_json_file = os.path.join(JSON_DIR, recent_json_files.pop(0))
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
with open(temp_file_path, 'wb') as temp_file:
# 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'))
# 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())
for csv_file in recent_csv_files:
file_path = os.path.join(CSV_DIR, csv_file)
for json_file in recent_json_files:
file_path = os.path.join(JSON_DIR, json_file)
# write an empty line
temp_file.write(b'\n')
# 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'))
# write the content of the file
with open(file_path, 'rb') as f:
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
# replace the original first json file with the temporary file
os.remove(first_json_file)
os.rename(temp_file_path, first_json_file)
num_of_json_files_saved = 0
# 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):
# os.makedirs(logging_dir)
#
@ -967,23 +952,23 @@ def create_batch_of_csv_files():
# manage_csv_files(logging_dir)
# 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:
print("error while reading csv as string")
if json_data is None:
print("error while reading json as string")
return
# zip-comp additions
compressed_csv = compress_csv_data(csv_data)
# Use the name of the last (most recent) CSV file in sorted csv_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
compressed_json = compress_json_data(json_data)
# Use the name of the last (most recent) JSON file in sorted json_files as the name for the compressed file
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])
compressed_filename = "{}.csv".format(numeric_part)
numeric_part = int(last_json_file_name.split('.')[0][:-2])
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:
os.remove(first_csv_file)
os.remove(first_json_file)
print("Successfully uploaded the compresseed batch of files in s3")
status_message = {
"InstallationId": INSTALLATION_ID,
@ -1001,14 +986,14 @@ def create_batch_of_csv_files():
channel.basic_publish(exchange="", routing_key="statusQueue", body=status_message)
print("Successfully sent the heartbit with timestamp")
else:
# we save data that were not successfully uploaded in s3 in a failed directory inside the CSV_DIR for logging
failed_dir = os.path.join(CSV_DIR, "failed")
# 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(JSON_DIR, "failed")
if not os.path.exists(failed_dir):
os.makedirs(failed_dir)
failed_path = os.path.join(failed_dir, first_csv_filename)
os.rename(first_csv_file, failed_path)
failed_path = os.path.join(failed_dir, first_json_filename)
os.rename(first_json_file, failed_path)
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
@ -1033,11 +1018,11 @@ def create_update_task(modbus, dbus, batteries, signals, csv_signals, main_loop)
elapsed_time = time.time() - start_time
print("11111111111111111111111111111111111111111111 elapsed time is ", elapsed_time)
# keep at most 1900 files at CSV_DIR for logging and aggregation
manage_csv_files(CSV_DIR, 1900)
# keep at most 1900 files at JSON_DIR for logging and aggregation
manage_json_files(JSON_DIR, 1900)
if elapsed_time >= 1200:
print("CREATE BATCH ======================================>")
create_batch_of_csv_files()
create_batch_of_json_files()
start_time = time.time()
#alive = update_for_testing(modbus, batteries, dbus, signals, csv_signals)
if not alive:
@ -1070,12 +1055,12 @@ def get_installation_name(file_path):
with open(file_path, 'r') as file:
return file.read().strip()
def manage_csv_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))]
csv_files.sort(key=lambda x: os.path.getctime(os.path.join(directory_path, x)))
def manage_json_files(directory_path, max_files=20):
json_files = [f for f in os.listdir(directory_path) if os.path.isfile(os.path.join(directory_path, f))]
json_files.sort(key=lambda x: os.path.getctime(os.path.join(directory_path, x)))
# Remove oldest files if exceeds maximum
while len(csv_files) > max_files:
file_to_delete = os.path.join(directory_path, csv_files.pop(0))
while len(json_files) > max_files:
file_to_delete = os.path.join(directory_path, json_files.pop(0))
os.remove(file_to_delete)
def insert_id(path, id_number):
@ -1084,36 +1069,102 @@ def insert_id(path, id_number):
parts.insert(insert_position, str(id_number))
return "/".join(parts)
def create_csv_files(signals, statuses, node_numbers, alarms_number_list, warnings_number_list):
global s3_config, num_of_csv_files_saved
def insert_nested_data(data, split_list, value, symbol):
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())
if timestamp % 2 != 0:
timestamp -= 1
# Create CSV directory if it doesn't exist
if not os.path.exists(CSV_DIR):
os.makedirs(CSV_DIR)
csv_filename = f"{timestamp}.csv"
csv_path = os.path.join(CSV_DIR, csv_filename)
num_of_csv_files_saved+=1
if not os.path.exists(JSON_DIR):
os.makedirs(JSON_DIR)
json_filename = "{}.json".format(timestamp)
json_path = os.path.join(JSON_DIR, json_filename)
num_of_json_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)
data = {
"Battery": {
"Devices": {}
}
}
# Iterate over each node and construct the data structure
for i, node in enumerate(node_numbers):
print("Inside json generation file, node num is", i, " and node is ", node)
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:
split_list = s.name.split("/")[3:]
# print(split_list)
value = s.get_value(statuses[i])
symbol = s.get_text
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'
def load_battery_counts():

View File

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

View File

@ -34,7 +34,7 @@ import json
from convert import first
import shutil
CSV_DIR = "/data/csv_files/"
JSON_DIR = "/data/json_files/"
INSTALLATION_NAME_FILE = '/data/innovenergy/openvpn/installation-name'
# trick the pycharm type-checker into thinking Callable is in scope, not used at runtime
@ -45,13 +45,13 @@ if False:
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()
# Create a zip archive in the memory buffer
with zipfile.ZipFile(memory_stream, 'w', zipfile.ZIP_DEFLATED) as archive:
# Add CSV data to the ZIP archive using writestr
archive.writestr(file_name, csv_data.encode('utf-8'))
# Add JSON data to the ZIP archive using writestr
archive.writestr(file_name, json_data.encode('utf-8'))
# Get the compressed byte array from the memory buffer
compressed_bytes = memory_stream.getvalue()
@ -155,7 +155,7 @@ INSTALLATION_ID = int(s3_config.bucket.split('-')[0])
PRODUCT_ID = 1
is_first_update = True
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):
@ -251,9 +251,9 @@ def update_state_from_dictionaries(current_warnings, current_alarms, node_number
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:
# 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)
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)
# keep at most 1900 files at CSV_DIR for logging and aggregation
manage_csv_files(CSV_DIR, 1900)
# keep at most 1900 files at JSON_DIR for logging and aggregation
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:
print("CREATE BATCH ======================================>")
create_batch_of_csv_files()
create_batch_of_json_files()
start_time = time.time()
upload_status_to_innovenergy(_socket, statuses)
@ -574,10 +575,10 @@ def create_update_task(modbus, service, batteries):
alive = True
except pika.exceptions.AMQPConnectionError:
logging.error("AMQPConnectionError encountered. Subscribing to queue.")
create_batch_of_csv_files()
create_batch_of_json_files()
except Exception as e:
create_batch_of_csv_files()
create_batch_of_json_files()
logging.error("Unexpected error")
raise
@ -587,14 +588,14 @@ def create_update_task(modbus, service, batteries):
return update_task
def manage_csv_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))]
csv_files.sort(key=lambda x: os.path.getctime(os.path.join(directory_path, x)))
def manage_json_files(directory_path, max_files=20):
json_files = [f for f in os.listdir(directory_path) if os.path.isfile(os.path.join(directory_path, f))]
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
while len(csv_files) > max_files:
file_to_delete = os.path.join(directory_path, csv_files.pop(0))
while len(json_files) > max_files:
file_to_delete = os.path.join(directory_path, json_files.pop(0))
os.remove(file_to_delete)
@ -605,89 +606,78 @@ def insert_id(path, id_number):
return "/".join(parts)
def create_batch_of_csv_files():
global prev_status, channel, INSTALLATION_ID, PRODUCT_ID, num_of_csv_files_saved
def create_batch_of_json_files():
global prev_status, channel, INSTALLATION_ID, PRODUCT_ID, num_of_json_files_saved
# list all files in the directory
files = os.listdir(CSV_DIR)
files = os.listdir(JSON_DIR)
# filter out only csv files
csv_files = [file for file in files if file.endswith('.csv')]
# filter out only json files
json_files = [file for file in files if file.endswith('.json')]
# sort csv files by creation time
csv_files.sort(key=lambda x: os.path.getctime(os.path.join(CSV_DIR, x)))
# sort json files by creation time
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
recent_csv_files = csv_files[-num_of_csv_files_saved:]
print("num_of_csv_files_saved is " + str(num_of_csv_files_saved))
# keep the recent_json_files MOST RECENT FILES
recent_json_files = json_files[-num_of_json_files_saved:]
print("num_of_json_files_saved is " + str(recent_json_files))
# get the name of the first csv file
if not csv_files:
print("No csv files found in the directory.")
# get the name of the first json file
if not json_files:
print("No json files found in the directory.")
exit(0)
first_csv_file = os.path.join(CSV_DIR, recent_csv_files.pop(0))
first_csv_filename = os.path.basename(first_csv_file)
first_json_file = os.path.join(JSON_DIR, recent_json_files.pop(0))
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
with open(temp_file_path, 'wb') as temp_file:
# Write the timestamp (filename) at the beginning
temp_file.write('Timestamp;{}\n'.format(first_csv_filename.split('.')[0]))
# write the original content of the first csv file
with open(first_csv_file, 'rb') as f:
temp_file.write('Timestamp;{}\n'.format(first_json_filename.split('.')[0]))
# write the original content of the first json file
with open(first_json_file, 'rb') as f:
temp_file.write(f.read())
for csv_file in recent_csv_files:
file_path = os.path.join(CSV_DIR, csv_file)
for json_file in recent_json_files:
file_path = os.path.join(JSON_DIR, json_file)
# write an empty line
temp_file.write('\n')
# 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
with open(file_path, 'rb') as f:
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
# logging_dir = os.path.join(CSV_DIR, 'logging_batch_files')
# if not os.path.exists(logging_dir):
# os.makedirs(logging_dir)
#
# shutil.copy(first_csv_file, logging_dir)
# manage_csv_files(logging_dir)
# print("The batch csv file is: {}".format(recent_csv_files[-1]))
# replace the original first json file with the temporary file
os.remove(first_json_file)
os.rename(temp_file_path, first_json_file)
num_of_json_files_saved = 0
# 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:
print("error while reading csv as string")
if json_data is None:
print("error while reading json as string")
return
# zip-comp additions
compressed_csv = compress_csv_data(csv_data)
# Use the name of the last (most recent) CSV file in sorted csv_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
compressed_json = compress_json_data(json_data)
# Use the name of the last (most recent) JSON file in sorted json_files as the name for the compressed file
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
numeric_part = int(last_csv_file_name.split('.')[0][:-2])
# we send the json files every 30 seconds and the timestamp is adjusted to be a multiple of 30
numeric_part = int(last_json_file_name.split('.')[0][:-2])
# compressed_filename = "{}.csv".format(new_numeric_part)
compressed_filename = "{}.csv".format(numeric_part)
compressed_filename = "{}.json".format(numeric_part)
print("FILE NAME =========================================================> ", compressed_filename)
response = s3_config.create_put_request(compressed_filename, compressed_csv)
# response = s3_config.create_put_request(first_csv_filename, csv_data)
response = s3_config.create_put_request(compressed_filename, compressed_json)
print(response)
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")
status_message = {
"InstallationId": INSTALLATION_ID,
@ -709,42 +699,80 @@ def create_batch_of_csv_files():
print("Successfully sent the heartbit with timestamp")
else:
# we save data that were not successfully uploaded in s3 in a failed directory inside the CSV_DIR for logging
failed_dir = os.path.join(CSV_DIR, "failed")
#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(JSON_DIR, "failed")
if not os.path.exists(failed_dir):
os.makedirs(failed_dir)
failed_path = os.path.join(failed_dir, first_csv_filename)
os.rename(first_csv_file, failed_path)
failed_path = os.path.join(failed_dir, first_json_filename)
os.rename(first_json_file, failed_path)
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):
global num_of_csv_files_saved
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())
if timestamp % 2 != 0:
timestamp -= 1
if not os.path.exists(CSV_DIR):
os.makedirs(CSV_DIR)
csv_filename = "{}.csv".format(timestamp)
csv_path = os.path.join(CSV_DIR, csv_filename)
num_of_csv_files_saved += 1
if not os.path.exists(JSON_DIR):
os.makedirs(JSON_DIR)
json_filename = "{}.json".format(timestamp)
json_path = os.path.join(JSON_DIR, json_filename)
num_of_json_files_saved += 1
if not os.path.exists(csv_path):
with open(csv_path, 'ab') as csvfile:
csv_writer = csv.writer(csvfile, delimiter=';')
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)
for i, node in enumerate(node_numbers):
csv_writer.writerow(["/Battery/Devices/{}/Alarms".format(str(i + 1)), alarms_number_list[i], ""])
csv_writer.writerow(["/Battery/Devices/{}/Warnings".format(str(i + 1)), 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)
data = {
"Battery": {
"Devices": {}
}
}
# Iterate over each node and construct the data structure
for i, node in enumerate(node_numbers):
print("Inside json generation file, node num is" , i," and node is ", node)
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:
split_list = s.name.split("/")[3:]
#print(split_list)
value = s.get_value(statuses[i])
symbol=s.get_text
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):
@ -796,14 +824,6 @@ def main(argv):
logging.basicConfig(level=cfg.LOG_LEVEL)
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)
battery_counts = load_battery_counts()
max_retry_attempts = 3 # Stop retrying in case it's a real battery loss case

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",
"chart.js": "^4.4.0",
"clsx": "1.1.1",
"crypto-js": "^4.2.0",
"cytoscape": "^3.26.0",
"date-fns": "^2.28.0",
"dayjs": "^1.11.10",
@ -7318,6 +7319,11 @@
"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": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
@ -26136,6 +26142,11 @@
"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": {
"version": "2.0.0",
"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';
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'
});
const axiosConfig = axios.create({
baseURL: 'https://stage.innov.energy/api'
baseURL: 'https://monitor.innov.energy/api'
//baseURL: 'http://127.0.0.1:7087/api'
});

View File

@ -11,7 +11,6 @@ import {
TableRow,
Typography
} from '@mui/material';
import { TopologyValues } from '../Log/graph.util';
import {
Link,
Route,
@ -19,16 +18,17 @@ import {
useLocation,
useNavigate
} from 'react-router-dom';
import Button from '@mui/material/Button';
import { FormattedMessage } from 'react-intl';
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 MainStats from './MainStats';
import DetailedBatteryView from './DetailedBatteryView';
import CircularProgress from '@mui/material/CircularProgress';
interface BatteryViewProps {
values: TopologyValues;
values: JSONRecordData;
s3Credentials: I_S3Credentials;
installationId: number;
productNum: number;
@ -39,12 +39,15 @@ function BatteryView(props: BatteryViewProps) {
if (props.values === null && props.connected == true) {
return null;
}
const currentLocation = useLocation();
const navigate = useNavigate();
const sortedBatteryView =
props.values != null
? [...props.values.batteryView].sort((a, b) => b.BatteryId - a.BatteryId)
: [];
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);
@ -52,13 +55,13 @@ function BatteryView(props: BatteryViewProps) {
navigate(routes.mainstats);
};
const findBatteryData = (batteryId: number) => {
for (let i = 0; i < props.values.batteryView.length; i++) {
if (props.values.batteryView[i].BatteryId == batteryId) {
return props.values.batteryView[i];
}
}
};
// const findBatteryData = (batteryId: number) => {
// for (let i = 0; i < props.values.batteryView.length; i++) {
// if (props.values.batteryView[i].BatteryId == batteryId) {
// return props.values.batteryView[i];
// }
// }
// };
useEffect(() => {
if (sortedBatteryView.length == 0) {
@ -176,20 +179,23 @@ function BatteryView(props: BatteryViewProps) {
></MainStats>
}
/>
{props.values.batteryView.map((battery) => (
<Route
key={routes.detailed_view + battery.BatteryId}
path={routes.detailed_view + battery.BatteryId}
element={
<DetailedBatteryView
s3Credentials={props.s3Credentials}
batteryData={findBatteryData(battery.BatteryId)}
installationId={props.installationId}
productNum={props.productNum}
></DetailedBatteryView>
}
/>
))}
{Object.entries(props.values.Battery.Devices).map(
([BatteryId, battery]) => (
<Route
key={routes.detailed_view + BatteryId}
path={routes.detailed_view + BatteryId}
element={
<DetailedBatteryView
batteryId={Number(BatteryId)}
s3Credentials={props.s3Credentials}
batteryData={battery}
installationId={props.installationId}
productNum={props.productNum}
></DetailedBatteryView>
}
/>
)
)}
</Routes>
</Grid>
@ -219,9 +225,9 @@ function BatteryView(props: BatteryViewProps) {
</TableRow>
</TableHead>
<TableBody>
{sortedBatteryView.map((battery) => (
{sortedBatteryView.map(({ BatteryId, battery }) => (
<TableRow
key={battery.BatteryId}
key={BatteryId}
style={{
height: '10px'
}}
@ -234,9 +240,9 @@ function BatteryView(props: BatteryViewProps) {
>
<Link
style={{ color: 'black' }}
to={routes.detailed_view + battery.BatteryId.toString()}
to={routes.detailed_view + BatteryId}
>
{'Node ' + battery.BatteryId}
{'Node ' + BatteryId}
</Link>
</TableCell>
<TableCell
@ -253,7 +259,7 @@ function BatteryView(props: BatteryViewProps) {
textAlign: 'center'
}}
>
{battery.Power.value + ' ' + battery.Power.unit}
{battery.Dc.Power.value + ' W'}
</TableCell>
<TableCell
sx={{
@ -261,15 +267,14 @@ function BatteryView(props: BatteryViewProps) {
textAlign: 'center',
backgroundColor:
battery.Voltage.value < 44 ||
battery.Voltage.value > 57
battery.Dc.Voltage.value < 44 ||
battery.Dc.Voltage.value > 57
? '#FF033E'
: '#32CD32',
color:
battery.Voltage.value === '' ? 'white' : 'inherit'
color: battery.Dc.Voltage.value ? 'inherit' : 'white'
}}
>
{battery.Voltage.value + ' ' + battery.Voltage.unit}
{battery.Dc.Voltage.value + ' V'}
</TableCell>
<TableCell
sx={{
@ -281,107 +286,105 @@ function BatteryView(props: BatteryViewProps) {
: battery.Soc.value < 50
? '#ffbf00'
: '#32CD32',
color: battery.Soc.value === '' ? 'white' : 'inherit'
color: battery.Soc.value ? 'inherit' : 'white'
}}
>
{battery.Soc.value + ' ' + battery.Soc.unit}
{battery.Soc.value + ' %'}
</TableCell>
<TableCell
sx={{
width: '10%',
textAlign: 'center',
backgroundColor:
battery.AverageTemperature.value > 300
battery.Temperatures.Cells.Average.value > 300
? '#FF033E'
: battery.AverageTemperature.value > 280
: battery.Temperatures.Cells.Average.value > 280
? '#ffbf00'
: battery.AverageTemperature.value < 245
: battery.Temperatures.Cells.Average.value < 245
? '#008FFB'
: '#32CD32'
}}
>
{battery.AverageTemperature.value +
' ' +
battery.AverageTemperature.unit}
{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 === 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 && (
<>
@ -391,22 +394,22 @@ function BatteryView(props: BatteryViewProps) {
textAlign: 'center',
padding: '8px',
fontWeight:
Number(battery.Warnings.value) !== 0
Number(battery.Warnings) !== 0
? 'bold'
: 'inherit',
backgroundColor:
Number(battery.Warnings.value) === 0
Number(battery.Warnings) === 0
? 'inherit'
: '#ff9900',
color:
Number(battery.Warnings.value) != 0
Number(battery.Warnings) != 0
? 'black'
: 'inherit'
}}
>
{Number(battery.Warnings.value) === 0 ? (
{Number(battery.Warnings) === 0 ? (
'None'
) : Number(battery.Warnings.value) === 1 ? (
) : Number(battery.Warnings) === 1 ? (
<Link
style={{ color: 'black' }}
to={
@ -441,22 +444,18 @@ function BatteryView(props: BatteryViewProps) {
width: '20%',
textAlign: 'center',
fontWeight:
Number(battery.Alarms.value) !== 0
? 'bold'
: 'inherit',
Number(battery.Alarms) !== 0 ? 'bold' : 'inherit',
backgroundColor:
Number(battery.Alarms.value) === 0
Number(battery.Alarms) === 0
? 'inherit'
: '#FF033E',
color:
Number(battery.Alarms.value) != 0
? 'black'
: 'inherit'
Number(battery.Alarms) != 0 ? 'black' : 'inherit'
}}
>
{Number(battery.Alarms.value) === 0 ? (
{Number(battery.Alarms) === 0 ? (
'None'
) : Number(battery.Alarms.value) === 1 ? (
) : Number(battery.Alarms) === 1 ? (
<Link
style={{ color: 'black' }}
to={

View File

@ -16,7 +16,7 @@ import {
TableRow,
Typography
} from '@mui/material';
import { Battery } from '../Log/graph.util';
import { Device } from '../Log/graph.util';
import { useNavigate } from 'react-router-dom';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import Button from '@mui/material/Button';
@ -25,8 +25,9 @@ import { UserType } from '../../../interfaces/UserTypes';
import { UserContext } from '../../../contexts/userContext';
interface DetailedBatteryViewProps {
batteryId: number;
s3Credentials: I_S3Credentials;
batteryData: Battery;
batteryData: Device;
installationId: number;
productNum: number;
}
@ -35,6 +36,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
if (props.batteryData === null) {
return null;
}
const navigate = useNavigate();
const [openModalFirmwareUpdate, setOpenModalFirmwareUpdate] = useState(false);
const [openModalResultFirmwareUpdate, setOpenModalResultFirmwareUpdate] =
@ -65,35 +67,35 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
};
const [GreenisBlinking, setGreenisBlinking] = useState(
props.batteryData.GreenLeds.value === 'Blinking'
props.batteryData.Leds.Green.value === 'Blinking'
);
const [AmberisBlinking, setAmberisBlinking] = useState(
props.batteryData.AmberLeds.value === 'Blinking'
props.batteryData.Leds.Amber.value === 'Blinking'
);
const [RedisBlinking, setRedisBlinking] = useState(
props.batteryData.RedLeds.value === 'Blinking'
props.batteryData.Leds.Red.value === 'Blinking'
);
const [BlueisBlinking, setBlueisBlinking] = useState(
props.batteryData.BlueLeds.value === 'Blinking'
props.batteryData.Leds.Blue.value === 'Blinking'
);
useEffect(() => {
const intervalId = setInterval(() => {
if (props.batteryData.AmberLeds.value === 'Blinking') {
if (props.batteryData.Leds.Amber.value === 'Blinking') {
setAmberisBlinking((prevIsBlinking) => !prevIsBlinking);
}
if (props.batteryData.RedLeds.value === 'Blinking') {
if (props.batteryData.Leds.Red.value === 'Blinking') {
setRedisBlinking((prevIsBlinking) => !prevIsBlinking);
}
if (props.batteryData.BlueLeds.value === 'Blinking') {
if (props.batteryData.Leds.Blue.value === 'Blinking') {
setBlueisBlinking((prevIsBlinking) => !prevIsBlinking);
}
if (props.batteryData.GreenLeds.value === 'Blinking') {
if (props.batteryData.Leds.Green.value === 'Blinking') {
setGreenisBlinking((prevIsBlinking) => !prevIsBlinking);
}
}, 500); // Blink every 500 milliseconds
@ -129,7 +131,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
const res = await axiosConfig
.post(
`/UpdateFirmware?batteryNode=${props.batteryData.BatteryId.toString()}&installationId=${
`/UpdateFirmware?batteryNode=${props.batteryId.toString()}&installationId=${
props.installationId
}&version=${selectedVersion}`
)
@ -169,7 +171,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
try {
// Start the job to generate the battery log
const startRes = await axiosConfig.post(
`/StartDownloadBatteryLog?batteryNode=${props.batteryData.BatteryId.toString()}&installationId=${
`/StartDownloadBatteryLog?batteryNode=${props.batteryId.toString()}&installationId=${
props.installationId
}`
);
@ -635,7 +637,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
fontWeight: 'bold'
}}
>
{'Node ' + props.batteryData.BatteryId}
{'Node ' + props.batteryId}
</Typography>
<div style={batteryStyle}>
@ -643,8 +645,11 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
style={{
...batteryStringStyle,
backgroundColor:
props.batteryData.String1Active.value == 'True' ||
Number(props.batteryData.String1Active.value) == 0
props.batteryData.BatteryStrings.String1Active.value ==
'True' ||
Number(
props.batteryData.BatteryStrings.String1Active.value
) == 0
? '#32CD32'
: '#FF033E'
}}
@ -653,8 +658,11 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
style={{
...batteryStringStyle,
backgroundColor:
props.batteryData.String2Active.value == 'True' ||
Number(props.batteryData.String2Active.value) == 0
props.batteryData.BatteryStrings.String2Active.value ==
'True' ||
Number(
props.batteryData.BatteryStrings.String2Active.value
) == 0
? '#32CD32'
: '#FF033E'
}}
@ -663,8 +671,11 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
style={{
...batteryStringStyle,
backgroundColor:
props.batteryData.String3Active.value == 'True' ||
Number(props.batteryData.String3Active.value) == 0
props.batteryData.BatteryStrings.String3Active.value ==
'True' ||
Number(
props.batteryData.BatteryStrings.String3Active.value
) == 0
? '#32CD32'
: '#FF033E'
}}
@ -673,8 +684,11 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
style={{
...batteryStringStyle,
backgroundColor:
props.batteryData.String4Active.value == 'True' ||
Number(props.batteryData.String4Active.value) == 0
props.batteryData.BatteryStrings.String4Active.value ==
'True' ||
Number(
props.batteryData.BatteryStrings.String4Active.value
) == 0
? '#32CD32'
: '#FF033E'
}}
@ -683,8 +697,11 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
style={{
...batteryStringStyle,
backgroundColor:
props.batteryData.String5Active.value == 'True' ||
Number(props.batteryData.String5Active.value) == 0
props.batteryData.BatteryStrings.String5Active.value ==
'True' ||
Number(
props.batteryData.BatteryStrings.String5Active.value
) == 0
? '#32CD32'
: '#FF033E'
}}
@ -699,7 +716,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
marginTop: '-10px',
borderRadius: '50%',
backgroundColor:
props.batteryData.GreenLeds.value === 'On' ||
props.batteryData.Leds.Green.value === 'On' ||
GreenisBlinking
? 'green'
: 'transparent'
@ -714,7 +731,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
marginTop: '10px',
borderRadius: '50%',
backgroundColor:
props.batteryData.AmberLeds.value === 'On' ||
props.batteryData.Leds.Amber.value === 'On' ||
AmberisBlinking
? 'orange'
: 'transparent'
@ -729,7 +746,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
marginTop: '10px',
borderRadius: '50%',
backgroundColor:
props.batteryData.BlueLeds.value === 'On' || BlueisBlinking
props.batteryData.Leds.Blue.value === 'On' || BlueisBlinking
? '#00ccff'
: 'transparent'
}}
@ -743,7 +760,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
marginTop: '10px',
borderRadius: '50%',
backgroundColor:
props.batteryData.RedLeds.value === 'On' || RedisBlinking
props.batteryData.Leds.Red.value === 'On' || RedisBlinking
? 'red'
: 'transparent'
}}
@ -803,9 +820,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
paddingRight: '12px'
}}
>
{props.batteryData.Voltage.value +
' ' +
props.batteryData.Voltage.unit}
{props.batteryData.Dc.Voltage.value + ' V'}
</TableCell>
</TableRow>
<TableRow>
@ -825,9 +840,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
paddingRight: '12px'
}}
>
{props.batteryData.Current.value +
' ' +
props.batteryData.Current.unit}
{props.batteryData.Dc.Current.value + ' A'}
</TableCell>
</TableRow>
<TableRow>
@ -847,9 +860,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
paddingRight: '12px'
}}
>
{props.batteryData.Power.value +
' ' +
props.batteryData.Power.unit}
{props.batteryData.Dc.Power.value + ' W'}
</TableCell>
</TableRow>
@ -870,9 +881,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
paddingRight: '12px'
}}
>
{props.batteryData.BusCurrent.value +
' ' +
props.batteryData.BusCurrent.unit}
{props.batteryData.BusCurrent.value + ' A'}
</TableCell>
</TableRow>
<TableRow>
@ -892,9 +901,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
paddingRight: '12px'
}}
>
{props.batteryData.CellsCurrent.value +
' ' +
props.batteryData.CellsCurrent.unit}
{props.batteryData.CellsCurrent.value + ' A'}
</TableCell>
</TableRow>
<TableRow>
@ -914,9 +921,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
paddingRight: '12px'
}}
>
{props.batteryData.HeatingCurrent.value +
' ' +
props.batteryData.HeatingCurrent.unit}
{props.batteryData.HeatingCurrent.value + ' A'}
</TableCell>
</TableRow>
@ -937,9 +942,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
paddingRight: '12px'
}}
>
{props.batteryData.Soc.value +
' ' +
props.batteryData.Soc.unit}
{props.batteryData.Soc.value + ' %'}
</TableCell>
</TableRow>
</TableBody>
@ -949,200 +952,200 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
</Grid>
{/*----------------------------------------------------------------------------------------------------------------------------------*/}
{props.productNum === 0 && (
<>
<Grid item md={3} xs={3}>
<Card
sx={{
overflow: 'visible',
marginTop: '30px',
marginLeft: '20px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
border: '2px solid #ccc',
borderRadius: '12px'
}}
>
<Typography
variant="h6"
component="div"
sx={{
marginTop: '10px',
borderBottom: '1px solid #ccc',
fontWeight: 'bold'
}}
>
Temperature
</Typography>
{/*{props.productNum === 0 && (*/}
{/* <>*/}
{/* <Grid item md={3} xs={3}>*/}
{/* <Card*/}
{/* sx={{*/}
{/* overflow: 'visible',*/}
{/* marginTop: '30px',*/}
{/* marginLeft: '20px',*/}
{/* display: 'flex',*/}
{/* flexDirection: 'column',*/}
{/* alignItems: 'center',*/}
{/* border: '2px solid #ccc',*/}
{/* borderRadius: '12px'*/}
{/* }}*/}
{/* >*/}
{/* <Typography*/}
{/* variant="h6"*/}
{/* component="div"*/}
{/* sx={{*/}
{/* marginTop: '10px',*/}
{/* borderBottom: '1px solid #ccc',*/}
{/* fontWeight: 'bold'*/}
{/* }}*/}
{/* >*/}
{/* Temperature*/}
{/* </Typography>*/}
<TableContainer
component={Paper}
sx={{ marginTop: '20px', width: '100%' }}
>
<Table size="medium" aria-label="a dense table">
<TableBody>
<TableRow>
<TableCell
component="th"
scope="row"
align="left"
sx={{ fontWeight: 'bold' }}
>
Heating
</TableCell>
<TableCell
align="right"
sx={{
width: '6ch',
whiteSpace: 'nowrap',
paddingRight: '12px'
}}
>
{props.batteryData.HeatingTemperature.value +
' ' +
props.batteryData.HeatingTemperature.unit}
</TableCell>
</TableRow>
<TableRow>
<TableCell
component="th"
scope="row"
align="left"
sx={{ fontWeight: 'bold' }}
>
Board Temperature
</TableCell>
<TableCell
align="right"
sx={{
width: '6ch',
whiteSpace: 'nowrap',
paddingRight: '12px'
}}
>
{props.batteryData.BoardTemperature.value +
' ' +
props.batteryData.BoardTemperature.unit}
</TableCell>
</TableRow>
<TableRow>
<TableCell
component="th"
scope="row"
align="left"
sx={{ fontWeight: 'bold' }}
>
Center Cells Temperature
</TableCell>
<TableCell
align="right"
sx={{
width: '6ch',
whiteSpace: 'nowrap',
paddingRight: '12px'
}}
>
{props.batteryData.AverageTemperature.value +
' ' +
props.batteryData.AverageTemperature.unit}
</TableCell>
</TableRow>
<TableRow>
<TableCell
component="th"
scope="row"
align="left"
sx={{ fontWeight: 'bold' }}
>
Left Cells Temperature
</TableCell>
<TableCell
align="right"
sx={{
width: '6ch',
whiteSpace: 'nowrap',
paddingRight: '12px'
}}
>
{props.batteryData.LeftCellsTemperature.value +
' ' +
props.batteryData.LeftCellsTemperature.unit}
</TableCell>
</TableRow>
<TableRow>
<TableCell
component="th"
scope="row"
align="left"
sx={{ fontWeight: 'bold' }}
>
Right Cells Temperature
</TableCell>
<TableCell
align="right"
sx={{
width: '6ch',
whiteSpace: 'nowrap',
paddingRight: '12px'
}}
>
{props.batteryData.RightCellsTemperature.value +
' ' +
props.batteryData.RightCellsTemperature.unit}
</TableCell>
</TableRow>
<TableRow>
<TableCell
component="th"
scope="row"
align="left"
sx={{ fontWeight: 'bold' }}
>
Average Temperature
</TableCell>
<TableCell
align="right"
sx={{
width: '6ch',
whiteSpace: 'nowrap',
paddingRight: '12px'
}}
>
{props.batteryData.AverageTemperature.value +
' ' +
props.batteryData.AverageTemperature.unit}
</TableCell>
</TableRow>
<TableRow>
<TableCell
component="th"
scope="row"
align="left"
sx={{ fontWeight: 'bold' }}
>
State
</TableCell>
<TableCell
align="right"
sx={{
width: '6ch',
whiteSpace: 'nowrap',
paddingRight: '12px'
}}
>
{props.batteryData.StateTemperature.value +
' ' +
props.batteryData.StateTemperature.unit}
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
</Card>
</Grid>
</>
)}
{/* <TableContainer*/}
{/* component={Paper}*/}
{/* sx={{ marginTop: '20px', width: '100%' }}*/}
{/* >*/}
{/* <Table size="medium" aria-label="a dense table">*/}
{/* <TableBody>*/}
{/* <TableRow>*/}
{/* <TableCell*/}
{/* component="th"*/}
{/* scope="row"*/}
{/* align="left"*/}
{/* sx={{ fontWeight: 'bold' }}*/}
{/* >*/}
{/* Heating*/}
{/* </TableCell>*/}
{/* <TableCell*/}
{/* align="right"*/}
{/* sx={{*/}
{/* width: '6ch',*/}
{/* whiteSpace: 'nowrap',*/}
{/* paddingRight: '12px'*/}
{/* }}*/}
{/* >*/}
{/* {props.batteryData.HeatingTemperature.value +*/}
{/* ' ' +*/}
{/* props.batteryData.HeatingTemperature.unit}*/}
{/* </TableCell>*/}
{/* </TableRow>*/}
{/* <TableRow>*/}
{/* <TableCell*/}
{/* component="th"*/}
{/* scope="row"*/}
{/* align="left"*/}
{/* sx={{ fontWeight: 'bold' }}*/}
{/* >*/}
{/* Board Temperature*/}
{/* </TableCell>*/}
{/* <TableCell*/}
{/* align="right"*/}
{/* sx={{*/}
{/* width: '6ch',*/}
{/* whiteSpace: 'nowrap',*/}
{/* paddingRight: '12px'*/}
{/* }}*/}
{/* >*/}
{/* {props.batteryData.BoardTemperature.value +*/}
{/* ' ' +*/}
{/* props.batteryData.BoardTemperature.unit}*/}
{/* </TableCell>*/}
{/* </TableRow>*/}
{/* <TableRow>*/}
{/* <TableCell*/}
{/* component="th"*/}
{/* scope="row"*/}
{/* align="left"*/}
{/* sx={{ fontWeight: 'bold' }}*/}
{/* >*/}
{/* Center Cells Temperature*/}
{/* </TableCell>*/}
{/* <TableCell*/}
{/* align="right"*/}
{/* sx={{*/}
{/* width: '6ch',*/}
{/* whiteSpace: 'nowrap',*/}
{/* paddingRight: '12px'*/}
{/* }}*/}
{/* >*/}
{/* {props.batteryData.AverageTemperature.value +*/}
{/* ' ' +*/}
{/* props.batteryData.AverageTemperature.unit}*/}
{/* </TableCell>*/}
{/* </TableRow>*/}
{/* <TableRow>*/}
{/* <TableCell*/}
{/* component="th"*/}
{/* scope="row"*/}
{/* align="left"*/}
{/* sx={{ fontWeight: 'bold' }}*/}
{/* >*/}
{/* Left Cells Temperature*/}
{/* </TableCell>*/}
{/* <TableCell*/}
{/* align="right"*/}
{/* sx={{*/}
{/* width: '6ch',*/}
{/* whiteSpace: 'nowrap',*/}
{/* paddingRight: '12px'*/}
{/* }}*/}
{/* >*/}
{/* {props.batteryData.LeftCellsTemperature.value +*/}
{/* ' ' +*/}
{/* props.batteryData.LeftCellsTemperature.unit}*/}
{/* </TableCell>*/}
{/* </TableRow>*/}
{/* <TableRow>*/}
{/* <TableCell*/}
{/* component="th"*/}
{/* scope="row"*/}
{/* align="left"*/}
{/* sx={{ fontWeight: 'bold' }}*/}
{/* >*/}
{/* Right Cells Temperature*/}
{/* </TableCell>*/}
{/* <TableCell*/}
{/* align="right"*/}
{/* sx={{*/}
{/* width: '6ch',*/}
{/* whiteSpace: 'nowrap',*/}
{/* paddingRight: '12px'*/}
{/* }}*/}
{/* >*/}
{/* {props.batteryData.RightCellsTemperature.value +*/}
{/* ' ' +*/}
{/* props.batteryData.RightCellsTemperature.unit}*/}
{/* </TableCell>*/}
{/* </TableRow>*/}
{/* <TableRow>*/}
{/* <TableCell*/}
{/* component="th"*/}
{/* scope="row"*/}
{/* align="left"*/}
{/* sx={{ fontWeight: 'bold' }}*/}
{/* >*/}
{/* Average Temperature*/}
{/* </TableCell>*/}
{/* <TableCell*/}
{/* align="right"*/}
{/* sx={{*/}
{/* width: '6ch',*/}
{/* whiteSpace: 'nowrap',*/}
{/* paddingRight: '12px'*/}
{/* }}*/}
{/* >*/}
{/* {props.batteryData.AverageTemperature.value +*/}
{/* ' ' +*/}
{/* props.batteryData.AverageTemperature.unit}*/}
{/* </TableCell>*/}
{/* </TableRow>*/}
{/* <TableRow>*/}
{/* <TableCell*/}
{/* component="th"*/}
{/* scope="row"*/}
{/* align="left"*/}
{/* sx={{ fontWeight: 'bold' }}*/}
{/* >*/}
{/* State*/}
{/* </TableCell>*/}
{/* <TableCell*/}
{/* align="right"*/}
{/* sx={{*/}
{/* width: '6ch',*/}
{/* whiteSpace: 'nowrap',*/}
{/* paddingRight: '12px'*/}
{/* }}*/}
{/* >*/}
{/* {props.batteryData.StateTemperature.value +*/}
{/* ' ' +*/}
{/* props.batteryData.StateTemperature.unit}*/}
{/* </TableCell>*/}
{/* </TableRow>*/}
{/* </TableBody>*/}
{/* </Table>*/}
{/* </TableContainer>*/}
{/* </Card>*/}
{/* </Grid>*/}
{/* </>*/}
{/*)}*/}
{/*----------------------------------------------------------------------------------------------------------------------------------*/}
<Grid item md={3} xs={3}>
@ -1193,9 +1196,9 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
paddingRight: '12px'
}}
>
{props.batteryData.ConnectedToDcBus.value +
' ' +
props.batteryData.ConnectedToDcBus.unit}
{props.batteryData.IoStatus.ConnectedToDcBus.value
? 'True'
: 'False'}
</TableCell>
</TableRow>
<TableRow>
@ -1215,9 +1218,9 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
paddingRight: '12px'
}}
>
{props.batteryData.AlarmOutActive.value +
' ' +
props.batteryData.AlarmOutActive.unit}
{props.batteryData.IoStatus.AlarmOutActive.value
? 'True'
: 'False'}
</TableCell>
</TableRow>
<TableRow>
@ -1237,9 +1240,9 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
paddingRight: '12px'
}}
>
{props.batteryData.InternalFanActive.value +
' ' +
props.batteryData.InternalFanActive.unit}
{props.batteryData.IoStatus.InternalFanActive.value
? 'True'
: 'False'}
</TableCell>
</TableRow>
<TableRow>
@ -1259,9 +1262,9 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
paddingRight: '12px'
}}
>
{props.batteryData.VoltMeasurementAllowed.value +
' ' +
props.batteryData.VoltMeasurementAllowed.unit}
{props.batteryData.IoStatus.VoltMeasurementAllowed.value
? 'True'
: 'False'}
</TableCell>
</TableRow>
<TableRow>
@ -1281,9 +1284,9 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
paddingRight: '12px'
}}
>
{props.batteryData.AuxRelayBus.value +
' ' +
props.batteryData.AuxRelayBus.unit}
{props.batteryData.IoStatus.AuxRelayBus.value
? 'True'
: 'False'}
</TableCell>
</TableRow>
<TableRow>
@ -1303,9 +1306,9 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
paddingRight: '12px'
}}
>
{props.batteryData.RemoteStateActive.value +
' ' +
props.batteryData.RemoteStateActive.unit}
{props.batteryData.IoStatus.RemoteStateActive.value
? 'True'
: 'False'}
</TableCell>
</TableRow>
<TableRow>
@ -1325,9 +1328,9 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
paddingRight: '12px'
}}
>
{props.batteryData.RiscActive.value +
' ' +
props.batteryData.RiscActive.unit}
{props.batteryData.IoStatus.RiscActive.value
? 'True'
: 'False'}
</TableCell>
</TableRow>
</TableBody>
@ -1385,9 +1388,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
paddingRight: '12px'
}}
>
{props.batteryData.Eoc.value +
' ' +
props.batteryData.Eoc.unit}
{props.batteryData.Eoc.value}
</TableCell>
</TableRow>
@ -1408,9 +1409,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
paddingRight: '12px'
}}
>
{props.batteryData.SerialNumber.value +
' ' +
props.batteryData.SerialNumber.unit}
{props.batteryData.SerialNumber.value}
</TableCell>
</TableRow>
@ -1431,9 +1430,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
paddingRight: '12px'
}}
>
{props.batteryData.FwVersion.value +
' ' +
props.batteryData.FwVersion.unit}
{props.batteryData.FwVersion.value}
</TableCell>
</TableRow>
<TableRow>
@ -1453,35 +1450,33 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
paddingRight: '12px'
}}
>
{props.batteryData.TimeSinceTOC.value +
' ' +
props.batteryData.TimeSinceTOC.unit}
{props.batteryData.TimeSinceTOC.value}
</TableCell>
</TableRow>
{props.productNum === 0 && (
<TableRow>
<TableCell
component="th"
scope="row"
align="left"
sx={{ fontWeight: 'bold' }}
>
Calibration Charge Requested
</TableCell>
<TableCell
align="right"
sx={{
width: '6ch',
whiteSpace: 'nowrap',
paddingRight: '12px'
}}
>
{props.batteryData.CalibrationChargeRequested.value +
' ' +
props.batteryData.CalibrationChargeRequested.unit}
</TableCell>
</TableRow>
)}
{/*{props.productNum === 0 && (*/}
{/* <TableRow>*/}
{/* <TableCell*/}
{/* component="th"*/}
{/* scope="row"*/}
{/* align="left"*/}
{/* sx={{ fontWeight: 'bold' }}*/}
{/* >*/}
{/* Calibration Charge Requested*/}
{/* </TableCell>*/}
{/* <TableCell*/}
{/* align="right"*/}
{/* sx={{*/}
{/* width: '6ch',*/}
{/* whiteSpace: 'nowrap',*/}
{/* paddingRight: '12px'*/}
{/* }}*/}
{/* >*/}
{/* {props.batteryData.CalibrationChargeRequested.value +*/}
{/* ' ' +*/}
{/* props.batteryData.CalibrationChargeRequested.unit}*/}
{/* </TableCell>*/}
{/* </TableRow>*/}
{/*)}*/}
<TableRow>
<TableCell
component="th"
@ -1499,9 +1494,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
paddingRight: '12px'
}}
>
{props.batteryData.MaxChargePower.value +
' ' +
props.batteryData.MaxChargePower.unit}
{props.batteryData.MaxChargePower.value + ' W'}
</TableCell>
</TableRow>
<TableRow>
@ -1521,9 +1514,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
paddingRight: '12px'
}}
>
{props.batteryData.MaxDischargePower.value +
' ' +
props.batteryData.MaxDischargePower.unit}
{props.batteryData.MaxDischargePower.value + ' W'}
</TableCell>
</TableRow>
</TableBody>

View File

@ -5,6 +5,7 @@ import {
Grid,
IconButton,
Modal,
TextField,
Typography
} from '@mui/material';
import { FormattedMessage } from 'react-intl';
@ -15,9 +16,9 @@ import { getChartOptions } from '../Overview/chartOptions';
import {
BatteryDataInterface,
BatteryOverviewInterface,
transformInputToBatteryViewData
transformInputToBatteryViewDataJson
} from '../../../interfaces/Chart';
import dayjs from 'dayjs';
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';
@ -95,7 +96,7 @@ function MainStats(props: MainStatsProps) {
const resultPromise: Promise<{
chartData: BatteryDataInterface;
chartOverview: BatteryOverviewInterface;
}> = transformInputToBatteryViewData(
}> = transformInputToBatteryViewDataJson(
props.s3Credentials,
props.id,
product,
@ -191,7 +192,7 @@ function MainStats(props: MainStatsProps) {
const resultPromise: Promise<{
chartData: BatteryDataInterface;
chartOverview: BatteryOverviewInterface;
}> = transformInputToBatteryViewData(
}> = transformInputToBatteryViewDataJson(
props.s3Credentials,
props.id,
product,
@ -253,7 +254,7 @@ function MainStats(props: MainStatsProps) {
const resultPromise: Promise<{
chartData: BatteryDataInterface;
chartOverview: BatteryOverviewInterface;
}> = transformInputToBatteryViewData(
}> = transformInputToBatteryViewDataJson(
props.s3Credentials,
props.id,
product,
@ -360,19 +361,25 @@ function MainStats(props: MainStatsProps) {
<DateTimePicker
label="Select Start Date"
value={startDate}
onChange={(newDate) => setStartDate(newDate)}
sx={{
marginTop: 2
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) => setEndDate(newDate)}
sx={{
marginTop: 2
onChange={(newDate: Dayjs | null) => {
// Type assertion to Dayjs
if (newDate) {
setEndDate(newDate);
}
}}
renderInput={(props) => <TextField {...props} />}
/>
<div

View File

@ -20,17 +20,16 @@ import { FormattedMessage } from 'react-intl';
import Button from '@mui/material/Button';
import { Close as CloseIcon } from '@mui/icons-material';
import MenuItem from '@mui/material/MenuItem';
import {
DateTimePicker,
LocalizationProvider,
TimePicker
} from '@mui/x-date-pickers';
import { LocalizationProvider } from '@mui/x-date-pickers';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import dayjs from 'dayjs';
import axiosConfig from '../../../Resources/axiosConfig';
import utc from 'dayjs/plugin/utc';
import { UserContext } from '../../../contexts/userContext';
import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker';
import { TimePicker } from '@mui/lab';
interface ConfigurationProps {
values: TopologyValues;
id: number;
@ -352,15 +351,30 @@ function Configuration(props: ConfigurationProps) {
<div>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DateTimePicker
format="DD/MM/YYYY HH:mm"
ampm={false}
label="Select Next Calibration Charge Date"
value={dayjs(formValues.calibrationChargeDate)}
onChange={handleConfirm}
sx={{
marginTop: 2
}}
renderInput={(params) => (
<TextField
{...params}
sx={{
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>
</div>
)}
@ -405,6 +419,15 @@ function Configuration(props: ConfigurationProps) {
label="Calibration Charge Hour"
value={dayjs(formValues.calibrationChargeDate)}
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>
</div>

View File

@ -185,28 +185,30 @@ function HistoryOfActions(props: HistoryProps) {
}}
>
<div>
<DateTimePicker
label="Select Action Date"
name="timestamp"
value={editMode ? dayjs(newAction.timestamp) : actionDate}
onChange={(newDate) => handleDateChange(newDate)}
sx={{
width: 450,
marginTop: 2
}}
/>
{/*<DateTimePicker*/}
{/* label="Select Action Date"*/}
{/* name="timestamp"*/}
{/* value={actionDate}*/}
{/* onChange={handleDateChange}*/}
{/* renderInput={(params) => (*/}
{/* <TextField*/}
{/* {...params}*/}
{/* sx={{ width: 450, marginTop: 2 }}*/}
{/* />*/}
{/* )}*/}
{/* onChange={(newDate: Dayjs | null) =>*/}
{/* handleDateChange(newDate)*/}
{/* }*/}
{/* InputProps={{*/}
{/* sx: {*/}
{/* width: 450,*/}
{/* marginTop: 2*/}
{/* }*/}
{/* }}*/}
{/*/>*/}
<DateTimePicker
label="Select Action Date"
value={actionDate}
onChange={handleDateChange}
renderInput={(params) => (
<TextField {...params} sx={{ width: 450, marginTop: 2 }} />
)}
/>
<TextField
label="Description"
variant="outlined"

View File

@ -27,7 +27,6 @@ import BuildIcon from '@mui/icons-material/Build';
import { Navigate, Route, Routes, useLocation } from 'react-router-dom';
import routes from '../../../Resources/routes.json';
import Information from '../Information/Information';
import BatteryView from '../BatteryView/BatteryView';
import { UserType } from '../../../interfaces/UserTypes';
import HistoryOfActions from '../History/History';
import PvView from '../PvView/PvView';
@ -38,6 +37,9 @@ interface singleInstallationProps {
}
function Installation(props: singleInstallationProps) {
if (props.current_installation == undefined) {
return null;
}
const context = useContext(UserContext);
const { currentUser } = context;
const location = useLocation().pathname;
@ -49,10 +51,6 @@ function Installation(props: singleInstallationProps) {
const [connected, setConnected] = useState(true);
const [loading, setLoading] = useState(true);
if (props.current_installation == undefined) {
return null;
}
const S3data = {
s3Region: props.current_installation.s3Region,
s3Provider: props.current_installation.s3Provider,
@ -62,11 +60,8 @@ function Installation(props: singleInstallationProps) {
};
const s3Bucket =
props.current_installation.product === 0
? props.current_installation.s3BucketId.toString() +
'-3e5b3069-214a-43ee-8d85-57d72000c19d'
: props.current_installation.s3BucketId.toString() +
'-c0436b6a-d276-4cd8-9c44-1eae86cf5d0e';
props.current_installation.s3BucketId.toString() +
'-3e5b3069-214a-43ee-8d85-57d72000c19d';
const s3Credentials = { s3Bucket, ...S3data };
@ -74,6 +69,8 @@ function Installation(props: singleInstallationProps) {
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 fetchDataForOneTime = async () => {
@ -193,6 +190,7 @@ function Installation(props: singleInstallationProps) {
setCurrentTab(path[path.length - 1]);
}, [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(() => {
if (status === -1) {
setConnected(false);
@ -206,19 +204,22 @@ function Installation(props: singleInstallationProps) {
currentTab == 'configuration' ||
location.includes('batteryview')
) {
//Fetch periodically if the tab is live, pvview or batteryview
if (
currentTab == 'live' ||
(location.includes('batteryview') && !location.includes('mainstats')) ||
currentTab == 'pvview'
currentTab == 'pvview' ||
(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();
}
}
}
//Fetch only one time in configuration tab
if (currentTab == 'configuration') {
fetchDataForOneTime();
}
@ -227,6 +228,7 @@ function Installation(props: singleInstallationProps) {
continueFetching.current = false;
};
} else {
//If the tab is not live, pvview, batteryview or configuration, then stop fetching.
continueFetching.current = false;
}
}, [currentTab, location]);
@ -419,18 +421,18 @@ function Installation(props: singleInstallationProps) {
}
/>
<Route
path={routes.batteryview + '*'}
element={
<BatteryView
values={values}
s3Credentials={s3Credentials}
installationId={props.current_installation.id}
productNum={props.current_installation.product}
connected={connected}
></BatteryView>
}
></Route>
{/*<Route*/}
{/* path={routes.batteryview + '*'}*/}
{/* element={*/}
{/* <BatteryView*/}
{/* values={values}*/}
{/* s3Credentials={s3Credentials}*/}
{/* installationId={props.current_installation.id}*/}
{/* productNum={props.current_installation.product}*/}
{/* connected={connected}*/}
{/* ></BatteryView>*/}
{/* }*/}
{/*></Route>*/}
<Route
path={routes.pvview + '*'}

View File

@ -3,9 +3,171 @@ import { I_S3Credentials } from 'src/interfaces/S3Types';
import { FetchResult } from 'src/dataCache/dataCache';
import { DataRecord } from 'src/dataCache/data';
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';
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 = (
date: string,
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

@ -55,16 +55,6 @@ function InstallationTabs() {
}
}, [location]);
// useEffect(() => {
// if (salimaxInstallations && salimaxInstallations.length > 0) {
// if (socket) {
// closeSocket();
// }
//
// openSocket(salimaxInstallations);
// }
// }, [salimaxInstallations]);
useEffect(() => {
if (salimaxInstallations.length === 0) {
fetchAllInstallations();
@ -75,7 +65,7 @@ function InstallationTabs() {
if (salimaxInstallations && salimaxInstallations.length > 0) {
if (!socket) {
openSocket(0);
} else if (currentProduct == 1) {
} else if (currentProduct != 0) {
closeSocket();
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';
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 React, { useContext, useEffect, useState } from 'react';
import { I_S3Credentials } from 'src/interfaces/S3Types';
@ -13,12 +21,13 @@ import {
import Button from '@mui/material/Button';
import { FormattedMessage } from 'react-intl';
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 dayjs from 'dayjs';
import { UserContext } from '../../../contexts/userContext';
import { UserType } from '../../../interfaces/UserTypes';
import { TimeSpan, UnixTime } from '../../../dataCache/time';
import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker';
interface OverviewProps {
s3Credentials: I_S3Credentials;
@ -73,6 +82,13 @@ function Overview(props: OverviewProps) {
const [endDate, setEndDate] = useState(dayjs());
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(() => {
if (isZooming) {
setLoading(true);
@ -408,18 +424,30 @@ function Overview(props: OverviewProps) {
label="Select Start Date"
value={startDate}
onChange={(newDate) => setStartDate(newDate)}
sx={{
marginTop: 2
}}
renderInput={(params) => (
<TextField
{...params}
sx={{
marginTop: 2, // Apply styles here
width: '100%' // Optional: You can adjust the width or other styling here
}}
/>
)}
/>
<DateTimePicker
label="Select End Date"
value={endDate}
onChange={(newDate) => setEndDate(newDate)}
sx={{
marginTop: 2
}}
renderInput={(params) => (
<TextField
{...params}
sx={{
marginTop: 2, // Apply styles here
width: '100%' // Optional: You can adjust the width or other styling here
}}
/>
)}
/>
<div

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 React, { useContext, useEffect, useState } from 'react';
import { I_S3Credentials } from 'src/interfaces/S3Types';
@ -6,14 +14,14 @@ import { getChartOptions } from './chartOptions';
import {
chartAggregatedDataInterface,
overviewInterface,
transformInputToAggregatedData
transformInputToAggregatedDataJson
} from 'src/interfaces/Chart';
import Button from '@mui/material/Button';
import { FormattedMessage } from 'react-intl';
import CircularProgress from '@mui/material/CircularProgress';
import { DateTimePicker, LocalizationProvider } from '@mui/x-date-pickers';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import dayjs from 'dayjs';
import dayjs, { Dayjs } from 'dayjs';
import { UserContext } from '../../../contexts/userContext';
import { UserType } from '../../../interfaces/UserTypes';
@ -37,6 +45,546 @@ const computeLast7Days = (): string[] => {
}
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) {
const context = useContext(UserContext);
@ -79,7 +627,7 @@ function SalidomoOverview(props: salidomoOverviewProps) {
chartAggregatedData: chartAggregatedDataInterface;
chartOverview: overviewInterface;
dateList: string[];
}> = transformInputToAggregatedData(
}> = transformInputToAggregatedDataJson(
props.s3Credentials,
dayjs().subtract(1, 'week'),
dayjs()
@ -149,7 +697,7 @@ function SalidomoOverview(props: salidomoOverviewProps) {
chartAggregatedData: chartAggregatedDataInterface;
chartOverview: overviewInterface;
dateList: string[];
}> = transformInputToAggregatedData(
}> = transformInputToAggregatedDataJson(
props.s3Credentials,
startDate,
endDate
@ -266,19 +814,25 @@ function SalidomoOverview(props: salidomoOverviewProps) {
<DateTimePicker
label="Select Start Date"
value={startDate}
onChange={(newDate) => setStartDate(newDate)}
sx={{
marginTop: 2
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) => setEndDate(newDate)}
sx={{
marginTop: 2
onChange={(newDate: Dayjs | null) => {
// Type assertion to Dayjs
if (newDate) {
setEndDate(newDate);
}
}}
renderInput={(props) => <TextField {...props} />}
/>
<div

View File

@ -10,12 +10,9 @@ import { I_Installation } from 'src/interfaces/InstallationTypes';
import { UserContext } from 'src/contexts/userContext';
import { TimeSpan, UnixTime } from 'src/dataCache/time';
import { FetchResult } from 'src/dataCache/dataCache';
import {
extractValues,
TopologyValues
} from 'src/content/dashboards/Log/graph.util';
import { JSONRecordData } from 'src/content/dashboards/Log/graph.util';
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 routes from '../../../Resources/routes.json';
import InformationSalidomo from '../Information/InformationSalidomo';
@ -40,7 +37,7 @@ function SalidomoInstallation(props: singleInstallationProps) {
const location = useLocation().pathname;
const [errorLoadingS3Data, setErrorLoadingS3Data] = useState(false);
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 [
failedToCommunicateWithInstallation,
@ -77,7 +74,7 @@ function SalidomoInstallation(props: singleInstallationProps) {
const fetchDataPeriodically = async () => {
var timeperiodToSearch = 30;
let res;
let res: FetchResult<Record<string, JSONRecordData>> | undefined;
let timestampToFetch;
for (var i = 0; i < timeperiodToSearch; i += 1) {
@ -88,7 +85,7 @@ function SalidomoInstallation(props: singleInstallationProps) {
console.log('timestamp to fetch is ' + timestampToFetch);
try {
res = await fetchData(timestampToFetch, s3Credentials, true);
res = await fetchDataJson(timestampToFetch, s3Credentials, true);
if (res !== FetchResult.notAvailable && res !== FetchResult.tryLater) {
break;
}
@ -114,15 +111,16 @@ function SalidomoInstallation(props: singleInstallationProps) {
return false;
}
console.log(`Timestamp: ${timestamp}`);
console.log(res[timestamp]);
//console.log('object is' + res[timestamp]);
// Set values asynchronously with delay
setValues(
extractValues({
time: UnixTime.fromTicks(parseInt(timestamp, 10)),
value: res[timestamp]
})
);
setValues(res[timestamp]);
// setValues(
// extractValues({
// time: UnixTime.fromTicks(parseInt(timestamp, 10)),
// value: res[timestamp]
// })
// );
// Wait for 2 seconds before processing next timestamp
await timeout(2000);
}
@ -137,7 +135,7 @@ function SalidomoInstallation(props: singleInstallationProps) {
try {
console.log('Trying to fetch timestamp ' + timestampToFetch);
res = await fetchData(timestampToFetch, s3Credentials, true);
res = await fetchDataJson(timestampToFetch, s3Credentials, true);
if (
res !== FetchResult.notAvailable &&
res !== FetchResult.tryLater

View File

@ -40,9 +40,7 @@ function SalidomoInstallationTabs() {
} = useContext(InstallationsContext);
const { product, setProduct } = useContext(ProductIdContext);
// const webSocketsContext = useContext(WebSocketContext);
// const { socket, openSocket, closeSocket } = webSocketsContext;
//The following useEffect is for the current tab to be bold. Based on the location, we set the corresponding tab to be bold
useEffect(() => {
let path = location.pathname.split('/');
@ -57,6 +55,7 @@ function SalidomoInstallationTabs() {
}, [location]);
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) {
fetchAllSalidomoInstallations();
setFetchedInstallations(true);
@ -64,10 +63,12 @@ function SalidomoInstallationTabs() {
}, [salidomoInstallations]);
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 (!socket) {
openSocket(1);
} else if (currentProduct == 0) {
} else if (currentProduct != 1) {
//If there is any other open websocket for another product, close it.
closeSocket();
openSocket(1);
}
@ -82,6 +83,7 @@ function SalidomoInstallationTabs() {
setCurrentTab(value);
};
//Manually compute the path when the user clicks a tab
const navigateToTabPath = (pathname: string, tab_value: string): string => {
let pathlist = pathname.split('/');
let ret_path = '';

View File

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

View File

@ -1,4 +1,4 @@
import React, { useContext, useEffect, useState } from 'react';
import React, { useContext, useEffect, useRef, useState } from 'react';
import {
Card,
CircularProgress,
@ -20,6 +20,7 @@ import BuildIcon from '@mui/icons-material/Build';
import AccessContextProvider from '../../../contexts/AccessContextProvider';
import Access from '../ManageAccess/Access';
import InformationSodioHome from '../Information/InformationSodioHome';
import CryptoJS from 'crypto-js';
interface singleInstallationProps {
current_installation?: I_Installation;
@ -27,6 +28,9 @@ interface singleInstallationProps {
}
function SodioHomeInstallation(props: singleInstallationProps) {
if (props.current_installation == undefined) {
return null;
}
const context = useContext(UserContext);
const { currentUser } = context;
const location = useLocation().pathname;
@ -40,17 +44,97 @@ function SodioHomeInstallation(props: singleInstallationProps) {
] = useState(0);
const [connected, setConnected] = useState(true);
const [loading, setLoading] = useState(true);
if (props.current_installation == undefined) {
return null;
}
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) {
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(() => {
let path = location.split('/');
setCurrentTab(path[path.length - 1]);
@ -62,6 +146,33 @@ function SodioHomeInstallation(props: singleInstallationProps) {
}
}, [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 (
<>
<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*/}
{/* path={routes.overview}*/}
{/* element={*/}
@ -279,7 +402,7 @@ function SodioHomeInstallation(props: singleInstallationProps) {
<Route
path={'*'}
element={<Navigate to={routes.information}></Navigate>}
element={<Navigate to={routes.live}></Navigate>}
/>
</Routes>
</Grid>

View File

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

View File

@ -41,18 +41,17 @@ const InstallationsContextProvider = ({
const [socket, setSocket] = useState<WebSocket>(null);
const [currentProduct, setcurrentProduct] = useState<number>(0);
//Store pending updates and apply them in batches
const pendingUpdates = useRef<
Record<number, { status: number; testingMode: boolean }>
>({}); // To store pending updates
>({});
const updateInstallationStatus = useCallback(
(product, id, status, testingMode) => {
// Buffer updates instead of applying them immediately
pendingUpdates.current[id] = { status: Number(status), testingMode }; // Ensure status is a number
},
[]
);
const updateInstallationStatus = useCallback((id, status, testingMode) => {
//Insert the incoming message to the pendingUpdates map
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(() => {
if (Object.keys(pendingUpdates.current).length === 0) return; // No updates to apply
@ -109,7 +108,7 @@ const InstallationsContextProvider = ({
setcurrentProduct(product);
const tokenString = localStorage.getItem('token');
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);
@ -124,7 +123,7 @@ const InstallationsContextProvider = ({
installationsToSend = sodiohomeInstallations;
}
// Send the corresponding installation IDs
// Send the corresponding installation IDs to the backend
socket.send(
JSON.stringify(
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
const pingInterval = setInterval(() => {
if (socket.readyState === WebSocket.OPEN) {
@ -152,8 +141,8 @@ const InstallationsContextProvider = ({
socket.addEventListener('message', (event) => {
const message = JSON.parse(event.data); // Parse the JSON data
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(
product,
message.id,
message.status,
message.testingMode

View File

@ -1,7 +1,9 @@
import dayjs from 'dayjs';
import {
fetchAggregatedData,
fetchData
fetchAggregatedDataJson,
fetchData,
fetchDataJson
} from '../content/dashboards/Installations/fetchData';
import { FetchResult } from '../dataCache/dataCache';
import { I_S3Credentials } from './S3Types';
@ -9,6 +11,7 @@ import { TimeSpan, UnixTime } from '../dataCache/time';
import { DataRecord } from '../dataCache/data';
import axiosConfig from '../Resources/axiosConfig';
import { AxiosError, AxiosResponse } from 'axios';
import { JSONRecordData } from '../content/dashboards/Log/graph.util';
export interface chartInfoInterface {
magnitude: number;
@ -68,18 +71,7 @@ export interface BatteryOverviewInterface {
Current: chartInfoInterface;
}
// 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]]},
// ]}
export const transformInputToBatteryViewData = async (
export const transformInputToBatteryViewDataJson = async (
s3Credentials: I_S3Credentials,
id: number,
product: number,
@ -93,24 +85,24 @@ export const transformInputToBatteryViewData = async (
const MAX_NUMBER = 9999999;
const categories = ['Soc', 'Temperature', 'Power', 'Voltage', 'Current'];
const pathCategories = [
'Soc',
'Temperatures/Cells/Average',
'Dc/Power',
'Dc/Voltage',
'Dc/Current'
'.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/'
'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 = [];
@ -120,7 +112,7 @@ export const transformInputToBatteryViewData = async (
Temperature: { name: 'Temperature', data: [] },
Power: { name: 'Power', data: [] },
Voltage: { name: 'Voltage', data: [] },
Current: { name: 'Voltage', data: [] }
Current: { name: 'Current', data: [] }
};
const chartOverview: BatteryOverviewInterface = {
@ -132,7 +124,6 @@ export const transformInputToBatteryViewData = async (
};
let initialiation = true;
let timestampArray: number[] = [];
let adjustedTimestampArray = [];
const timestampPromises = [];
@ -153,7 +144,7 @@ export const transformInputToBatteryViewData = async (
for (var i = 0; i < timestampArray.length; i++) {
timestampPromises.push(
fetchDataForOneTime(
fetchJsonDataForOneTime(
UnixTime.fromTicks(timestampArray[i], true),
s3Credentials
)
@ -170,7 +161,7 @@ export const transformInputToBatteryViewData = async (
adjustedTimestampArray.push(adjustedTimestamp);
}
const results: Promise<FetchResult<Record<string, DataRecord>>>[] =
const results: Promise<FetchResult<Record<string, JSONRecordData>>>[] =
await Promise.all(timestampPromises);
for (let i = 0; i < results.length; i++) {
@ -180,8 +171,10 @@ export const transformInputToBatteryViewData = async (
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
const battery_nodes = result.Config.Devices.BatteryNodes.value
.toString()
.split(',');
@ -227,28 +220,31 @@ export const transformInputToBatteryViewData = async (
let category = categories[category_index];
for (let j = 0; j < pathsToSave.length; j++) {
let path = pathsToSearch[j] + pathCategories[category_index];
let path =
pathsToSearch[j] + pathCategories[category_index] + '.value';
if (result[path]) {
const value = result[path];
//if (result[path]) {
const value = path
.split('.')
.reduce((o, key) => (o ? o[key] : undefined), result);
if (value.value < chartOverview[category].min) {
chartOverview[category].min = value.value;
}
if (value.value > chartOverview[category].max) {
chartOverview[category].max = value.value;
}
chartData[category].data[pathsToSave[j]].data.push([
adjustedTimestampArray[i],
value.value
]);
} else {
// chartData[category].data[pathsToSave[j]].data.push([
// adjustedTimestampArray[i],
// null
// ]);
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
// // ]);
// }
}
}
}
@ -288,6 +284,491 @@ export const transformInputToBatteryViewData = async (
};
};
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 {
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;
// result[path].value = -result[path].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.
//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]]},
// ]}
// export const transformInputToBatteryViewData = 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: 'Voltage', 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(
// fetchDataForOneTime(
// 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, DataRecord>>>[] =
// 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];
//
// if (result[path]) {
// const value = result[path];
//
// if (value.value < chartOverview[category].min) {
// chartOverview[category].min = value.value;
// }
//
// if (value.value > chartOverview[category].max) {
// chartOverview[category].max = value.value;
// }
// chartData[category].data[pathsToSave[j]].data.push([
// adjustedTimestampArray[i],
// value.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
// };
// };
// 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 (
startUnixTime: UnixTime,
s3Credentials: I_S3Credentials