Merge remote-tracking branch 'origin/main'
This commit is contained in:
commit
94829992aa
|
@ -233,7 +233,8 @@ public class Controller : ControllerBase
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Define a regex pattern to match the filenames without .csv extension
|
// Define a regex pattern to match the filenames without .csv extension
|
||||||
var pattern = @"/([^/]+)\.csv$";
|
|
||||||
|
var pattern = @"/([^/]+)\.(csv|json)$";
|
||||||
var regex = new Regex(pattern);
|
var regex = new Regex(pattern);
|
||||||
|
|
||||||
// Process each line of the output
|
// Process each line of the output
|
||||||
|
|
|
@ -25,8 +25,8 @@ public static class Program
|
||||||
Db.Init();
|
Db.Init();
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
//RabbitMqManager.InitializeEnvironment();
|
RabbitMqManager.InitializeEnvironment();
|
||||||
//RabbitMqManager.StartRabbitMqConsumer().SupressAwaitWarning();
|
RabbitMqManager.StartRabbitMqConsumer().SupressAwaitWarning();
|
||||||
WebsocketManager.MonitorSalimaxInstallationTable().SupressAwaitWarning();
|
WebsocketManager.MonitorSalimaxInstallationTable().SupressAwaitWarning();
|
||||||
WebsocketManager.MonitorSalidomoInstallationTable().SupressAwaitWarning();
|
WebsocketManager.MonitorSalidomoInstallationTable().SupressAwaitWarning();
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
#To deploy to the monitor server, uncomment the following line
|
#To deploy to the monitor server, uncomment the following line
|
||||||
#dotnet publish Backend.csproj -c Release -r linux-x64 --self-contained true -p:PublishTrimmed=false && rsync -av bin/Release/net6.0/linux-x64/publish/ ubuntu@194.182.190.208:~/backend && ssh ubuntu@194.182.190.208 'sudo systemctl restart backend'
|
dotnet publish Backend.csproj -c Release -r linux-x64 --self-contained true -p:PublishTrimmed=false && rsync -av bin/Release/net6.0/linux-x64/publish/ ubuntu@194.182.190.208:~/backend && ssh ubuntu@194.182.190.208 'sudo systemctl restart backend'
|
||||||
|
|
||||||
#To deploy to the stage server, uncomment the following line
|
#To deploy to the stage server, uncomment the following line
|
||||||
dotnet publish Backend.csproj -c Release -r linux-x64 --self-contained true -p:PublishTrimmed=false && rsync -av bin/Release/net6.0/linux-x64/publish/ ubuntu@91.92.154.141:~/backend && ssh ubuntu@91.92.154.141 'sudo systemctl restart backend'
|
#dotnet publish Backend.csproj -c Release -r linux-x64 --self-contained true -p:PublishTrimmed=false && rsync -av bin/Release/net6.0/linux-x64/publish/ ubuntu@91.92.154.141:~/backend && ssh ubuntu@91.92.154.141 'sudo systemctl restart backend'
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System.Text.Json;
|
||||||
using InnovEnergy.App.SaliMax.Devices;
|
using InnovEnergy.App.SaliMax.Devices;
|
||||||
using InnovEnergy.App.SaliMax.SaliMaxRelays;
|
using InnovEnergy.App.SaliMax.SaliMaxRelays;
|
||||||
using InnovEnergy.App.SaliMax.System;
|
using InnovEnergy.App.SaliMax.System;
|
||||||
|
@ -29,5 +30,26 @@ public record StatusRecord
|
||||||
public required SystemLog Log { get; init; } // TODO: init only
|
public required SystemLog Log { get; init; } // TODO: init only
|
||||||
|
|
||||||
public required EssControl EssControl { get; set; } // TODO: init only
|
public required EssControl EssControl { get; set; } // TODO: init only
|
||||||
public required StateMachine StateMachine { get; init; }
|
public required StateMachine StateMachine { get; init; }
|
||||||
|
|
||||||
|
|
||||||
|
public string ToJson()
|
||||||
|
{
|
||||||
|
// Try to get the "Battery" property via reflection
|
||||||
|
// var batteryProperty = thing.GetType().GetProperty("Battery");
|
||||||
|
// if (batteryProperty == null)
|
||||||
|
// throw new InvalidOperationException("The object does not have a 'Battery' property.");
|
||||||
|
//
|
||||||
|
// // Retrieve the value of the Battery property
|
||||||
|
// var batteryValue = Battery.GetValue(thing);
|
||||||
|
var jsonOptions = new JsonSerializerOptions { WriteIndented = true };
|
||||||
|
|
||||||
|
// Serialize the Battery property
|
||||||
|
Console.WriteLine("eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee");
|
||||||
|
string json = JsonSerializer.Serialize(this.Battery, jsonOptions);
|
||||||
|
Console.WriteLine(json);
|
||||||
|
|
||||||
|
|
||||||
|
return json;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -815,7 +815,8 @@ internal static class Program
|
||||||
|
|
||||||
private static async Task<Boolean> UploadCsv(StatusRecord status, DateTime timeStamp)
|
private static async Task<Boolean> UploadCsv(StatusRecord status, DateTime timeStamp)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
status.ToJson();
|
||||||
var csv = status.ToCsv().LogInfo();
|
var csv = status.ToCsv().LogInfo();
|
||||||
|
|
||||||
await RestApiSavingFile(csv);
|
await RestApiSavingFile(csv);
|
||||||
|
|
|
@ -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;
|
|
||||||
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -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);
|
|
||||||
|
|
||||||
}
|
|
|
@ -13,10 +13,11 @@ import hmac
|
||||||
import hashlib
|
import hashlib
|
||||||
from threading import Thread, Event
|
from threading import Thread, Event
|
||||||
import config as cfg
|
import config as cfg
|
||||||
|
import json
|
||||||
|
|
||||||
CSV_DIR = "/data/csv_files/"
|
JSON_DIR = "/data/json_files/"
|
||||||
HOURLY_DIR = "/data/csv_files/HourlyData"
|
HOURLY_DIR = "/data/json_files/HourlyData"
|
||||||
DAILY_DIR = "/data/csv_files/DailyData"
|
DAILY_DIR = "/data/json_files/DailyData"
|
||||||
|
|
||||||
# S3 Credentials
|
# S3 Credentials
|
||||||
print("Start with the correct credentials")
|
print("Start with the correct credentials")
|
||||||
|
@ -38,23 +39,24 @@ class AggregatedData:
|
||||||
self.charging_battery_power = charging_battery_power
|
self.charging_battery_power = charging_battery_power
|
||||||
self.heating_power = heating_power
|
self.heating_power = heating_power
|
||||||
|
|
||||||
def to_csv(self):
|
def to_json(self):
|
||||||
return ("/MinSoc;{};\n"
|
return json.dumps({
|
||||||
"/MaxSoc;{};\n"
|
"MinSoc": self.min_soc,
|
||||||
"/DischargingBatteryPower;{};\n"
|
"MaxSoc": self.max_soc,
|
||||||
"/ChargingBatteryPower;{};\n"
|
"DischargingBatteryPower": self.discharging_battery_power,
|
||||||
"/HeatingPower;{};").format(
|
"ChargingBatteryPower": self.charging_battery_power,
|
||||||
self.min_soc, self.max_soc, self.discharging_battery_power, self.charging_battery_power, self.heating_power)
|
"HeatingPower": self.heating_power
|
||||||
|
}, separators=(',', ':'))
|
||||||
|
|
||||||
def save(self, directory):
|
def save(self, directory):
|
||||||
timestamp = int(time.time())
|
timestamp = int(time.time())
|
||||||
if not os.path.exists(directory):
|
if not os.path.exists(directory):
|
||||||
os.makedirs(directory)
|
os.makedirs(directory)
|
||||||
csv_path = os.path.join(directory, "{}.csv".format(timestamp))
|
json_path = os.path.join(directory, "{}.json".format(timestamp))
|
||||||
with open(csv_path, 'w') as file:
|
with open(json_path, 'w') as file:
|
||||||
file.write(self.to_csv())
|
file.write(self.to_json())
|
||||||
print("Saved file to:", csv_path)
|
print("Saved file to:", json_path)
|
||||||
print("File content:\n", self.to_csv())
|
print("File content:\n", self.to_json())
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def delete_data(directory):
|
def delete_data(directory):
|
||||||
|
@ -67,16 +69,16 @@ class AggregatedData:
|
||||||
print("Deleted file: {}".format(file_path))
|
print("Deleted file: {}".format(file_path))
|
||||||
|
|
||||||
def push_to_s3(self, s3_config):
|
def push_to_s3(self, s3_config):
|
||||||
csv_data = self.to_csv()
|
json_data = self.to_json()
|
||||||
compressed_csv = self.compress_csv_data(csv_data)
|
compressed_json = self.compress_json_data(json_data)
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
if now.hour == 0 and now.minute < 30:
|
if now.hour == 0 and now.minute < 30:
|
||||||
adjusted_date = now - timedelta(days=1)
|
adjusted_date = now - timedelta(days=1)
|
||||||
else:
|
else:
|
||||||
adjusted_date = now
|
adjusted_date = now
|
||||||
|
|
||||||
s3_path = adjusted_date.strftime("%Y-%m-%d") + ".csv"
|
s3_path = adjusted_date.strftime("%Y-%m-%d") + ".json"
|
||||||
response = s3_config.create_put_request(s3_path, compressed_csv)
|
response = s3_config.create_put_request(s3_path, compressed_json)
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
print("ERROR: PUT", response.text)
|
print("ERROR: PUT", response.text)
|
||||||
return False
|
return False
|
||||||
|
@ -84,10 +86,10 @@ class AggregatedData:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def compress_csv_data(csv_data):
|
def compress_json_data(json_data):
|
||||||
memory_stream = io.BytesIO()
|
memory_stream = io.BytesIO()
|
||||||
with zipfile.ZipFile(memory_stream, 'w', zipfile.ZIP_DEFLATED) as archive:
|
with zipfile.ZipFile(memory_stream, 'w', zipfile.ZIP_DEFLATED) as archive:
|
||||||
archive.writestr("data.csv", csv_data.encode('utf-8'))
|
archive.writestr("data.json", json_data.encode('utf-8'))
|
||||||
compressed_bytes = memory_stream.getvalue()
|
compressed_bytes = memory_stream.getvalue()
|
||||||
return base64.b64encode(compressed_bytes).decode('utf-8')
|
return base64.b64encode(compressed_bytes).decode('utf-8')
|
||||||
|
|
||||||
|
@ -150,7 +152,7 @@ class Aggregator:
|
||||||
current_time = datetime.now()
|
current_time = datetime.now()
|
||||||
after_timestamp = datetime_to_timestamp(current_time - timedelta(hours=1))
|
after_timestamp = datetime_to_timestamp(current_time - timedelta(hours=1))
|
||||||
before_timestamp = datetime_to_timestamp(current_time)
|
before_timestamp = datetime_to_timestamp(current_time)
|
||||||
aggregated_data = Aggregator.create_hourly_data(CSV_DIR, after_timestamp, before_timestamp)
|
aggregated_data = Aggregator.create_hourly_data(JSON_DIR, after_timestamp, before_timestamp)
|
||||||
print("Saving in hourly directory")
|
print("Saving in hourly directory")
|
||||||
aggregated_data.save(HOURLY_DIR)
|
aggregated_data.save(HOURLY_DIR)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -195,31 +197,55 @@ class Aggregator:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_hourly_data(directory, after_timestamp, before_timestamp):
|
def create_hourly_data(directory, after_timestamp, before_timestamp):
|
||||||
node_data = {}
|
node_data = {}
|
||||||
|
print("INSIDE HOURLY MANAGER")
|
||||||
|
|
||||||
for filename in os.listdir(directory):
|
for filename in os.listdir(directory):
|
||||||
file_path = os.path.join(directory, filename)
|
file_path = os.path.join(directory, filename)
|
||||||
if os.path.isfile(file_path) and Aggregator.is_file_within_time_range(filename, after_timestamp, before_timestamp):
|
if os.path.isfile(file_path) and Aggregator.is_file_within_time_range(filename, after_timestamp, before_timestamp):
|
||||||
with open(file_path, 'r') as file:
|
with open(file_path, 'r') as file:
|
||||||
reader = csv.reader(file, delimiter=';')
|
|
||||||
for row in reader:
|
data = json.load(file)
|
||||||
if len(row) >= 2:
|
devices = data.get("Battery", {}).get("Devices", {})
|
||||||
variable_name, value = row[0], row[1]
|
|
||||||
try:
|
for node_number, device_data in devices.items():
|
||||||
value = float(value)
|
|
||||||
node_number = Aggregator.extract_node_number(variable_name)
|
if node_number not in node_data:
|
||||||
if node_number not in node_data:
|
node_data[node_number] = {'soc': [], 'discharge': [], 'charge': [], 'heating': []}
|
||||||
node_data[node_number] = {'soc': [], 'discharge': [], 'charge': [], 'heating': []}
|
|
||||||
if "Soc" in variable_name:
|
value = device_data.get("Soc", {}).get("value", "N/A")
|
||||||
node_data[node_number]['soc'].append(value)
|
node_data[node_number]['soc'].append(float(value))
|
||||||
elif "/Dc/Power" in variable_name or "/DischargingBatteryPower" in variable_name or "/ChargingBatteryPower" in variable_name:
|
|
||||||
if value < 0:
|
value = device_data.get("Dc", {}).get("Power", "N/A").get("value", "N/A")
|
||||||
node_data[node_number]['discharge'].append(value)
|
value = float(value)
|
||||||
else:
|
if value < 0:
|
||||||
node_data[node_number]['charge'].append(value)
|
node_data[node_number]['discharge'].append(value)
|
||||||
elif "/HeatingPower" in variable_name:
|
else:
|
||||||
node_data[node_number]['heating'].append(value)
|
node_data[node_number]['charge'].append(value)
|
||||||
except ValueError:
|
value = device_data.get("HeatingPower", "N/A").get("value", "N/A")
|
||||||
pass
|
value = float(value)
|
||||||
|
node_data[node_number]['heating'].append(value)
|
||||||
|
|
||||||
|
|
||||||
|
# reader = csv.reader(file, delimiter=';')
|
||||||
|
# for row in reader:
|
||||||
|
# if len(row) >= 2:
|
||||||
|
# variable_name, value = row[0], row[1]
|
||||||
|
# try:
|
||||||
|
# value = float(value)
|
||||||
|
# node_number = Aggregator.extract_node_number(variable_name)
|
||||||
|
# if node_number not in node_data:
|
||||||
|
# node_data[node_number] = {'soc': [], 'discharge': [], 'charge': [], 'heating': []}
|
||||||
|
# if "Soc" in variable_name:
|
||||||
|
# node_data[node_number]['soc'].append(value)
|
||||||
|
# elif "/Dc/Power" in variable_name or "/DischargingBatteryPower" in variable_name or "/ChargingBatteryPower" in variable_name:
|
||||||
|
# if value < 0:
|
||||||
|
# node_data[node_number]['discharge'].append(value)
|
||||||
|
# else:
|
||||||
|
# node_data[node_number]['charge'].append(value)
|
||||||
|
# elif "/HeatingPower" in variable_name:
|
||||||
|
# node_data[node_number]['heating'].append(value)
|
||||||
|
# except ValueError:
|
||||||
|
# pass
|
||||||
|
|
||||||
if len(node_data) == 0:
|
if len(node_data) == 0:
|
||||||
# No data collected, return default AggregatedData with zeros
|
# No data collected, return default AggregatedData with zeros
|
||||||
|
@ -249,7 +275,45 @@ class Aggregator:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_daily_data(directory, after_timestamp, before_timestamp):
|
def create_daily_data(directory, after_timestamp, before_timestamp):
|
||||||
return Aggregator.create_hourly_data(directory, after_timestamp, before_timestamp)
|
|
||||||
|
node_data = {'MinSoc': [], 'MaxSoc': [], 'ChargingBatteryPower': [], 'DischargingBatteryPower': [],
|
||||||
|
'HeatingPower': []}
|
||||||
|
|
||||||
|
for filename in os.listdir(directory):
|
||||||
|
file_path = os.path.join(directory, filename)
|
||||||
|
if os.path.isfile(file_path) and Aggregator.is_file_within_time_range(filename, after_timestamp,
|
||||||
|
before_timestamp):
|
||||||
|
with open(file_path, 'r') as file:
|
||||||
|
data = json.load(file)
|
||||||
|
|
||||||
|
value = data.get("MinSoc", "N/A")
|
||||||
|
node_data['MinSoc'].append(float(value))
|
||||||
|
|
||||||
|
value = data.get("MaxSoc", "N/A")
|
||||||
|
node_data['MaxSoc'].append(float(value))
|
||||||
|
|
||||||
|
value = data.get("ChargingBatteryPower", "N/A")
|
||||||
|
node_data['ChargingBatteryPower'].append(float(value))
|
||||||
|
|
||||||
|
value = data.get("DischargingBatteryPower", "N/A")
|
||||||
|
node_data['DischargingBatteryPower'].append(float(value))
|
||||||
|
|
||||||
|
value = data.get("HeatingPower", "N/A")
|
||||||
|
node_data['HeatingPower'].append(float(value))
|
||||||
|
|
||||||
|
print(node_data)
|
||||||
|
|
||||||
|
min_soc = min(node_data['MinSoc']) if node_data else 0.0
|
||||||
|
max_soc = max(node_data['MaxSoc']) if node_data else 0.0
|
||||||
|
total_discharging_power = sum(node_data['DischargingBatteryPower']) if node_data else 0.0
|
||||||
|
total_charging_power = sum(node_data['ChargingBatteryPower']) if node_data else 0.0
|
||||||
|
total_heating_power = sum(node_data['HeatingPower']) if node_data else 0.0
|
||||||
|
|
||||||
|
avg_discharging_power = total_discharging_power / len(node_data['DischargingBatteryPower'])
|
||||||
|
avg_charging_power = total_charging_power / len(node_data['ChargingBatteryPower'])
|
||||||
|
avg_heating_power = total_heating_power / len(node_data['HeatingPower'])
|
||||||
|
|
||||||
|
return AggregatedData(min_soc, max_soc, avg_discharging_power, avg_charging_power, avg_heating_power)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_file_within_time_range(filename, start_time, end_time):
|
def is_file_within_time_range(filename, start_time, end_time):
|
||||||
|
|
|
@ -23,21 +23,10 @@ from os import path
|
||||||
|
|
||||||
app_dir = path.dirname(path.realpath(__file__))
|
app_dir = path.dirname(path.realpath(__file__))
|
||||||
sys.path.insert(1, path.join(app_dir, 'ext', 'velib_python'))
|
sys.path.insert(1, path.join(app_dir, 'ext', 'velib_python'))
|
||||||
|
|
||||||
from vedbus import VeDbusService as DBus
|
from vedbus import VeDbusService as DBus
|
||||||
|
|
||||||
import time
|
|
||||||
import os
|
import os
|
||||||
import csv
|
import csv
|
||||||
|
|
||||||
import requests
|
|
||||||
import hmac
|
|
||||||
import hashlib
|
|
||||||
import base64
|
|
||||||
from datetime import datetime
|
|
||||||
import io
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import hmac
|
import hmac
|
||||||
import hashlib
|
import hashlib
|
||||||
|
@ -45,22 +34,18 @@ import base64
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import pika
|
import pika
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
|
||||||
# zip-comp additions
|
# zip-comp additions
|
||||||
import zipfile
|
import zipfile
|
||||||
import io
|
import io
|
||||||
import shutil
|
|
||||||
|
|
||||||
def compress_csv_data(csv_data, file_name="data.csv"):
|
def compress_json_data(json_data, file_name="data.json"):
|
||||||
|
|
||||||
memory_stream = io.BytesIO()
|
memory_stream = io.BytesIO()
|
||||||
|
|
||||||
# Create a zip archive in the memory buffer
|
# Create a zip archive in the memory buffer
|
||||||
with zipfile.ZipFile(memory_stream, 'w', zipfile.ZIP_DEFLATED) as archive:
|
with zipfile.ZipFile(memory_stream, 'w', zipfile.ZIP_DEFLATED) as archive:
|
||||||
# Add CSV data to the ZIP archive
|
# Add JSON data to the ZIP archive
|
||||||
with archive.open('data.csv', 'w') as entry_stream:
|
with archive.open('data.json', 'w') as entry_stream:
|
||||||
entry_stream.write(csv_data.encode('utf-8'))
|
entry_stream.write(json_data.encode('utf-8'))
|
||||||
|
|
||||||
# Get the compressed byte array from the memory buffer
|
# Get the compressed byte array from the memory buffer
|
||||||
compressed_bytes = memory_stream.getvalue()
|
compressed_bytes = memory_stream.getvalue()
|
||||||
|
@ -112,9 +97,9 @@ class S3config:
|
||||||
).decode()
|
).decode()
|
||||||
return f"AWS {s3_key}:{signature}"
|
return f"AWS {s3_key}:{signature}"
|
||||||
|
|
||||||
def read_csv_as_string(file_path):
|
def read_json_as_string(file_path):
|
||||||
"""
|
"""
|
||||||
Reads a CSV file from the given path and returns its content as a single string.
|
Reads a JSON file from the given path and returns its content as a single string.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
with open(file_path, 'r', encoding='utf-8') as file:
|
with open(file_path, 'r', encoding='utf-8') as file:
|
||||||
|
@ -126,8 +111,7 @@ def read_csv_as_string(file_path):
|
||||||
print(f"IO error occurred: {str(e)}")
|
print(f"IO error occurred: {str(e)}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
CSV_DIR = "/data/csv_files/"
|
JSON_DIR = "/data/json_files/"
|
||||||
#CSV_DIR = "csv_files/"
|
|
||||||
|
|
||||||
# Define the path to the file containing the installation name
|
# Define the path to the file containing the installation name
|
||||||
INSTALLATION_NAME_FILE = '/data/innovenergy/openvpn/installation-name'
|
INSTALLATION_NAME_FILE = '/data/innovenergy/openvpn/installation-name'
|
||||||
|
@ -645,7 +629,7 @@ def read_battery_status(modbus, battery):
|
||||||
return BatteryStatus(battery, data.registers)
|
return BatteryStatus(battery, data.registers)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"An error occurred: {e}")
|
logging.error(f"An error occurred: {e}")
|
||||||
create_batch_of_csv_files() # Call this only if there's an error
|
create_batch_of_json_files() # Call this only if there's an error
|
||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
modbus.close() # close in any case
|
modbus.close() # close in any case
|
||||||
|
@ -659,7 +643,7 @@ def publish_values(dbus, signals, statuses):
|
||||||
|
|
||||||
previous_warnings = {}
|
previous_warnings = {}
|
||||||
previous_alarms = {}
|
previous_alarms = {}
|
||||||
num_of_csv_files_saved=0
|
num_of_json_files_saved=0
|
||||||
|
|
||||||
class MessageType:
|
class MessageType:
|
||||||
ALARM_OR_WARNING = "AlarmOrWarning"
|
ALARM_OR_WARNING = "AlarmOrWarning"
|
||||||
|
@ -878,7 +862,8 @@ def update(modbus, batteries, dbus, signals, csv_signals):
|
||||||
status_message, alarms_number_list, warnings_number_list = update_state_from_dictionaries(current_warnings, current_alarms, node_numbers)
|
status_message, alarms_number_list, warnings_number_list = update_state_from_dictionaries(current_warnings, current_alarms, node_numbers)
|
||||||
|
|
||||||
publish_values(dbus, signals, statuses)
|
publish_values(dbus, signals, statuses)
|
||||||
create_csv_files(csv_signals, statuses, node_numbers, alarms_number_list, warnings_number_list)
|
|
||||||
|
create_json_files(csv_signals, statuses, node_numbers, alarms_number_list, warnings_number_list)
|
||||||
logging.debug('finished update cycle\n')
|
logging.debug('finished update cycle\n')
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -907,59 +892,59 @@ def count_files_in_folder(folder_path):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return str(e)
|
return str(e)
|
||||||
|
|
||||||
def create_batch_of_csv_files():
|
def create_batch_of_json_files():
|
||||||
|
|
||||||
global prev_status,INSTALLATION_ID, PRODUCT_ID, num_of_csv_files_saved
|
global prev_status,INSTALLATION_ID, PRODUCT_ID, num_of_json_files_saved
|
||||||
# list all files in the directory
|
# list all files in the directory
|
||||||
files = os.listdir(CSV_DIR)
|
files = os.listdir(JSON_DIR)
|
||||||
|
|
||||||
# filter out only csv files
|
# filter out only json files
|
||||||
csv_files = [file for file in files if file.endswith('.csv')]
|
json_files = [file for file in files if file.endswith('.json')]
|
||||||
|
|
||||||
# sort csv files by creation time
|
# sort json files by creation time
|
||||||
csv_files.sort(key=lambda x: os.path.getctime(os.path.join(CSV_DIR, x)))
|
json_files.sort(key=lambda x: os.path.getctime(os.path.join(JSON_DIR, x)))
|
||||||
|
|
||||||
# keep the 600 MOST RECENT FILES
|
# keep the 600 MOST RECENT FILES
|
||||||
recent_csv_files = csv_files[-num_of_csv_files_saved:]
|
recent_json_files = json_files[-num_of_json_files_saved:]
|
||||||
print("num_of_csv_files_saved is " + str(num_of_csv_files_saved))
|
print("num_of_json_files_saved is " + str(num_of_json_files_saved))
|
||||||
|
|
||||||
# get the name of the first csv file
|
# get the name of the first json file
|
||||||
if not csv_files:
|
if not json_files:
|
||||||
print("No csv files found in the directory.")
|
print("No json files found in the directory.")
|
||||||
exit(0)
|
exit(0)
|
||||||
|
|
||||||
first_csv_file = os.path.join(CSV_DIR, recent_csv_files.pop(0))
|
first_json_file = os.path.join(JSON_DIR, recent_json_files.pop(0))
|
||||||
first_csv_filename = os.path.basename(first_csv_file)
|
first_json_filename = os.path.basename(first_json_file)
|
||||||
|
|
||||||
|
|
||||||
temp_file_path = os.path.join(CSV_DIR, 'temp_batch_file.csv')
|
temp_file_path = os.path.join(JSON_DIR, 'temp_batch_file.json')
|
||||||
|
|
||||||
# create a temporary file and write the timestamp and the original content of the first file
|
# create a temporary file and write the timestamp and the original content of the first file
|
||||||
with open(temp_file_path, 'wb') as temp_file:
|
with open(temp_file_path, 'wb') as temp_file:
|
||||||
# Write the timestamp (filename) at the beginning
|
# Write the timestamp (filename) at the beginning
|
||||||
numeric_part = first_csv_filename.split('.')[0]
|
numeric_part = first_json_filename.split('.')[0]
|
||||||
temp_file.write(f'Timestamp;{numeric_part}\n'.encode('utf-8'))
|
temp_file.write(f'Timestamp;{numeric_part}\n'.encode('utf-8'))
|
||||||
# write the original content of the first csv file
|
# write the original content of the first csv file
|
||||||
with open(first_csv_file, 'rb') as f:
|
with open(first_json_file, 'rb') as f:
|
||||||
temp_file.write(f.read())
|
temp_file.write(f.read())
|
||||||
for csv_file in recent_csv_files:
|
for json_file in recent_json_files:
|
||||||
file_path = os.path.join(CSV_DIR, csv_file)
|
file_path = os.path.join(JSON_DIR, json_file)
|
||||||
# write an empty line
|
# write an empty line
|
||||||
temp_file.write(b'\n')
|
temp_file.write(b'\n')
|
||||||
# write the timestamp (filename)
|
# write the timestamp (filename)
|
||||||
numeric_part = csv_file.split('.')[0]
|
numeric_part = json_file.split('.')[0]
|
||||||
temp_file.write(f'Timestamp;{numeric_part}\n'.encode('utf-8'))
|
temp_file.write(f'Timestamp;{numeric_part}\n'.encode('utf-8'))
|
||||||
# write the content of the file
|
# write the content of the file
|
||||||
with open(file_path, 'rb') as f:
|
with open(file_path, 'rb') as f:
|
||||||
temp_file.write(f.read())
|
temp_file.write(f.read())
|
||||||
|
|
||||||
# replace the original first csv file with the temporary file
|
# replace the original first json file with the temporary file
|
||||||
os.remove(first_csv_file)
|
os.remove(first_json_file)
|
||||||
os.rename(temp_file_path, first_csv_file)
|
os.rename(temp_file_path, first_json_file)
|
||||||
num_of_csv_files_saved = 0
|
num_of_json_files_saved = 0
|
||||||
|
|
||||||
# create a loggin directory that contains at max 20 batch files for logging info
|
# create a loggin directory that contains at max 20 batch files for logging info
|
||||||
# logging_dir = os.path.join(CSV_DIR, 'logging_batch_files')
|
# logging_dir = os.path.join(JSON_DIR, 'logging_batch_files')
|
||||||
# if not os.path.exists(logging_dir):
|
# if not os.path.exists(logging_dir):
|
||||||
# os.makedirs(logging_dir)
|
# os.makedirs(logging_dir)
|
||||||
#
|
#
|
||||||
|
@ -967,23 +952,23 @@ def create_batch_of_csv_files():
|
||||||
# manage_csv_files(logging_dir)
|
# manage_csv_files(logging_dir)
|
||||||
|
|
||||||
# prepare for compression
|
# prepare for compression
|
||||||
csv_data = read_csv_as_string(first_csv_file)
|
json_data = read_json_as_string(first_json_file)
|
||||||
|
|
||||||
if csv_data is None:
|
if json_data is None:
|
||||||
print("error while reading csv as string")
|
print("error while reading json as string")
|
||||||
return
|
return
|
||||||
|
|
||||||
# zip-comp additions
|
# zip-comp additions
|
||||||
compressed_csv = compress_csv_data(csv_data)
|
compressed_json = compress_json_data(json_data)
|
||||||
# Use the name of the last (most recent) CSV file in sorted csv_files as the name for the compressed file
|
# Use the name of the last (most recent) JSON file in sorted json_files as the name for the compressed file
|
||||||
last_csv_file_name = os.path.basename(recent_csv_files[-1]) if recent_csv_files else first_csv_filename
|
last_json_file_name = os.path.basename(recent_json_files[-1]) if recent_json_files else first_json_filename
|
||||||
|
|
||||||
numeric_part = int(last_csv_file_name.split('.')[0][:-2])
|
numeric_part = int(last_json_file_name.split('.')[0][:-2])
|
||||||
compressed_filename = "{}.csv".format(numeric_part)
|
compressed_filename = "{}.json".format(numeric_part)
|
||||||
|
|
||||||
response = s3_config.create_put_request(compressed_filename, compressed_csv)
|
response = s3_config.create_put_request(compressed_filename, compressed_json)
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
os.remove(first_csv_file)
|
os.remove(first_json_file)
|
||||||
print("Successfully uploaded the compresseed batch of files in s3")
|
print("Successfully uploaded the compresseed batch of files in s3")
|
||||||
status_message = {
|
status_message = {
|
||||||
"InstallationId": INSTALLATION_ID,
|
"InstallationId": INSTALLATION_ID,
|
||||||
|
@ -1001,14 +986,14 @@ def create_batch_of_csv_files():
|
||||||
channel.basic_publish(exchange="", routing_key="statusQueue", body=status_message)
|
channel.basic_publish(exchange="", routing_key="statusQueue", body=status_message)
|
||||||
print("Successfully sent the heartbit with timestamp")
|
print("Successfully sent the heartbit with timestamp")
|
||||||
else:
|
else:
|
||||||
# we save data that were not successfully uploaded in s3 in a failed directory inside the CSV_DIR for logging
|
# we save data that were not successfully uploaded in s3 in a failed directory inside the JSON_DIR for logging
|
||||||
failed_dir = os.path.join(CSV_DIR, "failed")
|
failed_dir = os.path.join(JSON_DIR, "failed")
|
||||||
if not os.path.exists(failed_dir):
|
if not os.path.exists(failed_dir):
|
||||||
os.makedirs(failed_dir)
|
os.makedirs(failed_dir)
|
||||||
failed_path = os.path.join(failed_dir, first_csv_filename)
|
failed_path = os.path.join(failed_dir, first_json_filename)
|
||||||
os.rename(first_csv_file, failed_path)
|
os.rename(first_json_file, failed_path)
|
||||||
print("Uploading failed")
|
print("Uploading failed")
|
||||||
manage_csv_files(failed_dir, 100)
|
manage_json_files(failed_dir, 100)
|
||||||
|
|
||||||
|
|
||||||
alive = True # global alive flag, watchdog_task clears it, update_task sets it
|
alive = True # global alive flag, watchdog_task clears it, update_task sets it
|
||||||
|
@ -1033,11 +1018,11 @@ def create_update_task(modbus, dbus, batteries, signals, csv_signals, main_loop)
|
||||||
elapsed_time = time.time() - start_time
|
elapsed_time = time.time() - start_time
|
||||||
print("11111111111111111111111111111111111111111111 elapsed time is ", elapsed_time)
|
print("11111111111111111111111111111111111111111111 elapsed time is ", elapsed_time)
|
||||||
|
|
||||||
# keep at most 1900 files at CSV_DIR for logging and aggregation
|
# keep at most 1900 files at JSON_DIR for logging and aggregation
|
||||||
manage_csv_files(CSV_DIR, 1900)
|
manage_json_files(JSON_DIR, 1900)
|
||||||
if elapsed_time >= 1200:
|
if elapsed_time >= 1200:
|
||||||
print("CREATE BATCH ======================================>")
|
print("CREATE BATCH ======================================>")
|
||||||
create_batch_of_csv_files()
|
create_batch_of_json_files()
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
#alive = update_for_testing(modbus, batteries, dbus, signals, csv_signals)
|
#alive = update_for_testing(modbus, batteries, dbus, signals, csv_signals)
|
||||||
if not alive:
|
if not alive:
|
||||||
|
@ -1070,12 +1055,12 @@ def get_installation_name(file_path):
|
||||||
with open(file_path, 'r') as file:
|
with open(file_path, 'r') as file:
|
||||||
return file.read().strip()
|
return file.read().strip()
|
||||||
|
|
||||||
def manage_csv_files(directory_path, max_files=20):
|
def manage_json_files(directory_path, max_files=20):
|
||||||
csv_files = [f for f in os.listdir(directory_path) if os.path.isfile(os.path.join(directory_path, f))]
|
json_files = [f for f in os.listdir(directory_path) if os.path.isfile(os.path.join(directory_path, f))]
|
||||||
csv_files.sort(key=lambda x: os.path.getctime(os.path.join(directory_path, x)))
|
json_files.sort(key=lambda x: os.path.getctime(os.path.join(directory_path, x)))
|
||||||
# Remove oldest files if exceeds maximum
|
# Remove oldest files if exceeds maximum
|
||||||
while len(csv_files) > max_files:
|
while len(json_files) > max_files:
|
||||||
file_to_delete = os.path.join(directory_path, csv_files.pop(0))
|
file_to_delete = os.path.join(directory_path, json_files.pop(0))
|
||||||
os.remove(file_to_delete)
|
os.remove(file_to_delete)
|
||||||
|
|
||||||
def insert_id(path, id_number):
|
def insert_id(path, id_number):
|
||||||
|
@ -1084,36 +1069,102 @@ def insert_id(path, id_number):
|
||||||
parts.insert(insert_position, str(id_number))
|
parts.insert(insert_position, str(id_number))
|
||||||
return "/".join(parts)
|
return "/".join(parts)
|
||||||
|
|
||||||
def create_csv_files(signals, statuses, node_numbers, alarms_number_list, warnings_number_list):
|
def insert_nested_data(data, split_list, value, symbol):
|
||||||
global s3_config, num_of_csv_files_saved
|
key = split_list[0] # Get the first key in the list
|
||||||
|
|
||||||
|
if len(split_list) == 1:
|
||||||
|
data[key] = {
|
||||||
|
"value": round(value, 2) if isinstance(value, float) else value,
|
||||||
|
#"symbol": str(symbol)
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
if key not in data:
|
||||||
|
data[key] = {} # Create a new dictionary if key doesn't exist
|
||||||
|
insert_nested_data(data[key], split_list[1:], value, symbol)
|
||||||
|
|
||||||
|
def create_json_files(signals, statuses, node_numbers, alarms_number_list, warnings_number_list):
|
||||||
|
|
||||||
|
global num_of_json_files_saved
|
||||||
timestamp = int(time.time())
|
timestamp = int(time.time())
|
||||||
if timestamp % 2 != 0:
|
if timestamp % 2 != 0:
|
||||||
timestamp -= 1
|
timestamp -= 1
|
||||||
# Create CSV directory if it doesn't exist
|
if not os.path.exists(JSON_DIR):
|
||||||
if not os.path.exists(CSV_DIR):
|
os.makedirs(JSON_DIR)
|
||||||
os.makedirs(CSV_DIR)
|
json_filename = "{}.json".format(timestamp)
|
||||||
csv_filename = f"{timestamp}.csv"
|
json_path = os.path.join(JSON_DIR, json_filename)
|
||||||
csv_path = os.path.join(CSV_DIR, csv_filename)
|
num_of_json_files_saved += 1
|
||||||
num_of_csv_files_saved+=1
|
|
||||||
|
|
||||||
# Append values to the CSV file
|
data = {
|
||||||
if not os.path.exists(csv_path):
|
"Battery": {
|
||||||
with open(csv_path, 'a', newline='') as csvfile:
|
"Devices": {}
|
||||||
csv_writer = csv.writer(csvfile, delimiter=';')
|
}
|
||||||
# Add a special row for the nodes configuration
|
}
|
||||||
nodes_config_path = "/Config/Devices/BatteryNodes"
|
|
||||||
nodes_list = ",".join(str(node) for node in node_numbers)
|
# Iterate over each node and construct the data structure
|
||||||
config_row = [nodes_config_path, nodes_list, ""]
|
for i, node in enumerate(node_numbers):
|
||||||
csv_writer.writerow(config_row)
|
print("Inside json generation file, node num is", i, " and node is ", node)
|
||||||
# Iterate over each node and signal to create rows in the new format
|
device_data = {} # This dictionary will hold the data for a specific device
|
||||||
for i, node in enumerate(node_numbers):
|
|
||||||
csv_writer.writerow([f"/Battery/Devices/{str(i+1)}/Alarms", alarms_number_list[i], ""])
|
# Add Alarms and Warnings for this device
|
||||||
csv_writer.writerow([f"/Battery/Devices/{str(i+1)}/Warnings", warnings_number_list[i], ""])
|
device_data["Alarms"] = alarms_number_list[i]
|
||||||
for s in signals:
|
device_data["Warnings"] = warnings_number_list[i]
|
||||||
signal_name = insert_id(s.name, i+1)
|
|
||||||
value = s.get_value(statuses[i])
|
# Iterate over the signals and add their values
|
||||||
row_values = [signal_name, value, s.get_text]
|
for s in signals:
|
||||||
csv_writer.writerow(row_values)
|
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'
|
BATTERY_COUNTS_FILE = '/data/battery_count.csv'
|
||||||
def load_battery_counts():
|
def load_battery_counts():
|
||||||
|
|
|
@ -13,10 +13,11 @@ import hmac
|
||||||
import hashlib
|
import hashlib
|
||||||
from threading import Thread, Event
|
from threading import Thread, Event
|
||||||
import config as cfg
|
import config as cfg
|
||||||
|
import json
|
||||||
|
|
||||||
CSV_DIR = "/data/csv_files/"
|
JSON_DIR = "/data/json_files/"
|
||||||
HOURLY_DIR = "/data/csv_files/HourlyData"
|
HOURLY_DIR = "/data/json_files/HourlyData"
|
||||||
DAILY_DIR = "/data/csv_files/DailyData"
|
DAILY_DIR = "/data/json_files/DailyData"
|
||||||
|
|
||||||
print("start with the correct credentials")
|
print("start with the correct credentials")
|
||||||
|
|
||||||
|
@ -37,23 +38,24 @@ class AggregatedData:
|
||||||
self.charging_battery_power = charging_battery_power
|
self.charging_battery_power = charging_battery_power
|
||||||
self.heating_power = heating_power
|
self.heating_power = heating_power
|
||||||
|
|
||||||
def to_csv(self):
|
def to_json(self):
|
||||||
return ("/MinSoc;{};\n"
|
return json.dumps({
|
||||||
"/MaxSoc;{};\n"
|
"MinSoc": self.min_soc,
|
||||||
"/DischargingBatteryPower;{};\n"
|
"MaxSoc": self.max_soc,
|
||||||
"/ChargingBatteryPower;{};\n"
|
"DischargingBatteryPower": self.discharging_battery_power,
|
||||||
"/HeatingPower;{};").format(
|
"ChargingBatteryPower": self.charging_battery_power,
|
||||||
self.min_soc, self.max_soc, self.discharging_battery_power, self.charging_battery_power, self.heating_power)
|
"HeatingPower": self.heating_power
|
||||||
|
}, separators=(',', ':'))
|
||||||
|
|
||||||
def save(self, directory):
|
def save(self, directory):
|
||||||
timestamp = int(time.time())
|
timestamp = int(time.time())
|
||||||
if not os.path.exists(directory):
|
if not os.path.exists(directory):
|
||||||
os.makedirs(directory)
|
os.makedirs(directory)
|
||||||
csv_path = os.path.join(directory, "{}.csv".format(timestamp))
|
json_path = os.path.join(directory, "{}.json".format(timestamp))
|
||||||
with open(csv_path, 'w') as file:
|
with open(json_path, 'w') as file:
|
||||||
file.write(self.to_csv())
|
file.write(self.to_json())
|
||||||
print("Saved file to:", csv_path)
|
print("Saved file to:", json_path)
|
||||||
print("File content:\n", self.to_csv())
|
print("File content:\n", self.to_json())
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def delete_data(directory):
|
def delete_data(directory):
|
||||||
|
@ -66,16 +68,16 @@ class AggregatedData:
|
||||||
print("Deleted file: {}".format(file_path))
|
print("Deleted file: {}".format(file_path))
|
||||||
|
|
||||||
def push_to_s3(self, s3_config):
|
def push_to_s3(self, s3_config):
|
||||||
csv_data = self.to_csv()
|
json_data = self.to_json()
|
||||||
compressed_csv = self.compress_csv_data(csv_data)
|
compressed_json = self.compress_json_data(json_data)
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
if now.hour == 0 and now.minute < 30:
|
if now.hour == 0 and now.minute < 30:
|
||||||
adjusted_date = now - timedelta(days=1)
|
adjusted_date = now - timedelta(days=1)
|
||||||
else:
|
else:
|
||||||
adjusted_date = now
|
adjusted_date = now
|
||||||
|
|
||||||
s3_path = adjusted_date.strftime("%Y-%m-%d") + ".csv"
|
s3_path = adjusted_date.strftime("%Y-%m-%d") + ".json"
|
||||||
response = s3_config.create_put_request(s3_path, compressed_csv)
|
response = s3_config.create_put_request(s3_path, compressed_json)
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
print("ERROR: PUT", response.text)
|
print("ERROR: PUT", response.text)
|
||||||
return False
|
return False
|
||||||
|
@ -83,10 +85,10 @@ class AggregatedData:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def compress_csv_data(csv_data):
|
def compress_json_data(json_data):
|
||||||
memory_stream = io.BytesIO()
|
memory_stream = io.BytesIO()
|
||||||
with zipfile.ZipFile(memory_stream, 'w', zipfile.ZIP_DEFLATED) as archive:
|
with zipfile.ZipFile(memory_stream, 'w', zipfile.ZIP_DEFLATED) as archive:
|
||||||
archive.writestr("data.csv", csv_data.encode('utf-8'))
|
archive.writestr("data.json", json_data.encode('utf-8'))
|
||||||
compressed_bytes = memory_stream.getvalue()
|
compressed_bytes = memory_stream.getvalue()
|
||||||
return base64.b64encode(compressed_bytes).decode('utf-8')
|
return base64.b64encode(compressed_bytes).decode('utf-8')
|
||||||
|
|
||||||
|
@ -152,7 +154,7 @@ class Aggregator:
|
||||||
current_time = datetime.now()
|
current_time = datetime.now()
|
||||||
after_timestamp = datetime_to_timestamp(current_time - timedelta(hours=1))
|
after_timestamp = datetime_to_timestamp(current_time - timedelta(hours=1))
|
||||||
before_timestamp = datetime_to_timestamp(current_time)
|
before_timestamp = datetime_to_timestamp(current_time)
|
||||||
aggregated_data = Aggregator.create_hourly_data(CSV_DIR, after_timestamp, before_timestamp)
|
aggregated_data = Aggregator.create_hourly_data(JSON_DIR, after_timestamp, before_timestamp)
|
||||||
print("save in hourly dir")
|
print("save in hourly dir")
|
||||||
aggregated_data.save(HOURLY_DIR)
|
aggregated_data.save(HOURLY_DIR)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -205,26 +207,49 @@ class Aggregator:
|
||||||
file_path = os.path.join(directory, filename)
|
file_path = os.path.join(directory, filename)
|
||||||
if os.path.isfile(file_path) and Aggregator.is_file_within_time_range(filename, after_timestamp, before_timestamp):
|
if os.path.isfile(file_path) and Aggregator.is_file_within_time_range(filename, after_timestamp, before_timestamp):
|
||||||
with open(file_path, 'r') as file:
|
with open(file_path, 'r') as file:
|
||||||
reader = csv.reader(file, delimiter=';')
|
data = json.load(file)
|
||||||
for row in reader:
|
devices = data.get("Battery", {}).get("Devices", {})
|
||||||
if len(row) >= 2:
|
|
||||||
variable_name, value = row[0], row[1]
|
for node_number, device_data in devices.items():
|
||||||
try:
|
|
||||||
value = float(value)
|
if node_number not in node_data:
|
||||||
node_number = Aggregator.extract_node_number(variable_name)
|
node_data[node_number] = {'soc': [], 'discharge': [], 'charge': [], 'heating': []}
|
||||||
if node_number not in node_data:
|
|
||||||
node_data[node_number] = {'soc': [], 'discharge': [], 'charge': [], 'heating': []}
|
value = device_data.get("Soc", {}).get("value", "N/A")
|
||||||
if "Soc" in variable_name:
|
node_data[node_number]['soc'].append(float(value))
|
||||||
node_data[node_number]['soc'].append(value)
|
|
||||||
elif "/Dc/Power" in variable_name or "/DischargingBatteryPower" in variable_name or "/ChargingBatteryPower" in variable_name:
|
value = device_data.get("Dc", {}).get("Power", "N/A").get("value", "N/A")
|
||||||
if value < 0:
|
value=float(value)
|
||||||
node_data[node_number]['discharge'].append(value)
|
if value < 0:
|
||||||
else:
|
node_data[node_number]['discharge'].append(value)
|
||||||
node_data[node_number]['charge'].append(value)
|
else:
|
||||||
elif "/HeatingPower" in variable_name:
|
node_data[node_number]['charge'].append(value)
|
||||||
node_data[node_number]['heating'].append(value)
|
value = device_data.get("HeatingPower", "N/A").get("value", "N/A")
|
||||||
except ValueError:
|
value = float(value)
|
||||||
pass
|
node_data[node_number]['heating'].append(value)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# reader = csv.reader(file, delimiter=';')
|
||||||
|
# for row in reader:
|
||||||
|
# if len(row) >= 2:
|
||||||
|
# variable_name, value = row[0], row[1]
|
||||||
|
# try:
|
||||||
|
# value = float(value)
|
||||||
|
# node_number = Aggregator.extract_node_number(variable_name)
|
||||||
|
# if node_number not in node_data:
|
||||||
|
# node_data[node_number] = {'soc': [], 'discharge': [], 'charge': [], 'heating': []}
|
||||||
|
# if "Soc" in variable_name:
|
||||||
|
# node_data[node_number]['soc'].append(value)
|
||||||
|
# elif "/Dc/Power" in variable_name or "/DischargingBatteryPower" in variable_name or "/ChargingBatteryPower" in variable_name:
|
||||||
|
# if value < 0:
|
||||||
|
# node_data[node_number]['discharge'].append(value)
|
||||||
|
# else:
|
||||||
|
# node_data[node_number]['charge'].append(value)
|
||||||
|
# elif "/HeatingPower" in variable_name:
|
||||||
|
# node_data[node_number]['heating'].append(value)
|
||||||
|
# except ValueError:
|
||||||
|
# pass
|
||||||
|
|
||||||
if len(node_data) == 0:
|
if len(node_data) == 0:
|
||||||
# No data collected, return default AggregatedData with zeros
|
# No data collected, return default AggregatedData with zeros
|
||||||
|
@ -254,7 +279,42 @@ class Aggregator:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_daily_data(directory, after_timestamp, before_timestamp):
|
def create_daily_data(directory, after_timestamp, before_timestamp):
|
||||||
return Aggregator.create_hourly_data(directory, after_timestamp, before_timestamp)
|
node_data = {'MinSoc': [], 'MaxSoc': [], 'ChargingBatteryPower': [], 'DischargingBatteryPower': [], 'HeatingPower': []}
|
||||||
|
|
||||||
|
for filename in os.listdir(directory):
|
||||||
|
file_path = os.path.join(directory, filename)
|
||||||
|
if os.path.isfile(file_path) and Aggregator.is_file_within_time_range(filename, after_timestamp,before_timestamp):
|
||||||
|
with open(file_path, 'r') as file:
|
||||||
|
data = json.load(file)
|
||||||
|
|
||||||
|
value = data.get("MinSoc", "N/A")
|
||||||
|
node_data['MinSoc'].append(float(value))
|
||||||
|
|
||||||
|
value = data.get("MaxSoc", "N/A")
|
||||||
|
node_data['MaxSoc'].append(float(value))
|
||||||
|
|
||||||
|
value = data.get("ChargingBatteryPower", "N/A")
|
||||||
|
node_data['ChargingBatteryPower'].append(float(value))
|
||||||
|
|
||||||
|
value = data.get("DischargingBatteryPower", "N/A")
|
||||||
|
node_data['DischargingBatteryPower'].append(float(value))
|
||||||
|
|
||||||
|
value = data.get("HeatingPower", "N/A")
|
||||||
|
node_data['HeatingPower'].append(float(value))
|
||||||
|
|
||||||
|
print(node_data)
|
||||||
|
|
||||||
|
min_soc = min (node_data['MinSoc']) if node_data else 0.0
|
||||||
|
max_soc = max(node_data['MaxSoc']) if node_data else 0.0
|
||||||
|
total_discharging_power = sum(node_data['DischargingBatteryPower']) if node_data else 0.0
|
||||||
|
total_charging_power = sum(node_data['ChargingBatteryPower']) if node_data else 0.0
|
||||||
|
total_heating_power = sum(node_data['HeatingPower']) if node_data else 0.0
|
||||||
|
|
||||||
|
avg_discharging_power = total_discharging_power / len(node_data['DischargingBatteryPower'])
|
||||||
|
avg_charging_power = total_charging_power / len(node_data['ChargingBatteryPower'])
|
||||||
|
avg_heating_power = total_heating_power / len(node_data['HeatingPower'])
|
||||||
|
|
||||||
|
return AggregatedData(min_soc, max_soc, avg_discharging_power, avg_charging_power, avg_heating_power)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_file_within_time_range(filename, start_time, end_time):
|
def is_file_within_time_range(filename, start_time, end_time):
|
||||||
|
|
|
@ -54,6 +54,6 @@ INNOVENERGY_PROTOCOL_VERSION = '48TL200V3'
|
||||||
|
|
||||||
|
|
||||||
# S3 Credentials
|
# S3 Credentials
|
||||||
S3BUCKET = "436-c0436b6a-d276-4cd8-9c44-1eae86cf5d0e"
|
S3BUCKET = "357-c0436b6a-d276-4cd8-9c44-1eae86cf5d0e"
|
||||||
S3KEY = "EXO6bb2b06f3cebfdbbc8a9b240"
|
S3KEY = "EXOd9cc93bec729d1bb9ad337d0"
|
||||||
S3SECRET = "m6bEzM8z9t2lCQ13OptMcZcNf80p_TSjaMDtZTNdEjo"
|
S3SECRET = "Sgah7AmC7vUvnYqR_JmqZMOHpnUX3ERJZJynDUD3QdI"
|
||||||
|
|
|
@ -34,7 +34,7 @@ import json
|
||||||
from convert import first
|
from convert import first
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
CSV_DIR = "/data/csv_files/"
|
JSON_DIR = "/data/json_files/"
|
||||||
INSTALLATION_NAME_FILE = '/data/innovenergy/openvpn/installation-name'
|
INSTALLATION_NAME_FILE = '/data/innovenergy/openvpn/installation-name'
|
||||||
|
|
||||||
# trick the pycharm type-checker into thinking Callable is in scope, not used at runtime
|
# trick the pycharm type-checker into thinking Callable is in scope, not used at runtime
|
||||||
|
@ -45,13 +45,13 @@ if False:
|
||||||
RESET_REGISTER = 0x2087
|
RESET_REGISTER = 0x2087
|
||||||
|
|
||||||
|
|
||||||
def compress_csv_data(csv_data, file_name="data.csv"):
|
def compress_json_data(json_data, file_name="data.json"):
|
||||||
memory_stream = io.BytesIO()
|
memory_stream = io.BytesIO()
|
||||||
|
|
||||||
# Create a zip archive in the memory buffer
|
# Create a zip archive in the memory buffer
|
||||||
with zipfile.ZipFile(memory_stream, 'w', zipfile.ZIP_DEFLATED) as archive:
|
with zipfile.ZipFile(memory_stream, 'w', zipfile.ZIP_DEFLATED) as archive:
|
||||||
# Add CSV data to the ZIP archive using writestr
|
# Add JSON data to the ZIP archive using writestr
|
||||||
archive.writestr(file_name, csv_data.encode('utf-8'))
|
archive.writestr(file_name, json_data.encode('utf-8'))
|
||||||
|
|
||||||
# Get the compressed byte array from the memory buffer
|
# Get the compressed byte array from the memory buffer
|
||||||
compressed_bytes = memory_stream.getvalue()
|
compressed_bytes = memory_stream.getvalue()
|
||||||
|
@ -155,7 +155,7 @@ INSTALLATION_ID = int(s3_config.bucket.split('-')[0])
|
||||||
PRODUCT_ID = 1
|
PRODUCT_ID = 1
|
||||||
is_first_update = True
|
is_first_update = True
|
||||||
prev_status = 0
|
prev_status = 0
|
||||||
num_of_csv_files_saved = 0
|
num_of_json_files_saved = 0
|
||||||
|
|
||||||
|
|
||||||
def update_state_from_dictionaries(current_warnings, current_alarms, node_numbers):
|
def update_state_from_dictionaries(current_warnings, current_alarms, node_numbers):
|
||||||
|
@ -251,9 +251,9 @@ def update_state_from_dictionaries(current_warnings, current_alarms, node_number
|
||||||
return status_message, alarms_number_list, warnings_number_list
|
return status_message, alarms_number_list, warnings_number_list
|
||||||
|
|
||||||
|
|
||||||
def read_csv_as_string(file_path):
|
def read_json_as_string(file_path):
|
||||||
"""
|
"""
|
||||||
Reads a CSV file from the given path and returns its content as a single string.
|
Reads a JSON file from the given path and returns its content as a single string.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Note: 'encoding' is not available in open() in Python 2.7, so we'll use 'codecs' module.
|
# Note: 'encoding' is not available in open() in Python 2.7, so we'll use 'codecs' module.
|
||||||
|
@ -555,16 +555,17 @@ def create_update_task(modbus, service, batteries):
|
||||||
publish_values_on_dbus(service, _signals, statuses)
|
publish_values_on_dbus(service, _signals, statuses)
|
||||||
|
|
||||||
elapsed_time = time.time() - start_time
|
elapsed_time = time.time() - start_time
|
||||||
create_csv_files(csv_signals, statuses, node_numbers, alarms_number_list, warnings_number_list)
|
#create_csv_files(csv_signals, statuses, node_numbers, alarms_number_list, warnings_number_list)
|
||||||
|
create_json_files(csv_signals, statuses, node_numbers, alarms_number_list, warnings_number_list)
|
||||||
print("11111111111111111111111111111111111111111111 elapsed time is ", elapsed_time)
|
print("11111111111111111111111111111111111111111111 elapsed time is ", elapsed_time)
|
||||||
|
|
||||||
# keep at most 1900 files at CSV_DIR for logging and aggregation
|
# keep at most 1900 files at JSON_DIR for logging and aggregation
|
||||||
manage_csv_files(CSV_DIR, 1900)
|
manage_json_files(JSON_DIR, 1900)
|
||||||
|
|
||||||
num_files_in_csv_dir = count_files_in_folder(CSV_DIR)
|
num_files_in_json_dir = count_files_in_folder(JSON_DIR)
|
||||||
if elapsed_time >= 1200:
|
if elapsed_time >= 1200:
|
||||||
print("CREATE BATCH ======================================>")
|
print("CREATE BATCH ======================================>")
|
||||||
create_batch_of_csv_files()
|
create_batch_of_json_files()
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
upload_status_to_innovenergy(_socket, statuses)
|
upload_status_to_innovenergy(_socket, statuses)
|
||||||
|
@ -574,10 +575,10 @@ def create_update_task(modbus, service, batteries):
|
||||||
alive = True
|
alive = True
|
||||||
except pika.exceptions.AMQPConnectionError:
|
except pika.exceptions.AMQPConnectionError:
|
||||||
logging.error("AMQPConnectionError encountered. Subscribing to queue.")
|
logging.error("AMQPConnectionError encountered. Subscribing to queue.")
|
||||||
create_batch_of_csv_files()
|
create_batch_of_json_files()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
create_batch_of_csv_files()
|
create_batch_of_json_files()
|
||||||
logging.error("Unexpected error")
|
logging.error("Unexpected error")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
@ -587,14 +588,14 @@ def create_update_task(modbus, service, batteries):
|
||||||
return update_task
|
return update_task
|
||||||
|
|
||||||
|
|
||||||
def manage_csv_files(directory_path, max_files=20):
|
def manage_json_files(directory_path, max_files=20):
|
||||||
csv_files = [f for f in os.listdir(directory_path) if os.path.isfile(os.path.join(directory_path, f))]
|
json_files = [f for f in os.listdir(directory_path) if os.path.isfile(os.path.join(directory_path, f))]
|
||||||
csv_files.sort(key=lambda x: os.path.getctime(os.path.join(directory_path, x)))
|
json_files.sort(key=lambda x: os.path.getctime(os.path.join(directory_path, x)))
|
||||||
|
|
||||||
print("len of csv files is " + str(len(csv_files)))
|
print("len of json files is " + str(len(json_files)))
|
||||||
# Remove oldest files if exceeds maximum
|
# Remove oldest files if exceeds maximum
|
||||||
while len(csv_files) > max_files:
|
while len(json_files) > max_files:
|
||||||
file_to_delete = os.path.join(directory_path, csv_files.pop(0))
|
file_to_delete = os.path.join(directory_path, json_files.pop(0))
|
||||||
os.remove(file_to_delete)
|
os.remove(file_to_delete)
|
||||||
|
|
||||||
|
|
||||||
|
@ -605,89 +606,78 @@ def insert_id(path, id_number):
|
||||||
return "/".join(parts)
|
return "/".join(parts)
|
||||||
|
|
||||||
|
|
||||||
def create_batch_of_csv_files():
|
def create_batch_of_json_files():
|
||||||
global prev_status, channel, INSTALLATION_ID, PRODUCT_ID, num_of_csv_files_saved
|
global prev_status, channel, INSTALLATION_ID, PRODUCT_ID, num_of_json_files_saved
|
||||||
# list all files in the directory
|
# list all files in the directory
|
||||||
files = os.listdir(CSV_DIR)
|
files = os.listdir(JSON_DIR)
|
||||||
|
|
||||||
# filter out only csv files
|
# filter out only json files
|
||||||
csv_files = [file for file in files if file.endswith('.csv')]
|
json_files = [file for file in files if file.endswith('.json')]
|
||||||
|
|
||||||
# sort csv files by creation time
|
# sort json files by creation time
|
||||||
csv_files.sort(key=lambda x: os.path.getctime(os.path.join(CSV_DIR, x)))
|
json_files.sort(key=lambda x: os.path.getctime(os.path.join(JSON_DIR, x)))
|
||||||
|
|
||||||
# keep the num_of_csv_files_saved MOST RECENT FILES
|
# keep the recent_json_files MOST RECENT FILES
|
||||||
recent_csv_files = csv_files[-num_of_csv_files_saved:]
|
recent_json_files = json_files[-num_of_json_files_saved:]
|
||||||
print("num_of_csv_files_saved is " + str(num_of_csv_files_saved))
|
print("num_of_json_files_saved is " + str(recent_json_files))
|
||||||
|
|
||||||
# get the name of the first csv file
|
# get the name of the first json file
|
||||||
if not csv_files:
|
if not json_files:
|
||||||
print("No csv files found in the directory.")
|
print("No json files found in the directory.")
|
||||||
exit(0)
|
exit(0)
|
||||||
|
|
||||||
first_csv_file = os.path.join(CSV_DIR, recent_csv_files.pop(0))
|
first_json_file = os.path.join(JSON_DIR, recent_json_files.pop(0))
|
||||||
first_csv_filename = os.path.basename(first_csv_file)
|
first_json_filename = os.path.basename(first_json_file)
|
||||||
|
|
||||||
temp_file_path = os.path.join(CSV_DIR, 'temp_batch_file.csv')
|
temp_file_path = os.path.join(JSON_DIR, 'temp_batch_file.json')
|
||||||
|
|
||||||
# create a temporary file and write the timestamp and the original content of the first file
|
# create a temporary file and write the timestamp and the original content of the first file
|
||||||
with open(temp_file_path, 'wb') as temp_file:
|
with open(temp_file_path, 'wb') as temp_file:
|
||||||
# Write the timestamp (filename) at the beginning
|
# Write the timestamp (filename) at the beginning
|
||||||
temp_file.write('Timestamp;{}\n'.format(first_csv_filename.split('.')[0]))
|
temp_file.write('Timestamp;{}\n'.format(first_json_filename.split('.')[0]))
|
||||||
# write the original content of the first csv file
|
# write the original content of the first json file
|
||||||
with open(first_csv_file, 'rb') as f:
|
with open(first_json_file, 'rb') as f:
|
||||||
temp_file.write(f.read())
|
temp_file.write(f.read())
|
||||||
for csv_file in recent_csv_files:
|
for json_file in recent_json_files:
|
||||||
file_path = os.path.join(CSV_DIR, csv_file)
|
file_path = os.path.join(JSON_DIR, json_file)
|
||||||
# write an empty line
|
# write an empty line
|
||||||
temp_file.write('\n')
|
temp_file.write('\n')
|
||||||
# write the timestamp (filename)
|
# write the timestamp (filename)
|
||||||
temp_file.write('Timestamp;{}\n'.format(csv_file.split('.')[0]))
|
temp_file.write('Timestamp;{}\n'.format(json_file.split('.')[0]))
|
||||||
# write the content of the file
|
# write the content of the file
|
||||||
with open(file_path, 'rb') as f:
|
with open(file_path, 'rb') as f:
|
||||||
temp_file.write(f.read())
|
temp_file.write(f.read())
|
||||||
|
|
||||||
# replace the original first csv file with the temporary file
|
|
||||||
os.remove(first_csv_file)
|
|
||||||
os.rename(temp_file_path, first_csv_file)
|
|
||||||
num_of_csv_files_saved = 0
|
|
||||||
|
|
||||||
# create a loggin directory that contains at max 20 batch files for logging info
|
# replace the original first json file with the temporary file
|
||||||
# logging_dir = os.path.join(CSV_DIR, 'logging_batch_files')
|
os.remove(first_json_file)
|
||||||
# if not os.path.exists(logging_dir):
|
os.rename(temp_file_path, first_json_file)
|
||||||
# os.makedirs(logging_dir)
|
num_of_json_files_saved = 0
|
||||||
#
|
|
||||||
# shutil.copy(first_csv_file, logging_dir)
|
|
||||||
# manage_csv_files(logging_dir)
|
|
||||||
|
|
||||||
# print("The batch csv file is: {}".format(recent_csv_files[-1]))
|
|
||||||
|
|
||||||
# prepare for compression
|
# prepare for compression
|
||||||
csv_data = read_csv_as_string(first_csv_file)
|
json_data = read_json_as_string(first_json_file)
|
||||||
|
|
||||||
if csv_data is None:
|
if json_data is None:
|
||||||
print("error while reading csv as string")
|
print("error while reading json as string")
|
||||||
return
|
return
|
||||||
|
|
||||||
# zip-comp additions
|
# zip-comp additions
|
||||||
compressed_csv = compress_csv_data(csv_data)
|
compressed_json = compress_json_data(json_data)
|
||||||
# Use the name of the last (most recent) CSV file in sorted csv_files as the name for the compressed file
|
# Use the name of the last (most recent) JSON file in sorted json_files as the name for the compressed file
|
||||||
last_csv_file_name = os.path.basename(recent_csv_files[-1]) if recent_csv_files else first_csv_filename
|
last_json_file_name = os.path.basename(recent_json_files[-1]) if recent_json_files else first_json_filename
|
||||||
|
|
||||||
# we send the csv files every 30 seconds and the timestamp is adjusted to be a multiple of 30
|
# we send the json files every 30 seconds and the timestamp is adjusted to be a multiple of 30
|
||||||
numeric_part = int(last_csv_file_name.split('.')[0][:-2])
|
numeric_part = int(last_json_file_name.split('.')[0][:-2])
|
||||||
|
|
||||||
# compressed_filename = "{}.csv".format(new_numeric_part)
|
compressed_filename = "{}.json".format(numeric_part)
|
||||||
compressed_filename = "{}.csv".format(numeric_part)
|
|
||||||
|
|
||||||
print("FILE NAME =========================================================> ", compressed_filename)
|
print("FILE NAME =========================================================> ", compressed_filename)
|
||||||
|
|
||||||
response = s3_config.create_put_request(compressed_filename, compressed_csv)
|
response = s3_config.create_put_request(compressed_filename, compressed_json)
|
||||||
# response = s3_config.create_put_request(first_csv_filename, csv_data)
|
|
||||||
|
|
||||||
print(response)
|
print(response)
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
os.remove(first_csv_file)
|
os.remove(first_json_file)
|
||||||
print("Successfully uploaded the compresseed batch of files in s3")
|
print("Successfully uploaded the compresseed batch of files in s3")
|
||||||
status_message = {
|
status_message = {
|
||||||
"InstallationId": INSTALLATION_ID,
|
"InstallationId": INSTALLATION_ID,
|
||||||
|
@ -709,42 +699,80 @@ def create_batch_of_csv_files():
|
||||||
|
|
||||||
print("Successfully sent the heartbit with timestamp")
|
print("Successfully sent the heartbit with timestamp")
|
||||||
else:
|
else:
|
||||||
# we save data that were not successfully uploaded in s3 in a failed directory inside the CSV_DIR for logging
|
#we save data that were not successfully uploaded in s3 in a failed directory inside the JSON_DIR for logging
|
||||||
failed_dir = os.path.join(CSV_DIR, "failed")
|
failed_dir = os.path.join(JSON_DIR, "failed")
|
||||||
if not os.path.exists(failed_dir):
|
if not os.path.exists(failed_dir):
|
||||||
os.makedirs(failed_dir)
|
os.makedirs(failed_dir)
|
||||||
failed_path = os.path.join(failed_dir, first_csv_filename)
|
failed_path = os.path.join(failed_dir, first_json_filename)
|
||||||
os.rename(first_csv_file, failed_path)
|
os.rename(first_json_file, failed_path)
|
||||||
print("Uploading failed")
|
print("Uploading failed")
|
||||||
manage_csv_files(failed_dir, 100)
|
manage_json_files(failed_dir, 100)
|
||||||
|
|
||||||
|
def insert_nested_data(data, split_list, value, symbol):
|
||||||
|
key = split_list[0] # Get the first key in the list
|
||||||
|
|
||||||
def create_csv_files(signals, statuses, node_numbers, alarms_number_list, warnings_number_list):
|
if len(split_list) == 1:
|
||||||
global num_of_csv_files_saved
|
data[key] = {
|
||||||
|
"value": round(value, 2) if isinstance(value, float) else value,
|
||||||
|
#"symbol": str(symbol)
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
if key not in data:
|
||||||
|
data[key] = {} # Create a new dictionary if key doesn't exist
|
||||||
|
insert_nested_data(data[key], split_list[1:], value, symbol)
|
||||||
|
|
||||||
|
def create_json_files(signals, statuses, node_numbers, alarms_number_list, warnings_number_list):
|
||||||
|
|
||||||
|
global num_of_json_files_saved
|
||||||
timestamp = int(time.time())
|
timestamp = int(time.time())
|
||||||
if timestamp % 2 != 0:
|
if timestamp % 2 != 0:
|
||||||
timestamp -= 1
|
timestamp -= 1
|
||||||
if not os.path.exists(CSV_DIR):
|
if not os.path.exists(JSON_DIR):
|
||||||
os.makedirs(CSV_DIR)
|
os.makedirs(JSON_DIR)
|
||||||
csv_filename = "{}.csv".format(timestamp)
|
json_filename = "{}.json".format(timestamp)
|
||||||
csv_path = os.path.join(CSV_DIR, csv_filename)
|
json_path = os.path.join(JSON_DIR, json_filename)
|
||||||
num_of_csv_files_saved += 1
|
num_of_json_files_saved += 1
|
||||||
|
|
||||||
if not os.path.exists(csv_path):
|
data = {
|
||||||
with open(csv_path, 'ab') as csvfile:
|
"Battery": {
|
||||||
csv_writer = csv.writer(csvfile, delimiter=';')
|
"Devices": {}
|
||||||
nodes_config_path = "/Config/Devices/BatteryNodes"
|
}
|
||||||
nodes_list = ",".join(str(node) for node in node_numbers)
|
}
|
||||||
config_row = [nodes_config_path, nodes_list, ""]
|
|
||||||
csv_writer.writerow(config_row)
|
# Iterate over each node and construct the data structure
|
||||||
for i, node in enumerate(node_numbers):
|
for i, node in enumerate(node_numbers):
|
||||||
csv_writer.writerow(["/Battery/Devices/{}/Alarms".format(str(i + 1)), alarms_number_list[i], ""])
|
print("Inside json generation file, node num is" , i," and node is ", node)
|
||||||
csv_writer.writerow(["/Battery/Devices/{}/Warnings".format(str(i + 1)), warnings_number_list[i], ""])
|
device_data = {} # This dictionary will hold the data for a specific device
|
||||||
for s in signals:
|
|
||||||
signal_name = insert_id(s.name, i + 1)
|
# Add Alarms and Warnings for this device
|
||||||
value = s.get_value(statuses[i])
|
device_data["Alarms"] = alarms_number_list[i]
|
||||||
row_values = [signal_name, value, s.get_text]
|
device_data["Warnings"] = warnings_number_list[i]
|
||||||
csv_writer.writerow(row_values)
|
|
||||||
|
# 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):
|
def create_watchdog_task(main_loop):
|
||||||
|
@ -796,14 +824,6 @@ def main(argv):
|
||||||
logging.basicConfig(level=cfg.LOG_LEVEL)
|
logging.basicConfig(level=cfg.LOG_LEVEL)
|
||||||
logging.info('starting ' + __file__)
|
logging.info('starting ' + __file__)
|
||||||
|
|
||||||
# tty = parse_cmdline_args(argv)
|
|
||||||
# modbus = init_modbus(tty)
|
|
||||||
|
|
||||||
# batteries = identify_batteries(modbus)
|
|
||||||
|
|
||||||
# if len(batteries) <= 0:
|
|
||||||
# sys.exit(2)
|
|
||||||
|
|
||||||
tty = parse_cmdline_args(argv)
|
tty = parse_cmdline_args(argv)
|
||||||
battery_counts = load_battery_counts()
|
battery_counts = load_battery_counts()
|
||||||
max_retry_attempts = 3 # Stop retrying in case it's a real battery loss case
|
max_retry_attempts = 3 # Stop retrying in case it's a real battery loss case
|
||||||
|
|
|
@ -0,0 +1,123 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Handle Ctrl+C to ensure a clean exit
|
||||||
|
trap "echo -e '\nScript interrupted by user. Exiting...'; kill 0; exit 1" SIGINT
|
||||||
|
|
||||||
|
username='root'
|
||||||
|
root_password='salidomo'
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
venus_release_file_path="./Venus_Release/VenusReleaseFiles"
|
||||||
|
cerbo_release_file_path="./Cerbo_Release/CerboReleaseFiles"
|
||||||
|
|
||||||
|
venus_ip_addresses=("10.2.0.191" "10.2.1.36" "10.2.1.108")
|
||||||
|
cerbo_ip_addresses=("10.2.2.212" "10.2.4.181" "10.2.3.198")
|
||||||
|
|
||||||
|
deploy() {
|
||||||
|
local device_type=$1
|
||||||
|
local ip_list=("${!2}")
|
||||||
|
local release_file_path=$3
|
||||||
|
|
||||||
|
echo -e "\n============================ Deploying to $device_type ============================\n"
|
||||||
|
|
||||||
|
for ip_address in "${ip_list[@]}"; do
|
||||||
|
echo "Processing $ip_address for $device_type..."
|
||||||
|
|
||||||
|
# Check if SSH is reachable within 60 seconds
|
||||||
|
if ! timeout 60 ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no "$username@$ip_address" "echo 'SSH connection successful'" &>/dev/null; then
|
||||||
|
echo "Skipping $ip_address: SSH connection failed or timed out."
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "SSH connection successful: $ip_address"
|
||||||
|
|
||||||
|
# Stop battery service if changing battery-related files
|
||||||
|
if ssh -o StrictHostKeyChecking=no "$username@$ip_address" "svc -d /service/dbus-fzsonick-48tl.*"; then
|
||||||
|
echo "Stopped battery service on $ip_address"
|
||||||
|
else
|
||||||
|
echo "Warning: Failed to stop battery service on $ip_address"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "SSH connection successful: $ip_address"
|
||||||
|
|
||||||
|
# Stop aggregator service if changing aggregator-related files
|
||||||
|
if ssh -o StrictHostKeyChecking=no "$username@$ip_address" "svc -d /service/aggregator"; then
|
||||||
|
echo "Stopped aggregator service on $ip_address"
|
||||||
|
else
|
||||||
|
echo "Warning: Failed to stop aggregator service on $ip_address"
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# Copy files
|
||||||
|
if scp -o ConnectTimeout=10 "$release_file_path/dbus-fzsonick-48tl/dbus-fzsonick-48tl.py" "root@$ip_address:/opt/victronenergy/dbus-fzsonick-48tl"; then
|
||||||
|
echo "Copied file to /opt on $ip_address"
|
||||||
|
else
|
||||||
|
echo "Warning: Failed to copy file to /opt on $ip_address"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if scp -o ConnectTimeout=10 "$release_file_path/dbus-fzsonick-48tl/dbus-fzsonick-48tl.py" "root@$ip_address:/data/dbus-fzsonick-48tl"; then
|
||||||
|
echo "Copied file to /data on $ip_address"
|
||||||
|
else
|
||||||
|
echo "Warning: Failed to copy file to /data on $ip_address"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Copy files
|
||||||
|
if scp -o ConnectTimeout=10 "$release_file_path/dbus-fzsonick-48tl/aggregator.py" "root@$ip_address:/opt/victronenergy/dbus-fzsonick-48tl"; then
|
||||||
|
echo "Copied file to /opt on $ip_address"
|
||||||
|
else
|
||||||
|
echo "Warning: Failed to copy file to /opt on $ip_address"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if scp -o ConnectTimeout=10 "$release_file_path/dbus-fzsonick-48tl/aggregator.py" "root@$ip_address:/data/dbus-fzsonick-48tl"; then
|
||||||
|
echo "Copied file to /data on $ip_address"
|
||||||
|
else
|
||||||
|
echo "Warning: Failed to copy file to /data on $ip_address"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start battery service
|
||||||
|
if ssh -o StrictHostKeyChecking=no "$username@$ip_address" "svc -u /service/dbus-fzsonick-48tl.*"; then
|
||||||
|
echo "Started battery service on $ip_address"
|
||||||
|
else
|
||||||
|
echo "Warning: Failed to start battery service on $ip_address"
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# Start aggregator service
|
||||||
|
if ssh -o StrictHostKeyChecking=no "$username@$ip_address" "svc -u /service/aggregator"; then
|
||||||
|
echo "Started aggregator service on $ip_address"
|
||||||
|
else
|
||||||
|
echo "Warning: Failed to start aggregator service on $ip_address"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Deployment completed for $ip_address ($device_type)"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo -e "\n============================ Finished deploying to $device_type ============================\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prompt user for deployment type
|
||||||
|
echo "Select deployment type:"
|
||||||
|
echo "1) Deploy to Venus devices"
|
||||||
|
echo "2) Deploy to Cerbo devices"
|
||||||
|
echo "3) Deploy to both Venus and Cerbo devices"
|
||||||
|
read -p "Enter your choice (1/2/3): " choice
|
||||||
|
|
||||||
|
case $choice in
|
||||||
|
1)
|
||||||
|
deploy "Venus" venus_ip_addresses[@] "$venus_release_file_path"
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
deploy "Cerbo" cerbo_ip_addresses[@] "$cerbo_release_file_path"
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
deploy "Venus" venus_ip_addresses[@] "$venus_release_file_path"
|
||||||
|
deploy "Cerbo" cerbo_ip_addresses[@] "$cerbo_release_file_path"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Invalid choice. Exiting..."
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo -e "\n============================ All Deployments Completed ============================\n"
|
|
@ -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'
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
"axios": "^1.5.0",
|
"axios": "^1.5.0",
|
||||||
"chart.js": "^4.4.0",
|
"chart.js": "^4.4.0",
|
||||||
"clsx": "1.1.1",
|
"clsx": "1.1.1",
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
"cytoscape": "^3.26.0",
|
"cytoscape": "^3.26.0",
|
||||||
"date-fns": "^2.28.0",
|
"date-fns": "^2.28.0",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
|
@ -7318,6 +7319,11 @@
|
||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/crypto-js": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
|
||||||
|
},
|
||||||
"node_modules/crypto-random-string": {
|
"node_modules/crypto-random-string": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
|
||||||
|
@ -26136,6 +26142,11 @@
|
||||||
"which": "^2.0.1"
|
"which": "^2.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"crypto-js": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
|
||||||
|
},
|
||||||
"crypto-random-string": {
|
"crypto-random-string": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
export const axiosConfigWithoutToken = axios.create({
|
export const axiosConfigWithoutToken = axios.create({
|
||||||
baseURL: 'https://stage.innov.energy/api'
|
baseURL: 'https://monitor.innov.energy/api'
|
||||||
//baseURL: 'http://127.0.0.1:7087/api'
|
//baseURL: 'http://127.0.0.1:7087/api'
|
||||||
});
|
});
|
||||||
|
|
||||||
const axiosConfig = axios.create({
|
const axiosConfig = axios.create({
|
||||||
baseURL: 'https://stage.innov.energy/api'
|
baseURL: 'https://monitor.innov.energy/api'
|
||||||
//baseURL: 'http://127.0.0.1:7087/api'
|
//baseURL: 'http://127.0.0.1:7087/api'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -23,9 +23,9 @@ import Button from '@mui/material/Button';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import { I_S3Credentials } from '../../../interfaces/S3Types';
|
import { I_S3Credentials } from '../../../interfaces/S3Types';
|
||||||
import routes from '../../../Resources/routes.json';
|
import routes from '../../../Resources/routes.json';
|
||||||
import MainStats from './MainStats';
|
|
||||||
import DetailedBatteryView from './DetailedBatteryView';
|
|
||||||
import CircularProgress from '@mui/material/CircularProgress';
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
|
import DetailedBatteryView from './DetailedBatteryView';
|
||||||
|
import MainStats from './MainStats';
|
||||||
|
|
||||||
interface BatteryViewProps {
|
interface BatteryViewProps {
|
||||||
values: TopologyValues;
|
values: TopologyValues;
|
||||||
|
@ -305,189 +305,72 @@ function BatteryView(props: BatteryViewProps) {
|
||||||
battery.AverageTemperature.unit}
|
battery.AverageTemperature.unit}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
{props.productNum === 0 && (
|
<TableCell
|
||||||
<>
|
style={{
|
||||||
<TableCell
|
width: '20%',
|
||||||
style={{
|
textAlign: 'center',
|
||||||
width: '20%',
|
padding: '8px',
|
||||||
textAlign: 'center',
|
fontWeight:
|
||||||
padding: '8px',
|
battery.Warnings.value !== '' ? 'bold' : 'inherit',
|
||||||
fontWeight:
|
backgroundColor:
|
||||||
battery.Warnings.value !== ''
|
battery.Warnings.value === '' ? 'inherit' : '#ff9900',
|
||||||
? 'bold'
|
color:
|
||||||
: 'inherit',
|
battery.Warnings.value != '' ? 'black' : 'inherit'
|
||||||
backgroundColor:
|
}}
|
||||||
battery.Warnings.value === ''
|
>
|
||||||
? 'inherit'
|
{battery.Warnings.value === '' ? (
|
||||||
: '#ff9900',
|
'None'
|
||||||
color:
|
) : battery.Warnings.value.toString().split('-').length >
|
||||||
battery.Warnings.value != '' ? 'black' : 'inherit'
|
1 ? (
|
||||||
}}
|
<Link
|
||||||
|
style={{ color: 'black' }}
|
||||||
|
to={
|
||||||
|
currentLocation.pathname.substring(
|
||||||
|
0,
|
||||||
|
currentLocation.pathname.lastIndexOf('/') + 1
|
||||||
|
) +
|
||||||
|
routes.log +
|
||||||
|
'?open=warning'
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{battery.Warnings.value === '' ? (
|
Multiple Warnings
|
||||||
'None'
|
</Link>
|
||||||
) : battery.Warnings.value.toString().split('-')
|
) : (
|
||||||
.length > 1 ? (
|
battery.Warnings.value
|
||||||
<Link
|
)}
|
||||||
style={{ color: 'black' }}
|
</TableCell>
|
||||||
to={
|
<TableCell
|
||||||
currentLocation.pathname.substring(
|
sx={{
|
||||||
0,
|
width: '20%',
|
||||||
currentLocation.pathname.lastIndexOf('/') + 1
|
textAlign: 'center',
|
||||||
) +
|
fontWeight:
|
||||||
routes.log +
|
battery.Alarms.value !== '' ? 'bold' : 'inherit',
|
||||||
'?open=warning'
|
backgroundColor:
|
||||||
}
|
battery.Alarms.value === '' ? 'inherit' : '#FF033E',
|
||||||
>
|
color: battery.Alarms.value != '' ? 'black' : 'inherit'
|
||||||
Multiple Warnings
|
}}
|
||||||
</Link>
|
>
|
||||||
) : (
|
{battery.Alarms.value === '' ? (
|
||||||
battery.Warnings.value
|
'None'
|
||||||
)}
|
) : battery.Alarms.value.toString().split('-').length >
|
||||||
</TableCell>
|
1 ? (
|
||||||
<TableCell
|
<Link
|
||||||
sx={{
|
style={{ color: 'black' }}
|
||||||
width: '20%',
|
to={
|
||||||
textAlign: 'center',
|
currentLocation.pathname.substring(
|
||||||
fontWeight:
|
0,
|
||||||
battery.Alarms.value !== '' ? 'bold' : 'inherit',
|
currentLocation.pathname.lastIndexOf('/') + 1
|
||||||
backgroundColor:
|
) +
|
||||||
battery.Alarms.value === ''
|
routes.log +
|
||||||
? 'inherit'
|
'?open=error'
|
||||||
: '#FF033E',
|
}
|
||||||
color:
|
|
||||||
battery.Alarms.value != '' ? 'black' : 'inherit'
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{battery.Alarms.value === '' ? (
|
Multiple Alarms
|
||||||
'None'
|
</Link>
|
||||||
) : battery.Alarms.value.toString().split('-')
|
) : (
|
||||||
.length > 1 ? (
|
battery.Alarms.value
|
||||||
<Link
|
)}
|
||||||
style={{ color: 'black' }}
|
</TableCell>
|
||||||
to={
|
|
||||||
currentLocation.pathname.substring(
|
|
||||||
0,
|
|
||||||
currentLocation.pathname.lastIndexOf('/') + 1
|
|
||||||
) +
|
|
||||||
routes.log +
|
|
||||||
'?open=error'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Multiple Alarms
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
battery.Alarms.value
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{props.productNum === 1 && (
|
|
||||||
<>
|
|
||||||
<TableCell
|
|
||||||
style={{
|
|
||||||
width: '20%',
|
|
||||||
textAlign: 'center',
|
|
||||||
padding: '8px',
|
|
||||||
fontWeight:
|
|
||||||
Number(battery.Warnings.value) !== 0
|
|
||||||
? 'bold'
|
|
||||||
: 'inherit',
|
|
||||||
backgroundColor:
|
|
||||||
Number(battery.Warnings.value) === 0
|
|
||||||
? 'inherit'
|
|
||||||
: '#ff9900',
|
|
||||||
color:
|
|
||||||
Number(battery.Warnings.value) != 0
|
|
||||||
? 'black'
|
|
||||||
: 'inherit'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{Number(battery.Warnings.value) === 0 ? (
|
|
||||||
'None'
|
|
||||||
) : Number(battery.Warnings.value) === 1 ? (
|
|
||||||
<Link
|
|
||||||
style={{ color: 'black' }}
|
|
||||||
to={
|
|
||||||
currentLocation.pathname.substring(
|
|
||||||
0,
|
|
||||||
currentLocation.pathname.lastIndexOf('/') + 1
|
|
||||||
) +
|
|
||||||
routes.log +
|
|
||||||
'?open=warning'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
New Warning
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<Link
|
|
||||||
style={{ color: 'black' }}
|
|
||||||
to={
|
|
||||||
currentLocation.pathname.substring(
|
|
||||||
0,
|
|
||||||
currentLocation.pathname.lastIndexOf('/') + 1
|
|
||||||
) +
|
|
||||||
routes.log +
|
|
||||||
'?open=warning'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Multiple Warnings
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell
|
|
||||||
sx={{
|
|
||||||
width: '20%',
|
|
||||||
textAlign: 'center',
|
|
||||||
fontWeight:
|
|
||||||
Number(battery.Alarms.value) !== 0
|
|
||||||
? 'bold'
|
|
||||||
: 'inherit',
|
|
||||||
backgroundColor:
|
|
||||||
Number(battery.Alarms.value) === 0
|
|
||||||
? 'inherit'
|
|
||||||
: '#FF033E',
|
|
||||||
color:
|
|
||||||
Number(battery.Alarms.value) != 0
|
|
||||||
? 'black'
|
|
||||||
: 'inherit'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{Number(battery.Alarms.value) === 0 ? (
|
|
||||||
'None'
|
|
||||||
) : Number(battery.Alarms.value) === 1 ? (
|
|
||||||
<Link
|
|
||||||
style={{ color: 'black' }}
|
|
||||||
to={
|
|
||||||
currentLocation.pathname.substring(
|
|
||||||
0,
|
|
||||||
currentLocation.pathname.lastIndexOf('/') + 1
|
|
||||||
) +
|
|
||||||
routes.log +
|
|
||||||
'?open=error'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
New Alarm
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<Link
|
|
||||||
style={{ color: 'black' }}
|
|
||||||
to={
|
|
||||||
currentLocation.pathname.substring(
|
|
||||||
0,
|
|
||||||
currentLocation.pathname.lastIndexOf('/') + 1
|
|
||||||
) +
|
|
||||||
routes.log +
|
|
||||||
'?open=error'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Multiple Alarms
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
|
|
|
@ -0,0 +1,493 @@
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Container,
|
||||||
|
Grid,
|
||||||
|
Paper,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableContainer,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
Typography
|
||||||
|
} from '@mui/material';
|
||||||
|
import {
|
||||||
|
Link,
|
||||||
|
Route,
|
||||||
|
Routes,
|
||||||
|
useLocation,
|
||||||
|
useNavigate
|
||||||
|
} from 'react-router-dom';
|
||||||
|
import { I_S3Credentials } from '../../../interfaces/S3Types';
|
||||||
|
import routes from '../../../Resources/routes.json';
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
|
import { JSONRecordData } from '../Log/graph.util';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import MainStatsSalidomo from './MainStatsSalidomo';
|
||||||
|
import DetailedBatteryViewSalidomo from './DetailedBatteryViewSalidomo';
|
||||||
|
|
||||||
|
interface BatteryViewProps {
|
||||||
|
values: JSONRecordData;
|
||||||
|
s3Credentials: I_S3Credentials;
|
||||||
|
installationId: number;
|
||||||
|
productNum: number;
|
||||||
|
connected: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function BatteryViewSalidomo(props: BatteryViewProps) {
|
||||||
|
if (props.values === null && props.connected == true) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentLocation = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const sortedBatteryView = Object.entries(props.values.Battery.Devices)
|
||||||
|
.map(([BatteryId, battery]) => {
|
||||||
|
return { BatteryId, battery }; // Here we return an object with the id and device
|
||||||
|
})
|
||||||
|
.sort((a, b) => parseInt(b.BatteryId) - parseInt(a.BatteryId));
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(sortedBatteryView.length == 0);
|
||||||
|
|
||||||
|
const handleMainStatsButton = () => {
|
||||||
|
navigate(routes.mainstats);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (sortedBatteryView.length == 0) {
|
||||||
|
setLoading(true);
|
||||||
|
} else {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [sortedBatteryView]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!props.connected && (
|
||||||
|
<Container
|
||||||
|
maxWidth="xl"
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
height: '70vh'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CircularProgress size={60} style={{ color: '#ffc04d' }} />
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
style={{ color: 'black', fontWeight: 'bold' }}
|
||||||
|
mt={2}
|
||||||
|
>
|
||||||
|
Unable to communicate with the installation
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" style={{ color: 'black' }}>
|
||||||
|
Please wait or refresh the page
|
||||||
|
</Typography>
|
||||||
|
</Container>
|
||||||
|
)}
|
||||||
|
{loading && props.connected && (
|
||||||
|
<Container
|
||||||
|
maxWidth="xl"
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
height: '70vh'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CircularProgress size={60} style={{ color: '#ffc04d' }} />
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
style={{ color: 'black', fontWeight: 'bold' }}
|
||||||
|
mt={2}
|
||||||
|
>
|
||||||
|
Battery service is not available at the moment
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" style={{ color: 'black' }}>
|
||||||
|
Please wait or refresh the page
|
||||||
|
</Typography>
|
||||||
|
</Container>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!loading && props.connected && (
|
||||||
|
<Container maxWidth="xl">
|
||||||
|
<Grid container>
|
||||||
|
<Grid
|
||||||
|
item
|
||||||
|
xs={6}
|
||||||
|
md={6}
|
||||||
|
sx={{
|
||||||
|
display:
|
||||||
|
!currentLocation.pathname.includes('detailed_view') &&
|
||||||
|
!currentLocation.pathname.includes('mainstats')
|
||||||
|
? 'block'
|
||||||
|
: 'none'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
sx={{
|
||||||
|
marginTop: '20px',
|
||||||
|
backgroundColor: '#808080',
|
||||||
|
color: '#000000',
|
||||||
|
'&:hover': { bgcolor: '#f7b34d' }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id="battery_view"
|
||||||
|
defaultMessage="Battery View"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={handleMainStatsButton}
|
||||||
|
sx={{
|
||||||
|
marginTop: '20px',
|
||||||
|
marginLeft: '20px',
|
||||||
|
backgroundColor: '#ffc04d',
|
||||||
|
color: '#000000',
|
||||||
|
'&:hover': { bgcolor: '#f7b34d' }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormattedMessage id="main_stats" defaultMessage="Main Stats" />
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid container>
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path={routes.mainstats + '*'}
|
||||||
|
element={
|
||||||
|
<MainStatsSalidomo
|
||||||
|
s3Credentials={props.s3Credentials}
|
||||||
|
id={props.installationId}
|
||||||
|
></MainStatsSalidomo>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{Object.entries(props.values.Battery.Devices).map(
|
||||||
|
([BatteryId, battery]) => (
|
||||||
|
<Route
|
||||||
|
key={routes.detailed_view + BatteryId}
|
||||||
|
path={routes.detailed_view + BatteryId}
|
||||||
|
element={
|
||||||
|
<DetailedBatteryViewSalidomo
|
||||||
|
batteryId={Number(BatteryId)}
|
||||||
|
s3Credentials={props.s3Credentials}
|
||||||
|
batteryData={battery}
|
||||||
|
installationId={props.installationId}
|
||||||
|
productNum={props.productNum}
|
||||||
|
></DetailedBatteryViewSalidomo>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Routes>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<TableContainer
|
||||||
|
component={Paper}
|
||||||
|
sx={{
|
||||||
|
marginTop: '20px',
|
||||||
|
marginBottom: '20px',
|
||||||
|
display:
|
||||||
|
!currentLocation.pathname.includes('detailed_view') &&
|
||||||
|
!currentLocation.pathname.includes('mainstats')
|
||||||
|
? 'block'
|
||||||
|
: 'none'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Table sx={{ minWidth: 650 }} aria-label="simple table">
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell align="center">Battery</TableCell>
|
||||||
|
<TableCell align="center">Firmware</TableCell>
|
||||||
|
<TableCell align="center">Power</TableCell>
|
||||||
|
<TableCell align="center">Voltage</TableCell>
|
||||||
|
<TableCell align="center">SoC</TableCell>
|
||||||
|
<TableCell align="center">Temperature</TableCell>
|
||||||
|
<TableCell align="center">Warnings</TableCell>
|
||||||
|
<TableCell align="center">Alarms</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{sortedBatteryView.map(({ BatteryId, battery }) => (
|
||||||
|
<TableRow
|
||||||
|
key={BatteryId}
|
||||||
|
style={{
|
||||||
|
height: '10px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TableCell
|
||||||
|
component="th"
|
||||||
|
scope="row"
|
||||||
|
align="center"
|
||||||
|
sx={{ fontWeight: 'bold' }}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
style={{ color: 'black' }}
|
||||||
|
to={routes.detailed_view + BatteryId}
|
||||||
|
>
|
||||||
|
{'Node ' + BatteryId}
|
||||||
|
</Link>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell
|
||||||
|
sx={{
|
||||||
|
width: '10%',
|
||||||
|
textAlign: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{battery.FwVersion.value}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell
|
||||||
|
sx={{
|
||||||
|
width: '10%',
|
||||||
|
textAlign: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{battery.Dc.Power.value + ' W'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell
|
||||||
|
sx={{
|
||||||
|
width: '10%',
|
||||||
|
textAlign: 'center',
|
||||||
|
|
||||||
|
backgroundColor:
|
||||||
|
battery.Dc.Voltage.value < 44 ||
|
||||||
|
battery.Dc.Voltage.value > 57
|
||||||
|
? '#FF033E'
|
||||||
|
: '#32CD32',
|
||||||
|
color: battery.Dc.Voltage.value ? 'inherit' : 'white'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{battery.Dc.Voltage.value + ' V'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell
|
||||||
|
sx={{
|
||||||
|
width: '10%',
|
||||||
|
textAlign: 'center',
|
||||||
|
backgroundColor:
|
||||||
|
battery.Soc.value < 20
|
||||||
|
? '#FF033E'
|
||||||
|
: battery.Soc.value < 50
|
||||||
|
? '#ffbf00'
|
||||||
|
: '#32CD32',
|
||||||
|
color: battery.Soc.value ? 'inherit' : 'white'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{battery.Soc.value + ' %'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell
|
||||||
|
sx={{
|
||||||
|
width: '10%',
|
||||||
|
textAlign: 'center',
|
||||||
|
backgroundColor:
|
||||||
|
battery.Temperatures.Cells.Average.value > 300
|
||||||
|
? '#FF033E'
|
||||||
|
: battery.Temperatures.Cells.Average.value > 280
|
||||||
|
? '#ffbf00'
|
||||||
|
: battery.Temperatures.Cells.Average.value < 245
|
||||||
|
? '#008FFB'
|
||||||
|
: '#32CD32'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{battery.Temperatures.Cells.Average.value + ' C'}
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
{/*{props.productNum === 0 && (*/}
|
||||||
|
{/* <>*/}
|
||||||
|
{/* <TableCell*/}
|
||||||
|
{/* style={{*/}
|
||||||
|
{/* width: '20%',*/}
|
||||||
|
{/* textAlign: 'center',*/}
|
||||||
|
{/* padding: '8px',*/}
|
||||||
|
{/* fontWeight:*/}
|
||||||
|
{/* battery.Warnings.value !== ''*/}
|
||||||
|
{/* ? 'bold'*/}
|
||||||
|
{/* : 'inherit',*/}
|
||||||
|
{/* backgroundColor:*/}
|
||||||
|
{/* battery.Warnings.value === ''*/}
|
||||||
|
{/* ? 'inherit'*/}
|
||||||
|
{/* : '#ff9900',*/}
|
||||||
|
{/* color:*/}
|
||||||
|
{/* battery.Warnings.value != '' ? 'black' : 'inherit'*/}
|
||||||
|
{/* }}*/}
|
||||||
|
{/* >*/}
|
||||||
|
{/* {battery.Warnings.value === '' ? (*/}
|
||||||
|
{/* 'None'*/}
|
||||||
|
{/* ) : battery.Warnings.value.toString().split('-')*/}
|
||||||
|
{/* .length > 1 ? (*/}
|
||||||
|
{/* <Link*/}
|
||||||
|
{/* style={{ color: 'black' }}*/}
|
||||||
|
{/* to={*/}
|
||||||
|
{/* currentLocation.pathname.substring(*/}
|
||||||
|
{/* 0,*/}
|
||||||
|
{/* currentLocation.pathname.lastIndexOf('/') + 1*/}
|
||||||
|
{/* ) +*/}
|
||||||
|
{/* routes.log +*/}
|
||||||
|
{/* '?open=warning'*/}
|
||||||
|
{/* }*/}
|
||||||
|
{/* >*/}
|
||||||
|
{/* Multiple Warnings*/}
|
||||||
|
{/* </Link>*/}
|
||||||
|
{/* ) : (*/}
|
||||||
|
{/* battery.Warnings.value*/}
|
||||||
|
{/* )}*/}
|
||||||
|
{/* </TableCell>*/}
|
||||||
|
{/* <TableCell*/}
|
||||||
|
{/* sx={{*/}
|
||||||
|
{/* width: '20%',*/}
|
||||||
|
{/* textAlign: 'center',*/}
|
||||||
|
{/* fontWeight:*/}
|
||||||
|
{/* battery.Alarms.value !== '' ? 'bold' : 'inherit',*/}
|
||||||
|
{/* backgroundColor:*/}
|
||||||
|
{/* battery.Alarms.value === ''*/}
|
||||||
|
{/* ? 'inherit'*/}
|
||||||
|
{/* : '#FF033E',*/}
|
||||||
|
{/* color:*/}
|
||||||
|
{/* battery.Alarms.value != '' ? 'black' : 'inherit'*/}
|
||||||
|
{/* }}*/}
|
||||||
|
{/* >*/}
|
||||||
|
{/* {battery.Alarms.value === '' ? (*/}
|
||||||
|
{/* 'None'*/}
|
||||||
|
{/* ) : battery.Alarms.value.toString().split('-')*/}
|
||||||
|
{/* .length > 1 ? (*/}
|
||||||
|
{/* <Link*/}
|
||||||
|
{/* style={{ color: 'black' }}*/}
|
||||||
|
{/* to={*/}
|
||||||
|
{/* currentLocation.pathname.substring(*/}
|
||||||
|
{/* 0,*/}
|
||||||
|
{/* currentLocation.pathname.lastIndexOf('/') + 1*/}
|
||||||
|
{/* ) +*/}
|
||||||
|
{/* routes.log +*/}
|
||||||
|
{/* '?open=error'*/}
|
||||||
|
{/* }*/}
|
||||||
|
{/* >*/}
|
||||||
|
{/* Multiple Alarms*/}
|
||||||
|
{/* </Link>*/}
|
||||||
|
{/* ) : (*/}
|
||||||
|
{/* battery.Alarms.value*/}
|
||||||
|
{/* )}*/}
|
||||||
|
{/* </TableCell>*/}
|
||||||
|
{/* </>*/}
|
||||||
|
{/*)}*/}
|
||||||
|
|
||||||
|
{props.productNum === 1 && (
|
||||||
|
<>
|
||||||
|
<TableCell
|
||||||
|
style={{
|
||||||
|
width: '20%',
|
||||||
|
textAlign: 'center',
|
||||||
|
padding: '8px',
|
||||||
|
fontWeight:
|
||||||
|
Number(battery.Warnings) !== 0
|
||||||
|
? 'bold'
|
||||||
|
: 'inherit',
|
||||||
|
backgroundColor:
|
||||||
|
Number(battery.Warnings) === 0
|
||||||
|
? 'inherit'
|
||||||
|
: '#ff9900',
|
||||||
|
color:
|
||||||
|
Number(battery.Warnings) != 0
|
||||||
|
? 'black'
|
||||||
|
: 'inherit'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Number(battery.Warnings) === 0 ? (
|
||||||
|
'None'
|
||||||
|
) : Number(battery.Warnings) === 1 ? (
|
||||||
|
<Link
|
||||||
|
style={{ color: 'black' }}
|
||||||
|
to={
|
||||||
|
currentLocation.pathname.substring(
|
||||||
|
0,
|
||||||
|
currentLocation.pathname.lastIndexOf('/') + 1
|
||||||
|
) +
|
||||||
|
routes.log +
|
||||||
|
'?open=warning'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
New Warning
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<Link
|
||||||
|
style={{ color: 'black' }}
|
||||||
|
to={
|
||||||
|
currentLocation.pathname.substring(
|
||||||
|
0,
|
||||||
|
currentLocation.pathname.lastIndexOf('/') + 1
|
||||||
|
) +
|
||||||
|
routes.log +
|
||||||
|
'?open=warning'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Multiple Warnings
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell
|
||||||
|
sx={{
|
||||||
|
width: '20%',
|
||||||
|
textAlign: 'center',
|
||||||
|
fontWeight:
|
||||||
|
Number(battery.Alarms) !== 0 ? 'bold' : 'inherit',
|
||||||
|
backgroundColor:
|
||||||
|
Number(battery.Alarms) === 0
|
||||||
|
? 'inherit'
|
||||||
|
: '#FF033E',
|
||||||
|
color:
|
||||||
|
Number(battery.Alarms) != 0 ? 'black' : 'inherit'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Number(battery.Alarms) === 0 ? (
|
||||||
|
'None'
|
||||||
|
) : Number(battery.Alarms) === 1 ? (
|
||||||
|
<Link
|
||||||
|
style={{ color: 'black' }}
|
||||||
|
to={
|
||||||
|
currentLocation.pathname.substring(
|
||||||
|
0,
|
||||||
|
currentLocation.pathname.lastIndexOf('/') + 1
|
||||||
|
) +
|
||||||
|
routes.log +
|
||||||
|
'?open=error'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
New Alarm
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<Link
|
||||||
|
style={{ color: 'black' }}
|
||||||
|
to={
|
||||||
|
currentLocation.pathname.substring(
|
||||||
|
0,
|
||||||
|
currentLocation.pathname.lastIndexOf('/') + 1
|
||||||
|
) +
|
||||||
|
routes.log +
|
||||||
|
'?open=error'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Multiple Alarms
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</Container>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BatteryViewSalidomo;
|
|
@ -1531,111 +1531,6 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
</Card>
|
</Card>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{/*----------------------------------------------------------------------------------------------------------------------------------*/}
|
|
||||||
|
|
||||||
{/*<Grid item md={1.5} xs={1.5}>*/}
|
|
||||||
{/* <Card*/}
|
|
||||||
{/* sx={{*/}
|
|
||||||
{/* overflow: 'visible',*/}
|
|
||||||
{/* marginTop: '30px',*/}
|
|
||||||
{/* marginLeft: '20px',*/}
|
|
||||||
{/* marginBottom: '30px',*/}
|
|
||||||
{/* backgroundColor: 'red'*/}
|
|
||||||
{/* }}*/}
|
|
||||||
{/* >*/}
|
|
||||||
{/* <TableContainer*/}
|
|
||||||
{/* component={Paper}*/}
|
|
||||||
{/* sx={{ marginTop: '20px', marginBottom: '20px', width: '100%' }}*/}
|
|
||||||
{/* >*/}
|
|
||||||
{/* <Table size="medium" aria-label="a dense table">*/}
|
|
||||||
{/* <TableBody>*/}
|
|
||||||
{/* <TableRow>*/}
|
|
||||||
{/* <TableCell*/}
|
|
||||||
{/* component="th"*/}
|
|
||||||
{/* scope="row"*/}
|
|
||||||
{/* align="left"*/}
|
|
||||||
{/* sx={{ fontWeight: 'bold' }}*/}
|
|
||||||
{/* >*/}
|
|
||||||
{/* Green Led*/}
|
|
||||||
{/* </TableCell>*/}
|
|
||||||
{/* <TableCell*/}
|
|
||||||
{/* align="right"*/}
|
|
||||||
{/* sx={{*/}
|
|
||||||
{/* width: '6ch',*/}
|
|
||||||
{/* whiteSpace: 'nowrap',*/}
|
|
||||||
{/* paddingRight: '12px'*/}
|
|
||||||
{/* }}*/}
|
|
||||||
{/* >*/}
|
|
||||||
{/* {props.batteryData.GreenLeds.value}*/}
|
|
||||||
{/* </TableCell>*/}
|
|
||||||
{/* </TableRow>*/}
|
|
||||||
{/* <TableRow>*/}
|
|
||||||
{/* <TableCell*/}
|
|
||||||
{/* component="th"*/}
|
|
||||||
{/* scope="row"*/}
|
|
||||||
{/* align="left"*/}
|
|
||||||
{/* sx={{ fontWeight: 'bold' }}*/}
|
|
||||||
{/* >*/}
|
|
||||||
{/* Amber Led*/}
|
|
||||||
{/* </TableCell>*/}
|
|
||||||
{/* <TableCell*/}
|
|
||||||
{/* align="right"*/}
|
|
||||||
{/* sx={{*/}
|
|
||||||
{/* width: '6ch',*/}
|
|
||||||
{/* whiteSpace: 'nowrap',*/}
|
|
||||||
{/* paddingRight: '12px'*/}
|
|
||||||
{/* }}*/}
|
|
||||||
{/* >*/}
|
|
||||||
{/* {props.batteryData.AmberLeds.value}*/}
|
|
||||||
{/* </TableCell>*/}
|
|
||||||
{/* </TableRow>*/}
|
|
||||||
{/* <TableRow>*/}
|
|
||||||
{/* <TableCell*/}
|
|
||||||
{/* component="th"*/}
|
|
||||||
{/* scope="row"*/}
|
|
||||||
{/* align="left"*/}
|
|
||||||
{/* sx={{ fontWeight: 'bold' }}*/}
|
|
||||||
{/* >*/}
|
|
||||||
{/* Blue Led*/}
|
|
||||||
{/* </TableCell>*/}
|
|
||||||
{/* <TableCell*/}
|
|
||||||
{/* align="right"*/}
|
|
||||||
{/* sx={{*/}
|
|
||||||
{/* width: '6ch',*/}
|
|
||||||
{/* whiteSpace: 'nowrap',*/}
|
|
||||||
{/* paddingRight: '12px'*/}
|
|
||||||
{/* }}*/}
|
|
||||||
{/* >*/}
|
|
||||||
{/* {props.batteryData.BlueLeds.value}*/}
|
|
||||||
{/* </TableCell>*/}
|
|
||||||
{/* </TableRow>*/}
|
|
||||||
|
|
||||||
{/* <TableRow>*/}
|
|
||||||
{/* <TableCell*/}
|
|
||||||
{/* component="th"*/}
|
|
||||||
{/* scope="row"*/}
|
|
||||||
{/* align="left"*/}
|
|
||||||
{/* sx={{ fontWeight: 'bold' }}*/}
|
|
||||||
{/* >*/}
|
|
||||||
{/* Red Led*/}
|
|
||||||
{/* </TableCell>*/}
|
|
||||||
{/* <TableCell*/}
|
|
||||||
{/* align="right"*/}
|
|
||||||
{/* sx={{*/}
|
|
||||||
{/* width: '6ch',*/}
|
|
||||||
{/* whiteSpace: 'nowrap',*/}
|
|
||||||
{/* paddingRight: '12px'*/}
|
|
||||||
{/* }}*/}
|
|
||||||
{/* >*/}
|
|
||||||
{/* {props.batteryData.RedLeds.value}*/}
|
|
||||||
{/* </TableCell>*/}
|
|
||||||
{/* </TableRow>*/}
|
|
||||||
{/* </TableBody>*/}
|
|
||||||
{/* </Table>*/}
|
|
||||||
{/* </TableContainer>*/}
|
|
||||||
{/* </Card>*/}
|
|
||||||
{/*</Grid>*/}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -5,6 +5,7 @@ import {
|
||||||
Grid,
|
Grid,
|
||||||
IconButton,
|
IconButton,
|
||||||
Modal,
|
Modal,
|
||||||
|
TextField,
|
||||||
Typography
|
Typography
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
@ -17,7 +18,7 @@ import {
|
||||||
BatteryOverviewInterface,
|
BatteryOverviewInterface,
|
||||||
transformInputToBatteryViewData
|
transformInputToBatteryViewData
|
||||||
} from '../../../interfaces/Chart';
|
} from '../../../interfaces/Chart';
|
||||||
import dayjs from 'dayjs';
|
import dayjs, { Dayjs } from 'dayjs';
|
||||||
import { TimeSpan, UnixTime } from '../../../dataCache/time';
|
import { TimeSpan, UnixTime } from '../../../dataCache/time';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import { DateTimePicker, LocalizationProvider } from '@mui/x-date-pickers';
|
import { DateTimePicker, LocalizationProvider } from '@mui/x-date-pickers';
|
||||||
|
@ -360,19 +361,25 @@ function MainStats(props: MainStatsProps) {
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
label="Select Start Date"
|
label="Select Start Date"
|
||||||
value={startDate}
|
value={startDate}
|
||||||
onChange={(newDate) => setStartDate(newDate)}
|
onChange={(newDate: Dayjs | null) => {
|
||||||
sx={{
|
// Type assertion to Dayjs
|
||||||
marginTop: 2
|
if (newDate) {
|
||||||
|
setStartDate(newDate);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
|
renderInput={(props) => <TextField {...props} />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
label="Select End Date"
|
label="Select End Date"
|
||||||
value={endDate}
|
value={endDate}
|
||||||
onChange={(newDate) => setEndDate(newDate)}
|
onChange={(newDate: Dayjs | null) => {
|
||||||
sx={{
|
// Type assertion to Dayjs
|
||||||
marginTop: 2
|
if (newDate) {
|
||||||
|
setEndDate(newDate);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
|
renderInput={(props) => <TextField {...props} />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -0,0 +1,812 @@
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Card,
|
||||||
|
Container,
|
||||||
|
Grid,
|
||||||
|
IconButton,
|
||||||
|
Modal,
|
||||||
|
TextField,
|
||||||
|
Typography
|
||||||
|
} from '@mui/material';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
|
import { I_S3Credentials } from '../../../interfaces/S3Types';
|
||||||
|
import ReactApexChart from 'react-apexcharts';
|
||||||
|
import { getChartOptions } from '../Overview/chartOptions';
|
||||||
|
import {
|
||||||
|
BatteryDataInterface,
|
||||||
|
BatteryOverviewInterface,
|
||||||
|
transformInputToBatteryViewDataJson
|
||||||
|
} from '../../../interfaces/Chart';
|
||||||
|
import dayjs, { Dayjs } from 'dayjs';
|
||||||
|
import { TimeSpan, UnixTime } from '../../../dataCache/time';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import { DateTimePicker, LocalizationProvider } from '@mui/x-date-pickers';
|
||||||
|
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
|
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||||
|
import { ProductIdContext } from '../../../contexts/ProductIdContextProvider';
|
||||||
|
|
||||||
|
interface MainStatsProps {
|
||||||
|
s3Credentials: I_S3Credentials;
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MainStatsSalidomo(props: MainStatsProps) {
|
||||||
|
const [chartState, setChartState] = useState(0);
|
||||||
|
const [batteryViewDataArray, setBatteryViewDataArray] = useState<
|
||||||
|
{
|
||||||
|
chartData: BatteryDataInterface;
|
||||||
|
chartOverview: BatteryOverviewInterface;
|
||||||
|
}[]
|
||||||
|
>([]);
|
||||||
|
|
||||||
|
const [isDateModalOpen, setIsDateModalOpen] = useState(false);
|
||||||
|
const [dateOpen, setDateOpen] = useState(false);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [startDate, setStartDate] = useState(dayjs().add(-1, 'day'));
|
||||||
|
const [endDate, setEndDate] = useState(dayjs());
|
||||||
|
const [isErrorDateModalOpen, setErrorDateModalOpen] = useState(false);
|
||||||
|
const [dateSelectionError, setDateSelectionError] = useState('');
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const location = useLocation();
|
||||||
|
const { product, setProduct } = useContext(ProductIdContext);
|
||||||
|
|
||||||
|
const blueColors = [
|
||||||
|
'#99CCFF',
|
||||||
|
'#80BFFF',
|
||||||
|
'#6699CC',
|
||||||
|
'#4D99FF',
|
||||||
|
'#2670E6',
|
||||||
|
'#3366CC',
|
||||||
|
'#1A4D99',
|
||||||
|
'#133366',
|
||||||
|
'#0D274D',
|
||||||
|
'#081A33'
|
||||||
|
];
|
||||||
|
const redColors = [
|
||||||
|
'#ff9090',
|
||||||
|
'#ff7070',
|
||||||
|
'#ff3f3f',
|
||||||
|
'#ff1e1e',
|
||||||
|
'#ff0606',
|
||||||
|
'#fc0000',
|
||||||
|
'#f40000',
|
||||||
|
'#d40000',
|
||||||
|
'#a30000',
|
||||||
|
'#7a0000'
|
||||||
|
];
|
||||||
|
const orangeColors = [
|
||||||
|
'#ffdb99',
|
||||||
|
'#ffc968',
|
||||||
|
'#ffb837',
|
||||||
|
'#ffac16',
|
||||||
|
'#ffa706',
|
||||||
|
'#FF8C00',
|
||||||
|
'#d48900',
|
||||||
|
'#CC7A00',
|
||||||
|
'#a36900',
|
||||||
|
'#993D00'
|
||||||
|
];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const resultPromise: Promise<{
|
||||||
|
chartData: BatteryDataInterface;
|
||||||
|
chartOverview: BatteryOverviewInterface;
|
||||||
|
}> = transformInputToBatteryViewDataJson(
|
||||||
|
props.s3Credentials,
|
||||||
|
props.id,
|
||||||
|
product,
|
||||||
|
UnixTime.fromTicks(new Date().getTime() / 1000).earlier(
|
||||||
|
TimeSpan.fromDays(1)
|
||||||
|
),
|
||||||
|
UnixTime.fromTicks(new Date().getTime() / 1000)
|
||||||
|
);
|
||||||
|
|
||||||
|
resultPromise
|
||||||
|
.then((result) => {
|
||||||
|
setBatteryViewDataArray((prevData) =>
|
||||||
|
prevData.concat({
|
||||||
|
chartData: result.chartData,
|
||||||
|
chartOverview: result.chartOverview
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const [isZooming, setIsZooming] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isZooming) {
|
||||||
|
setLoading(true);
|
||||||
|
} else if (!isZooming && batteryViewDataArray.length > 0) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [isZooming, batteryViewDataArray]);
|
||||||
|
|
||||||
|
function generateSeries(chartData, category, color) {
|
||||||
|
const series = [];
|
||||||
|
const pathsToSearch = [
|
||||||
|
'Node2',
|
||||||
|
'Node3',
|
||||||
|
'Node4',
|
||||||
|
'Node5',
|
||||||
|
'Node6',
|
||||||
|
'Node7',
|
||||||
|
'Node8',
|
||||||
|
'Node9',
|
||||||
|
'Node10',
|
||||||
|
'Node11'
|
||||||
|
];
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
pathsToSearch.forEach((devicePath) => {
|
||||||
|
if (
|
||||||
|
Object.hasOwnProperty.call(chartData[category].data, devicePath) &&
|
||||||
|
chartData[category].data[devicePath].data.length != 0
|
||||||
|
) {
|
||||||
|
series.push({
|
||||||
|
...chartData[category].data[devicePath],
|
||||||
|
color:
|
||||||
|
color === 'blue'
|
||||||
|
? blueColors[i]
|
||||||
|
: color === 'red'
|
||||||
|
? redColors[i]
|
||||||
|
: orangeColors[i]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
});
|
||||||
|
|
||||||
|
return series;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setIsDateModalOpen(false);
|
||||||
|
setDateOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
setIsDateModalOpen(false);
|
||||||
|
setDateOpen(false);
|
||||||
|
|
||||||
|
if (endDate.isAfter(dayjs())) {
|
||||||
|
setDateSelectionError('You cannot ask for future data');
|
||||||
|
setErrorDateModalOpen(true);
|
||||||
|
return;
|
||||||
|
} else if (startDate.isAfter(endDate)) {
|
||||||
|
setDateSelectionError('Εnd date must precede start date');
|
||||||
|
setErrorDateModalOpen(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
const resultPromise: Promise<{
|
||||||
|
chartData: BatteryDataInterface;
|
||||||
|
chartOverview: BatteryOverviewInterface;
|
||||||
|
}> = transformInputToBatteryViewDataJson(
|
||||||
|
props.s3Credentials,
|
||||||
|
props.id,
|
||||||
|
product,
|
||||||
|
UnixTime.fromTicks(startDate.unix()),
|
||||||
|
UnixTime.fromTicks(endDate.unix())
|
||||||
|
);
|
||||||
|
|
||||||
|
resultPromise
|
||||||
|
.then((result) => {
|
||||||
|
setBatteryViewDataArray((prevData) =>
|
||||||
|
prevData.concat({
|
||||||
|
chartData: result.chartData,
|
||||||
|
chartOverview: result.chartOverview
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
setChartState(batteryViewDataArray.length);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const handleSetDate = () => {
|
||||||
|
setDateOpen(true);
|
||||||
|
setIsDateModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBatteryViewButton = () => {
|
||||||
|
navigate(
|
||||||
|
location.pathname.split('/').slice(0, -2).join('/') + '/batteryview'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGoBack = () => {
|
||||||
|
if (chartState > 0) {
|
||||||
|
setChartState(chartState - 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGoForward = () => {
|
||||||
|
if (chartState + 1 < batteryViewDataArray.length) {
|
||||||
|
setChartState(chartState + 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOkOnErrorDateModal = () => {
|
||||||
|
setErrorDateModalOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const startZoom = () => {
|
||||||
|
setIsZooming(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBeforeZoom = (chartContext, { xaxis }) => {
|
||||||
|
const startX = parseInt(xaxis.min) / 1000;
|
||||||
|
const endX = parseInt(xaxis.max) / 1000;
|
||||||
|
|
||||||
|
const resultPromise: Promise<{
|
||||||
|
chartData: BatteryDataInterface;
|
||||||
|
chartOverview: BatteryOverviewInterface;
|
||||||
|
}> = transformInputToBatteryViewDataJson(
|
||||||
|
props.s3Credentials,
|
||||||
|
props.id,
|
||||||
|
product,
|
||||||
|
UnixTime.fromTicks(startX).earlier(TimeSpan.fromHours(2)),
|
||||||
|
UnixTime.fromTicks(endX).earlier(TimeSpan.fromHours(2))
|
||||||
|
);
|
||||||
|
|
||||||
|
resultPromise
|
||||||
|
.then((result) => {
|
||||||
|
setBatteryViewDataArray((prevData) =>
|
||||||
|
prevData.concat({
|
||||||
|
chartData: result.chartData,
|
||||||
|
chartOverview: result.chartOverview
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
setIsZooming(false);
|
||||||
|
setChartState(batteryViewDataArray.length);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{loading && (
|
||||||
|
<Container
|
||||||
|
maxWidth="xl"
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
height: '100vh'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CircularProgress size={60} style={{ color: '#ffc04d' }} />
|
||||||
|
<Typography variant="body2" style={{ color: 'black' }} mt={2}>
|
||||||
|
Fetching data...
|
||||||
|
</Typography>
|
||||||
|
</Container>
|
||||||
|
)}
|
||||||
|
{isErrorDateModalOpen && (
|
||||||
|
<Modal open={isErrorDateModalOpen} onClose={() => {}}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
width: 450,
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
borderRadius: 4,
|
||||||
|
boxShadow: 24,
|
||||||
|
p: 4,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
gutterBottom
|
||||||
|
sx={{ fontWeight: 'bold' }}
|
||||||
|
>
|
||||||
|
{dateSelectionError}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
sx={{
|
||||||
|
marginTop: 2,
|
||||||
|
textTransform: 'none',
|
||||||
|
bgcolor: '#ffc04d',
|
||||||
|
color: '#111111',
|
||||||
|
'&:hover': { bgcolor: '#f7b34d' }
|
||||||
|
}}
|
||||||
|
onClick={handleOkOnErrorDateModal}
|
||||||
|
>
|
||||||
|
Ok
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
{isDateModalOpen && (
|
||||||
|
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||||
|
<Modal open={isDateModalOpen} onClose={() => {}}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
width: 450,
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
borderRadius: 4,
|
||||||
|
boxShadow: 24,
|
||||||
|
p: 4,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DateTimePicker
|
||||||
|
label="Select Start Date"
|
||||||
|
value={startDate}
|
||||||
|
onChange={(newDate: Dayjs | null) => {
|
||||||
|
// Type assertion to Dayjs
|
||||||
|
if (newDate) {
|
||||||
|
setStartDate(newDate);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
renderInput={(props) => <TextField {...props} />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DateTimePicker
|
||||||
|
label="Select End Date"
|
||||||
|
value={endDate}
|
||||||
|
onChange={(newDate: Dayjs | null) => {
|
||||||
|
// Type assertion to Dayjs
|
||||||
|
if (newDate) {
|
||||||
|
setEndDate(newDate);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
renderInput={(props) => <TextField {...props} />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginTop: 10
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
sx={{
|
||||||
|
marginTop: 2,
|
||||||
|
textTransform: 'none',
|
||||||
|
bgcolor: '#ffc04d',
|
||||||
|
color: '#111111',
|
||||||
|
'&:hover': { bgcolor: '#f7b34d' }
|
||||||
|
}}
|
||||||
|
onClick={handleConfirm}
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
sx={{
|
||||||
|
marginTop: 2,
|
||||||
|
marginLeft: 2,
|
||||||
|
textTransform: 'none',
|
||||||
|
bgcolor: '#ffc04d',
|
||||||
|
color: '#111111',
|
||||||
|
'&:hover': { bgcolor: '#f7b34d' }
|
||||||
|
}}
|
||||||
|
onClick={handleCancel}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
</LocalizationProvider>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!loading && (
|
||||||
|
<>
|
||||||
|
<Grid item xs={6} md={6}>
|
||||||
|
<IconButton
|
||||||
|
aria-label="go back"
|
||||||
|
sx={{
|
||||||
|
marginTop: '20px',
|
||||||
|
backgroundColor: 'grey',
|
||||||
|
color: '#000000',
|
||||||
|
'&:hover': { bgcolor: '#f7b34d' }
|
||||||
|
}}
|
||||||
|
onClick={handleBatteryViewButton}
|
||||||
|
>
|
||||||
|
<ArrowBackIcon />
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={handleSetDate}
|
||||||
|
disabled={loading}
|
||||||
|
sx={{
|
||||||
|
marginTop: '20px',
|
||||||
|
marginLeft: '20px',
|
||||||
|
backgroundColor: dateOpen ? '#808080' : '#ffc04d',
|
||||||
|
color: '#000000',
|
||||||
|
'&:hover': { bgcolor: '#f7b34d' }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormattedMessage id="set_date" defaultMessage="Set Date" />
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
justifyContent="flex-end"
|
||||||
|
alignItems="center"
|
||||||
|
item
|
||||||
|
xs={6}
|
||||||
|
md={6}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
disabled={!(chartState > 0)}
|
||||||
|
onClick={handleGoBack}
|
||||||
|
sx={{
|
||||||
|
marginTop: '20px',
|
||||||
|
marginLeft: '10px',
|
||||||
|
backgroundColor: '#ffc04d',
|
||||||
|
color: '#000000',
|
||||||
|
'&:hover': { bgcolor: '#f7b34d' }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormattedMessage id="goback" defaultMessage="Zoom out" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
disabled={!(chartState < batteryViewDataArray.length - 1)}
|
||||||
|
onClick={handleGoForward}
|
||||||
|
sx={{
|
||||||
|
marginTop: '20px',
|
||||||
|
marginLeft: '10px',
|
||||||
|
backgroundColor: '#ffc04d',
|
||||||
|
color: '#000000',
|
||||||
|
'&:hover': { bgcolor: '#f7b34d' }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormattedMessage id="goback" defaultMessage="Zoom in" />
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
direction="row"
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="stretch"
|
||||||
|
spacing={3}
|
||||||
|
>
|
||||||
|
<Grid item md={12} xs={12}>
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
overflow: 'visible',
|
||||||
|
marginTop: '30px',
|
||||||
|
marginBottom: '30px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
marginLeft: '20px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box display="flex" alignItems="center">
|
||||||
|
<Box>
|
||||||
|
<Typography variant="subtitle1" noWrap>
|
||||||
|
<FormattedMessage
|
||||||
|
id="battery_soc"
|
||||||
|
defaultMessage="Battery SOC (State Of Charge)"
|
||||||
|
/>
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
pt: 3
|
||||||
|
}}
|
||||||
|
></Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<ReactApexChart
|
||||||
|
options={{
|
||||||
|
...getChartOptions(
|
||||||
|
batteryViewDataArray[chartState].chartOverview.Soc,
|
||||||
|
'daily',
|
||||||
|
[],
|
||||||
|
true
|
||||||
|
),
|
||||||
|
chart: {
|
||||||
|
events: {
|
||||||
|
beforeZoom: (chartContext, options) => {
|
||||||
|
startZoom();
|
||||||
|
handleBeforeZoom(chartContext, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
series={generateSeries(
|
||||||
|
batteryViewDataArray[chartState].chartData,
|
||||||
|
'Soc',
|
||||||
|
'blue'
|
||||||
|
)}
|
||||||
|
type="line"
|
||||||
|
height={420}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item md={12} xs={12}>
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
overflow: 'visible',
|
||||||
|
marginTop: '10px',
|
||||||
|
marginBottom: '30px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
marginLeft: '20px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box display="flex" alignItems="center">
|
||||||
|
<Box>
|
||||||
|
<Typography variant="subtitle1" noWrap>
|
||||||
|
<FormattedMessage
|
||||||
|
id="battery_soc"
|
||||||
|
defaultMessage="Battery Temperature"
|
||||||
|
/>
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
pt: 3
|
||||||
|
}}
|
||||||
|
></Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<ReactApexChart
|
||||||
|
options={{
|
||||||
|
...getChartOptions(
|
||||||
|
batteryViewDataArray[chartState].chartOverview
|
||||||
|
.Temperature,
|
||||||
|
'daily',
|
||||||
|
[],
|
||||||
|
true
|
||||||
|
),
|
||||||
|
chart: {
|
||||||
|
events: {
|
||||||
|
beforeZoom: (chartContext, options) => {
|
||||||
|
startZoom();
|
||||||
|
handleBeforeZoom(chartContext, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
series={generateSeries(
|
||||||
|
batteryViewDataArray[chartState].chartData,
|
||||||
|
'Temperature',
|
||||||
|
'blue'
|
||||||
|
)}
|
||||||
|
type="line"
|
||||||
|
height={420}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item md={12} xs={12}>
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
overflow: 'visible',
|
||||||
|
marginTop: '10px',
|
||||||
|
marginBottom: '30px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
marginLeft: '20px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box display="flex" alignItems="center">
|
||||||
|
<Box>
|
||||||
|
<Typography variant="subtitle1" noWrap>
|
||||||
|
<FormattedMessage
|
||||||
|
id="battery_power"
|
||||||
|
defaultMessage="Battery Power"
|
||||||
|
/>
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
pt: 3
|
||||||
|
}}
|
||||||
|
></Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<ReactApexChart
|
||||||
|
options={{
|
||||||
|
...getChartOptions(
|
||||||
|
batteryViewDataArray[chartState].chartOverview.Power,
|
||||||
|
'daily',
|
||||||
|
[],
|
||||||
|
true
|
||||||
|
),
|
||||||
|
chart: {
|
||||||
|
events: {
|
||||||
|
beforeZoom: (chartContext, options) => {
|
||||||
|
startZoom();
|
||||||
|
handleBeforeZoom(chartContext, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
series={generateSeries(
|
||||||
|
batteryViewDataArray[chartState].chartData,
|
||||||
|
'Power',
|
||||||
|
'red'
|
||||||
|
)}
|
||||||
|
type="line"
|
||||||
|
height={420}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item md={12} xs={12}>
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
overflow: 'visible',
|
||||||
|
marginTop: '10px',
|
||||||
|
marginBottom: '30px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
marginLeft: '20px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box display="flex" alignItems="center">
|
||||||
|
<Box>
|
||||||
|
<Typography variant="subtitle1" noWrap>
|
||||||
|
<FormattedMessage
|
||||||
|
id="battery_voltage"
|
||||||
|
defaultMessage="Battery Voltage"
|
||||||
|
/>
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
pt: 3
|
||||||
|
}}
|
||||||
|
></Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<ReactApexChart
|
||||||
|
options={{
|
||||||
|
...getChartOptions(
|
||||||
|
batteryViewDataArray[chartState].chartOverview.Voltage,
|
||||||
|
'daily',
|
||||||
|
[],
|
||||||
|
true
|
||||||
|
),
|
||||||
|
chart: {
|
||||||
|
events: {
|
||||||
|
beforeZoom: (chartContext, options) => {
|
||||||
|
startZoom();
|
||||||
|
handleBeforeZoom(chartContext, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
series={generateSeries(
|
||||||
|
batteryViewDataArray[chartState].chartData,
|
||||||
|
'Voltage',
|
||||||
|
'orange'
|
||||||
|
)}
|
||||||
|
type="line"
|
||||||
|
height={420}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item md={12} xs={12}>
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
overflow: 'visible',
|
||||||
|
marginTop: '10px',
|
||||||
|
marginBottom: '30px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
marginLeft: '20px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box display="flex" alignItems="center">
|
||||||
|
<Box>
|
||||||
|
<Typography variant="subtitle1" noWrap>
|
||||||
|
<FormattedMessage
|
||||||
|
id="battery_current"
|
||||||
|
defaultMessage="Battery Current"
|
||||||
|
/>
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
pt: 3
|
||||||
|
}}
|
||||||
|
></Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<ReactApexChart
|
||||||
|
options={{
|
||||||
|
...getChartOptions(
|
||||||
|
batteryViewDataArray[chartState].chartOverview.Current,
|
||||||
|
'daily',
|
||||||
|
[],
|
||||||
|
true
|
||||||
|
),
|
||||||
|
chart: {
|
||||||
|
events: {
|
||||||
|
beforeZoom: (chartContext, options) => {
|
||||||
|
startZoom();
|
||||||
|
handleBeforeZoom(chartContext, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
series={generateSeries(
|
||||||
|
batteryViewDataArray[chartState].chartData,
|
||||||
|
'Current',
|
||||||
|
'orange'
|
||||||
|
)}
|
||||||
|
type="line"
|
||||||
|
height={420}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MainStatsSalidomo;
|
|
@ -20,17 +20,16 @@ import { FormattedMessage } from 'react-intl';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import { Close as CloseIcon } from '@mui/icons-material';
|
import { Close as CloseIcon } from '@mui/icons-material';
|
||||||
import MenuItem from '@mui/material/MenuItem';
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
import {
|
import { LocalizationProvider } from '@mui/x-date-pickers';
|
||||||
DateTimePicker,
|
|
||||||
LocalizationProvider,
|
|
||||||
TimePicker
|
|
||||||
} from '@mui/x-date-pickers';
|
|
||||||
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import axiosConfig from '../../../Resources/axiosConfig';
|
import axiosConfig from '../../../Resources/axiosConfig';
|
||||||
import utc from 'dayjs/plugin/utc';
|
import utc from 'dayjs/plugin/utc';
|
||||||
import { UserContext } from '../../../contexts/userContext';
|
import { UserContext } from '../../../contexts/userContext';
|
||||||
|
|
||||||
|
import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker';
|
||||||
|
import { TimePicker } from '@mui/lab';
|
||||||
|
|
||||||
interface ConfigurationProps {
|
interface ConfigurationProps {
|
||||||
values: TopologyValues;
|
values: TopologyValues;
|
||||||
id: number;
|
id: number;
|
||||||
|
@ -352,15 +351,30 @@ function Configuration(props: ConfigurationProps) {
|
||||||
<div>
|
<div>
|
||||||
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
format="DD/MM/YYYY HH:mm"
|
|
||||||
ampm={false}
|
|
||||||
label="Select Next Calibration Charge Date"
|
label="Select Next Calibration Charge Date"
|
||||||
value={dayjs(formValues.calibrationChargeDate)}
|
value={dayjs(formValues.calibrationChargeDate)}
|
||||||
onChange={handleConfirm}
|
onChange={handleConfirm}
|
||||||
sx={{
|
renderInput={(params) => (
|
||||||
marginTop: 2
|
<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>
|
</LocalizationProvider>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -405,6 +419,15 @@ function Configuration(props: ConfigurationProps) {
|
||||||
label="Calibration Charge Hour"
|
label="Calibration Charge Hour"
|
||||||
value={dayjs(formValues.calibrationChargeDate)}
|
value={dayjs(formValues.calibrationChargeDate)}
|
||||||
onChange={(newTime) => handleConfirm(dayjs(newTime))}
|
onChange={(newTime) => handleConfirm(dayjs(newTime))}
|
||||||
|
renderInput={(params) => (
|
||||||
|
<TextField
|
||||||
|
{...params}
|
||||||
|
sx={{
|
||||||
|
marginTop: 2, // Apply styles here
|
||||||
|
width: '100%' // Optional: You can adjust the width or other styling here
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</LocalizationProvider>
|
</LocalizationProvider>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -25,10 +25,15 @@ import Button from '@mui/material/Button';
|
||||||
import { DateTimePicker, LocalizationProvider } from '@mui/x-date-pickers';
|
import { DateTimePicker, LocalizationProvider } from '@mui/x-date-pickers';
|
||||||
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import utc from 'dayjs/plugin/utc';
|
||||||
|
import timezone from 'dayjs/plugin/timezone';
|
||||||
import EditIcon from '@mui/icons-material/Edit';
|
import EditIcon from '@mui/icons-material/Edit';
|
||||||
import DeleteIcon from '@mui/icons-material/Delete';
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
import { UserContext } from '../../../contexts/userContext';
|
import { UserContext } from '../../../contexts/userContext';
|
||||||
|
|
||||||
|
dayjs.extend(utc);
|
||||||
|
dayjs.extend(timezone);
|
||||||
|
|
||||||
interface HistoryProps {
|
interface HistoryProps {
|
||||||
errorLoadingS3Data: boolean;
|
errorLoadingS3Data: boolean;
|
||||||
id: number;
|
id: number;
|
||||||
|
@ -180,28 +185,30 @@ function HistoryOfActions(props: HistoryProps) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<DateTimePicker
|
|
||||||
label="Select Action Date"
|
|
||||||
name="timestamp"
|
|
||||||
value={editMode ? dayjs(newAction.timestamp) : actionDate}
|
|
||||||
onChange={(newDate) => handleDateChange(newDate)}
|
|
||||||
sx={{
|
|
||||||
width: 450,
|
|
||||||
marginTop: 2
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{/*<DateTimePicker*/}
|
{/*<DateTimePicker*/}
|
||||||
{/* label="Select Action Date"*/}
|
{/* label="Select Action Date"*/}
|
||||||
|
{/* name="timestamp"*/}
|
||||||
{/* value={actionDate}*/}
|
{/* value={actionDate}*/}
|
||||||
{/* onChange={handleDateChange}*/}
|
{/* onChange={(newDate: Dayjs | null) =>*/}
|
||||||
{/* renderInput={(params) => (*/}
|
{/* handleDateChange(newDate)*/}
|
||||||
{/* <TextField*/}
|
{/* }*/}
|
||||||
{/* {...params}*/}
|
{/* InputProps={{*/}
|
||||||
{/* sx={{ width: 450, marginTop: 2 }}*/}
|
{/* sx: {*/}
|
||||||
{/* />*/}
|
{/* width: 450,*/}
|
||||||
{/* )}*/}
|
{/* marginTop: 2*/}
|
||||||
|
{/* }*/}
|
||||||
|
{/* }}*/}
|
||||||
{/*/>*/}
|
{/*/>*/}
|
||||||
|
|
||||||
|
<DateTimePicker
|
||||||
|
label="Select Action Date"
|
||||||
|
value={actionDate}
|
||||||
|
onChange={handleDateChange}
|
||||||
|
renderInput={(params) => (
|
||||||
|
<TextField {...params} sx={{ width: 450, marginTop: 2 }} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
label="Description"
|
label="Description"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
|
@ -411,14 +418,18 @@ function HistoryOfActions(props: HistoryProps) {
|
||||||
<Divider />
|
<Divider />
|
||||||
<div style={{ maxHeight: '600px', overflowY: 'auto' }}>
|
<div style={{ maxHeight: '600px', overflowY: 'auto' }}>
|
||||||
{history.map((action, index) => {
|
{history.map((action, index) => {
|
||||||
// Parse the timestamp string to a Date object
|
// // Parse the timestamp string to a Date object
|
||||||
const date = new Date(action.timestamp);
|
// const date = new Date(action.timestamp);
|
||||||
|
//
|
||||||
|
// // Extract the date part (e.g., "2023-05-31")
|
||||||
|
// const datePart = date.toLocaleDateString();
|
||||||
|
//
|
||||||
|
// // Extract the time part (e.g., "12:34:56")
|
||||||
|
// const timePart = date.toLocaleTimeString();
|
||||||
|
const dateCET = dayjs.utc(action.timestamp).tz("Europe/Paris");
|
||||||
|
const datePart = dateCET.format("YYYY-MM-DD");
|
||||||
|
const timePart = dateCET.format("HH:mm:ss");
|
||||||
|
|
||||||
// Extract the date part (e.g., "2023-05-31")
|
|
||||||
const datePart = date.toLocaleDateString();
|
|
||||||
|
|
||||||
// Extract the time part (e.g., "12:34:56")
|
|
||||||
const timePart = date.toLocaleTimeString();
|
|
||||||
const iconStyle =
|
const iconStyle =
|
||||||
action.userName === currentUser.name
|
action.userName === currentUser.name
|
||||||
? {}
|
? {}
|
||||||
|
|
|
@ -27,10 +27,10 @@ import BuildIcon from '@mui/icons-material/Build';
|
||||||
import { Navigate, Route, Routes, useLocation } from 'react-router-dom';
|
import { Navigate, Route, Routes, useLocation } from 'react-router-dom';
|
||||||
import routes from '../../../Resources/routes.json';
|
import routes from '../../../Resources/routes.json';
|
||||||
import Information from '../Information/Information';
|
import Information from '../Information/Information';
|
||||||
import BatteryView from '../BatteryView/BatteryView';
|
|
||||||
import { UserType } from '../../../interfaces/UserTypes';
|
import { UserType } from '../../../interfaces/UserTypes';
|
||||||
import HistoryOfActions from '../History/History';
|
import HistoryOfActions from '../History/History';
|
||||||
import PvView from '../PvView/PvView';
|
import PvView from '../PvView/PvView';
|
||||||
|
import BatteryView from '../BatteryView/BatteryView';
|
||||||
|
|
||||||
interface singleInstallationProps {
|
interface singleInstallationProps {
|
||||||
current_installation?: I_Installation;
|
current_installation?: I_Installation;
|
||||||
|
@ -38,6 +38,9 @@ interface singleInstallationProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
function Installation(props: singleInstallationProps) {
|
function Installation(props: singleInstallationProps) {
|
||||||
|
if (props.current_installation == undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const context = useContext(UserContext);
|
const context = useContext(UserContext);
|
||||||
const { currentUser } = context;
|
const { currentUser } = context;
|
||||||
const location = useLocation().pathname;
|
const location = useLocation().pathname;
|
||||||
|
@ -49,10 +52,6 @@ function Installation(props: singleInstallationProps) {
|
||||||
const [connected, setConnected] = useState(true);
|
const [connected, setConnected] = useState(true);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
if (props.current_installation == undefined) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const S3data = {
|
const S3data = {
|
||||||
s3Region: props.current_installation.s3Region,
|
s3Region: props.current_installation.s3Region,
|
||||||
s3Provider: props.current_installation.s3Provider,
|
s3Provider: props.current_installation.s3Provider,
|
||||||
|
@ -62,11 +61,8 @@ function Installation(props: singleInstallationProps) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const s3Bucket =
|
const s3Bucket =
|
||||||
props.current_installation.product === 0
|
props.current_installation.s3BucketId.toString() +
|
||||||
? props.current_installation.s3BucketId.toString() +
|
'-3e5b3069-214a-43ee-8d85-57d72000c19d';
|
||||||
'-3e5b3069-214a-43ee-8d85-57d72000c19d'
|
|
||||||
: props.current_installation.s3BucketId.toString() +
|
|
||||||
'-c0436b6a-d276-4cd8-9c44-1eae86cf5d0e';
|
|
||||||
|
|
||||||
const s3Credentials = { s3Bucket, ...S3data };
|
const s3Credentials = { s3Bucket, ...S3data };
|
||||||
|
|
||||||
|
@ -74,6 +70,8 @@ function Installation(props: singleInstallationProps) {
|
||||||
return new Promise((res) => setTimeout(res, delay));
|
return new Promise((res) => setTimeout(res, delay));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//In React, useRef creates a mutable object that persists across renders without triggering re-renders when its value changes.
|
||||||
|
//While fetching, we check the value of continueFetching, if its false, we break. This means that either the user changed tab or the object has been finished its execution (return)
|
||||||
const continueFetching = useRef(false);
|
const continueFetching = useRef(false);
|
||||||
|
|
||||||
const fetchDataForOneTime = async () => {
|
const fetchDataForOneTime = async () => {
|
||||||
|
@ -193,6 +191,7 @@ function Installation(props: singleInstallationProps) {
|
||||||
setCurrentTab(path[path.length - 1]);
|
setCurrentTab(path[path.length - 1]);
|
||||||
}, [location]);
|
}, [location]);
|
||||||
|
|
||||||
|
//If status is -1, the topology will be empty and "Unable to communicate with the installation" should be rendered from the Topology component
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (status === -1) {
|
if (status === -1) {
|
||||||
setConnected(false);
|
setConnected(false);
|
||||||
|
@ -206,19 +205,22 @@ function Installation(props: singleInstallationProps) {
|
||||||
currentTab == 'configuration' ||
|
currentTab == 'configuration' ||
|
||||||
location.includes('batteryview')
|
location.includes('batteryview')
|
||||||
) {
|
) {
|
||||||
|
//Fetch periodically if the tab is live, pvview or batteryview
|
||||||
if (
|
if (
|
||||||
currentTab == 'live' ||
|
currentTab == 'live' ||
|
||||||
(location.includes('batteryview') && !location.includes('mainstats')) ||
|
currentTab == 'pvview' ||
|
||||||
currentTab == 'pvview'
|
(location.includes('batteryview') && !location.includes('mainstats'))
|
||||||
) {
|
) {
|
||||||
if (!continueFetching.current) {
|
if (!continueFetching.current) {
|
||||||
continueFetching.current = true;
|
continueFetching.current = true;
|
||||||
|
//Call the function only one time. When the location and the currentTab change, this useEffect will be called 2 times
|
||||||
if (!fetchFunctionCalled) {
|
if (!fetchFunctionCalled) {
|
||||||
setFetchFunctionCalled(true);
|
setFetchFunctionCalled(true);
|
||||||
fetchDataPeriodically();
|
fetchDataPeriodically();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//Fetch only one time in configuration tab
|
||||||
if (currentTab == 'configuration') {
|
if (currentTab == 'configuration') {
|
||||||
fetchDataForOneTime();
|
fetchDataForOneTime();
|
||||||
}
|
}
|
||||||
|
@ -227,6 +229,7 @@ function Installation(props: singleInstallationProps) {
|
||||||
continueFetching.current = false;
|
continueFetching.current = false;
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
|
//If the tab is not live, pvview, batteryview or configuration, then stop fetching.
|
||||||
continueFetching.current = false;
|
continueFetching.current = false;
|
||||||
}
|
}
|
||||||
}, [currentTab, location]);
|
}, [currentTab, location]);
|
||||||
|
|
|
@ -3,9 +3,171 @@ import { I_S3Credentials } from 'src/interfaces/S3Types';
|
||||||
import { FetchResult } from 'src/dataCache/dataCache';
|
import { FetchResult } from 'src/dataCache/dataCache';
|
||||||
import { DataRecord } from 'src/dataCache/data';
|
import { DataRecord } from 'src/dataCache/data';
|
||||||
import { S3Access } from 'src/dataCache/S3/S3Access';
|
import { S3Access } from 'src/dataCache/S3/S3Access';
|
||||||
import { parseChunk, parseCsv } from '../Log/graph.util';
|
import {
|
||||||
|
JSONRecordData,
|
||||||
|
parseChunk,
|
||||||
|
parseChunkJson,
|
||||||
|
parseCsv
|
||||||
|
} from '../Log/graph.util';
|
||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
|
|
||||||
|
export const fetchDataJson = (
|
||||||
|
timestamp: UnixTime,
|
||||||
|
s3Credentials?: I_S3Credentials,
|
||||||
|
cutdigits?: boolean
|
||||||
|
): Promise<FetchResult<Record<string, JSONRecordData>>> => {
|
||||||
|
const s3Path = cutdigits
|
||||||
|
? `${timestamp.ticks.toString().slice(0, -2)}.json`
|
||||||
|
: `${timestamp.ticks}.json`;
|
||||||
|
if (s3Credentials && s3Credentials.s3Bucket) {
|
||||||
|
const s3Access = new S3Access(
|
||||||
|
s3Credentials.s3Bucket,
|
||||||
|
s3Credentials.s3Region,
|
||||||
|
s3Credentials.s3Provider,
|
||||||
|
s3Credentials.s3Key,
|
||||||
|
s3Credentials.s3Secret
|
||||||
|
);
|
||||||
|
|
||||||
|
return s3Access
|
||||||
|
.get(s3Path)
|
||||||
|
.then(async (r) => {
|
||||||
|
if (r.status === 404) {
|
||||||
|
return Promise.resolve(FetchResult.notAvailable);
|
||||||
|
} else if (r.status === 200) {
|
||||||
|
console.log('FOUND ITTTTTTTTTTTT');
|
||||||
|
const jsontext = await r.text(); // Assuming the server returns the Base64 encoded ZIP file as text
|
||||||
|
const byteArray = Uint8Array.from(atob(jsontext), (c) =>
|
||||||
|
c.charCodeAt(0)
|
||||||
|
);
|
||||||
|
|
||||||
|
//Decompress the byte array using JSZip
|
||||||
|
const zip = await JSZip.loadAsync(byteArray);
|
||||||
|
// Assuming the Json file is named "data.json" inside the ZIP archive
|
||||||
|
const jsonContent = await zip.file('data.json').async('text');
|
||||||
|
//console.log(jsonContent);
|
||||||
|
return parseChunkJson(jsonContent);
|
||||||
|
} else {
|
||||||
|
return Promise.resolve(FetchResult.notAvailable);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
return Promise.resolve(FetchResult.tryLater);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchAggregatedDataJson = (
|
||||||
|
date: string,
|
||||||
|
s3Credentials?: I_S3Credentials
|
||||||
|
): Promise<FetchResult<any>> => {
|
||||||
|
const s3Path = `${date}.json`;
|
||||||
|
if (s3Credentials && s3Credentials.s3Bucket) {
|
||||||
|
const s3Access = new S3Access(
|
||||||
|
s3Credentials.s3Bucket,
|
||||||
|
s3Credentials.s3Region,
|
||||||
|
s3Credentials.s3Provider,
|
||||||
|
s3Credentials.s3Key,
|
||||||
|
s3Credentials.s3Secret
|
||||||
|
);
|
||||||
|
return s3Access
|
||||||
|
.get(s3Path)
|
||||||
|
.then(async (r) => {
|
||||||
|
if (r.status === 404) {
|
||||||
|
return Promise.resolve(FetchResult.notAvailable);
|
||||||
|
} else if (r.status === 200) {
|
||||||
|
const jsontext = await r.text(); // Assuming the server returns the Base64 encoded ZIP file as text
|
||||||
|
const contentEncoding = r.headers.get('content-type');
|
||||||
|
|
||||||
|
if (contentEncoding != 'application/base64; charset=utf-8') {
|
||||||
|
return JSON.parse(jsontext);
|
||||||
|
}
|
||||||
|
const byteArray = Uint8Array.from(atob(jsontext), (c) =>
|
||||||
|
c.charCodeAt(0)
|
||||||
|
);
|
||||||
|
|
||||||
|
//Decompress the byte array using JSZip
|
||||||
|
const zip = await JSZip.loadAsync(byteArray);
|
||||||
|
// Assuming the CSV file is named "data.csv" inside the ZIP archive
|
||||||
|
const jsonContent = await zip.file('data.json').async('text');
|
||||||
|
// console.log(jsonContent);
|
||||||
|
return JSON.parse(jsonContent);
|
||||||
|
} else {
|
||||||
|
return Promise.resolve(FetchResult.notAvailable);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
return Promise.resolve(FetchResult.tryLater);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//--------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||||
|
//--------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||||
|
//--------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||||
|
//--------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||||
|
//--------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||||
|
// For CSV manipulation, check the following functions
|
||||||
|
//--------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||||
|
//--------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||||
|
//--------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||||
|
//--------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export const fetchData = (
|
||||||
|
timestamp: UnixTime,
|
||||||
|
s3Credentials?: I_S3Credentials,
|
||||||
|
cutdigits?: boolean
|
||||||
|
): Promise<FetchResult<Record<string, DataRecord>>> => {
|
||||||
|
const s3Path = cutdigits
|
||||||
|
? `${timestamp.ticks.toString().slice(0, -2)}.csv`
|
||||||
|
: `${timestamp.ticks}.csv`;
|
||||||
|
if (s3Credentials && s3Credentials.s3Bucket) {
|
||||||
|
const s3Access = new S3Access(
|
||||||
|
s3Credentials.s3Bucket,
|
||||||
|
s3Credentials.s3Region,
|
||||||
|
s3Credentials.s3Provider,
|
||||||
|
s3Credentials.s3Key,
|
||||||
|
s3Credentials.s3Secret
|
||||||
|
);
|
||||||
|
|
||||||
|
return s3Access
|
||||||
|
.get(s3Path)
|
||||||
|
.then(async (r) => {
|
||||||
|
if (r.status === 404) {
|
||||||
|
return Promise.resolve(FetchResult.notAvailable);
|
||||||
|
} else if (r.status === 200) {
|
||||||
|
console.log('FOUND ITTTTTTTTTTTT');
|
||||||
|
const csvtext = await r.text(); // Assuming the server returns the Base64 encoded ZIP file as text
|
||||||
|
const contentEncoding = r.headers.get('content-type');
|
||||||
|
|
||||||
|
//console.log(contentEncoding);
|
||||||
|
|
||||||
|
if (contentEncoding != 'application/base64; charset=utf-8') {
|
||||||
|
// console.log('uncompressed');
|
||||||
|
return parseChunk(csvtext);
|
||||||
|
}
|
||||||
|
|
||||||
|
const byteArray = Uint8Array.from(atob(csvtext), (c) =>
|
||||||
|
c.charCodeAt(0)
|
||||||
|
);
|
||||||
|
|
||||||
|
//Decompress the byte array using JSZip
|
||||||
|
const zip = await JSZip.loadAsync(byteArray);
|
||||||
|
// Assuming the CSV file is named "data.csv" inside the ZIP archive
|
||||||
|
const csvContent = await zip.file('data.csv').async('text');
|
||||||
|
|
||||||
|
//console.log(csvContent);
|
||||||
|
|
||||||
|
return parseChunk(csvContent);
|
||||||
|
} else {
|
||||||
|
return Promise.resolve(FetchResult.notAvailable);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
return Promise.resolve(FetchResult.tryLater);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const fetchAggregatedData = (
|
export const fetchAggregatedData = (
|
||||||
date: string,
|
date: string,
|
||||||
s3Credentials?: I_S3Credentials
|
s3Credentials?: I_S3Credentials
|
||||||
|
@ -50,59 +212,3 @@ export const fetchAggregatedData = (
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchData = (
|
|
||||||
timestamp: UnixTime,
|
|
||||||
s3Credentials?: I_S3Credentials,
|
|
||||||
cutdigits?: boolean
|
|
||||||
): Promise<FetchResult<Record<string, DataRecord>>> => {
|
|
||||||
const s3Path = cutdigits
|
|
||||||
? `${timestamp.ticks.toString().slice(0, -2)}.csv`
|
|
||||||
: `${timestamp.ticks}.csv`;
|
|
||||||
if (s3Credentials && s3Credentials.s3Bucket) {
|
|
||||||
const s3Access = new S3Access(
|
|
||||||
s3Credentials.s3Bucket,
|
|
||||||
s3Credentials.s3Region,
|
|
||||||
s3Credentials.s3Provider,
|
|
||||||
s3Credentials.s3Key,
|
|
||||||
s3Credentials.s3Secret
|
|
||||||
);
|
|
||||||
|
|
||||||
return s3Access
|
|
||||||
.get(s3Path)
|
|
||||||
.then(async (r) => {
|
|
||||||
if (r.status === 404) {
|
|
||||||
return Promise.resolve(FetchResult.notAvailable);
|
|
||||||
} else if (r.status === 200) {
|
|
||||||
//console.log('FOUND ITTTTTTTTTTTT');
|
|
||||||
const csvtext = await r.text(); // Assuming the server returns the Base64 encoded ZIP file as text
|
|
||||||
const contentEncoding = r.headers.get('content-type');
|
|
||||||
|
|
||||||
//console.log(contentEncoding);
|
|
||||||
|
|
||||||
if (contentEncoding != 'application/base64; charset=utf-8') {
|
|
||||||
// console.log('uncompressed');
|
|
||||||
return parseChunk(csvtext);
|
|
||||||
}
|
|
||||||
|
|
||||||
const byteArray = Uint8Array.from(atob(csvtext), (c) =>
|
|
||||||
c.charCodeAt(0)
|
|
||||||
);
|
|
||||||
|
|
||||||
//Decompress the byte array using JSZip
|
|
||||||
const zip = await JSZip.loadAsync(byteArray);
|
|
||||||
// Assuming the CSV file is named "data.csv" inside the ZIP archive
|
|
||||||
const csvContent = await zip.file('data.csv').async('text');
|
|
||||||
|
|
||||||
//console.log(csvContent);
|
|
||||||
|
|
||||||
return parseChunk(csvContent);
|
|
||||||
} else {
|
|
||||||
return Promise.resolve(FetchResult.notAvailable);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
return Promise.resolve(FetchResult.tryLater);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
|
@ -50,21 +50,11 @@ function InstallationTabs() {
|
||||||
} else if (path[path.length - 2] === 'tree') {
|
} else if (path[path.length - 2] === 'tree') {
|
||||||
setCurrentTab('tree');
|
setCurrentTab('tree');
|
||||||
} else {
|
} else {
|
||||||
//Even if we are located at path: /batteryview/mainstats, we want the BatteryView tab to be bold
|
//Even if we are located at path: /batteryview/mainstats, we want the BatteryViewSalidomo tab to be bold
|
||||||
setCurrentTab(path.find((pathElement) => tabList.includes(pathElement)));
|
setCurrentTab(path.find((pathElement) => tabList.includes(pathElement)));
|
||||||
}
|
}
|
||||||
}, [location]);
|
}, [location]);
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// if (salimaxInstallations && salimaxInstallations.length > 0) {
|
|
||||||
// if (socket) {
|
|
||||||
// closeSocket();
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// openSocket(salimaxInstallations);
|
|
||||||
// }
|
|
||||||
// }, [salimaxInstallations]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (salimaxInstallations.length === 0) {
|
if (salimaxInstallations.length === 0) {
|
||||||
fetchAllInstallations();
|
fetchAllInstallations();
|
||||||
|
@ -75,7 +65,7 @@ function InstallationTabs() {
|
||||||
if (salimaxInstallations && salimaxInstallations.length > 0) {
|
if (salimaxInstallations && salimaxInstallations.length > 0) {
|
||||||
if (!socket) {
|
if (!socket) {
|
||||||
openSocket(0);
|
openSocket(0);
|
||||||
} else if (currentProduct == 1) {
|
} else if (currentProduct != 0) {
|
||||||
closeSocket();
|
closeSocket();
|
||||||
openSocket(0);
|
openSocket(0);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,112 @@
|
||||||
|
// The interface for each device in the Battery object
|
||||||
|
interface Leds {
|
||||||
|
Blue: { value: string };
|
||||||
|
Amber: { value: string };
|
||||||
|
Green: { value: string };
|
||||||
|
Red: { value: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Dc {
|
||||||
|
Current: { value: number };
|
||||||
|
Voltage: { value: number };
|
||||||
|
Power: { value: number };
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Temperatures {
|
||||||
|
Cells: {
|
||||||
|
Average: { value: number };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IoStatus {
|
||||||
|
ConnectedToDcBus: { value: boolean };
|
||||||
|
AuxRelayBus: { value: boolean };
|
||||||
|
AlarmOutActive: { value: boolean };
|
||||||
|
InternalFanActive: { value: boolean };
|
||||||
|
RemoteStateActive: { value: boolean };
|
||||||
|
VoltMeasurementAllowed: { value: boolean };
|
||||||
|
RiscActive: { value: boolean };
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BatteryStrings {
|
||||||
|
String1Active: { value: string };
|
||||||
|
String2Active: { value: string };
|
||||||
|
String3Active: { value: string };
|
||||||
|
String4Active: { value: string };
|
||||||
|
String5Active: { value: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Device {
|
||||||
|
Leds: Leds;
|
||||||
|
Eoc: { value: boolean };
|
||||||
|
Soc: { value: number };
|
||||||
|
SerialNumber: { value: string };
|
||||||
|
TimeSinceTOC: { value: string };
|
||||||
|
MaxChargePower: { value: number };
|
||||||
|
CellsCurrent: { value: number };
|
||||||
|
SOCAh: { value: number };
|
||||||
|
Dc: Dc;
|
||||||
|
FwVersion: { value: string };
|
||||||
|
HeatingCurrent: { value: number };
|
||||||
|
MaxDischargePower: { value: number };
|
||||||
|
Temperatures: Temperatures;
|
||||||
|
BusCurrent: { value: number };
|
||||||
|
HeatingPower: { value: number };
|
||||||
|
IoStatus: IoStatus;
|
||||||
|
BatteryStrings: BatteryStrings;
|
||||||
|
Alarms: number;
|
||||||
|
Warnings: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The interface for the Battery structure, with dynamic keys (Device IDs)
|
||||||
|
export interface JSONRecordData {
|
||||||
|
Battery: {
|
||||||
|
Devices: {
|
||||||
|
[deviceId: string]: Device; // Device ID as the key
|
||||||
|
};
|
||||||
|
};
|
||||||
|
Config: {
|
||||||
|
Devices: {
|
||||||
|
BatteryNodes: {
|
||||||
|
value: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const parseChunkJson = (
|
||||||
|
text: string
|
||||||
|
): Record<string, JSONRecordData> => {
|
||||||
|
const lines = text.split(/\r?\n/).filter((line) => line.length > 0);
|
||||||
|
|
||||||
|
let result: Record<string, any> = {};
|
||||||
|
let currentTimestamp = null;
|
||||||
|
|
||||||
|
lines.forEach((line) => {
|
||||||
|
//console.log(line);
|
||||||
|
|
||||||
|
const fields = line.split(';');
|
||||||
|
if (fields[0] === 'Timestamp') {
|
||||||
|
currentTimestamp = fields[1];
|
||||||
|
result[currentTimestamp] = {};
|
||||||
|
} else if (currentTimestamp) {
|
||||||
|
result[currentTimestamp] = JSON.parse(line);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
//--------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||||
|
//--------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||||
|
//--------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||||
|
//--------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||||
|
//--------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||||
|
// For CSV manipulation, check the following functions
|
||||||
|
//--------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||||
|
//--------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||||
|
//--------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||||
|
//--------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
import { DataPoint, DataRecord } from 'src/dataCache/data';
|
import { DataPoint, DataRecord } from 'src/dataCache/data';
|
||||||
|
|
||||||
export interface I_CsvEntry {
|
export interface I_CsvEntry {
|
||||||
|
|
|
@ -1,4 +1,12 @@
|
||||||
import { Box, Card, Container, Grid, Modal, Typography } from '@mui/material';
|
import {
|
||||||
|
Box,
|
||||||
|
Card,
|
||||||
|
Container,
|
||||||
|
Grid,
|
||||||
|
Modal,
|
||||||
|
TextField,
|
||||||
|
Typography
|
||||||
|
} from '@mui/material';
|
||||||
import ReactApexChart from 'react-apexcharts';
|
import ReactApexChart from 'react-apexcharts';
|
||||||
import React, { useContext, useEffect, useState } from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
import { I_S3Credentials } from 'src/interfaces/S3Types';
|
import { I_S3Credentials } from 'src/interfaces/S3Types';
|
||||||
|
@ -13,12 +21,13 @@ import {
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import CircularProgress from '@mui/material/CircularProgress';
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
import { DateTimePicker, LocalizationProvider } from '@mui/x-date-pickers';
|
import { LocalizationProvider } from '@mui/x-date-pickers';
|
||||||
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { UserContext } from '../../../contexts/userContext';
|
import { UserContext } from '../../../contexts/userContext';
|
||||||
import { UserType } from '../../../interfaces/UserTypes';
|
import { UserType } from '../../../interfaces/UserTypes';
|
||||||
import { TimeSpan, UnixTime } from '../../../dataCache/time';
|
import { TimeSpan, UnixTime } from '../../../dataCache/time';
|
||||||
|
import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker';
|
||||||
|
|
||||||
interface OverviewProps {
|
interface OverviewProps {
|
||||||
s3Credentials: I_S3Credentials;
|
s3Credentials: I_S3Credentials;
|
||||||
|
@ -73,6 +82,13 @@ function Overview(props: OverviewProps) {
|
||||||
const [endDate, setEndDate] = useState(dayjs());
|
const [endDate, setEndDate] = useState(dayjs());
|
||||||
const [isZooming, setIsZooming] = useState(false);
|
const [isZooming, setIsZooming] = useState(false);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
UnixTime.fromTicks(new Date().getTime() / 1000).earlier(
|
||||||
|
TimeSpan.fromDays(1)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(UnixTime.fromTicks(new Date().getTime() / 1000));
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isZooming) {
|
if (isZooming) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
@ -408,18 +424,30 @@ function Overview(props: OverviewProps) {
|
||||||
label="Select Start Date"
|
label="Select Start Date"
|
||||||
value={startDate}
|
value={startDate}
|
||||||
onChange={(newDate) => setStartDate(newDate)}
|
onChange={(newDate) => setStartDate(newDate)}
|
||||||
sx={{
|
renderInput={(params) => (
|
||||||
marginTop: 2
|
<TextField
|
||||||
}}
|
{...params}
|
||||||
|
sx={{
|
||||||
|
marginTop: 2, // Apply styles here
|
||||||
|
width: '100%' // Optional: You can adjust the width or other styling here
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
label="Select End Date"
|
label="Select End Date"
|
||||||
value={endDate}
|
value={endDate}
|
||||||
onChange={(newDate) => setEndDate(newDate)}
|
onChange={(newDate) => setEndDate(newDate)}
|
||||||
sx={{
|
renderInput={(params) => (
|
||||||
marginTop: 2
|
<TextField
|
||||||
}}
|
{...params}
|
||||||
|
sx={{
|
||||||
|
marginTop: 2, // Apply styles here
|
||||||
|
width: '100%' // Optional: You can adjust the width or other styling here
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -1,4 +1,12 @@
|
||||||
import { Box, Card, Container, Grid, Modal, Typography } from '@mui/material';
|
import {
|
||||||
|
Box,
|
||||||
|
Card,
|
||||||
|
Container,
|
||||||
|
Grid,
|
||||||
|
Modal,
|
||||||
|
TextField,
|
||||||
|
Typography
|
||||||
|
} from '@mui/material';
|
||||||
import ReactApexChart from 'react-apexcharts';
|
import ReactApexChart from 'react-apexcharts';
|
||||||
import React, { useContext, useEffect, useState } from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
import { I_S3Credentials } from 'src/interfaces/S3Types';
|
import { I_S3Credentials } from 'src/interfaces/S3Types';
|
||||||
|
@ -6,14 +14,14 @@ import { getChartOptions } from './chartOptions';
|
||||||
import {
|
import {
|
||||||
chartAggregatedDataInterface,
|
chartAggregatedDataInterface,
|
||||||
overviewInterface,
|
overviewInterface,
|
||||||
transformInputToAggregatedData
|
transformInputToAggregatedDataJson
|
||||||
} from 'src/interfaces/Chart';
|
} from 'src/interfaces/Chart';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import CircularProgress from '@mui/material/CircularProgress';
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
import { DateTimePicker, LocalizationProvider } from '@mui/x-date-pickers';
|
import { DateTimePicker, LocalizationProvider } from '@mui/x-date-pickers';
|
||||||
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
||||||
import dayjs from 'dayjs';
|
import dayjs, { Dayjs } from 'dayjs';
|
||||||
import { UserContext } from '../../../contexts/userContext';
|
import { UserContext } from '../../../contexts/userContext';
|
||||||
import { UserType } from '../../../interfaces/UserTypes';
|
import { UserType } from '../../../interfaces/UserTypes';
|
||||||
|
|
||||||
|
@ -37,6 +45,546 @@ const computeLast7Days = (): string[] => {
|
||||||
}
|
}
|
||||||
return last7Days;
|
return last7Days;
|
||||||
};
|
};
|
||||||
|
//
|
||||||
|
// function SalidomoOverview(props: salidomoOverviewProps) {
|
||||||
|
// const context = useContext(UserContext);
|
||||||
|
// const { currentUser } = context;
|
||||||
|
// const [loading, setLoading] = useState(true);
|
||||||
|
// const [aggregatedChartState, setAggregatedChartState] = useState(0);
|
||||||
|
// const [isDateModalOpen, setIsDateModalOpen] = useState(false);
|
||||||
|
// const [isErrorDateModalOpen, setErrorDateModalOpen] = useState(false);
|
||||||
|
// const [dateSelectionError, setDateSelectionError] = useState('');
|
||||||
|
// const [dateOpen, setDateOpen] = useState(false);
|
||||||
|
//
|
||||||
|
// const [aggregatedDataArray, setAggregatedDataArray] = useState<
|
||||||
|
// {
|
||||||
|
// chartData: chartAggregatedDataInterface;
|
||||||
|
// chartOverview: overviewInterface;
|
||||||
|
// datelist: any[];
|
||||||
|
// netbalance: any[];
|
||||||
|
// }[]
|
||||||
|
// >([]);
|
||||||
|
//
|
||||||
|
// const [startDate, setStartDate] = useState(dayjs().add(-1, 'day'));
|
||||||
|
// const [endDate, setEndDate] = useState(dayjs());
|
||||||
|
//
|
||||||
|
// useEffect(() => {
|
||||||
|
// handleWeekData();
|
||||||
|
// }, []);
|
||||||
|
//
|
||||||
|
// const handleWeekData = () => {
|
||||||
|
// setAggregatedChartState(0);
|
||||||
|
//
|
||||||
|
// if (
|
||||||
|
// aggregatedDataArray[aggregatedChartState] &&
|
||||||
|
// aggregatedDataArray[aggregatedChartState].chartData != null
|
||||||
|
// ) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// setLoading(true);
|
||||||
|
//
|
||||||
|
// const resultPromise: Promise<{
|
||||||
|
// chartAggregatedData: chartAggregatedDataInterface;
|
||||||
|
// chartOverview: overviewInterface;
|
||||||
|
// dateList: string[];
|
||||||
|
// }> = transformInputToAggregatedData(
|
||||||
|
// props.s3Credentials,
|
||||||
|
// dayjs().subtract(1, 'week'),
|
||||||
|
// dayjs()
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// resultPromise
|
||||||
|
// .then((result) => {
|
||||||
|
// const powerDifference = [];
|
||||||
|
// for (
|
||||||
|
// let i = 0;
|
||||||
|
// i < result.chartAggregatedData.gridImportPower.data.length;
|
||||||
|
// i++
|
||||||
|
// ) {
|
||||||
|
// powerDifference.push(
|
||||||
|
// result.chartAggregatedData.gridImportPower.data[i] -
|
||||||
|
// Math.abs(result.chartAggregatedData.gridExportPower.data[i])
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// setAggregatedDataArray((prevData) =>
|
||||||
|
// prevData.concat({
|
||||||
|
// chartData: result.chartAggregatedData,
|
||||||
|
// chartOverview: result.chartOverview,
|
||||||
|
// datelist: result.dateList,
|
||||||
|
// netbalance: powerDifference
|
||||||
|
// })
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// setAggregatedChartState(aggregatedDataArray.length);
|
||||||
|
// setLoading(false);
|
||||||
|
// })
|
||||||
|
// .catch((error) => {
|
||||||
|
// console.error('Error:', error);
|
||||||
|
// });
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// const handleSetDate = () => {
|
||||||
|
// setDateOpen(true);
|
||||||
|
// setIsDateModalOpen(true);
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// const handleOkOnErrorDateModal = () => {
|
||||||
|
// setErrorDateModalOpen(false);
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// const handleCancel = () => {
|
||||||
|
// setIsDateModalOpen(false);
|
||||||
|
// setDateOpen(false);
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// const handleConfirm = () => {
|
||||||
|
// setIsDateModalOpen(false);
|
||||||
|
// setDateOpen(false);
|
||||||
|
//
|
||||||
|
// if (endDate.isAfter(dayjs())) {
|
||||||
|
// setDateSelectionError('You cannot ask for future data');
|
||||||
|
// setErrorDateModalOpen(true);
|
||||||
|
// return;
|
||||||
|
// } else if (startDate.isAfter(endDate)) {
|
||||||
|
// setDateSelectionError('Εnd date must precede start date');
|
||||||
|
// setErrorDateModalOpen(true);
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// setLoading(true);
|
||||||
|
//
|
||||||
|
// const resultPromise: Promise<{
|
||||||
|
// chartAggregatedData: chartAggregatedDataInterface;
|
||||||
|
// chartOverview: overviewInterface;
|
||||||
|
// dateList: string[];
|
||||||
|
// }> = transformInputToAggregatedData(
|
||||||
|
// props.s3Credentials,
|
||||||
|
// startDate,
|
||||||
|
// endDate
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// resultPromise
|
||||||
|
// .then((result) => {
|
||||||
|
// const powerDifference = [];
|
||||||
|
//
|
||||||
|
// for (
|
||||||
|
// let i = 0;
|
||||||
|
// i < result.chartAggregatedData.gridImportPower.data.length;
|
||||||
|
// i++
|
||||||
|
// ) {
|
||||||
|
// powerDifference.push(
|
||||||
|
// result.chartAggregatedData.gridImportPower.data[i] -
|
||||||
|
// Math.abs(result.chartAggregatedData.gridExportPower.data[i])
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// setAggregatedDataArray((prevData) =>
|
||||||
|
// prevData.concat({
|
||||||
|
// chartData: result.chartAggregatedData,
|
||||||
|
// chartOverview: result.chartOverview,
|
||||||
|
// datelist: result.dateList,
|
||||||
|
// netbalance: powerDifference
|
||||||
|
// })
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// setAggregatedChartState(aggregatedDataArray.length);
|
||||||
|
// setLoading(false);
|
||||||
|
// })
|
||||||
|
// .catch((error) => {
|
||||||
|
// console.error('Error:', error);
|
||||||
|
// });
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// const handleGoBack = () => {
|
||||||
|
// if (aggregatedChartState > 0) {
|
||||||
|
// setAggregatedChartState(aggregatedChartState - 1);
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// const handleGoForward = () => {
|
||||||
|
// if (aggregatedChartState + 1 < aggregatedDataArray.length) {
|
||||||
|
// setAggregatedChartState(aggregatedChartState + 1);
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// const renderGraphs = () => {
|
||||||
|
// return (
|
||||||
|
// <Container maxWidth="xl">
|
||||||
|
// {isErrorDateModalOpen && (
|
||||||
|
// <Modal open={isErrorDateModalOpen} onClose={() => {}}>
|
||||||
|
// <Box
|
||||||
|
// sx={{
|
||||||
|
// position: 'absolute',
|
||||||
|
// top: '50%',
|
||||||
|
// left: '50%',
|
||||||
|
// transform: 'translate(-50%, -50%)',
|
||||||
|
// width: 450,
|
||||||
|
// bgcolor: 'background.paper',
|
||||||
|
// borderRadius: 4,
|
||||||
|
// boxShadow: 24,
|
||||||
|
// p: 4,
|
||||||
|
// display: 'flex',
|
||||||
|
// flexDirection: 'column',
|
||||||
|
// alignItems: 'center'
|
||||||
|
// }}
|
||||||
|
// >
|
||||||
|
// <Typography
|
||||||
|
// variant="body1"
|
||||||
|
// gutterBottom
|
||||||
|
// sx={{ fontWeight: 'bold' }}
|
||||||
|
// >
|
||||||
|
// {dateSelectionError}
|
||||||
|
// </Typography>
|
||||||
|
//
|
||||||
|
// <Button
|
||||||
|
// sx={{
|
||||||
|
// marginTop: 2,
|
||||||
|
// textTransform: 'none',
|
||||||
|
// bgcolor: '#ffc04d',
|
||||||
|
// color: '#111111',
|
||||||
|
// '&:hover': { bgcolor: '#f7b34d' }
|
||||||
|
// }}
|
||||||
|
// onClick={handleOkOnErrorDateModal}
|
||||||
|
// >
|
||||||
|
// Ok
|
||||||
|
// </Button>
|
||||||
|
// </Box>
|
||||||
|
// </Modal>
|
||||||
|
// )}
|
||||||
|
//
|
||||||
|
// {isDateModalOpen && (
|
||||||
|
// <LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||||
|
// <Modal open={isDateModalOpen} onClose={() => {}}>
|
||||||
|
// <Box
|
||||||
|
// sx={{
|
||||||
|
// position: 'absolute',
|
||||||
|
// top: '50%',
|
||||||
|
// left: '50%',
|
||||||
|
// transform: 'translate(-50%, -50%)',
|
||||||
|
// width: 450,
|
||||||
|
// bgcolor: 'background.paper',
|
||||||
|
// borderRadius: 4,
|
||||||
|
// boxShadow: 24,
|
||||||
|
// p: 4,
|
||||||
|
// display: 'flex',
|
||||||
|
// flexDirection: 'column',
|
||||||
|
// alignItems: 'center'
|
||||||
|
// }}
|
||||||
|
// >
|
||||||
|
// <DateTimePicker
|
||||||
|
// label="Select Start Date"
|
||||||
|
// value={startDate}
|
||||||
|
// onChange={(newDate: Dayjs | null) => {
|
||||||
|
// // Type assertion to Dayjs
|
||||||
|
// if (newDate) {
|
||||||
|
// setStartDate(newDate);
|
||||||
|
// }
|
||||||
|
// }}
|
||||||
|
// renderInput={(props) => <TextField {...props} />}
|
||||||
|
// />
|
||||||
|
//
|
||||||
|
// <DateTimePicker
|
||||||
|
// label="Select End Date"
|
||||||
|
// value={endDate}
|
||||||
|
// onChange={(newDate: Dayjs | null) => {
|
||||||
|
// // Type assertion to Dayjs
|
||||||
|
// if (newDate) {
|
||||||
|
// setEndDate(newDate);
|
||||||
|
// }
|
||||||
|
// }}
|
||||||
|
// renderInput={(props) => <TextField {...props} />}
|
||||||
|
// />
|
||||||
|
//
|
||||||
|
// <div
|
||||||
|
// style={{
|
||||||
|
// display: 'flex',
|
||||||
|
// alignItems: 'center',
|
||||||
|
// marginTop: 10
|
||||||
|
// }}
|
||||||
|
// >
|
||||||
|
// <Button
|
||||||
|
// sx={{
|
||||||
|
// marginTop: 2,
|
||||||
|
// textTransform: 'none',
|
||||||
|
// bgcolor: '#ffc04d',
|
||||||
|
// color: '#111111',
|
||||||
|
// '&:hover': { bgcolor: '#f7b34d' }
|
||||||
|
// }}
|
||||||
|
// onClick={handleConfirm}
|
||||||
|
// >
|
||||||
|
// Confirm
|
||||||
|
// </Button>
|
||||||
|
//
|
||||||
|
// <Button
|
||||||
|
// sx={{
|
||||||
|
// marginTop: 2,
|
||||||
|
// marginLeft: 2,
|
||||||
|
// textTransform: 'none',
|
||||||
|
// bgcolor: '#ffc04d',
|
||||||
|
// color: '#111111',
|
||||||
|
// '&:hover': { bgcolor: '#f7b34d' }
|
||||||
|
// }}
|
||||||
|
// onClick={handleCancel}
|
||||||
|
// >
|
||||||
|
// Cancel
|
||||||
|
// </Button>
|
||||||
|
// </div>
|
||||||
|
// </Box>
|
||||||
|
// </Modal>
|
||||||
|
// </LocalizationProvider>
|
||||||
|
// )}
|
||||||
|
// <Grid container>
|
||||||
|
// <Grid item xs={6} md={6}>
|
||||||
|
// <Button
|
||||||
|
// variant="contained"
|
||||||
|
// onClick={handleSetDate}
|
||||||
|
// disabled={loading}
|
||||||
|
// sx={{
|
||||||
|
// marginTop: '20px',
|
||||||
|
// marginLeft: '10px',
|
||||||
|
// backgroundColor: dateOpen ? '#808080' : '#ffc04d',
|
||||||
|
// color: '#000000',
|
||||||
|
// '&:hover': { bgcolor: '#f7b34d' }
|
||||||
|
// }}
|
||||||
|
// >
|
||||||
|
// <FormattedMessage id="set_date" defaultMessage="Set Date" />
|
||||||
|
// </Button>
|
||||||
|
// </Grid>
|
||||||
|
//
|
||||||
|
// <Grid
|
||||||
|
// container
|
||||||
|
// justifyContent="flex-end"
|
||||||
|
// alignItems="center"
|
||||||
|
// item
|
||||||
|
// xs={6}
|
||||||
|
// md={6}
|
||||||
|
// >
|
||||||
|
// <Button
|
||||||
|
// variant="contained"
|
||||||
|
// disabled={!(aggregatedChartState > 0)}
|
||||||
|
// onClick={handleGoBack}
|
||||||
|
// sx={{
|
||||||
|
// marginTop: '20px',
|
||||||
|
// marginLeft: '10px',
|
||||||
|
// backgroundColor: '#ffc04d',
|
||||||
|
// color: '#000000',
|
||||||
|
// '&:hover': { bgcolor: '#f7b34d' }
|
||||||
|
// }}
|
||||||
|
// >
|
||||||
|
// <FormattedMessage id="goback" defaultMessage="Zoom out" />
|
||||||
|
// </Button>
|
||||||
|
//
|
||||||
|
// <Button
|
||||||
|
// variant="contained"
|
||||||
|
// disabled={
|
||||||
|
// !(aggregatedChartState < aggregatedDataArray.length - 1)
|
||||||
|
// }
|
||||||
|
// onClick={handleGoForward}
|
||||||
|
// sx={{
|
||||||
|
// marginTop: '20px',
|
||||||
|
// marginLeft: '10px',
|
||||||
|
// backgroundColor: '#ffc04d',
|
||||||
|
// color: '#000000',
|
||||||
|
// '&:hover': { bgcolor: '#f7b34d' }
|
||||||
|
// }}
|
||||||
|
// >
|
||||||
|
// <FormattedMessage id="goback" defaultMessage="Zoom in" />
|
||||||
|
// </Button>
|
||||||
|
// </Grid>
|
||||||
|
//
|
||||||
|
// {loading && (
|
||||||
|
// <Container
|
||||||
|
// maxWidth="xl"
|
||||||
|
// sx={{
|
||||||
|
// display: 'flex',
|
||||||
|
// flexDirection: 'column',
|
||||||
|
// justifyContent: 'center',
|
||||||
|
// alignItems: 'center',
|
||||||
|
// height: '100vh'
|
||||||
|
// }}
|
||||||
|
// >
|
||||||
|
// <CircularProgress size={60} style={{ color: '#ffc04d' }} />
|
||||||
|
// <Typography variant="body2" style={{ color: 'black' }} mt={2}>
|
||||||
|
// Fetching data...
|
||||||
|
// </Typography>
|
||||||
|
// </Container>
|
||||||
|
// )}
|
||||||
|
//
|
||||||
|
// {!loading && (
|
||||||
|
// <Grid item xs={12} md={12}>
|
||||||
|
// <Grid
|
||||||
|
// container
|
||||||
|
// direction="row"
|
||||||
|
// justifyContent="center"
|
||||||
|
// alignItems="stretch"
|
||||||
|
// spacing={3}
|
||||||
|
// >
|
||||||
|
// <Grid item md={6} xs={12}>
|
||||||
|
// <Card
|
||||||
|
// sx={{
|
||||||
|
// overflow: 'visible',
|
||||||
|
// marginTop: '30px',
|
||||||
|
// marginBottom: '30px'
|
||||||
|
// }}
|
||||||
|
// >
|
||||||
|
// <Box
|
||||||
|
// sx={{
|
||||||
|
// marginLeft: '20px'
|
||||||
|
// }}
|
||||||
|
// >
|
||||||
|
// <Box display="flex" alignItems="center">
|
||||||
|
// <Box>
|
||||||
|
// <Typography variant="subtitle1" noWrap>
|
||||||
|
// <FormattedMessage
|
||||||
|
// id="battery_soc"
|
||||||
|
// defaultMessage="Battery SOC (State Of Charge)"
|
||||||
|
// />
|
||||||
|
// </Typography>
|
||||||
|
// </Box>
|
||||||
|
// </Box>
|
||||||
|
// <Box
|
||||||
|
// sx={{
|
||||||
|
// display: 'flex',
|
||||||
|
// alignItems: 'center',
|
||||||
|
// justifyContent: 'flex-start',
|
||||||
|
// pt: 3
|
||||||
|
// }}
|
||||||
|
// ></Box>
|
||||||
|
// </Box>
|
||||||
|
//
|
||||||
|
// <ReactApexChart
|
||||||
|
// options={{
|
||||||
|
// ...getChartOptions(
|
||||||
|
// aggregatedDataArray[aggregatedChartState]
|
||||||
|
// .chartOverview.soc,
|
||||||
|
// 'weekly',
|
||||||
|
// aggregatedDataArray[aggregatedChartState].datelist,
|
||||||
|
// true
|
||||||
|
// )
|
||||||
|
// }}
|
||||||
|
// series={[
|
||||||
|
// {
|
||||||
|
// ...aggregatedDataArray[aggregatedChartState].chartData
|
||||||
|
// .minsoc,
|
||||||
|
// color: '#69d2e7'
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// ...aggregatedDataArray[aggregatedChartState].chartData
|
||||||
|
// .maxsoc,
|
||||||
|
// color: '#008FFB'
|
||||||
|
// }
|
||||||
|
// ]}
|
||||||
|
// type="bar"
|
||||||
|
// height={400}
|
||||||
|
// />
|
||||||
|
// </Card>
|
||||||
|
// </Grid>
|
||||||
|
// <Grid item md={6} xs={12}>
|
||||||
|
// <Card
|
||||||
|
// sx={{
|
||||||
|
// overflow: 'visible',
|
||||||
|
// marginTop: '30px',
|
||||||
|
// marginBottom: '30px'
|
||||||
|
// }}
|
||||||
|
// >
|
||||||
|
// <Box
|
||||||
|
// sx={{
|
||||||
|
// marginLeft: '20px'
|
||||||
|
// }}
|
||||||
|
// >
|
||||||
|
// <Box display="flex" alignItems="center">
|
||||||
|
// <Box>
|
||||||
|
// <Typography variant="subtitle1" noWrap>
|
||||||
|
// <FormattedMessage
|
||||||
|
// id="battery_power"
|
||||||
|
// defaultMessage={'Battery Energy'}
|
||||||
|
// />
|
||||||
|
// </Typography>
|
||||||
|
// </Box>
|
||||||
|
// </Box>
|
||||||
|
// <Box
|
||||||
|
// sx={{
|
||||||
|
// display: 'flex',
|
||||||
|
// alignItems: 'center',
|
||||||
|
// justifyContent: 'flex-start',
|
||||||
|
// pt: 3
|
||||||
|
// }}
|
||||||
|
// ></Box>
|
||||||
|
// </Box>
|
||||||
|
//
|
||||||
|
// {currentUser.userType == UserType.admin && (
|
||||||
|
// <ReactApexChart
|
||||||
|
// options={{
|
||||||
|
// ...getChartOptions(
|
||||||
|
// aggregatedDataArray[aggregatedChartState]
|
||||||
|
// .chartOverview.dcPower,
|
||||||
|
// 'weekly',
|
||||||
|
// aggregatedDataArray[aggregatedChartState].datelist,
|
||||||
|
// false
|
||||||
|
// )
|
||||||
|
// }}
|
||||||
|
// series={[
|
||||||
|
// {
|
||||||
|
// ...aggregatedDataArray[aggregatedChartState]
|
||||||
|
// .chartData.dcChargingPower,
|
||||||
|
// color: '#008FFB'
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// ...aggregatedDataArray[aggregatedChartState]
|
||||||
|
// .chartData.heatingPower,
|
||||||
|
// color: '#ff9900'
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// ...aggregatedDataArray[aggregatedChartState]
|
||||||
|
// .chartData.dcDischargingPower,
|
||||||
|
// color: '#69d2e7'
|
||||||
|
// }
|
||||||
|
// ]}
|
||||||
|
// type="bar"
|
||||||
|
// height={400}
|
||||||
|
// />
|
||||||
|
// )}
|
||||||
|
//
|
||||||
|
// {currentUser.userType == UserType.client && (
|
||||||
|
// <ReactApexChart
|
||||||
|
// options={{
|
||||||
|
// ...getChartOptions(
|
||||||
|
// aggregatedDataArray[aggregatedChartState]
|
||||||
|
// .chartOverview.dcPowerWithoutHeating,
|
||||||
|
// 'weekly',
|
||||||
|
// aggregatedDataArray[aggregatedChartState].datelist,
|
||||||
|
// true
|
||||||
|
// )
|
||||||
|
// }}
|
||||||
|
// series={[
|
||||||
|
// {
|
||||||
|
// ...aggregatedDataArray[aggregatedChartState]
|
||||||
|
// .chartData.dcChargingPower,
|
||||||
|
// color: '#008FFB'
|
||||||
|
// },
|
||||||
|
//
|
||||||
|
// {
|
||||||
|
// ...aggregatedDataArray[aggregatedChartState]
|
||||||
|
// .chartData.dcDischargingPower,
|
||||||
|
// color: '#69d2e7'
|
||||||
|
// }
|
||||||
|
// ]}
|
||||||
|
// type="bar"
|
||||||
|
// height={400}
|
||||||
|
// />
|
||||||
|
// )}
|
||||||
|
// </Card>
|
||||||
|
// </Grid>
|
||||||
|
// </Grid>
|
||||||
|
// </Grid>
|
||||||
|
// )}
|
||||||
|
// </Grid>
|
||||||
|
// </Container>
|
||||||
|
// );
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// return <>{renderGraphs()}</>;
|
||||||
|
// }
|
||||||
|
|
||||||
function SalidomoOverview(props: salidomoOverviewProps) {
|
function SalidomoOverview(props: salidomoOverviewProps) {
|
||||||
const context = useContext(UserContext);
|
const context = useContext(UserContext);
|
||||||
|
@ -79,7 +627,7 @@ function SalidomoOverview(props: salidomoOverviewProps) {
|
||||||
chartAggregatedData: chartAggregatedDataInterface;
|
chartAggregatedData: chartAggregatedDataInterface;
|
||||||
chartOverview: overviewInterface;
|
chartOverview: overviewInterface;
|
||||||
dateList: string[];
|
dateList: string[];
|
||||||
}> = transformInputToAggregatedData(
|
}> = transformInputToAggregatedDataJson(
|
||||||
props.s3Credentials,
|
props.s3Credentials,
|
||||||
dayjs().subtract(1, 'week'),
|
dayjs().subtract(1, 'week'),
|
||||||
dayjs()
|
dayjs()
|
||||||
|
@ -149,7 +697,7 @@ function SalidomoOverview(props: salidomoOverviewProps) {
|
||||||
chartAggregatedData: chartAggregatedDataInterface;
|
chartAggregatedData: chartAggregatedDataInterface;
|
||||||
chartOverview: overviewInterface;
|
chartOverview: overviewInterface;
|
||||||
dateList: string[];
|
dateList: string[];
|
||||||
}> = transformInputToAggregatedData(
|
}> = transformInputToAggregatedDataJson(
|
||||||
props.s3Credentials,
|
props.s3Credentials,
|
||||||
startDate,
|
startDate,
|
||||||
endDate
|
endDate
|
||||||
|
@ -266,19 +814,25 @@ function SalidomoOverview(props: salidomoOverviewProps) {
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
label="Select Start Date"
|
label="Select Start Date"
|
||||||
value={startDate}
|
value={startDate}
|
||||||
onChange={(newDate) => setStartDate(newDate)}
|
onChange={(newDate: Dayjs | null) => {
|
||||||
sx={{
|
// Type assertion to Dayjs
|
||||||
marginTop: 2
|
if (newDate) {
|
||||||
|
setStartDate(newDate);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
|
renderInput={(props) => <TextField {...props} />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
label="Select End Date"
|
label="Select End Date"
|
||||||
value={endDate}
|
value={endDate}
|
||||||
onChange={(newDate) => setEndDate(newDate)}
|
onChange={(newDate: Dayjs | null) => {
|
||||||
sx={{
|
// Type assertion to Dayjs
|
||||||
marginTop: 2
|
if (newDate) {
|
||||||
|
setEndDate(newDate);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
|
renderInput={(props) => <TextField {...props} />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -10,16 +10,13 @@ import { I_Installation } from 'src/interfaces/InstallationTypes';
|
||||||
import { UserContext } from 'src/contexts/userContext';
|
import { UserContext } from 'src/contexts/userContext';
|
||||||
import { TimeSpan, UnixTime } from 'src/dataCache/time';
|
import { TimeSpan, UnixTime } from 'src/dataCache/time';
|
||||||
import { FetchResult } from 'src/dataCache/dataCache';
|
import { FetchResult } from 'src/dataCache/dataCache';
|
||||||
import {
|
import { JSONRecordData } from 'src/content/dashboards/Log/graph.util';
|
||||||
extractValues,
|
|
||||||
TopologyValues
|
|
||||||
} from 'src/content/dashboards/Log/graph.util';
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import { fetchData } from 'src/content/dashboards/Installations/fetchData';
|
import { fetchDataJson } from 'src/content/dashboards/Installations/fetchData';
|
||||||
import { Navigate, Route, Routes, useLocation } from 'react-router-dom';
|
import { Navigate, Route, Routes, useLocation } from 'react-router-dom';
|
||||||
import routes from '../../../Resources/routes.json';
|
import routes from '../../../Resources/routes.json';
|
||||||
import InformationSalidomo from '../Information/InformationSalidomo';
|
import InformationSalidomo from '../Information/InformationSalidomo';
|
||||||
import BatteryView from '../BatteryView/BatteryView';
|
import BatteryViewSalidomo from '../BatteryView/BatteryViewSalidomo';
|
||||||
import Log from '../Log/Log';
|
import Log from '../Log/Log';
|
||||||
import CancelIcon from '@mui/icons-material/Cancel';
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
import SalidomoOverview from '../Overview/salidomoOverview';
|
import SalidomoOverview from '../Overview/salidomoOverview';
|
||||||
|
@ -40,7 +37,7 @@ function SalidomoInstallation(props: singleInstallationProps) {
|
||||||
const location = useLocation().pathname;
|
const location = useLocation().pathname;
|
||||||
const [errorLoadingS3Data, setErrorLoadingS3Data] = useState(false);
|
const [errorLoadingS3Data, setErrorLoadingS3Data] = useState(false);
|
||||||
const [currentTab, setCurrentTab] = useState<string>(undefined);
|
const [currentTab, setCurrentTab] = useState<string>(undefined);
|
||||||
const [values, setValues] = useState<TopologyValues | null>(null);
|
const [values, setValues] = useState<JSONRecordData | null>(null);
|
||||||
const status = props.current_installation.status;
|
const status = props.current_installation.status;
|
||||||
const [
|
const [
|
||||||
failedToCommunicateWithInstallation,
|
failedToCommunicateWithInstallation,
|
||||||
|
@ -77,7 +74,7 @@ function SalidomoInstallation(props: singleInstallationProps) {
|
||||||
|
|
||||||
const fetchDataPeriodically = async () => {
|
const fetchDataPeriodically = async () => {
|
||||||
var timeperiodToSearch = 30;
|
var timeperiodToSearch = 30;
|
||||||
let res;
|
let res: FetchResult<Record<string, JSONRecordData>> | undefined;
|
||||||
let timestampToFetch;
|
let timestampToFetch;
|
||||||
|
|
||||||
for (var i = 0; i < timeperiodToSearch; i += 1) {
|
for (var i = 0; i < timeperiodToSearch; i += 1) {
|
||||||
|
@ -88,7 +85,7 @@ function SalidomoInstallation(props: singleInstallationProps) {
|
||||||
console.log('timestamp to fetch is ' + timestampToFetch);
|
console.log('timestamp to fetch is ' + timestampToFetch);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
res = await fetchData(timestampToFetch, s3Credentials, true);
|
res = await fetchDataJson(timestampToFetch, s3Credentials, true);
|
||||||
if (res !== FetchResult.notAvailable && res !== FetchResult.tryLater) {
|
if (res !== FetchResult.notAvailable && res !== FetchResult.tryLater) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -114,15 +111,16 @@ function SalidomoInstallation(props: singleInstallationProps) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
console.log(`Timestamp: ${timestamp}`);
|
console.log(`Timestamp: ${timestamp}`);
|
||||||
console.log(res[timestamp]);
|
//console.log('object is' + res[timestamp]);
|
||||||
|
|
||||||
// Set values asynchronously with delay
|
// Set values asynchronously with delay
|
||||||
setValues(
|
setValues(res[timestamp]);
|
||||||
extractValues({
|
// setValues(
|
||||||
time: UnixTime.fromTicks(parseInt(timestamp, 10)),
|
// extractValues({
|
||||||
value: res[timestamp]
|
// time: UnixTime.fromTicks(parseInt(timestamp, 10)),
|
||||||
})
|
// value: res[timestamp]
|
||||||
);
|
// })
|
||||||
|
// );
|
||||||
// Wait for 2 seconds before processing next timestamp
|
// Wait for 2 seconds before processing next timestamp
|
||||||
await timeout(2000);
|
await timeout(2000);
|
||||||
}
|
}
|
||||||
|
@ -137,7 +135,7 @@ function SalidomoInstallation(props: singleInstallationProps) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('Trying to fetch timestamp ' + timestampToFetch);
|
console.log('Trying to fetch timestamp ' + timestampToFetch);
|
||||||
res = await fetchData(timestampToFetch, s3Credentials, true);
|
res = await fetchDataJson(timestampToFetch, s3Credentials, true);
|
||||||
if (
|
if (
|
||||||
res !== FetchResult.notAvailable &&
|
res !== FetchResult.notAvailable &&
|
||||||
res !== FetchResult.tryLater
|
res !== FetchResult.tryLater
|
||||||
|
@ -363,13 +361,13 @@ function SalidomoInstallation(props: singleInstallationProps) {
|
||||||
<Route
|
<Route
|
||||||
path={routes.batteryview + '*'}
|
path={routes.batteryview + '*'}
|
||||||
element={
|
element={
|
||||||
<BatteryView
|
<BatteryViewSalidomo
|
||||||
values={values}
|
values={values}
|
||||||
s3Credentials={s3Credentials}
|
s3Credentials={s3Credentials}
|
||||||
installationId={props.current_installation.id}
|
installationId={props.current_installation.id}
|
||||||
productNum={props.current_installation.product}
|
productNum={props.current_installation.product}
|
||||||
connected={connected}
|
connected={connected}
|
||||||
></BatteryView>
|
></BatteryViewSalidomo>
|
||||||
}
|
}
|
||||||
></Route>
|
></Route>
|
||||||
|
|
||||||
|
|
|
@ -40,9 +40,7 @@ function SalidomoInstallationTabs() {
|
||||||
} = useContext(InstallationsContext);
|
} = useContext(InstallationsContext);
|
||||||
const { product, setProduct } = useContext(ProductIdContext);
|
const { product, setProduct } = useContext(ProductIdContext);
|
||||||
|
|
||||||
// const webSocketsContext = useContext(WebSocketContext);
|
//The following useEffect is for the current tab to be bold. Based on the location, we set the corresponding tab to be bold
|
||||||
// const { socket, openSocket, closeSocket } = webSocketsContext;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let path = location.pathname.split('/');
|
let path = location.pathname.split('/');
|
||||||
|
|
||||||
|
@ -51,12 +49,13 @@ function SalidomoInstallationTabs() {
|
||||||
} else if (path[path.length - 2] === 'tree') {
|
} else if (path[path.length - 2] === 'tree') {
|
||||||
setCurrentTab('tree');
|
setCurrentTab('tree');
|
||||||
} else {
|
} else {
|
||||||
//Even if we are located at path: /batteryview/mainstats, we want the BatteryView tab to be bold
|
//Even if we are located at path: /batteryview/mainstats, we want the BatteryViewSalidomo tab to be bold
|
||||||
setCurrentTab(path.find((pathElement) => tabList.includes(pathElement)));
|
setCurrentTab(path.find((pathElement) => tabList.includes(pathElement)));
|
||||||
}
|
}
|
||||||
}, [location]);
|
}, [location]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
//The first time this component will be loaded, it needs to call the fetchAllSalidomoInstallations function from the InstallationsContextProvider
|
||||||
if (salidomoInstallations.length === 0 && fetchedInstallations === false) {
|
if (salidomoInstallations.length === 0 && fetchedInstallations === false) {
|
||||||
fetchAllSalidomoInstallations();
|
fetchAllSalidomoInstallations();
|
||||||
setFetchedInstallations(true);
|
setFetchedInstallations(true);
|
||||||
|
@ -64,10 +63,12 @@ function SalidomoInstallationTabs() {
|
||||||
}, [salidomoInstallations]);
|
}, [salidomoInstallations]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
//Since we know the ids of the installations we have access to, we need to open a web socket with the backend.
|
||||||
if (salidomoInstallations && salidomoInstallations.length > 0) {
|
if (salidomoInstallations && salidomoInstallations.length > 0) {
|
||||||
if (!socket) {
|
if (!socket) {
|
||||||
openSocket(1);
|
openSocket(1);
|
||||||
} else if (currentProduct == 0) {
|
} else if (currentProduct != 1) {
|
||||||
|
//If there is any other open websocket for another product, close it.
|
||||||
closeSocket();
|
closeSocket();
|
||||||
openSocket(1);
|
openSocket(1);
|
||||||
}
|
}
|
||||||
|
@ -82,6 +83,7 @@ function SalidomoInstallationTabs() {
|
||||||
setCurrentTab(value);
|
setCurrentTab(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//Manually compute the path when the user clicks a tab
|
||||||
const navigateToTabPath = (pathname: string, tab_value: string): string => {
|
const navigateToTabPath = (pathname: string, tab_value: string): string => {
|
||||||
let pathlist = pathname.split('/');
|
let pathlist = pathname.split('/');
|
||||||
let ret_path = '';
|
let ret_path = '';
|
||||||
|
|
|
@ -25,8 +25,6 @@ interface FlatInstallationViewProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const FlatInstallationView = (props: FlatInstallationViewProps) => {
|
const FlatInstallationView = (props: FlatInstallationViewProps) => {
|
||||||
// const webSocketContext = useContext(WebSocketContext);
|
|
||||||
// const { getSortedInstallations } = webSocketContext;
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [selectedInstallation, setSelectedInstallation] = useState<number>(-1);
|
const [selectedInstallation, setSelectedInstallation] = useState<number>(-1);
|
||||||
const currentLocation = useLocation();
|
const currentLocation = useLocation();
|
||||||
|
@ -57,7 +55,7 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
|
||||||
routes.installation +
|
routes.installation +
|
||||||
`${installationID}` +
|
`${installationID}` +
|
||||||
'/' +
|
'/' +
|
||||||
routes.information,
|
routes.live,
|
||||||
{
|
{
|
||||||
replace: true
|
replace: true
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useContext, useEffect, useState } from 'react';
|
import React, { useContext, useEffect, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
|
@ -20,6 +20,7 @@ import BuildIcon from '@mui/icons-material/Build';
|
||||||
import AccessContextProvider from '../../../contexts/AccessContextProvider';
|
import AccessContextProvider from '../../../contexts/AccessContextProvider';
|
||||||
import Access from '../ManageAccess/Access';
|
import Access from '../ManageAccess/Access';
|
||||||
import InformationSodioHome from '../Information/InformationSodioHome';
|
import InformationSodioHome from '../Information/InformationSodioHome';
|
||||||
|
import CryptoJS from 'crypto-js';
|
||||||
|
|
||||||
interface singleInstallationProps {
|
interface singleInstallationProps {
|
||||||
current_installation?: I_Installation;
|
current_installation?: I_Installation;
|
||||||
|
@ -27,6 +28,9 @@ interface singleInstallationProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
function SodioHomeInstallation(props: singleInstallationProps) {
|
function SodioHomeInstallation(props: singleInstallationProps) {
|
||||||
|
if (props.current_installation == undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const context = useContext(UserContext);
|
const context = useContext(UserContext);
|
||||||
const { currentUser } = context;
|
const { currentUser } = context;
|
||||||
const location = useLocation().pathname;
|
const location = useLocation().pathname;
|
||||||
|
@ -40,17 +44,97 @@ function SodioHomeInstallation(props: singleInstallationProps) {
|
||||||
] = useState(0);
|
] = useState(0);
|
||||||
const [connected, setConnected] = useState(true);
|
const [connected, setConnected] = useState(true);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
if (props.current_installation == undefined) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [fetchFunctionCalled, setFetchFunctionCalled] = useState(false);
|
const [fetchFunctionCalled, setFetchFunctionCalled] = useState(false);
|
||||||
|
|
||||||
|
//In React, useRef creates a mutable object that persists across renders without triggering re-renders when its value changes.
|
||||||
|
//While fetching, we check the value of continueFetching, if its false, we break. This means that either the user changed tab or the object has been finished its execution (return)
|
||||||
|
const continueFetching = useRef(false);
|
||||||
|
|
||||||
function timeout(delay: number) {
|
function timeout(delay: number) {
|
||||||
return new Promise((res) => setTimeout(res, delay));
|
return new Promise((res) => setTimeout(res, delay));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const API_BASE_URL =
|
||||||
|
'https://www.biwattpower.com/gateway/admin/open/device/currentEnergyFlowData/';
|
||||||
|
|
||||||
|
async function encryptAES_CBC(data: string, secretKey: string) {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const keyBuffer = encoder.encode(secretKey.padEnd(32, ' ')); // Ensure 256-bit key
|
||||||
|
|
||||||
|
const cryptoKey = await crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
keyBuffer,
|
||||||
|
{ name: 'AES-CBC' },
|
||||||
|
false,
|
||||||
|
['encrypt']
|
||||||
|
);
|
||||||
|
|
||||||
|
// Generate a random IV (initialization vector)
|
||||||
|
const iv = crypto.getRandomValues(new Uint8Array(16));
|
||||||
|
const dataBuffer = encoder.encode(
|
||||||
|
data.padEnd(Math.ceil(data.length / 16) * 16, '\0')
|
||||||
|
);
|
||||||
|
|
||||||
|
const encryptedBuffer = await crypto.subtle.encrypt(
|
||||||
|
{ name: 'AES-CBC', iv },
|
||||||
|
cryptoKey,
|
||||||
|
dataBuffer
|
||||||
|
);
|
||||||
|
|
||||||
|
// Convert encrypted data + IV to Base64
|
||||||
|
const encryptedArray = new Uint8Array(encryptedBuffer);
|
||||||
|
const combined = new Uint8Array(iv.length + encryptedArray.length);
|
||||||
|
combined.set(iv);
|
||||||
|
combined.set(encryptedArray, iv.length);
|
||||||
|
|
||||||
|
return btoa(String.fromCharCode(...combined));
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchDataPeriodically = async () => {
|
||||||
|
while (continueFetching.current) {
|
||||||
|
//Fetch data from Bitwatt cloud
|
||||||
|
console.log('Fetching from Bitwatt cloud');
|
||||||
|
|
||||||
|
console.log(props.current_installation.serialNumber);
|
||||||
|
console.log(props.current_installation.s3WriteKey);
|
||||||
|
console.log(props.current_installation.s3WriteSecret);
|
||||||
|
|
||||||
|
const timeStamp = Date.now().toString();
|
||||||
|
|
||||||
|
// Encrypt timestamp using AES-ECB with PKCS7 padding
|
||||||
|
const key = CryptoJS.enc.Utf8.parse(
|
||||||
|
props.current_installation.s3WriteSecret
|
||||||
|
);
|
||||||
|
const encrypted = CryptoJS.AES.encrypt(timeStamp, key, {
|
||||||
|
mode: CryptoJS.mode.ECB,
|
||||||
|
padding: CryptoJS.pad.Pkcs7
|
||||||
|
}).toString();
|
||||||
|
|
||||||
|
// Set headers
|
||||||
|
const headers = {
|
||||||
|
'X-Signature': encrypted,
|
||||||
|
'X-AccessKey': props.current_installation.s3WriteKey,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
};
|
||||||
|
|
||||||
|
// API URL
|
||||||
|
const url = `https://www.biwattpower.com/gateway/admin/open/device/currentEnergyFlowData/${props.current_installation.serialNumber}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, { method: 'GET', headers });
|
||||||
|
const result = await response.json();
|
||||||
|
console.log('API Response:', result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Request failed:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for 2 seconds before fetching again
|
||||||
|
await timeout(200000);
|
||||||
|
console.log('ssssssssssssssssssssssssssssssssssssss');
|
||||||
|
}
|
||||||
|
setFetchFunctionCalled(false);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let path = location.split('/');
|
let path = location.split('/');
|
||||||
setCurrentTab(path[path.length - 1]);
|
setCurrentTab(path[path.length - 1]);
|
||||||
|
@ -62,6 +146,33 @@ function SodioHomeInstallation(props: singleInstallationProps) {
|
||||||
}
|
}
|
||||||
}, [status]);
|
}, [status]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(currentTab);
|
||||||
|
if (currentTab == 'live' || location.includes('batteryview')) {
|
||||||
|
//Fetch periodically if the tab is live or batteryview
|
||||||
|
if (
|
||||||
|
currentTab == 'live' ||
|
||||||
|
(location.includes('batteryview') && !location.includes('mainstats'))
|
||||||
|
) {
|
||||||
|
if (!continueFetching.current) {
|
||||||
|
continueFetching.current = true;
|
||||||
|
//Call the function only one time. When the location and the currentTab change, this useEffect will be called 2 times
|
||||||
|
if (!fetchFunctionCalled) {
|
||||||
|
setFetchFunctionCalled(true);
|
||||||
|
fetchDataPeriodically();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
continueFetching.current = false;
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
//If the tab is not live, pvview, batteryview or configuration, then stop fetching.
|
||||||
|
continueFetching.current = false;
|
||||||
|
}
|
||||||
|
}, [currentTab, location]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Grid item xs={12} md={12}>
|
<Grid item xs={12} md={12}>
|
||||||
|
@ -228,6 +339,18 @@ function SodioHomeInstallation(props: singleInstallationProps) {
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path={routes.live}
|
||||||
|
element={
|
||||||
|
<div></div>
|
||||||
|
// <Topology
|
||||||
|
// values={values}
|
||||||
|
// connected={connected}
|
||||||
|
// loading={loading}
|
||||||
|
// ></Topology>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
{/*<Route*/}
|
{/*<Route*/}
|
||||||
{/* path={routes.overview}*/}
|
{/* path={routes.overview}*/}
|
||||||
{/* element={*/}
|
{/* element={*/}
|
||||||
|
@ -241,13 +364,13 @@ function SodioHomeInstallation(props: singleInstallationProps) {
|
||||||
{/*<Route*/}
|
{/*<Route*/}
|
||||||
{/* path={routes.batteryview + '*'}*/}
|
{/* path={routes.batteryview + '*'}*/}
|
||||||
{/* element={*/}
|
{/* element={*/}
|
||||||
{/* <BatteryView*/}
|
{/* <BatteryViewSalidomo*/}
|
||||||
{/* values={values}*/}
|
{/* values={values}*/}
|
||||||
{/* s3Credentials={s3Credentials}*/}
|
{/* s3Credentials={s3Credentials}*/}
|
||||||
{/* installationId={props.current_installation.id}*/}
|
{/* installationId={props.current_installation.id}*/}
|
||||||
{/* productNum={props.current_installation.product}*/}
|
{/* productNum={props.current_installation.product}*/}
|
||||||
{/* connected={connected}*/}
|
{/* connected={connected}*/}
|
||||||
{/* ></BatteryView>*/}
|
{/* ></BatteryViewSalidomo>*/}
|
||||||
{/* }*/}
|
{/* }*/}
|
||||||
{/*></Route>*/}
|
{/*></Route>*/}
|
||||||
|
|
||||||
|
@ -279,7 +402,7 @@ function SodioHomeInstallation(props: singleInstallationProps) {
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path={'*'}
|
path={'*'}
|
||||||
element={<Navigate to={routes.information}></Navigate>}
|
element={<Navigate to={routes.live}></Navigate>}
|
||||||
/>
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
|
@ -19,7 +19,7 @@ function SodioHomeInstallationTabs() {
|
||||||
const context = useContext(UserContext);
|
const context = useContext(UserContext);
|
||||||
const { currentUser } = context;
|
const { currentUser } = context;
|
||||||
const tabList = [
|
const tabList = [
|
||||||
'batteryview',
|
'live',
|
||||||
'information',
|
'information',
|
||||||
'manage',
|
'manage',
|
||||||
'overview',
|
'overview',
|
||||||
|
@ -40,9 +40,6 @@ function SodioHomeInstallationTabs() {
|
||||||
} = useContext(InstallationsContext);
|
} = useContext(InstallationsContext);
|
||||||
const { product, setProduct } = useContext(ProductIdContext);
|
const { product, setProduct } = useContext(ProductIdContext);
|
||||||
|
|
||||||
// const webSocketsContext = useContext(WebSocketContext);
|
|
||||||
// const { socket, openSocket, closeSocket } = webSocketsContext;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let path = location.pathname.split('/');
|
let path = location.pathname.split('/');
|
||||||
|
|
||||||
|
@ -51,7 +48,7 @@ function SodioHomeInstallationTabs() {
|
||||||
} else if (path[path.length - 2] === 'tree') {
|
} else if (path[path.length - 2] === 'tree') {
|
||||||
setCurrentTab('tree');
|
setCurrentTab('tree');
|
||||||
} else {
|
} else {
|
||||||
//Even if we are located at path: /batteryview/mainstats, we want the BatteryView tab to be bold
|
//Even if we are located at path: /batteryview/mainstats, we want the BatteryViewSalidomo tab to be bold
|
||||||
setCurrentTab(path.find((pathElement) => tabList.includes(pathElement)));
|
setCurrentTab(path.find((pathElement) => tabList.includes(pathElement)));
|
||||||
}
|
}
|
||||||
}, [location]);
|
}, [location]);
|
||||||
|
@ -90,6 +87,7 @@ function SodioHomeInstallationTabs() {
|
||||||
ret_path += '/';
|
ret_path += '/';
|
||||||
ret_path += pathlist[i];
|
ret_path += pathlist[i];
|
||||||
} else {
|
} else {
|
||||||
|
//When finding the installation id (number), break and then add the tab_value
|
||||||
ret_path += '/';
|
ret_path += '/';
|
||||||
ret_path += pathlist[i];
|
ret_path += pathlist[i];
|
||||||
ret_path += '/';
|
ret_path += '/';
|
||||||
|
@ -105,13 +103,8 @@ function SodioHomeInstallationTabs() {
|
||||||
currentUser.userType == UserType.admin
|
currentUser.userType == UserType.admin
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
value: 'batteryview',
|
value: 'live',
|
||||||
label: (
|
label: <FormattedMessage id="live" defaultMessage="Live" />
|
||||||
<FormattedMessage
|
|
||||||
id="batteryview"
|
|
||||||
defaultMessage="Battery View"
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'overview',
|
value: 'overview',
|
||||||
|
@ -150,13 +143,8 @@ function SodioHomeInstallationTabs() {
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
value: 'batteryview',
|
value: 'live',
|
||||||
label: (
|
label: <FormattedMessage id="live" defaultMessage="Live" />
|
||||||
<FormattedMessage
|
|
||||||
id="batteryview"
|
|
||||||
defaultMessage="Battery View"
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'overview',
|
value: 'overview',
|
||||||
|
@ -186,13 +174,8 @@ function SodioHomeInstallationTabs() {
|
||||||
icon: <AccountTreeIcon id="mode-toggle-button-tree-icon" />
|
icon: <AccountTreeIcon id="mode-toggle-button-tree-icon" />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'batteryview',
|
value: 'live',
|
||||||
label: (
|
label: <FormattedMessage id="live" defaultMessage="Live" />
|
||||||
<FormattedMessage
|
|
||||||
id="batteryview"
|
|
||||||
defaultMessage="Battery View"
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'overview',
|
value: 'overview',
|
||||||
|
@ -243,13 +226,8 @@ function SodioHomeInstallationTabs() {
|
||||||
icon: <AccountTreeIcon id="mode-toggle-button-tree-icon" />
|
icon: <AccountTreeIcon id="mode-toggle-button-tree-icon" />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'batteryview',
|
value: 'live',
|
||||||
label: (
|
label: <FormattedMessage id="live" defaultMessage="Live" />
|
||||||
<FormattedMessage
|
|
||||||
id="batteryview"
|
|
||||||
defaultMessage="Battery View"
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'overview',
|
value: 'overview',
|
||||||
|
|
|
@ -41,18 +41,17 @@ const InstallationsContextProvider = ({
|
||||||
const [socket, setSocket] = useState<WebSocket>(null);
|
const [socket, setSocket] = useState<WebSocket>(null);
|
||||||
const [currentProduct, setcurrentProduct] = useState<number>(0);
|
const [currentProduct, setcurrentProduct] = useState<number>(0);
|
||||||
|
|
||||||
|
//Store pending updates and apply them in batches
|
||||||
const pendingUpdates = useRef<
|
const pendingUpdates = useRef<
|
||||||
Record<number, { status: number; testingMode: boolean }>
|
Record<number, { status: number; testingMode: boolean }>
|
||||||
>({}); // To store pending updates
|
>({});
|
||||||
|
|
||||||
const updateInstallationStatus = useCallback(
|
const updateInstallationStatus = useCallback((id, status, testingMode) => {
|
||||||
(product, id, status, testingMode) => {
|
//Insert the incoming message to the pendingUpdates map
|
||||||
// Buffer updates instead of applying them immediately
|
pendingUpdates.current[id] = { status: Number(status), testingMode }; // Ensure status is a number
|
||||||
pendingUpdates.current[id] = { status: Number(status), testingMode }; // Ensure status is a number
|
}, []);
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
|
//This function will be called every 1 minute and it will update only the installations for which the status and/or the testingMOde has been changed
|
||||||
const applyBatchUpdates = useCallback(() => {
|
const applyBatchUpdates = useCallback(() => {
|
||||||
if (Object.keys(pendingUpdates.current).length === 0) return; // No updates to apply
|
if (Object.keys(pendingUpdates.current).length === 0) return; // No updates to apply
|
||||||
|
|
||||||
|
@ -109,7 +108,7 @@ const InstallationsContextProvider = ({
|
||||||
setcurrentProduct(product);
|
setcurrentProduct(product);
|
||||||
const tokenString = localStorage.getItem('token');
|
const tokenString = localStorage.getItem('token');
|
||||||
const token = tokenString !== null ? tokenString : '';
|
const token = tokenString !== null ? tokenString : '';
|
||||||
const urlWithToken = `wss://stage.innov.energy/api/CreateWebSocket?authToken=${token}`;
|
const urlWithToken = `wss://monitor.innov.energy/api/CreateWebSocket?authToken=${token}`;
|
||||||
|
|
||||||
const socket = new WebSocket(urlWithToken);
|
const socket = new WebSocket(urlWithToken);
|
||||||
|
|
||||||
|
@ -124,7 +123,7 @@ const InstallationsContextProvider = ({
|
||||||
installationsToSend = sodiohomeInstallations;
|
installationsToSend = sodiohomeInstallations;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send the corresponding installation IDs
|
// Send the corresponding installation IDs to the backend
|
||||||
socket.send(
|
socket.send(
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
installationsToSend.map((installation) => installation.id)
|
installationsToSend.map((installation) => installation.id)
|
||||||
|
@ -132,16 +131,6 @@ const InstallationsContextProvider = ({
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// socket.addEventListener('open', () => {
|
|
||||||
// socket.send(
|
|
||||||
// JSON.stringify(
|
|
||||||
// product === 1
|
|
||||||
// ? salidomoInstallations.map((installation) => installation.id)
|
|
||||||
// : salimaxInstallations.map((installation) => installation.id)
|
|
||||||
// )
|
|
||||||
// );
|
|
||||||
// });
|
|
||||||
|
|
||||||
// Periodically send ping messages to keep the connection alive
|
// Periodically send ping messages to keep the connection alive
|
||||||
const pingInterval = setInterval(() => {
|
const pingInterval = setInterval(() => {
|
||||||
if (socket.readyState === WebSocket.OPEN) {
|
if (socket.readyState === WebSocket.OPEN) {
|
||||||
|
@ -152,8 +141,8 @@ const InstallationsContextProvider = ({
|
||||||
socket.addEventListener('message', (event) => {
|
socket.addEventListener('message', (event) => {
|
||||||
const message = JSON.parse(event.data); // Parse the JSON data
|
const message = JSON.parse(event.data); // Parse the JSON data
|
||||||
if (message.id !== -1) {
|
if (message.id !== -1) {
|
||||||
|
//For each received message (except the first one which is a batch, call the updateInstallationStatus function in order to import the message to the pendingUpdates list
|
||||||
updateInstallationStatus(
|
updateInstallationStatus(
|
||||||
product,
|
|
||||||
message.id,
|
message.id,
|
||||||
message.status,
|
message.status,
|
||||||
message.testingMode
|
message.testingMode
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import {
|
import {
|
||||||
fetchAggregatedData,
|
fetchAggregatedData,
|
||||||
fetchData
|
fetchAggregatedDataJson,
|
||||||
|
fetchData,
|
||||||
|
fetchDataJson
|
||||||
} from '../content/dashboards/Installations/fetchData';
|
} from '../content/dashboards/Installations/fetchData';
|
||||||
import { FetchResult } from '../dataCache/dataCache';
|
import { FetchResult } from '../dataCache/dataCache';
|
||||||
import { I_S3Credentials } from './S3Types';
|
import { I_S3Credentials } from './S3Types';
|
||||||
|
@ -9,6 +11,7 @@ import { TimeSpan, UnixTime } from '../dataCache/time';
|
||||||
import { DataRecord } from '../dataCache/data';
|
import { DataRecord } from '../dataCache/data';
|
||||||
import axiosConfig from '../Resources/axiosConfig';
|
import axiosConfig from '../Resources/axiosConfig';
|
||||||
import { AxiosError, AxiosResponse } from 'axios';
|
import { AxiosError, AxiosResponse } from 'axios';
|
||||||
|
import { JSONRecordData } from '../content/dashboards/Log/graph.util';
|
||||||
|
|
||||||
export interface chartInfoInterface {
|
export interface chartInfoInterface {
|
||||||
magnitude: number;
|
magnitude: number;
|
||||||
|
@ -68,6 +71,473 @@ export interface BatteryOverviewInterface {
|
||||||
Current: chartInfoInterface;
|
Current: chartInfoInterface;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const transformInputToBatteryViewDataJson = async (
|
||||||
|
s3Credentials: I_S3Credentials,
|
||||||
|
id: number,
|
||||||
|
product: number,
|
||||||
|
start_time?: UnixTime,
|
||||||
|
end_time?: UnixTime
|
||||||
|
): Promise<{
|
||||||
|
chartData: BatteryDataInterface;
|
||||||
|
chartOverview: BatteryOverviewInterface;
|
||||||
|
}> => {
|
||||||
|
const prefixes = ['', 'k', 'M', 'G', 'T'];
|
||||||
|
const MAX_NUMBER = 9999999;
|
||||||
|
const categories = ['Soc', 'Temperature', 'Power', 'Voltage', 'Current'];
|
||||||
|
const pathCategories = [
|
||||||
|
'.Soc',
|
||||||
|
'.Temperatures.Cells.Average',
|
||||||
|
'.Dc.Power',
|
||||||
|
'.Dc.Voltage',
|
||||||
|
'.Dc.Current'
|
||||||
|
];
|
||||||
|
|
||||||
|
const pathsToSearch = [
|
||||||
|
'Battery.Devices.1',
|
||||||
|
'Battery.Devices.2',
|
||||||
|
'Battery.Devices.3',
|
||||||
|
'Battery.Devices.4',
|
||||||
|
'Battery.Devices.5',
|
||||||
|
'Battery.Devices.6',
|
||||||
|
'Battery.Devices.7',
|
||||||
|
'Battery.Devices.8',
|
||||||
|
'Battery.Devices.9',
|
||||||
|
'Battery.Devices.10'
|
||||||
|
];
|
||||||
|
|
||||||
|
const pathsToSave = [];
|
||||||
|
|
||||||
|
const chartData: BatteryDataInterface = {
|
||||||
|
Soc: { name: 'State Of Charge', data: [] },
|
||||||
|
Temperature: { name: 'Temperature', data: [] },
|
||||||
|
Power: { name: 'Power', data: [] },
|
||||||
|
Voltage: { name: 'Voltage', data: [] },
|
||||||
|
Current: { name: 'Current', data: [] }
|
||||||
|
};
|
||||||
|
|
||||||
|
const chartOverview: BatteryOverviewInterface = {
|
||||||
|
Soc: { magnitude: 0, unit: '', min: 0, max: 0 },
|
||||||
|
Temperature: { magnitude: 0, unit: '', min: 0, max: 0 },
|
||||||
|
Power: { magnitude: 0, unit: '', min: 0, max: 0 },
|
||||||
|
Voltage: { magnitude: 0, unit: '', min: 0, max: 0 },
|
||||||
|
Current: { magnitude: 0, unit: '', min: 0, max: 0 }
|
||||||
|
};
|
||||||
|
|
||||||
|
let initialiation = true;
|
||||||
|
let timestampArray: number[] = [];
|
||||||
|
let adjustedTimestampArray = [];
|
||||||
|
const timestampPromises = [];
|
||||||
|
|
||||||
|
await axiosConfig
|
||||||
|
.get(
|
||||||
|
`/GetCsvTimestampsForInstallation?id=${id}&start=${start_time.ticks}&end=${end_time.ticks}`
|
||||||
|
)
|
||||||
|
.then((res: AxiosResponse<number[]>) => {
|
||||||
|
timestampArray = res.data;
|
||||||
|
})
|
||||||
|
.catch((err: AxiosError) => {
|
||||||
|
if (err.response && err.response.status == 401) {
|
||||||
|
//removeToken();
|
||||||
|
//navigate(routes.login);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (var i = 0; i < timestampArray.length; i++) {
|
||||||
|
timestampPromises.push(
|
||||||
|
fetchJsonDataForOneTime(
|
||||||
|
UnixTime.fromTicks(timestampArray[i], true),
|
||||||
|
s3Credentials
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const adjustedTimestamp =
|
||||||
|
product == 0
|
||||||
|
? new Date(timestampArray[i] * 1000)
|
||||||
|
: new Date(timestampArray[i] * 100000);
|
||||||
|
//Timezone offset is negative, so we convert the timestamp to the current zone by subtracting the corresponding offset
|
||||||
|
adjustedTimestamp.setHours(
|
||||||
|
adjustedTimestamp.getHours() - adjustedTimestamp.getTimezoneOffset() / 60
|
||||||
|
);
|
||||||
|
adjustedTimestampArray.push(adjustedTimestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
const results: Promise<FetchResult<Record<string, JSONRecordData>>>[] =
|
||||||
|
await Promise.all(timestampPromises);
|
||||||
|
|
||||||
|
for (let i = 0; i < results.length; i++) {
|
||||||
|
if (results[i] == null) {
|
||||||
|
// Handle not available or try later case
|
||||||
|
} else {
|
||||||
|
const timestamp = Object.keys(results[i])[
|
||||||
|
Object.keys(results[i]).length - 1
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = results[i][timestamp];
|
||||||
|
|
||||||
|
const battery_nodes = result.Config.Devices.BatteryNodes.value
|
||||||
|
.toString()
|
||||||
|
.split(',');
|
||||||
|
|
||||||
|
//Initialize the chartData structure based on the node names extracted from the first result
|
||||||
|
let old_length = pathsToSave.length;
|
||||||
|
|
||||||
|
if (battery_nodes.length > old_length) {
|
||||||
|
battery_nodes.forEach((node) => {
|
||||||
|
if (!pathsToSave.includes('Node' + node)) {
|
||||||
|
pathsToSave.push('Node' + node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (initialiation) {
|
||||||
|
initialiation = false;
|
||||||
|
categories.forEach((category) => {
|
||||||
|
chartData[category].data = [];
|
||||||
|
chartOverview[category] = {
|
||||||
|
magnitude: 0,
|
||||||
|
unit: '',
|
||||||
|
min: MAX_NUMBER,
|
||||||
|
max: -MAX_NUMBER
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (battery_nodes.length > old_length) {
|
||||||
|
categories.forEach((category) => {
|
||||||
|
pathsToSave.forEach((path) => {
|
||||||
|
if (pathsToSave.indexOf(path) >= old_length) {
|
||||||
|
chartData[category].data[path] = { name: path, data: [] };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (
|
||||||
|
let category_index = 0;
|
||||||
|
category_index < pathCategories.length;
|
||||||
|
category_index++
|
||||||
|
) {
|
||||||
|
let category = categories[category_index];
|
||||||
|
|
||||||
|
for (let j = 0; j < pathsToSave.length; j++) {
|
||||||
|
let path =
|
||||||
|
pathsToSearch[j] + pathCategories[category_index] + '.value';
|
||||||
|
|
||||||
|
//if (result[path]) {
|
||||||
|
const value = path
|
||||||
|
.split('.')
|
||||||
|
.reduce((o, key) => (o ? o[key] : undefined), result);
|
||||||
|
|
||||||
|
if (value < chartOverview[category].min) {
|
||||||
|
chartOverview[category].min = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value > chartOverview[category].max) {
|
||||||
|
chartOverview[category].max = value;
|
||||||
|
}
|
||||||
|
chartData[category].data[pathsToSave[j]].data.push([
|
||||||
|
adjustedTimestampArray[i],
|
||||||
|
value
|
||||||
|
]);
|
||||||
|
// } else {
|
||||||
|
// // chartData[category].data[pathsToSave[j]].data.push([
|
||||||
|
// // adjustedTimestampArray[i],
|
||||||
|
// // null
|
||||||
|
// // ]);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
categories.forEach((category) => {
|
||||||
|
let value = Math.max(
|
||||||
|
Math.abs(chartOverview[category].max),
|
||||||
|
Math.abs(chartOverview[category].min)
|
||||||
|
);
|
||||||
|
let magnitude = 0;
|
||||||
|
|
||||||
|
if (value < 0) {
|
||||||
|
value = -value;
|
||||||
|
}
|
||||||
|
while (value >= 1000) {
|
||||||
|
value /= 1000;
|
||||||
|
magnitude++;
|
||||||
|
}
|
||||||
|
chartOverview[category].magnitude = magnitude;
|
||||||
|
});
|
||||||
|
|
||||||
|
chartOverview.Soc.unit = '(%)';
|
||||||
|
chartOverview.Soc.min = 0;
|
||||||
|
chartOverview.Soc.max = 100;
|
||||||
|
chartOverview.Temperature.unit = '(°C)';
|
||||||
|
chartOverview.Power.unit =
|
||||||
|
'(' + prefixes[chartOverview['Power'].magnitude] + 'W' + ')';
|
||||||
|
chartOverview.Voltage.unit =
|
||||||
|
'(' + prefixes[chartOverview['Voltage'].magnitude] + 'V' + ')';
|
||||||
|
|
||||||
|
chartOverview.Current.unit =
|
||||||
|
'(' + prefixes[chartOverview['Current'].magnitude] + 'A' + ')';
|
||||||
|
|
||||||
|
return {
|
||||||
|
chartData: chartData,
|
||||||
|
chartOverview: chartOverview
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchJsonDataForOneTime = async (
|
||||||
|
startUnixTime: UnixTime,
|
||||||
|
s3Credentials: I_S3Credentials
|
||||||
|
): Promise<FetchResult<Record<string, DataRecord>>> => {
|
||||||
|
var timeperiodToSearch = 2;
|
||||||
|
let res;
|
||||||
|
let timestampToFetch;
|
||||||
|
|
||||||
|
for (var i = 0; i < timeperiodToSearch; i++) {
|
||||||
|
timestampToFetch = startUnixTime.later(TimeSpan.fromSeconds(i));
|
||||||
|
|
||||||
|
try {
|
||||||
|
res = await fetchDataJson(timestampToFetch, s3Credentials);
|
||||||
|
|
||||||
|
if (res !== FetchResult.notAvailable && res !== FetchResult.tryLater) {
|
||||||
|
//console.log('Successfully fetched ' + timestampToFetch);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching data:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const transformInputToAggregatedDataJson = async (
|
||||||
|
s3Credentials: I_S3Credentials,
|
||||||
|
start_date: dayjs.Dayjs,
|
||||||
|
end_date: dayjs.Dayjs
|
||||||
|
): Promise<{
|
||||||
|
chartAggregatedData: chartAggregatedDataInterface;
|
||||||
|
chartOverview: overviewInterface;
|
||||||
|
dateList: string[];
|
||||||
|
}> => {
|
||||||
|
const data = {};
|
||||||
|
const overviewData = {};
|
||||||
|
const MAX_NUMBER = 9999999;
|
||||||
|
const dateList = [];
|
||||||
|
|
||||||
|
let currentDay = start_date;
|
||||||
|
|
||||||
|
const pathsToSearch = [
|
||||||
|
'MinSoc',
|
||||||
|
'MaxSoc',
|
||||||
|
'PvPower',
|
||||||
|
'DischargingBatteryPower',
|
||||||
|
'ChargingBatteryPower',
|
||||||
|
'GridImportPower',
|
||||||
|
'GridExportPower',
|
||||||
|
'HeatingPower'
|
||||||
|
];
|
||||||
|
|
||||||
|
const categories = [
|
||||||
|
'minsoc',
|
||||||
|
'maxsoc',
|
||||||
|
'pvProduction',
|
||||||
|
'dcChargingPower',
|
||||||
|
'heatingPower',
|
||||||
|
'dcDischargingPower',
|
||||||
|
'gridImportPower',
|
||||||
|
'gridExportPower'
|
||||||
|
];
|
||||||
|
|
||||||
|
const chartAggregatedData: chartAggregatedDataInterface = {
|
||||||
|
minsoc: { name: 'min SOC', data: [] },
|
||||||
|
maxsoc: { name: 'max SOC', data: [] },
|
||||||
|
pvProduction: { name: 'Pv Energy', data: [] },
|
||||||
|
dcChargingPower: { name: 'Charging Battery Energy', data: [] },
|
||||||
|
heatingPower: { name: 'Heating Energy', data: [] },
|
||||||
|
dcDischargingPower: { name: 'Discharging Battery Energy', data: [] },
|
||||||
|
gridImportPower: { name: 'Grid Import Energy', data: [] },
|
||||||
|
gridExportPower: { name: 'Grid Export Energy', data: [] }
|
||||||
|
};
|
||||||
|
|
||||||
|
const chartOverview: overviewInterface = {
|
||||||
|
soc: { magnitude: 0, unit: '', min: 0, max: 0 },
|
||||||
|
temperature: { magnitude: 0, unit: '', min: 0, max: 0 },
|
||||||
|
dcPower: { magnitude: 0, unit: '', min: 0, max: 0 },
|
||||||
|
dcPowerWithoutHeating: { magnitude: 0, unit: '', min: 0, max: 0 },
|
||||||
|
gridPower: { magnitude: 0, unit: '', min: 0, max: 0 },
|
||||||
|
pvProduction: { magnitude: 0, unit: '', min: 0, max: 0 },
|
||||||
|
dcBusVoltage: { magnitude: 0, unit: '', min: 0, max: 0 },
|
||||||
|
overview: { magnitude: 0, unit: '', min: 0, max: 0 },
|
||||||
|
ACLoad: { magnitude: 0, unit: '', min: 0, max: 0 },
|
||||||
|
DCLoad: { magnitude: 0, unit: '', min: 0, max: 0 }
|
||||||
|
};
|
||||||
|
|
||||||
|
pathsToSearch.forEach((path) => {
|
||||||
|
data[path] = [];
|
||||||
|
overviewData[path] = {
|
||||||
|
magnitude: 0,
|
||||||
|
unit: '',
|
||||||
|
min: MAX_NUMBER,
|
||||||
|
max: -MAX_NUMBER
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const timestampPromises = [];
|
||||||
|
|
||||||
|
while (currentDay.isBefore(end_date)) {
|
||||||
|
timestampPromises.push(
|
||||||
|
fetchAggregatedDataJson(currentDay.format('YYYY-MM-DD'), s3Credentials)
|
||||||
|
);
|
||||||
|
currentDay = currentDay.add(1, 'day');
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await Promise.all(timestampPromises);
|
||||||
|
currentDay = start_date;
|
||||||
|
|
||||||
|
for (let i = 0; i < results.length; i++) {
|
||||||
|
const result = results[i];
|
||||||
|
if (
|
||||||
|
result === FetchResult.notAvailable ||
|
||||||
|
result === FetchResult.tryLater
|
||||||
|
) {
|
||||||
|
// Handle not available or try later case
|
||||||
|
} else {
|
||||||
|
console.log(result);
|
||||||
|
dateList.push(currentDay.format('DD-MM'));
|
||||||
|
pathsToSearch.forEach((path) => {
|
||||||
|
const value = path
|
||||||
|
.split('.')
|
||||||
|
.reduce((o, key) => (o ? o[key] : undefined), result);
|
||||||
|
|
||||||
|
if (value !== undefined) {
|
||||||
|
if (path === '.GridExportPower') {
|
||||||
|
result.GridExportPower = -value;
|
||||||
|
}
|
||||||
|
if (value < overviewData[path].min) {
|
||||||
|
overviewData[path].min = value;
|
||||||
|
}
|
||||||
|
if (value > overviewData[path].max) {
|
||||||
|
overviewData[path].max = value;
|
||||||
|
}
|
||||||
|
data[path].push(value as number);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
currentDay = currentDay.add(1, 'day');
|
||||||
|
}
|
||||||
|
|
||||||
|
pathsToSearch.forEach((path) => {
|
||||||
|
let value = Math.max(
|
||||||
|
Math.abs(overviewData[path].max),
|
||||||
|
Math.abs(overviewData[path].min)
|
||||||
|
);
|
||||||
|
let magnitude = 0;
|
||||||
|
|
||||||
|
if (value < 0) {
|
||||||
|
value = -value;
|
||||||
|
}
|
||||||
|
while (value >= 1000) {
|
||||||
|
value /= 1000;
|
||||||
|
magnitude++;
|
||||||
|
}
|
||||||
|
overviewData[path].magnitude = magnitude;
|
||||||
|
});
|
||||||
|
|
||||||
|
let path = 'MinSoc';
|
||||||
|
chartAggregatedData.minsoc.data = data[path];
|
||||||
|
|
||||||
|
path = 'MaxSoc';
|
||||||
|
chartAggregatedData.maxsoc.data = data[path];
|
||||||
|
|
||||||
|
chartOverview.soc = {
|
||||||
|
unit: '(%)',
|
||||||
|
magnitude: overviewData[path].magnitude,
|
||||||
|
min: 0,
|
||||||
|
max: 100
|
||||||
|
};
|
||||||
|
|
||||||
|
path = 'PvPower';
|
||||||
|
chartAggregatedData.pvProduction.data = data[path];
|
||||||
|
|
||||||
|
chartOverview.pvProduction = {
|
||||||
|
magnitude: overviewData[path].magnitude,
|
||||||
|
unit: '(kWh)',
|
||||||
|
min: overviewData[path].min,
|
||||||
|
max: overviewData[path].max
|
||||||
|
};
|
||||||
|
|
||||||
|
path = 'ChargingBatteryPower';
|
||||||
|
chartAggregatedData.dcChargingPower.data = data[path];
|
||||||
|
|
||||||
|
path = 'DischargingBatteryPower';
|
||||||
|
chartAggregatedData.dcDischargingPower.data = data[path];
|
||||||
|
|
||||||
|
path = 'HeatingPower';
|
||||||
|
chartAggregatedData.heatingPower.data = data[path];
|
||||||
|
|
||||||
|
chartOverview.dcPowerWithoutHeating = {
|
||||||
|
magnitude: Math.max(
|
||||||
|
overviewData['ChargingBatteryPower'].magnitude,
|
||||||
|
overviewData['DischargingBatteryPower'].magnitude
|
||||||
|
),
|
||||||
|
unit: '(kWh)',
|
||||||
|
min: overviewData['DischargingBatteryPower'].min,
|
||||||
|
max: overviewData['ChargingBatteryPower'].max
|
||||||
|
};
|
||||||
|
|
||||||
|
chartOverview.dcPower = {
|
||||||
|
magnitude: Math.max(
|
||||||
|
overviewData['ChargingBatteryPower'].magnitude,
|
||||||
|
overviewData['HeatingPower'].magnitude,
|
||||||
|
overviewData['DischargingBatteryPower'].magnitude
|
||||||
|
),
|
||||||
|
unit: '(kWh)',
|
||||||
|
min: overviewData['DischargingBatteryPower'].min,
|
||||||
|
max:
|
||||||
|
overviewData['ChargingBatteryPower'].max +
|
||||||
|
overviewData['HeatingPower'].max
|
||||||
|
};
|
||||||
|
|
||||||
|
path = 'GridImportPower';
|
||||||
|
chartAggregatedData.gridImportPower.data = data[path];
|
||||||
|
|
||||||
|
path = 'GridExportPower';
|
||||||
|
chartAggregatedData.gridExportPower.data = data[path];
|
||||||
|
|
||||||
|
chartOverview.gridPower = {
|
||||||
|
magnitude: Math.max(
|
||||||
|
overviewData['GridImportPower'].magnitude,
|
||||||
|
overviewData['GridExportPower'].magnitude
|
||||||
|
),
|
||||||
|
unit: '(kWh)',
|
||||||
|
min: overviewData['GridExportPower'].min,
|
||||||
|
max: overviewData['GridImportPower'].max
|
||||||
|
};
|
||||||
|
|
||||||
|
chartOverview.overview = {
|
||||||
|
magnitude: 0,
|
||||||
|
unit: '(kWh)',
|
||||||
|
min: Math.min(
|
||||||
|
overviewData['GridImportPower'].min,
|
||||||
|
overviewData['GridExportPower'].min,
|
||||||
|
overviewData['PvPower'].min
|
||||||
|
),
|
||||||
|
max: Math.max(
|
||||||
|
overviewData['GridImportPower'].max,
|
||||||
|
overviewData['GridExportPower'].max,
|
||||||
|
overviewData['PvPower'].max
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
// console.log(chartAggregatedData);
|
||||||
|
|
||||||
|
return {
|
||||||
|
chartAggregatedData: chartAggregatedData,
|
||||||
|
chartOverview: chartOverview,
|
||||||
|
dateList: dateList
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
//UNCOMMENT THE FOLLOWING FOR CSV
|
||||||
// We use this function in order to retrieve data for main stats.
|
// We use this function in order to retrieve data for main stats.
|
||||||
//The data is of the following form:
|
//The data is of the following form:
|
||||||
//'Soc' : {name:'Soc',data:[
|
//'Soc' : {name:'Soc',data:[
|
||||||
|
@ -288,6 +758,17 @@ export const transformInputToBatteryViewData = async (
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// We use this function in order to retrieve data for main stats.
|
||||||
|
// The data is of the following form:
|
||||||
|
// 'Soc' : {name:'Soc',data:[
|
||||||
|
// 'Node1': {name:'Node1', data: [[timestamp,value],[timestamp,value]]},
|
||||||
|
// 'Node2': {name:'Node2', data: [[timestamp,value],[timestamp,value]]},
|
||||||
|
// ]},
|
||||||
|
// 'Temperature' : {name:'Temperature',data:[
|
||||||
|
// 'Node1': {name:'Node1', data: [[timestamp,value],[timestamp,value]]},
|
||||||
|
// 'Node2': {name:'Node2', data: [[timestamp,value],[timestamp,value]]},
|
||||||
|
// ]}
|
||||||
|
|
||||||
const fetchDataForOneTime = async (
|
const fetchDataForOneTime = async (
|
||||||
startUnixTime: UnixTime,
|
startUnixTime: UnixTime,
|
||||||
s3Credentials: I_S3Credentials
|
s3Credentials: I_S3Credentials
|
||||||
|
|
Loading…
Reference in New Issue