From 57b5eb7e95ef2556771a9aaaac0c4b3d88210330 Mon Sep 17 00:00:00 2001 From: Yinyin Liu Date: Wed, 19 Feb 2025 10:52:34 +0100 Subject: [PATCH 1/5] fix Time column in History of Action tab --- .../content/dashboards/History/History.tsx | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/typescript/frontend-marios2/src/content/dashboards/History/History.tsx b/typescript/frontend-marios2/src/content/dashboards/History/History.tsx index e2a953e92..afb629aa6 100644 --- a/typescript/frontend-marios2/src/content/dashboards/History/History.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/History/History.tsx @@ -25,10 +25,15 @@ import Button from '@mui/material/Button'; import { DateTimePicker, LocalizationProvider } from '@mui/x-date-pickers'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import timezone from 'dayjs/plugin/timezone'; import EditIcon from '@mui/icons-material/Edit'; import DeleteIcon from '@mui/icons-material/Delete'; import { UserContext } from '../../../contexts/userContext'; +dayjs.extend(utc); +dayjs.extend(timezone); + interface HistoryProps { errorLoadingS3Data: boolean; id: number; @@ -411,14 +416,18 @@ function HistoryOfActions(props: HistoryProps) {
{history.map((action, index) => { - // Parse the timestamp string to a Date object - const date = new Date(action.timestamp); + // // Parse the timestamp string to a Date object + // 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 = action.userName === currentUser.name ? {} From d6267952e976d2e5eabaccc81e01e1a4c8b904a6 Mon Sep 17 00:00:00 2001 From: Noe Date: Fri, 21 Feb 2025 15:25:40 +0100 Subject: [PATCH 2/5] Frontend supports json for salidomo --- csharp/App/Backend/Controller.cs | 3 +- csharp/App/Backend/Program.cs | 4 +- csharp/App/Backend/deploy.sh | 4 +- csharp/App/SaliMax/src/Ess/StatusRecord.cs | 24 +- csharp/App/SaliMax/src/Program.cs | 3 +- .../SaliMaxRelays/CombinedAdamRelaysRecord.cs | 211 ------ .../src/SaliMaxRelays/IRelaysRecord.cs | 78 --- .../src/SaliMaxRelays/RelaysDeviceADAM6360.cs | 40 -- .../src/SaliMaxRelays/RelaysDeviceAdam6060.cs | 38 -- .../src/SaliMaxRelays/RelaysDeviceAmax.cs | 37 -- .../src/SaliMaxRelays/RelaysRecordAdam6060.cs | 24 - .../SaliMaxRelays/RelaysRecordAdam6360D.cs | 81 --- .../src/SaliMaxRelays/RelaysRecordAmax.cs | 134 ---- .../dbus-fzsonick-48tl/dbus-fzsonick-48tl.py | 251 +++++--- .../dbus-fzsonick-48tl/config.py | 6 +- .../dbus-fzsonick-48tl/dbus-fzsonick-48tl.py | 226 ++++--- typescript/frontend-marios2/deploy.sh | 5 +- typescript/frontend-marios2/package-lock.json | 11 + .../src/Resources/axiosConfig.tsx | 4 +- .../dashboards/BatteryView/BatteryView.tsx | 277 ++++---- .../BatteryView/DetailedBatteryView.tsx | 599 +++++++++--------- .../dashboards/BatteryView/MainStats.tsx | 29 +- .../Configuration/Configuration.tsx | 43 +- .../content/dashboards/History/History.tsx | 36 +- .../dashboards/Installations/Installation.tsx | 50 +- .../dashboards/Installations/fetchData.tsx | 220 +++++-- .../dashboards/Installations/index.tsx | 12 +- .../src/content/dashboards/Log/graph.util.tsx | 109 ++++ .../content/dashboards/Overview/overview.tsx | 44 +- .../dashboards/Overview/salidomoOverview.tsx | 576 ++++++++++++++++- .../SalidomoInstallations/Installation.tsx | 30 +- .../SalidomoInstallations/index.tsx | 10 +- .../FlatInstallationView.tsx | 4 +- .../SodiohomeInstallations/Installation.tsx | 137 +++- .../SodiohomeInstallations/index.tsx | 42 +- .../contexts/InstallationsContextProvider.tsx | 31 +- .../frontend-marios2/src/interfaces/Chart.tsx | 585 +++++++++++++++-- 37 files changed, 2432 insertions(+), 1586 deletions(-) diff --git a/csharp/App/Backend/Controller.cs b/csharp/App/Backend/Controller.cs index 8d1c94f28..cb1ec796e 100644 --- a/csharp/App/Backend/Controller.cs +++ b/csharp/App/Backend/Controller.cs @@ -233,7 +233,8 @@ public class Controller : ControllerBase else { // Define a regex pattern to match the filenames without .csv extension - var pattern = @"/([^/]+)\.csv$"; + + var pattern = @"/([^/]+)\.(csv|json)$"; var regex = new Regex(pattern); // Process each line of the output diff --git a/csharp/App/Backend/Program.cs b/csharp/App/Backend/Program.cs index 1d6b618dd..780ba91fa 100644 --- a/csharp/App/Backend/Program.cs +++ b/csharp/App/Backend/Program.cs @@ -25,8 +25,8 @@ public static class Program Db.Init(); var builder = WebApplication.CreateBuilder(args); - //RabbitMqManager.InitializeEnvironment(); - //RabbitMqManager.StartRabbitMqConsumer().SupressAwaitWarning(); + RabbitMqManager.InitializeEnvironment(); + RabbitMqManager.StartRabbitMqConsumer().SupressAwaitWarning(); WebsocketManager.MonitorSalimaxInstallationTable().SupressAwaitWarning(); WebsocketManager.MonitorSalidomoInstallationTable().SupressAwaitWarning(); diff --git a/csharp/App/Backend/deploy.sh b/csharp/App/Backend/deploy.sh index 52b6913db..ff3675a7d 100755 --- a/csharp/App/Backend/deploy.sh +++ b/csharp/App/Backend/deploy.sh @@ -1,5 +1,5 @@ #To deploy to the monitor server, uncomment the following line -#dotnet publish Backend.csproj -c Release -r linux-x64 --self-contained true -p:PublishTrimmed=false && rsync -av bin/Release/net6.0/linux-x64/publish/ ubuntu@194.182.190.208:~/backend && ssh ubuntu@194.182.190.208 'sudo systemctl restart backend' +dotnet publish Backend.csproj -c Release -r linux-x64 --self-contained true -p:PublishTrimmed=false && rsync -av bin/Release/net6.0/linux-x64/publish/ ubuntu@194.182.190.208:~/backend && ssh ubuntu@194.182.190.208 'sudo systemctl restart backend' #To deploy to the stage server, uncomment the following line -dotnet publish Backend.csproj -c Release -r linux-x64 --self-contained true -p:PublishTrimmed=false && rsync -av bin/Release/net6.0/linux-x64/publish/ ubuntu@91.92.154.141:~/backend && ssh ubuntu@91.92.154.141 'sudo systemctl restart backend' +#dotnet publish Backend.csproj -c Release -r linux-x64 --self-contained true -p:PublishTrimmed=false && rsync -av bin/Release/net6.0/linux-x64/publish/ ubuntu@91.92.154.141:~/backend && ssh ubuntu@91.92.154.141 'sudo systemctl restart backend' diff --git a/csharp/App/SaliMax/src/Ess/StatusRecord.cs b/csharp/App/SaliMax/src/Ess/StatusRecord.cs index 2af17e202..b7c72426c 100644 --- a/csharp/App/SaliMax/src/Ess/StatusRecord.cs +++ b/csharp/App/SaliMax/src/Ess/StatusRecord.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using InnovEnergy.App.SaliMax.Devices; using InnovEnergy.App.SaliMax.SaliMaxRelays; using InnovEnergy.App.SaliMax.System; @@ -29,5 +30,26 @@ public record StatusRecord public required SystemLog Log { get; init; } // TODO: init only public required EssControl EssControl { get; set; } // TODO: init only - public required StateMachine StateMachine { get; init; } + public required StateMachine StateMachine { get; init; } + + + public string ToJson() + { + // Try to get the "Battery" property via reflection + // var batteryProperty = thing.GetType().GetProperty("Battery"); + // if (batteryProperty == null) + // throw new InvalidOperationException("The object does not have a 'Battery' property."); + // + // // Retrieve the value of the Battery property + // var batteryValue = Battery.GetValue(thing); + var jsonOptions = new JsonSerializerOptions { WriteIndented = true }; + + // Serialize the Battery property + Console.WriteLine("eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"); + string json = JsonSerializer.Serialize(this.Battery, jsonOptions); + Console.WriteLine(json); + + + return json; + } } \ No newline at end of file diff --git a/csharp/App/SaliMax/src/Program.cs b/csharp/App/SaliMax/src/Program.cs index 7691b2ce2..9cf80d254 100644 --- a/csharp/App/SaliMax/src/Program.cs +++ b/csharp/App/SaliMax/src/Program.cs @@ -815,7 +815,8 @@ internal static class Program private static async Task UploadCsv(StatusRecord status, DateTime timeStamp) { - + + status.ToJson(); var csv = status.ToCsv().LogInfo(); await RestApiSavingFile(csv); diff --git a/csharp/App/SaliMax/src/SaliMaxRelays/CombinedAdamRelaysRecord.cs b/csharp/App/SaliMax/src/SaliMaxRelays/CombinedAdamRelaysRecord.cs index 23f777169..e69de29bb 100644 --- a/csharp/App/SaliMax/src/SaliMaxRelays/CombinedAdamRelaysRecord.cs +++ b/csharp/App/SaliMax/src/SaliMaxRelays/CombinedAdamRelaysRecord.cs @@ -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 K3InverterIsConnectedToIslandBus => _recordAdam6360D.K3InverterIsConnectedToIslandBus; - -} \ No newline at end of file diff --git a/csharp/App/SaliMax/src/SaliMaxRelays/IRelaysRecord.cs b/csharp/App/SaliMax/src/SaliMaxRelays/IRelaysRecord.cs index df89c283e..e69de29bb 100644 --- a/csharp/App/SaliMax/src/SaliMaxRelays/IRelaysRecord.cs +++ b/csharp/App/SaliMax/src/SaliMaxRelays/IRelaysRecord.cs @@ -1,78 +0,0 @@ -namespace InnovEnergy.App.SaliMax.SaliMaxRelays; - - -public interface IRelaysRecord -{ - Boolean K1GridBusIsConnectedToGrid { get; } - Boolean K2IslandBusIsConnectedToGridBus { get; } - IEnumerable 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(); - -} \ No newline at end of file diff --git a/csharp/App/SaliMax/src/SaliMaxRelays/RelaysDeviceADAM6360.cs b/csharp/App/SaliMax/src/SaliMaxRelays/RelaysDeviceADAM6360.cs index 221646d7c..e69de29bb 100644 --- a/csharp/App/SaliMax/src/SaliMaxRelays/RelaysDeviceADAM6360.cs +++ b/csharp/App/SaliMax/src/SaliMaxRelays/RelaysDeviceADAM6360.cs @@ -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(); - } - } -} - - diff --git a/csharp/App/SaliMax/src/SaliMaxRelays/RelaysDeviceAdam6060.cs b/csharp/App/SaliMax/src/SaliMaxRelays/RelaysDeviceAdam6060.cs index 2533cbd70..e69de29bb 100644 --- a/csharp/App/SaliMax/src/SaliMaxRelays/RelaysDeviceAdam6060.cs +++ b/csharp/App/SaliMax/src/SaliMaxRelays/RelaysDeviceAdam6060.cs @@ -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(); - } - } -} \ No newline at end of file diff --git a/csharp/App/SaliMax/src/SaliMaxRelays/RelaysDeviceAmax.cs b/csharp/App/SaliMax/src/SaliMaxRelays/RelaysDeviceAmax.cs index 9ea91cb83..e69de29bb 100644 --- a/csharp/App/SaliMax/src/SaliMaxRelays/RelaysDeviceAmax.cs +++ b/csharp/App/SaliMax/src/SaliMaxRelays/RelaysDeviceAmax.cs @@ -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(); - } - } -} \ No newline at end of file diff --git a/csharp/App/SaliMax/src/SaliMaxRelays/RelaysRecordAdam6060.cs b/csharp/App/SaliMax/src/SaliMaxRelays/RelaysRecordAdam6060.cs index 2882736fa..e69de29bb 100644 --- a/csharp/App/SaliMax/src/SaliMaxRelays/RelaysRecordAdam6060.cs +++ b/csharp/App/SaliMax/src/SaliMaxRelays/RelaysRecordAdam6060.cs @@ -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); - -} \ No newline at end of file diff --git a/csharp/App/SaliMax/src/SaliMaxRelays/RelaysRecordAdam6360D.cs b/csharp/App/SaliMax/src/SaliMaxRelays/RelaysRecordAdam6360D.cs index 1adba57a9..e69de29bb 100644 --- a/csharp/App/SaliMax/src/SaliMaxRelays/RelaysRecordAdam6360D.cs +++ b/csharp/App/SaliMax/src/SaliMaxRelays/RelaysRecordAdam6360D.cs @@ -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 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); - -} - - diff --git a/csharp/App/SaliMax/src/SaliMaxRelays/RelaysRecordAmax.cs b/csharp/App/SaliMax/src/SaliMaxRelays/RelaysRecordAmax.cs index cc02cab48..e69de29bb 100644 --- a/csharp/App/SaliMax/src/SaliMaxRelays/RelaysRecordAmax.cs +++ b/csharp/App/SaliMax/src/SaliMaxRelays/RelaysRecordAmax.cs @@ -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 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); - -} diff --git a/firmware/Cerbo_Release/CerboReleaseFiles/dbus-fzsonick-48tl/dbus-fzsonick-48tl.py b/firmware/Cerbo_Release/CerboReleaseFiles/dbus-fzsonick-48tl/dbus-fzsonick-48tl.py index 0b3054421..6e5185fcf 100755 --- a/firmware/Cerbo_Release/CerboReleaseFiles/dbus-fzsonick-48tl/dbus-fzsonick-48tl.py +++ b/firmware/Cerbo_Release/CerboReleaseFiles/dbus-fzsonick-48tl/dbus-fzsonick-48tl.py @@ -23,21 +23,10 @@ from os import path app_dir = path.dirname(path.realpath(__file__)) sys.path.insert(1, path.join(app_dir, 'ext', 'velib_python')) - from vedbus import VeDbusService as DBus - -import time import os import csv - -import requests -import hmac -import hashlib -import base64 -from datetime import datetime -import io import json - import requests import hmac import hashlib @@ -45,22 +34,18 @@ import base64 from datetime import datetime import pika import time - - # zip-comp additions import zipfile import io -import shutil -def compress_csv_data(csv_data, file_name="data.csv"): +def compress_json_data(json_data, file_name="data.json"): memory_stream = io.BytesIO() - # Create a zip archive in the memory buffer with zipfile.ZipFile(memory_stream, 'w', zipfile.ZIP_DEFLATED) as archive: - # Add CSV data to the ZIP archive - with archive.open('data.csv', 'w') as entry_stream: - entry_stream.write(csv_data.encode('utf-8')) + # Add JSON data to the ZIP archive + with archive.open('data.json', 'w') as entry_stream: + entry_stream.write(json_data.encode('utf-8')) # Get the compressed byte array from the memory buffer compressed_bytes = memory_stream.getvalue() @@ -112,9 +97,9 @@ class S3config: ).decode() return f"AWS {s3_key}:{signature}" -def read_csv_as_string(file_path): +def read_json_as_string(file_path): """ - Reads a CSV file from the given path and returns its content as a single string. + Reads a JSON file from the given path and returns its content as a single string. """ try: with open(file_path, 'r', encoding='utf-8') as file: @@ -126,8 +111,7 @@ def read_csv_as_string(file_path): print(f"IO error occurred: {str(e)}") return None -CSV_DIR = "/data/csv_files/" -#CSV_DIR = "csv_files/" +JSON_DIR = "/data/json_files/" # Define the path to the file containing the installation name INSTALLATION_NAME_FILE = '/data/innovenergy/openvpn/installation-name' @@ -645,7 +629,7 @@ def read_battery_status(modbus, battery): return BatteryStatus(battery, data.registers) except Exception as e: logging.error(f"An error occurred: {e}") - create_batch_of_csv_files() # Call this only if there's an error + create_batch_of_json_files() # Call this only if there's an error raise finally: modbus.close() # close in any case @@ -659,7 +643,7 @@ def publish_values(dbus, signals, statuses): previous_warnings = {} previous_alarms = {} -num_of_csv_files_saved=0 +num_of_json_files_saved=0 class MessageType: ALARM_OR_WARNING = "AlarmOrWarning" @@ -878,7 +862,8 @@ def update(modbus, batteries, dbus, signals, csv_signals): status_message, alarms_number_list, warnings_number_list = update_state_from_dictionaries(current_warnings, current_alarms, node_numbers) publish_values(dbus, signals, statuses) - create_csv_files(csv_signals, statuses, node_numbers, alarms_number_list, warnings_number_list) + + create_json_files(csv_signals, statuses, node_numbers, alarms_number_list, warnings_number_list) logging.debug('finished update cycle\n') return True @@ -907,59 +892,59 @@ def count_files_in_folder(folder_path): except Exception as e: return str(e) -def create_batch_of_csv_files(): +def create_batch_of_json_files(): - global prev_status,INSTALLATION_ID, PRODUCT_ID, num_of_csv_files_saved + global prev_status,INSTALLATION_ID, PRODUCT_ID, num_of_json_files_saved # list all files in the directory - files = os.listdir(CSV_DIR) + files = os.listdir(JSON_DIR) - # filter out only csv files - csv_files = [file for file in files if file.endswith('.csv')] + # filter out only json files + json_files = [file for file in files if file.endswith('.json')] - # sort csv files by creation time - csv_files.sort(key=lambda x: os.path.getctime(os.path.join(CSV_DIR, x))) + # sort json files by creation time + json_files.sort(key=lambda x: os.path.getctime(os.path.join(JSON_DIR, x))) # keep the 600 MOST RECENT FILES - recent_csv_files = csv_files[-num_of_csv_files_saved:] - print("num_of_csv_files_saved is " + str(num_of_csv_files_saved)) + recent_json_files = json_files[-num_of_json_files_saved:] + print("num_of_json_files_saved is " + str(num_of_json_files_saved)) - # get the name of the first csv file - if not csv_files: - print("No csv files found in the directory.") + # get the name of the first json file + if not json_files: + print("No json files found in the directory.") exit(0) - first_csv_file = os.path.join(CSV_DIR, recent_csv_files.pop(0)) - first_csv_filename = os.path.basename(first_csv_file) + first_json_file = os.path.join(JSON_DIR, recent_json_files.pop(0)) + first_json_filename = os.path.basename(first_json_file) - temp_file_path = os.path.join(CSV_DIR, 'temp_batch_file.csv') + temp_file_path = os.path.join(JSON_DIR, 'temp_batch_file.json') # create a temporary file and write the timestamp and the original content of the first file with open(temp_file_path, 'wb') as temp_file: # Write the timestamp (filename) at the beginning - numeric_part = first_csv_filename.split('.')[0] + numeric_part = first_json_filename.split('.')[0] temp_file.write(f'Timestamp;{numeric_part}\n'.encode('utf-8')) # write the original content of the first csv file - with open(first_csv_file, 'rb') as f: + with open(first_json_file, 'rb') as f: temp_file.write(f.read()) - for csv_file in recent_csv_files: - file_path = os.path.join(CSV_DIR, csv_file) + for json_file in recent_json_files: + file_path = os.path.join(JSON_DIR, json_file) # write an empty line temp_file.write(b'\n') # write the timestamp (filename) - numeric_part = csv_file.split('.')[0] + numeric_part = json_file.split('.')[0] temp_file.write(f'Timestamp;{numeric_part}\n'.encode('utf-8')) # write the content of the file with open(file_path, 'rb') as f: temp_file.write(f.read()) - # replace the original first csv file with the temporary file - os.remove(first_csv_file) - os.rename(temp_file_path, first_csv_file) - num_of_csv_files_saved = 0 + # replace the original first json file with the temporary file + os.remove(first_json_file) + os.rename(temp_file_path, first_json_file) + num_of_json_files_saved = 0 # create a loggin directory that contains at max 20 batch files for logging info - # logging_dir = os.path.join(CSV_DIR, 'logging_batch_files') + # logging_dir = os.path.join(JSON_DIR, 'logging_batch_files') # if not os.path.exists(logging_dir): # os.makedirs(logging_dir) # @@ -967,23 +952,23 @@ def create_batch_of_csv_files(): # manage_csv_files(logging_dir) # prepare for compression - csv_data = read_csv_as_string(first_csv_file) + json_data = read_json_as_string(first_json_file) - if csv_data is None: - print("error while reading csv as string") + if json_data is None: + print("error while reading json as string") return # zip-comp additions - compressed_csv = compress_csv_data(csv_data) - # Use the name of the last (most recent) CSV file in sorted csv_files as the name for the compressed file - last_csv_file_name = os.path.basename(recent_csv_files[-1]) if recent_csv_files else first_csv_filename + compressed_json = compress_json_data(json_data) + # Use the name of the last (most recent) JSON file in sorted json_files as the name for the compressed file + last_json_file_name = os.path.basename(recent_json_files[-1]) if recent_json_files else first_json_filename - numeric_part = int(last_csv_file_name.split('.')[0][:-2]) - compressed_filename = "{}.csv".format(numeric_part) + numeric_part = int(last_json_file_name.split('.')[0][:-2]) + compressed_filename = "{}.json".format(numeric_part) - response = s3_config.create_put_request(compressed_filename, compressed_csv) + response = s3_config.create_put_request(compressed_filename, compressed_json) if response.status_code == 200: - os.remove(first_csv_file) + os.remove(first_json_file) print("Successfully uploaded the compresseed batch of files in s3") status_message = { "InstallationId": INSTALLATION_ID, @@ -1001,14 +986,14 @@ def create_batch_of_csv_files(): channel.basic_publish(exchange="", routing_key="statusQueue", body=status_message) print("Successfully sent the heartbit with timestamp") else: - # we save data that were not successfully uploaded in s3 in a failed directory inside the CSV_DIR for logging - failed_dir = os.path.join(CSV_DIR, "failed") + # we save data that were not successfully uploaded in s3 in a failed directory inside the JSON_DIR for logging + failed_dir = os.path.join(JSON_DIR, "failed") if not os.path.exists(failed_dir): os.makedirs(failed_dir) - failed_path = os.path.join(failed_dir, first_csv_filename) - os.rename(first_csv_file, failed_path) + failed_path = os.path.join(failed_dir, first_json_filename) + os.rename(first_json_file, failed_path) print("Uploading failed") - manage_csv_files(failed_dir, 100) + manage_json_files(failed_dir, 100) alive = True # global alive flag, watchdog_task clears it, update_task sets it @@ -1033,11 +1018,11 @@ def create_update_task(modbus, dbus, batteries, signals, csv_signals, main_loop) elapsed_time = time.time() - start_time print("11111111111111111111111111111111111111111111 elapsed time is ", elapsed_time) - # keep at most 1900 files at CSV_DIR for logging and aggregation - manage_csv_files(CSV_DIR, 1900) + # keep at most 1900 files at JSON_DIR for logging and aggregation + manage_json_files(JSON_DIR, 1900) if elapsed_time >= 1200: print("CREATE BATCH ======================================>") - create_batch_of_csv_files() + create_batch_of_json_files() start_time = time.time() #alive = update_for_testing(modbus, batteries, dbus, signals, csv_signals) if not alive: @@ -1070,12 +1055,12 @@ def get_installation_name(file_path): with open(file_path, 'r') as file: return file.read().strip() -def manage_csv_files(directory_path, max_files=20): - csv_files = [f for f in os.listdir(directory_path) if os.path.isfile(os.path.join(directory_path, f))] - csv_files.sort(key=lambda x: os.path.getctime(os.path.join(directory_path, x))) +def manage_json_files(directory_path, max_files=20): + json_files = [f for f in os.listdir(directory_path) if os.path.isfile(os.path.join(directory_path, f))] + json_files.sort(key=lambda x: os.path.getctime(os.path.join(directory_path, x))) # Remove oldest files if exceeds maximum - while len(csv_files) > max_files: - file_to_delete = os.path.join(directory_path, csv_files.pop(0)) + while len(json_files) > max_files: + file_to_delete = os.path.join(directory_path, json_files.pop(0)) os.remove(file_to_delete) def insert_id(path, id_number): @@ -1084,36 +1069,102 @@ def insert_id(path, id_number): parts.insert(insert_position, str(id_number)) return "/".join(parts) -def create_csv_files(signals, statuses, node_numbers, alarms_number_list, warnings_number_list): - global s3_config, num_of_csv_files_saved +def insert_nested_data(data, split_list, value, symbol): + key = split_list[0] # Get the first key in the list + + if len(split_list) == 1: + data[key] = { + "value": round(value, 2) if isinstance(value, float) else value, + #"symbol": str(symbol) + } + else: + if key not in data: + data[key] = {} # Create a new dictionary if key doesn't exist + insert_nested_data(data[key], split_list[1:], value, symbol) + +def create_json_files(signals, statuses, node_numbers, alarms_number_list, warnings_number_list): + + global num_of_json_files_saved timestamp = int(time.time()) if timestamp % 2 != 0: timestamp -= 1 - # Create CSV directory if it doesn't exist - if not os.path.exists(CSV_DIR): - os.makedirs(CSV_DIR) - csv_filename = f"{timestamp}.csv" - csv_path = os.path.join(CSV_DIR, csv_filename) - num_of_csv_files_saved+=1 + if not os.path.exists(JSON_DIR): + os.makedirs(JSON_DIR) + json_filename = "{}.json".format(timestamp) + json_path = os.path.join(JSON_DIR, json_filename) + num_of_json_files_saved += 1 - # Append values to the CSV file - if not os.path.exists(csv_path): - with open(csv_path, 'a', newline='') as csvfile: - csv_writer = csv.writer(csvfile, delimiter=';') - # Add a special row for the nodes configuration - nodes_config_path = "/Config/Devices/BatteryNodes" - nodes_list = ",".join(str(node) for node in node_numbers) - config_row = [nodes_config_path, nodes_list, ""] - csv_writer.writerow(config_row) - # Iterate over each node and signal to create rows in the new format - for i, node in enumerate(node_numbers): - csv_writer.writerow([f"/Battery/Devices/{str(i+1)}/Alarms", alarms_number_list[i], ""]) - csv_writer.writerow([f"/Battery/Devices/{str(i+1)}/Warnings", warnings_number_list[i], ""]) - for s in signals: - signal_name = insert_id(s.name, i+1) - value = s.get_value(statuses[i]) - row_values = [signal_name, value, s.get_text] - csv_writer.writerow(row_values) + data = { + "Battery": { + "Devices": {} + } + } + + # Iterate over each node and construct the data structure + for i, node in enumerate(node_numbers): + print("Inside json generation file, node num is", i, " and node is ", node) + device_data = {} # This dictionary will hold the data for a specific device + + # Add Alarms and Warnings for this device + device_data["Alarms"] = alarms_number_list[i] + device_data["Warnings"] = warnings_number_list[i] + + # Iterate over the signals and add their values + for s in signals: + split_list = s.name.split("/")[3:] + # print(split_list) + value = s.get_value(statuses[i]) + symbol = s.get_text + insert_nested_data(device_data, split_list, value, symbol) + + # print(device_data) + + # Add this device's data to the "Devices" section + data["Battery"]["Devices"][str(i + 1)] = device_data + + # Add the node configuration row (optional) + nodes_config_path = "/Config/Devices/BatteryNodes" + nodes_list = [str(node) for node in node_numbers] + + insert_nested_data(data, nodes_config_path.split("/")[1:], nodes_list, "") + # data[nodes_config_path] = nodes_list + # print(json.dumps(data, indent=4)) + + # Write the JSON data to the file + with open(json_path, 'w') as jsonfile: + # json.dump(data, jsonfile, indent=4) + json.dump(data, jsonfile, separators=(',', ':')) +# +# def create_csv_files(signals, statuses, node_numbers, alarms_number_list, warnings_number_list): +# global s3_config, num_of_csv_files_saved +# timestamp = int(time.time()) +# if timestamp % 2 != 0: +# timestamp -= 1 +# # Create CSV directory if it doesn't exist +# if not os.path.exists(JSON_DIR): +# os.makedirs(JSON_DIR) +# csv_filename = f"{timestamp}.csv" +# csv_path = os.path.join(JSON_DIR, csv_filename) +# num_of_csv_files_saved+=1 +# +# # Append values to the CSV file +# if not os.path.exists(csv_path): +# with open(csv_path, 'a', newline='') as csvfile: +# csv_writer = csv.writer(csvfile, delimiter=';') +# # Add a special row for the nodes configuration +# nodes_config_path = "/Config/Devices/BatteryNodes" +# nodes_list = ",".join(str(node) for node in node_numbers) +# config_row = [nodes_config_path, nodes_list, ""] +# csv_writer.writerow(config_row) +# # Iterate over each node and signal to create rows in the new format +# for i, node in enumerate(node_numbers): +# csv_writer.writerow([f"/Battery/Devices/{str(i+1)}/Alarms", alarms_number_list[i], ""]) +# csv_writer.writerow([f"/Battery/Devices/{str(i+1)}/Warnings", warnings_number_list[i], ""]) +# for s in signals: +# signal_name = insert_id(s.name, i+1) +# value = s.get_value(statuses[i]) +# row_values = [signal_name, value, s.get_text] +# csv_writer.writerow(row_values) BATTERY_COUNTS_FILE = '/data/battery_count.csv' def load_battery_counts(): diff --git a/firmware/Venus_Release/VenusReleaseFiles/dbus-fzsonick-48tl/config.py b/firmware/Venus_Release/VenusReleaseFiles/dbus-fzsonick-48tl/config.py index 120bde255..b59d28196 100755 --- a/firmware/Venus_Release/VenusReleaseFiles/dbus-fzsonick-48tl/config.py +++ b/firmware/Venus_Release/VenusReleaseFiles/dbus-fzsonick-48tl/config.py @@ -54,6 +54,6 @@ INNOVENERGY_PROTOCOL_VERSION = '48TL200V3' # S3 Credentials -S3BUCKET = "436-c0436b6a-d276-4cd8-9c44-1eae86cf5d0e" -S3KEY = "EXO6bb2b06f3cebfdbbc8a9b240" -S3SECRET = "m6bEzM8z9t2lCQ13OptMcZcNf80p_TSjaMDtZTNdEjo" +S3BUCKET = "357-c0436b6a-d276-4cd8-9c44-1eae86cf5d0e" +S3KEY = "EXOd9cc93bec729d1bb9ad337d0" +S3SECRET = "Sgah7AmC7vUvnYqR_JmqZMOHpnUX3ERJZJynDUD3QdI" diff --git a/firmware/Venus_Release/VenusReleaseFiles/dbus-fzsonick-48tl/dbus-fzsonick-48tl.py b/firmware/Venus_Release/VenusReleaseFiles/dbus-fzsonick-48tl/dbus-fzsonick-48tl.py index 094e9e617..68c986cc5 100755 --- a/firmware/Venus_Release/VenusReleaseFiles/dbus-fzsonick-48tl/dbus-fzsonick-48tl.py +++ b/firmware/Venus_Release/VenusReleaseFiles/dbus-fzsonick-48tl/dbus-fzsonick-48tl.py @@ -34,7 +34,7 @@ import json from convert import first import shutil -CSV_DIR = "/data/csv_files/" +JSON_DIR = "/data/json_files/" INSTALLATION_NAME_FILE = '/data/innovenergy/openvpn/installation-name' # trick the pycharm type-checker into thinking Callable is in scope, not used at runtime @@ -45,13 +45,13 @@ if False: RESET_REGISTER = 0x2087 -def compress_csv_data(csv_data, file_name="data.csv"): +def compress_json_data(json_data, file_name="data.json"): memory_stream = io.BytesIO() # Create a zip archive in the memory buffer with zipfile.ZipFile(memory_stream, 'w', zipfile.ZIP_DEFLATED) as archive: - # Add CSV data to the ZIP archive using writestr - archive.writestr(file_name, csv_data.encode('utf-8')) + # Add JSON data to the ZIP archive using writestr + archive.writestr(file_name, json_data.encode('utf-8')) # Get the compressed byte array from the memory buffer compressed_bytes = memory_stream.getvalue() @@ -155,7 +155,7 @@ INSTALLATION_ID = int(s3_config.bucket.split('-')[0]) PRODUCT_ID = 1 is_first_update = True prev_status = 0 -num_of_csv_files_saved = 0 +num_of_json_files_saved = 0 def update_state_from_dictionaries(current_warnings, current_alarms, node_numbers): @@ -251,9 +251,9 @@ def update_state_from_dictionaries(current_warnings, current_alarms, node_number return status_message, alarms_number_list, warnings_number_list -def read_csv_as_string(file_path): +def read_json_as_string(file_path): """ - Reads a CSV file from the given path and returns its content as a single string. + Reads a JSON file from the given path and returns its content as a single string. """ try: # Note: 'encoding' is not available in open() in Python 2.7, so we'll use 'codecs' module. @@ -555,16 +555,17 @@ def create_update_task(modbus, service, batteries): publish_values_on_dbus(service, _signals, statuses) elapsed_time = time.time() - start_time - create_csv_files(csv_signals, statuses, node_numbers, alarms_number_list, warnings_number_list) + #create_csv_files(csv_signals, statuses, node_numbers, alarms_number_list, warnings_number_list) + create_json_files(csv_signals, statuses, node_numbers, alarms_number_list, warnings_number_list) print("11111111111111111111111111111111111111111111 elapsed time is ", elapsed_time) - # keep at most 1900 files at CSV_DIR for logging and aggregation - manage_csv_files(CSV_DIR, 1900) + # keep at most 1900 files at JSON_DIR for logging and aggregation + manage_json_files(JSON_DIR, 1900) - num_files_in_csv_dir = count_files_in_folder(CSV_DIR) + num_files_in_json_dir = count_files_in_folder(JSON_DIR) if elapsed_time >= 1200: print("CREATE BATCH ======================================>") - create_batch_of_csv_files() + create_batch_of_json_files() start_time = time.time() upload_status_to_innovenergy(_socket, statuses) @@ -574,10 +575,10 @@ def create_update_task(modbus, service, batteries): alive = True except pika.exceptions.AMQPConnectionError: logging.error("AMQPConnectionError encountered. Subscribing to queue.") - create_batch_of_csv_files() + create_batch_of_json_files() except Exception as e: - create_batch_of_csv_files() + create_batch_of_json_files() logging.error("Unexpected error") raise @@ -587,14 +588,14 @@ def create_update_task(modbus, service, batteries): return update_task -def manage_csv_files(directory_path, max_files=20): - csv_files = [f for f in os.listdir(directory_path) if os.path.isfile(os.path.join(directory_path, f))] - csv_files.sort(key=lambda x: os.path.getctime(os.path.join(directory_path, x))) +def manage_json_files(directory_path, max_files=20): + json_files = [f for f in os.listdir(directory_path) if os.path.isfile(os.path.join(directory_path, f))] + json_files.sort(key=lambda x: os.path.getctime(os.path.join(directory_path, x))) - print("len of csv files is " + str(len(csv_files))) + print("len of json files is " + str(len(json_files))) # Remove oldest files if exceeds maximum - while len(csv_files) > max_files: - file_to_delete = os.path.join(directory_path, csv_files.pop(0)) + while len(json_files) > max_files: + file_to_delete = os.path.join(directory_path, json_files.pop(0)) os.remove(file_to_delete) @@ -605,89 +606,78 @@ def insert_id(path, id_number): return "/".join(parts) -def create_batch_of_csv_files(): - global prev_status, channel, INSTALLATION_ID, PRODUCT_ID, num_of_csv_files_saved +def create_batch_of_json_files(): + global prev_status, channel, INSTALLATION_ID, PRODUCT_ID, num_of_json_files_saved # list all files in the directory - files = os.listdir(CSV_DIR) + files = os.listdir(JSON_DIR) - # filter out only csv files - csv_files = [file for file in files if file.endswith('.csv')] + # filter out only json files + json_files = [file for file in files if file.endswith('.json')] - # sort csv files by creation time - csv_files.sort(key=lambda x: os.path.getctime(os.path.join(CSV_DIR, x))) + # sort json files by creation time + json_files.sort(key=lambda x: os.path.getctime(os.path.join(JSON_DIR, x))) - # keep the num_of_csv_files_saved MOST RECENT FILES - recent_csv_files = csv_files[-num_of_csv_files_saved:] - print("num_of_csv_files_saved is " + str(num_of_csv_files_saved)) + # keep the recent_json_files MOST RECENT FILES + recent_json_files = json_files[-num_of_json_files_saved:] + print("num_of_json_files_saved is " + str(recent_json_files)) - # get the name of the first csv file - if not csv_files: - print("No csv files found in the directory.") + # get the name of the first json file + if not json_files: + print("No json files found in the directory.") exit(0) - first_csv_file = os.path.join(CSV_DIR, recent_csv_files.pop(0)) - first_csv_filename = os.path.basename(first_csv_file) + first_json_file = os.path.join(JSON_DIR, recent_json_files.pop(0)) + first_json_filename = os.path.basename(first_json_file) - temp_file_path = os.path.join(CSV_DIR, 'temp_batch_file.csv') + temp_file_path = os.path.join(JSON_DIR, 'temp_batch_file.json') # create a temporary file and write the timestamp and the original content of the first file with open(temp_file_path, 'wb') as temp_file: # Write the timestamp (filename) at the beginning - temp_file.write('Timestamp;{}\n'.format(first_csv_filename.split('.')[0])) - # write the original content of the first csv file - with open(first_csv_file, 'rb') as f: + temp_file.write('Timestamp;{}\n'.format(first_json_filename.split('.')[0])) + # write the original content of the first json file + with open(first_json_file, 'rb') as f: temp_file.write(f.read()) - for csv_file in recent_csv_files: - file_path = os.path.join(CSV_DIR, csv_file) + for json_file in recent_json_files: + file_path = os.path.join(JSON_DIR, json_file) # write an empty line temp_file.write('\n') # write the timestamp (filename) - temp_file.write('Timestamp;{}\n'.format(csv_file.split('.')[0])) + temp_file.write('Timestamp;{}\n'.format(json_file.split('.')[0])) # write the content of the file with open(file_path, 'rb') as f: temp_file.write(f.read()) - # replace the original first csv file with the temporary file - os.remove(first_csv_file) - os.rename(temp_file_path, first_csv_file) - num_of_csv_files_saved = 0 - # create a loggin directory that contains at max 20 batch files for logging info - # logging_dir = os.path.join(CSV_DIR, 'logging_batch_files') - # if not os.path.exists(logging_dir): - # os.makedirs(logging_dir) - # - # shutil.copy(first_csv_file, logging_dir) - # manage_csv_files(logging_dir) - - # print("The batch csv file is: {}".format(recent_csv_files[-1])) + # replace the original first json file with the temporary file + os.remove(first_json_file) + os.rename(temp_file_path, first_json_file) + num_of_json_files_saved = 0 # prepare for compression - csv_data = read_csv_as_string(first_csv_file) + json_data = read_json_as_string(first_json_file) - if csv_data is None: - print("error while reading csv as string") + if json_data is None: + print("error while reading json as string") return # zip-comp additions - compressed_csv = compress_csv_data(csv_data) - # Use the name of the last (most recent) CSV file in sorted csv_files as the name for the compressed file - last_csv_file_name = os.path.basename(recent_csv_files[-1]) if recent_csv_files else first_csv_filename + compressed_json = compress_json_data(json_data) + # Use the name of the last (most recent) JSON file in sorted json_files as the name for the compressed file + last_json_file_name = os.path.basename(recent_json_files[-1]) if recent_json_files else first_json_filename - # we send the csv files every 30 seconds and the timestamp is adjusted to be a multiple of 30 - numeric_part = int(last_csv_file_name.split('.')[0][:-2]) + # we send the json files every 30 seconds and the timestamp is adjusted to be a multiple of 30 + numeric_part = int(last_json_file_name.split('.')[0][:-2]) - # compressed_filename = "{}.csv".format(new_numeric_part) - compressed_filename = "{}.csv".format(numeric_part) + compressed_filename = "{}.json".format(numeric_part) print("FILE NAME =========================================================> ", compressed_filename) - response = s3_config.create_put_request(compressed_filename, compressed_csv) - # response = s3_config.create_put_request(first_csv_filename, csv_data) + response = s3_config.create_put_request(compressed_filename, compressed_json) print(response) if response.status_code == 200: - os.remove(first_csv_file) + os.remove(first_json_file) print("Successfully uploaded the compresseed batch of files in s3") status_message = { "InstallationId": INSTALLATION_ID, @@ -709,42 +699,80 @@ def create_batch_of_csv_files(): print("Successfully sent the heartbit with timestamp") else: - # we save data that were not successfully uploaded in s3 in a failed directory inside the CSV_DIR for logging - failed_dir = os.path.join(CSV_DIR, "failed") + #we save data that were not successfully uploaded in s3 in a failed directory inside the JSON_DIR for logging + failed_dir = os.path.join(JSON_DIR, "failed") if not os.path.exists(failed_dir): os.makedirs(failed_dir) - failed_path = os.path.join(failed_dir, first_csv_filename) - os.rename(first_csv_file, failed_path) + failed_path = os.path.join(failed_dir, first_json_filename) + os.rename(first_json_file, failed_path) print("Uploading failed") - manage_csv_files(failed_dir, 100) + manage_json_files(failed_dir, 100) +def insert_nested_data(data, split_list, value, symbol): + key = split_list[0] # Get the first key in the list -def create_csv_files(signals, statuses, node_numbers, alarms_number_list, warnings_number_list): - global num_of_csv_files_saved + if len(split_list) == 1: + data[key] = { + "value": round(value, 2) if isinstance(value, float) else value, + #"symbol": str(symbol) + } + else: + if key not in data: + data[key] = {} # Create a new dictionary if key doesn't exist + insert_nested_data(data[key], split_list[1:], value, symbol) + +def create_json_files(signals, statuses, node_numbers, alarms_number_list, warnings_number_list): + + global num_of_json_files_saved timestamp = int(time.time()) if timestamp % 2 != 0: timestamp -= 1 - if not os.path.exists(CSV_DIR): - os.makedirs(CSV_DIR) - csv_filename = "{}.csv".format(timestamp) - csv_path = os.path.join(CSV_DIR, csv_filename) - num_of_csv_files_saved += 1 + if not os.path.exists(JSON_DIR): + os.makedirs(JSON_DIR) + json_filename = "{}.json".format(timestamp) + json_path = os.path.join(JSON_DIR, json_filename) + num_of_json_files_saved += 1 - if not os.path.exists(csv_path): - with open(csv_path, 'ab') as csvfile: - csv_writer = csv.writer(csvfile, delimiter=';') - nodes_config_path = "/Config/Devices/BatteryNodes" - nodes_list = ",".join(str(node) for node in node_numbers) - config_row = [nodes_config_path, nodes_list, ""] - csv_writer.writerow(config_row) - for i, node in enumerate(node_numbers): - csv_writer.writerow(["/Battery/Devices/{}/Alarms".format(str(i + 1)), alarms_number_list[i], ""]) - csv_writer.writerow(["/Battery/Devices/{}/Warnings".format(str(i + 1)), warnings_number_list[i], ""]) - for s in signals: - signal_name = insert_id(s.name, i + 1) - value = s.get_value(statuses[i]) - row_values = [signal_name, value, s.get_text] - csv_writer.writerow(row_values) + data = { + "Battery": { + "Devices": {} + } + } + + # Iterate over each node and construct the data structure + for i, node in enumerate(node_numbers): + print("Inside json generation file, node num is" , i," and node is ", node) + device_data = {} # This dictionary will hold the data for a specific device + + # Add Alarms and Warnings for this device + device_data["Alarms"] = alarms_number_list[i] + device_data["Warnings"] = warnings_number_list[i] + + # Iterate over the signals and add their values + for s in signals: + split_list = s.name.split("/")[3:] + #print(split_list) + value = s.get_value(statuses[i]) + symbol=s.get_text + insert_nested_data(device_data, split_list, value, symbol) + + #print(device_data) + + # Add this device's data to the "Devices" section + data["Battery"]["Devices"][str(i + 1)] = device_data + + # Add the node configuration row (optional) + nodes_config_path = "/Config/Devices/BatteryNodes" + nodes_list = [str(node) for node in node_numbers] + + insert_nested_data(data,nodes_config_path.split("/")[1:], nodes_list, "") + #data[nodes_config_path] = nodes_list + # print(json.dumps(data, indent=4)) + + # Write the JSON data to the file + with open(json_path, 'w') as jsonfile: + #json.dump(data, jsonfile, indent=4) + json.dump(data, jsonfile, separators=(',', ':')) def create_watchdog_task(main_loop): @@ -796,14 +824,6 @@ def main(argv): logging.basicConfig(level=cfg.LOG_LEVEL) logging.info('starting ' + __file__) - # tty = parse_cmdline_args(argv) - # modbus = init_modbus(tty) - - # batteries = identify_batteries(modbus) - - # if len(batteries) <= 0: - # sys.exit(2) - tty = parse_cmdline_args(argv) battery_counts = load_battery_counts() max_retry_attempts = 3 # Stop retrying in case it's a real battery loss case diff --git a/typescript/frontend-marios2/deploy.sh b/typescript/frontend-marios2/deploy.sh index 00a440a01..13a10bf39 100755 --- a/typescript/frontend-marios2/deploy.sh +++ b/typescript/frontend-marios2/deploy.sh @@ -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' diff --git a/typescript/frontend-marios2/package-lock.json b/typescript/frontend-marios2/package-lock.json index b8a7c6960..f8632153f 100644 --- a/typescript/frontend-marios2/package-lock.json +++ b/typescript/frontend-marios2/package-lock.json @@ -20,6 +20,7 @@ "axios": "^1.5.0", "chart.js": "^4.4.0", "clsx": "1.1.1", + "crypto-js": "^4.2.0", "cytoscape": "^3.26.0", "date-fns": "^2.28.0", "dayjs": "^1.11.10", @@ -7318,6 +7319,11 @@ "node": ">= 8" } }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" + }, "node_modules/crypto-random-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", @@ -26136,6 +26142,11 @@ "which": "^2.0.1" } }, + "crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" + }, "crypto-random-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", diff --git a/typescript/frontend-marios2/src/Resources/axiosConfig.tsx b/typescript/frontend-marios2/src/Resources/axiosConfig.tsx index 9f230ab85..0d4606588 100644 --- a/typescript/frontend-marios2/src/Resources/axiosConfig.tsx +++ b/typescript/frontend-marios2/src/Resources/axiosConfig.tsx @@ -1,12 +1,12 @@ import axios from 'axios'; export const axiosConfigWithoutToken = axios.create({ - baseURL: 'https://stage.innov.energy/api' + baseURL: 'https://monitor.innov.energy/api' //baseURL: 'http://127.0.0.1:7087/api' }); const axiosConfig = axios.create({ - baseURL: 'https://stage.innov.energy/api' + baseURL: 'https://monitor.innov.energy/api' //baseURL: 'http://127.0.0.1:7087/api' }); diff --git a/typescript/frontend-marios2/src/content/dashboards/BatteryView/BatteryView.tsx b/typescript/frontend-marios2/src/content/dashboards/BatteryView/BatteryView.tsx index e01a7b65f..652504a90 100644 --- a/typescript/frontend-marios2/src/content/dashboards/BatteryView/BatteryView.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/BatteryView/BatteryView.tsx @@ -11,7 +11,6 @@ import { TableRow, Typography } from '@mui/material'; -import { TopologyValues } from '../Log/graph.util'; import { Link, Route, @@ -19,16 +18,17 @@ import { useLocation, useNavigate } from 'react-router-dom'; -import Button from '@mui/material/Button'; -import { FormattedMessage } from 'react-intl'; import { I_S3Credentials } from '../../../interfaces/S3Types'; import routes from '../../../Resources/routes.json'; +import CircularProgress from '@mui/material/CircularProgress'; +import { JSONRecordData } from '../Log/graph.util'; +import Button from '@mui/material/Button'; +import { FormattedMessage } from 'react-intl'; import MainStats from './MainStats'; import DetailedBatteryView from './DetailedBatteryView'; -import CircularProgress from '@mui/material/CircularProgress'; interface BatteryViewProps { - values: TopologyValues; + values: JSONRecordData; s3Credentials: I_S3Credentials; installationId: number; productNum: number; @@ -39,12 +39,15 @@ function BatteryView(props: BatteryViewProps) { if (props.values === null && props.connected == true) { return null; } + const currentLocation = useLocation(); const navigate = useNavigate(); - const sortedBatteryView = - props.values != null - ? [...props.values.batteryView].sort((a, b) => b.BatteryId - a.BatteryId) - : []; + + const sortedBatteryView = Object.entries(props.values.Battery.Devices) + .map(([BatteryId, battery]) => { + return { BatteryId, battery }; // Here we return an object with the id and device + }) + .sort((a, b) => parseInt(b.BatteryId) - parseInt(a.BatteryId)); const [loading, setLoading] = useState(sortedBatteryView.length == 0); @@ -52,13 +55,13 @@ function BatteryView(props: BatteryViewProps) { navigate(routes.mainstats); }; - const findBatteryData = (batteryId: number) => { - for (let i = 0; i < props.values.batteryView.length; i++) { - if (props.values.batteryView[i].BatteryId == batteryId) { - return props.values.batteryView[i]; - } - } - }; + // const findBatteryData = (batteryId: number) => { + // for (let i = 0; i < props.values.batteryView.length; i++) { + // if (props.values.batteryView[i].BatteryId == batteryId) { + // return props.values.batteryView[i]; + // } + // } + // }; useEffect(() => { if (sortedBatteryView.length == 0) { @@ -176,20 +179,23 @@ function BatteryView(props: BatteryViewProps) { > } /> - {props.values.batteryView.map((battery) => ( - - } - /> - ))} + {Object.entries(props.values.Battery.Devices).map( + ([BatteryId, battery]) => ( + + } + /> + ) + )} @@ -219,9 +225,9 @@ function BatteryView(props: BatteryViewProps) { - {sortedBatteryView.map((battery) => ( + {sortedBatteryView.map(({ BatteryId, battery }) => ( - {'Node ' + battery.BatteryId} + {'Node ' + BatteryId} - {battery.Power.value + ' ' + battery.Power.unit} + {battery.Dc.Power.value + ' W'} 57 + battery.Dc.Voltage.value < 44 || + battery.Dc.Voltage.value > 57 ? '#FF033E' : '#32CD32', - color: - battery.Voltage.value === '' ? 'white' : 'inherit' + color: battery.Dc.Voltage.value ? 'inherit' : 'white' }} > - {battery.Voltage.value + ' ' + battery.Voltage.unit} + {battery.Dc.Voltage.value + ' V'} - {battery.Soc.value + ' ' + battery.Soc.unit} + {battery.Soc.value + ' %'} 300 + battery.Temperatures.Cells.Average.value > 300 ? '#FF033E' - : battery.AverageTemperature.value > 280 + : battery.Temperatures.Cells.Average.value > 280 ? '#ffbf00' - : battery.AverageTemperature.value < 245 + : battery.Temperatures.Cells.Average.value < 245 ? '#008FFB' : '#32CD32' }} > - {battery.AverageTemperature.value + - ' ' + - battery.AverageTemperature.unit} + {battery.Temperatures.Cells.Average.value + ' C'} - {props.productNum === 0 && ( - <> - - {battery.Warnings.value === '' ? ( - 'None' - ) : battery.Warnings.value.toString().split('-') - .length > 1 ? ( - - Multiple Warnings - - ) : ( - battery.Warnings.value - )} - - - {battery.Alarms.value === '' ? ( - 'None' - ) : battery.Alarms.value.toString().split('-') - .length > 1 ? ( - - Multiple Alarms - - ) : ( - battery.Alarms.value - )} - - - )} + {/*{props.productNum === 0 && (*/} + {/* <>*/} + {/* */} + {/* {battery.Warnings.value === '' ? (*/} + {/* 'None'*/} + {/* ) : battery.Warnings.value.toString().split('-')*/} + {/* .length > 1 ? (*/} + {/* */} + {/* Multiple Warnings*/} + {/* */} + {/* ) : (*/} + {/* battery.Warnings.value*/} + {/* )}*/} + {/* */} + {/* */} + {/* {battery.Alarms.value === '' ? (*/} + {/* 'None'*/} + {/* ) : battery.Alarms.value.toString().split('-')*/} + {/* .length > 1 ? (*/} + {/* */} + {/* Multiple Alarms*/} + {/* */} + {/* ) : (*/} + {/* battery.Alarms.value*/} + {/* )}*/} + {/* */} + {/* */} + {/*)}*/} {props.productNum === 1 && ( <> @@ -391,22 +394,22 @@ function BatteryView(props: BatteryViewProps) { textAlign: 'center', padding: '8px', fontWeight: - Number(battery.Warnings.value) !== 0 + Number(battery.Warnings) !== 0 ? 'bold' : 'inherit', backgroundColor: - Number(battery.Warnings.value) === 0 + Number(battery.Warnings) === 0 ? 'inherit' : '#ff9900', color: - Number(battery.Warnings.value) != 0 + Number(battery.Warnings) != 0 ? 'black' : 'inherit' }} > - {Number(battery.Warnings.value) === 0 ? ( + {Number(battery.Warnings) === 0 ? ( 'None' - ) : Number(battery.Warnings.value) === 1 ? ( + ) : Number(battery.Warnings) === 1 ? ( - {Number(battery.Alarms.value) === 0 ? ( + {Number(battery.Alarms) === 0 ? ( 'None' - ) : Number(battery.Alarms.value) === 1 ? ( + ) : Number(battery.Alarms) === 1 ? ( { const intervalId = setInterval(() => { - if (props.batteryData.AmberLeds.value === 'Blinking') { + if (props.batteryData.Leds.Amber.value === 'Blinking') { setAmberisBlinking((prevIsBlinking) => !prevIsBlinking); } - if (props.batteryData.RedLeds.value === 'Blinking') { + if (props.batteryData.Leds.Red.value === 'Blinking') { setRedisBlinking((prevIsBlinking) => !prevIsBlinking); } - if (props.batteryData.BlueLeds.value === 'Blinking') { + if (props.batteryData.Leds.Blue.value === 'Blinking') { setBlueisBlinking((prevIsBlinking) => !prevIsBlinking); } - if (props.batteryData.GreenLeds.value === 'Blinking') { + if (props.batteryData.Leds.Green.value === 'Blinking') { setGreenisBlinking((prevIsBlinking) => !prevIsBlinking); } }, 500); // Blink every 500 milliseconds @@ -129,7 +131,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { const res = await axiosConfig .post( - `/UpdateFirmware?batteryNode=${props.batteryData.BatteryId.toString()}&installationId=${ + `/UpdateFirmware?batteryNode=${props.batteryId.toString()}&installationId=${ props.installationId }&version=${selectedVersion}` ) @@ -169,7 +171,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { try { // Start the job to generate the battery log const startRes = await axiosConfig.post( - `/StartDownloadBatteryLog?batteryNode=${props.batteryData.BatteryId.toString()}&installationId=${ + `/StartDownloadBatteryLog?batteryNode=${props.batteryId.toString()}&installationId=${ props.installationId }` ); @@ -635,7 +637,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { fontWeight: 'bold' }} > - {'Node ' + props.batteryData.BatteryId} + {'Node ' + props.batteryId}
@@ -643,8 +645,11 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { style={{ ...batteryStringStyle, backgroundColor: - props.batteryData.String1Active.value == 'True' || - Number(props.batteryData.String1Active.value) == 0 + props.batteryData.BatteryStrings.String1Active.value == + 'True' || + Number( + props.batteryData.BatteryStrings.String1Active.value + ) == 0 ? '#32CD32' : '#FF033E' }} @@ -653,8 +658,11 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { style={{ ...batteryStringStyle, backgroundColor: - props.batteryData.String2Active.value == 'True' || - Number(props.batteryData.String2Active.value) == 0 + props.batteryData.BatteryStrings.String2Active.value == + 'True' || + Number( + props.batteryData.BatteryStrings.String2Active.value + ) == 0 ? '#32CD32' : '#FF033E' }} @@ -663,8 +671,11 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { style={{ ...batteryStringStyle, backgroundColor: - props.batteryData.String3Active.value == 'True' || - Number(props.batteryData.String3Active.value) == 0 + props.batteryData.BatteryStrings.String3Active.value == + 'True' || + Number( + props.batteryData.BatteryStrings.String3Active.value + ) == 0 ? '#32CD32' : '#FF033E' }} @@ -673,8 +684,11 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { style={{ ...batteryStringStyle, backgroundColor: - props.batteryData.String4Active.value == 'True' || - Number(props.batteryData.String4Active.value) == 0 + props.batteryData.BatteryStrings.String4Active.value == + 'True' || + Number( + props.batteryData.BatteryStrings.String4Active.value + ) == 0 ? '#32CD32' : '#FF033E' }} @@ -683,8 +697,11 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { style={{ ...batteryStringStyle, backgroundColor: - props.batteryData.String5Active.value == 'True' || - Number(props.batteryData.String5Active.value) == 0 + props.batteryData.BatteryStrings.String5Active.value == + 'True' || + Number( + props.batteryData.BatteryStrings.String5Active.value + ) == 0 ? '#32CD32' : '#FF033E' }} @@ -699,7 +716,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { marginTop: '-10px', borderRadius: '50%', backgroundColor: - props.batteryData.GreenLeds.value === 'On' || + props.batteryData.Leds.Green.value === 'On' || GreenisBlinking ? 'green' : 'transparent' @@ -714,7 +731,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { marginTop: '10px', borderRadius: '50%', backgroundColor: - props.batteryData.AmberLeds.value === 'On' || + props.batteryData.Leds.Amber.value === 'On' || AmberisBlinking ? 'orange' : 'transparent' @@ -729,7 +746,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { marginTop: '10px', borderRadius: '50%', backgroundColor: - props.batteryData.BlueLeds.value === 'On' || BlueisBlinking + props.batteryData.Leds.Blue.value === 'On' || BlueisBlinking ? '#00ccff' : 'transparent' }} @@ -743,7 +760,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { marginTop: '10px', borderRadius: '50%', backgroundColor: - props.batteryData.RedLeds.value === 'On' || RedisBlinking + props.batteryData.Leds.Red.value === 'On' || RedisBlinking ? 'red' : 'transparent' }} @@ -803,9 +820,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { paddingRight: '12px' }} > - {props.batteryData.Voltage.value + - ' ' + - props.batteryData.Voltage.unit} + {props.batteryData.Dc.Voltage.value + ' V'} @@ -825,9 +840,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { paddingRight: '12px' }} > - {props.batteryData.Current.value + - ' ' + - props.batteryData.Current.unit} + {props.batteryData.Dc.Current.value + ' A'} @@ -847,9 +860,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { paddingRight: '12px' }} > - {props.batteryData.Power.value + - ' ' + - props.batteryData.Power.unit} + {props.batteryData.Dc.Power.value + ' W'} @@ -870,9 +881,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { paddingRight: '12px' }} > - {props.batteryData.BusCurrent.value + - ' ' + - props.batteryData.BusCurrent.unit} + {props.batteryData.BusCurrent.value + ' A'} @@ -892,9 +901,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { paddingRight: '12px' }} > - {props.batteryData.CellsCurrent.value + - ' ' + - props.batteryData.CellsCurrent.unit} + {props.batteryData.CellsCurrent.value + ' A'} @@ -914,9 +921,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { paddingRight: '12px' }} > - {props.batteryData.HeatingCurrent.value + - ' ' + - props.batteryData.HeatingCurrent.unit} + {props.batteryData.HeatingCurrent.value + ' A'} @@ -937,9 +942,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { paddingRight: '12px' }} > - {props.batteryData.Soc.value + - ' ' + - props.batteryData.Soc.unit} + {props.batteryData.Soc.value + ' %'} @@ -949,200 +952,200 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { {/*----------------------------------------------------------------------------------------------------------------------------------*/} - {props.productNum === 0 && ( - <> - - - - Temperature - + {/*{props.productNum === 0 && (*/} + {/* <>*/} + {/* */} + {/* */} + {/* */} + {/* Temperature*/} + {/* */} - - - - - - Heating - - - {props.batteryData.HeatingTemperature.value + - ' ' + - props.batteryData.HeatingTemperature.unit} - - - - - Board Temperature - - - {props.batteryData.BoardTemperature.value + - ' ' + - props.batteryData.BoardTemperature.unit} - - - - - Center Cells Temperature - - - {props.batteryData.AverageTemperature.value + - ' ' + - props.batteryData.AverageTemperature.unit} - - - - - Left Cells Temperature - - - {props.batteryData.LeftCellsTemperature.value + - ' ' + - props.batteryData.LeftCellsTemperature.unit} - - - - - Right Cells Temperature - - - {props.batteryData.RightCellsTemperature.value + - ' ' + - props.batteryData.RightCellsTemperature.unit} - - - - - Average Temperature - - - {props.batteryData.AverageTemperature.value + - ' ' + - props.batteryData.AverageTemperature.unit} - - - - - State - - - {props.batteryData.StateTemperature.value + - ' ' + - props.batteryData.StateTemperature.unit} - - - -
-
-
-
- - )} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* Heating*/} + {/* */} + {/* */} + {/* {props.batteryData.HeatingTemperature.value +*/} + {/* ' ' +*/} + {/* props.batteryData.HeatingTemperature.unit}*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* Board Temperature*/} + {/* */} + {/* */} + {/* {props.batteryData.BoardTemperature.value +*/} + {/* ' ' +*/} + {/* props.batteryData.BoardTemperature.unit}*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* Center Cells Temperature*/} + {/* */} + {/* */} + {/* {props.batteryData.AverageTemperature.value +*/} + {/* ' ' +*/} + {/* props.batteryData.AverageTemperature.unit}*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* Left Cells Temperature*/} + {/* */} + {/* */} + {/* {props.batteryData.LeftCellsTemperature.value +*/} + {/* ' ' +*/} + {/* props.batteryData.LeftCellsTemperature.unit}*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* Right Cells Temperature*/} + {/* */} + {/* */} + {/* {props.batteryData.RightCellsTemperature.value +*/} + {/* ' ' +*/} + {/* props.batteryData.RightCellsTemperature.unit}*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* Average Temperature*/} + {/* */} + {/* */} + {/* {props.batteryData.AverageTemperature.value +*/} + {/* ' ' +*/} + {/* props.batteryData.AverageTemperature.unit}*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* State*/} + {/* */} + {/* */} + {/* {props.batteryData.StateTemperature.value +*/} + {/* ' ' +*/} + {/* props.batteryData.StateTemperature.unit}*/} + {/* */} + {/* */} + {/* */} + {/*
*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/*)}*/} {/*----------------------------------------------------------------------------------------------------------------------------------*/} @@ -1193,9 +1196,9 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { paddingRight: '12px' }} > - {props.batteryData.ConnectedToDcBus.value + - ' ' + - props.batteryData.ConnectedToDcBus.unit} + {props.batteryData.IoStatus.ConnectedToDcBus.value + ? 'True' + : 'False'} @@ -1215,9 +1218,9 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { paddingRight: '12px' }} > - {props.batteryData.AlarmOutActive.value + - ' ' + - props.batteryData.AlarmOutActive.unit} + {props.batteryData.IoStatus.AlarmOutActive.value + ? 'True' + : 'False'} @@ -1237,9 +1240,9 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { paddingRight: '12px' }} > - {props.batteryData.InternalFanActive.value + - ' ' + - props.batteryData.InternalFanActive.unit} + {props.batteryData.IoStatus.InternalFanActive.value + ? 'True' + : 'False'} @@ -1259,9 +1262,9 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { paddingRight: '12px' }} > - {props.batteryData.VoltMeasurementAllowed.value + - ' ' + - props.batteryData.VoltMeasurementAllowed.unit} + {props.batteryData.IoStatus.VoltMeasurementAllowed.value + ? 'True' + : 'False'} @@ -1281,9 +1284,9 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { paddingRight: '12px' }} > - {props.batteryData.AuxRelayBus.value + - ' ' + - props.batteryData.AuxRelayBus.unit} + {props.batteryData.IoStatus.AuxRelayBus.value + ? 'True' + : 'False'} @@ -1303,9 +1306,9 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { paddingRight: '12px' }} > - {props.batteryData.RemoteStateActive.value + - ' ' + - props.batteryData.RemoteStateActive.unit} + {props.batteryData.IoStatus.RemoteStateActive.value + ? 'True' + : 'False'} @@ -1325,9 +1328,9 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { paddingRight: '12px' }} > - {props.batteryData.RiscActive.value + - ' ' + - props.batteryData.RiscActive.unit} + {props.batteryData.IoStatus.RiscActive.value + ? 'True' + : 'False'} @@ -1385,9 +1388,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { paddingRight: '12px' }} > - {props.batteryData.Eoc.value + - ' ' + - props.batteryData.Eoc.unit} + {props.batteryData.Eoc.value} @@ -1408,9 +1409,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { paddingRight: '12px' }} > - {props.batteryData.SerialNumber.value + - ' ' + - props.batteryData.SerialNumber.unit} + {props.batteryData.SerialNumber.value} @@ -1431,9 +1430,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { paddingRight: '12px' }} > - {props.batteryData.FwVersion.value + - ' ' + - props.batteryData.FwVersion.unit} + {props.batteryData.FwVersion.value} @@ -1453,35 +1450,33 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { paddingRight: '12px' }} > - {props.batteryData.TimeSinceTOC.value + - ' ' + - props.batteryData.TimeSinceTOC.unit} + {props.batteryData.TimeSinceTOC.value} - {props.productNum === 0 && ( - - - Calibration Charge Requested - - - {props.batteryData.CalibrationChargeRequested.value + - ' ' + - props.batteryData.CalibrationChargeRequested.unit} - - - )} + {/*{props.productNum === 0 && (*/} + {/* */} + {/* */} + {/* Calibration Charge Requested*/} + {/* */} + {/* */} + {/* {props.batteryData.CalibrationChargeRequested.value +*/} + {/* ' ' +*/} + {/* props.batteryData.CalibrationChargeRequested.unit}*/} + {/* */} + {/* */} + {/*)}*/} - {props.batteryData.MaxChargePower.value + - ' ' + - props.batteryData.MaxChargePower.unit} + {props.batteryData.MaxChargePower.value + ' W'} @@ -1521,9 +1514,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { paddingRight: '12px' }} > - {props.batteryData.MaxDischargePower.value + - ' ' + - props.batteryData.MaxDischargePower.unit} + {props.batteryData.MaxDischargePower.value + ' W'} diff --git a/typescript/frontend-marios2/src/content/dashboards/BatteryView/MainStats.tsx b/typescript/frontend-marios2/src/content/dashboards/BatteryView/MainStats.tsx index 4dcf35e1e..75c60f8fa 100644 --- a/typescript/frontend-marios2/src/content/dashboards/BatteryView/MainStats.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/BatteryView/MainStats.tsx @@ -5,6 +5,7 @@ import { Grid, IconButton, Modal, + TextField, Typography } from '@mui/material'; import { FormattedMessage } from 'react-intl'; @@ -15,9 +16,9 @@ import { getChartOptions } from '../Overview/chartOptions'; import { BatteryDataInterface, BatteryOverviewInterface, - transformInputToBatteryViewData + transformInputToBatteryViewDataJson } from '../../../interfaces/Chart'; -import dayjs from 'dayjs'; +import dayjs, { Dayjs } from 'dayjs'; import { TimeSpan, UnixTime } from '../../../dataCache/time'; import Button from '@mui/material/Button'; import { DateTimePicker, LocalizationProvider } from '@mui/x-date-pickers'; @@ -95,7 +96,7 @@ function MainStats(props: MainStatsProps) { const resultPromise: Promise<{ chartData: BatteryDataInterface; chartOverview: BatteryOverviewInterface; - }> = transformInputToBatteryViewData( + }> = transformInputToBatteryViewDataJson( props.s3Credentials, props.id, product, @@ -191,7 +192,7 @@ function MainStats(props: MainStatsProps) { const resultPromise: Promise<{ chartData: BatteryDataInterface; chartOverview: BatteryOverviewInterface; - }> = transformInputToBatteryViewData( + }> = transformInputToBatteryViewDataJson( props.s3Credentials, props.id, product, @@ -253,7 +254,7 @@ function MainStats(props: MainStatsProps) { const resultPromise: Promise<{ chartData: BatteryDataInterface; chartOverview: BatteryOverviewInterface; - }> = transformInputToBatteryViewData( + }> = transformInputToBatteryViewDataJson( props.s3Credentials, props.id, product, @@ -360,19 +361,25 @@ function MainStats(props: MainStatsProps) { setStartDate(newDate)} - sx={{ - marginTop: 2 + onChange={(newDate: Dayjs | null) => { + // Type assertion to Dayjs + if (newDate) { + setStartDate(newDate); + } }} + renderInput={(props) => } /> setEndDate(newDate)} - sx={{ - marginTop: 2 + onChange={(newDate: Dayjs | null) => { + // Type assertion to Dayjs + if (newDate) { + setEndDate(newDate); + } }} + renderInput={(props) => } />
( + + )} /> + + {/**/}
)} @@ -405,6 +419,15 @@ function Configuration(props: ConfigurationProps) { label="Calibration Charge Hour" value={dayjs(formValues.calibrationChargeDate)} onChange={(newTime) => handleConfirm(dayjs(newTime))} + renderInput={(params) => ( + + )} />
diff --git a/typescript/frontend-marios2/src/content/dashboards/History/History.tsx b/typescript/frontend-marios2/src/content/dashboards/History/History.tsx index afb629aa6..36e2693c0 100644 --- a/typescript/frontend-marios2/src/content/dashboards/History/History.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/History/History.tsx @@ -185,28 +185,30 @@ function HistoryOfActions(props: HistoryProps) { }} >
- handleDateChange(newDate)} - sx={{ - width: 450, - marginTop: 2 - }} - /> {/* (*/} - {/* */} - {/* )}*/} + {/* onChange={(newDate: Dayjs | null) =>*/} + {/* handleDateChange(newDate)*/} + {/* }*/} + {/* InputProps={{*/} + {/* sx: {*/} + {/* width: 450,*/} + {/* marginTop: 2*/} + {/* }*/} + {/* }}*/} {/*/>*/} + ( + + )} + /> + setTimeout(res, delay)); } + //In React, useRef creates a mutable object that persists across renders without triggering re-renders when its value changes. + //While fetching, we check the value of continueFetching, if its false, we break. This means that either the user changed tab or the object has been finished its execution (return) const continueFetching = useRef(false); const fetchDataForOneTime = async () => { @@ -193,6 +190,7 @@ function Installation(props: singleInstallationProps) { setCurrentTab(path[path.length - 1]); }, [location]); + //If status is -1, the topology will be empty and "Unable to communicate with the installation" should be rendered from the Topology component useEffect(() => { if (status === -1) { setConnected(false); @@ -206,19 +204,22 @@ function Installation(props: singleInstallationProps) { currentTab == 'configuration' || location.includes('batteryview') ) { + //Fetch periodically if the tab is live, pvview or batteryview if ( currentTab == 'live' || - (location.includes('batteryview') && !location.includes('mainstats')) || - currentTab == 'pvview' + currentTab == 'pvview' || + (location.includes('batteryview') && !location.includes('mainstats')) ) { if (!continueFetching.current) { continueFetching.current = true; + //Call the function only one time. When the location and the currentTab change, this useEffect will be called 2 times if (!fetchFunctionCalled) { setFetchFunctionCalled(true); fetchDataPeriodically(); } } } + //Fetch only one time in configuration tab if (currentTab == 'configuration') { fetchDataForOneTime(); } @@ -227,6 +228,7 @@ function Installation(props: singleInstallationProps) { continueFetching.current = false; }; } else { + //If the tab is not live, pvview, batteryview or configuration, then stop fetching. continueFetching.current = false; } }, [currentTab, location]); @@ -419,18 +421,18 @@ function Installation(props: singleInstallationProps) { } /> - - } - > + {/**/} + {/* }*/} + {/*>*/} >> => { + 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> => { + 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>> => { + const s3Path = cutdigits + ? `${timestamp.ticks.toString().slice(0, -2)}.csv` + : `${timestamp.ticks}.csv`; + if (s3Credentials && s3Credentials.s3Bucket) { + const s3Access = new S3Access( + s3Credentials.s3Bucket, + s3Credentials.s3Region, + s3Credentials.s3Provider, + s3Credentials.s3Key, + s3Credentials.s3Secret + ); + + return s3Access + .get(s3Path) + .then(async (r) => { + if (r.status === 404) { + return Promise.resolve(FetchResult.notAvailable); + } else if (r.status === 200) { + console.log('FOUND ITTTTTTTTTTTT'); + const csvtext = await r.text(); // Assuming the server returns the Base64 encoded ZIP file as text + const contentEncoding = r.headers.get('content-type'); + + //console.log(contentEncoding); + + if (contentEncoding != 'application/base64; charset=utf-8') { + // console.log('uncompressed'); + return parseChunk(csvtext); + } + + const byteArray = Uint8Array.from(atob(csvtext), (c) => + c.charCodeAt(0) + ); + + //Decompress the byte array using JSZip + const zip = await JSZip.loadAsync(byteArray); + // Assuming the CSV file is named "data.csv" inside the ZIP archive + const csvContent = await zip.file('data.csv').async('text'); + + //console.log(csvContent); + + return parseChunk(csvContent); + } else { + return Promise.resolve(FetchResult.notAvailable); + } + }) + .catch((e) => { + return Promise.resolve(FetchResult.tryLater); + }); + } +}; + export const fetchAggregatedData = ( date: string, s3Credentials?: I_S3Credentials @@ -50,59 +212,3 @@ export const fetchAggregatedData = ( }); } }; - -export const fetchData = ( - timestamp: UnixTime, - s3Credentials?: I_S3Credentials, - cutdigits?: boolean -): Promise>> => { - 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); - }); - } -}; diff --git a/typescript/frontend-marios2/src/content/dashboards/Installations/index.tsx b/typescript/frontend-marios2/src/content/dashboards/Installations/index.tsx index 8a28553ba..1d65daca3 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Installations/index.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Installations/index.tsx @@ -55,16 +55,6 @@ function InstallationTabs() { } }, [location]); - // useEffect(() => { - // if (salimaxInstallations && salimaxInstallations.length > 0) { - // if (socket) { - // closeSocket(); - // } - // - // openSocket(salimaxInstallations); - // } - // }, [salimaxInstallations]); - useEffect(() => { if (salimaxInstallations.length === 0) { fetchAllInstallations(); @@ -75,7 +65,7 @@ function InstallationTabs() { if (salimaxInstallations && salimaxInstallations.length > 0) { if (!socket) { openSocket(0); - } else if (currentProduct == 1) { + } else if (currentProduct != 0) { closeSocket(); openSocket(0); } diff --git a/typescript/frontend-marios2/src/content/dashboards/Log/graph.util.tsx b/typescript/frontend-marios2/src/content/dashboards/Log/graph.util.tsx index 49035ab9a..ba6fe98eb 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Log/graph.util.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Log/graph.util.tsx @@ -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 => { + const lines = text.split(/\r?\n/).filter((line) => line.length > 0); + + let result: Record = {}; + let currentTimestamp = null; + + lines.forEach((line) => { + //console.log(line); + + const fields = line.split(';'); + if (fields[0] === 'Timestamp') { + currentTimestamp = fields[1]; + result[currentTimestamp] = {}; + } else if (currentTimestamp) { + result[currentTimestamp] = JSON.parse(line); + } + }); + + return result; +}; +//-------------------------------------------------------------------------------------------------------------------------------------------------------------------- +//-------------------------------------------------------------------------------------------------------------------------------------------------------------------- +//-------------------------------------------------------------------------------------------------------------------------------------------------------------------- +//-------------------------------------------------------------------------------------------------------------------------------------------------------------------- +//-------------------------------------------------------------------------------------------------------------------------------------------------------------------- +// For CSV manipulation, check the following functions +//-------------------------------------------------------------------------------------------------------------------------------------------------------------------- +//-------------------------------------------------------------------------------------------------------------------------------------------------------------------- +//-------------------------------------------------------------------------------------------------------------------------------------------------------------------- +//-------------------------------------------------------------------------------------------------------------------------------------------------------------------- + import { DataPoint, DataRecord } from 'src/dataCache/data'; export interface I_CsvEntry { diff --git a/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx b/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx index 8857be8d2..0b3c81f96 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx @@ -1,4 +1,12 @@ -import { Box, Card, Container, Grid, Modal, Typography } from '@mui/material'; +import { + Box, + Card, + Container, + Grid, + Modal, + TextField, + Typography +} from '@mui/material'; import ReactApexChart from 'react-apexcharts'; import React, { useContext, useEffect, useState } from 'react'; import { I_S3Credentials } from 'src/interfaces/S3Types'; @@ -13,12 +21,13 @@ import { import Button from '@mui/material/Button'; import { FormattedMessage } from 'react-intl'; import CircularProgress from '@mui/material/CircularProgress'; -import { DateTimePicker, LocalizationProvider } from '@mui/x-date-pickers'; +import { LocalizationProvider } from '@mui/x-date-pickers'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import dayjs from 'dayjs'; import { UserContext } from '../../../contexts/userContext'; import { UserType } from '../../../interfaces/UserTypes'; import { TimeSpan, UnixTime } from '../../../dataCache/time'; +import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; interface OverviewProps { s3Credentials: I_S3Credentials; @@ -73,6 +82,13 @@ function Overview(props: OverviewProps) { const [endDate, setEndDate] = useState(dayjs()); const [isZooming, setIsZooming] = useState(false); + console.log( + UnixTime.fromTicks(new Date().getTime() / 1000).earlier( + TimeSpan.fromDays(1) + ) + ); + + console.log(UnixTime.fromTicks(new Date().getTime() / 1000)); useEffect(() => { if (isZooming) { setLoading(true); @@ -408,18 +424,30 @@ function Overview(props: OverviewProps) { label="Select Start Date" value={startDate} onChange={(newDate) => setStartDate(newDate)} - sx={{ - marginTop: 2 - }} + renderInput={(params) => ( + + )} /> setEndDate(newDate)} - sx={{ - marginTop: 2 - }} + renderInput={(params) => ( + + )} />
{ } 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 ( +// +// {isErrorDateModalOpen && ( +// {}}> +// +// +// {dateSelectionError} +// +// +// +// +// +// )} +// +// {isDateModalOpen && ( +// +// {}}> +// +// { +// // Type assertion to Dayjs +// if (newDate) { +// setStartDate(newDate); +// } +// }} +// renderInput={(props) => } +// /> +// +// { +// // Type assertion to Dayjs +// if (newDate) { +// setEndDate(newDate); +// } +// }} +// renderInput={(props) => } +// /> +// +//
+// +// +// +//
+//
+//
+//
+// )} +// +// +// +// +// +// +// +// +// +// +// +// {loading && ( +// +// +// +// Fetching data... +// +// +// )} +// +// {!loading && ( +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// {currentUser.userType == UserType.admin && ( +// +// )} +// +// {currentUser.userType == UserType.client && ( +// +// )} +// +// +// +// +// )} +// +//
+// ); +// }; +// +// return <>{renderGraphs()}; +// } function SalidomoOverview(props: salidomoOverviewProps) { const context = useContext(UserContext); @@ -79,7 +627,7 @@ function SalidomoOverview(props: salidomoOverviewProps) { chartAggregatedData: chartAggregatedDataInterface; chartOverview: overviewInterface; dateList: string[]; - }> = transformInputToAggregatedData( + }> = transformInputToAggregatedDataJson( props.s3Credentials, dayjs().subtract(1, 'week'), dayjs() @@ -149,7 +697,7 @@ function SalidomoOverview(props: salidomoOverviewProps) { chartAggregatedData: chartAggregatedDataInterface; chartOverview: overviewInterface; dateList: string[]; - }> = transformInputToAggregatedData( + }> = transformInputToAggregatedDataJson( props.s3Credentials, startDate, endDate @@ -266,19 +814,25 @@ function SalidomoOverview(props: salidomoOverviewProps) { setStartDate(newDate)} - sx={{ - marginTop: 2 + onChange={(newDate: Dayjs | null) => { + // Type assertion to Dayjs + if (newDate) { + setStartDate(newDate); + } }} + renderInput={(props) => } /> setEndDate(newDate)} - sx={{ - marginTop: 2 + onChange={(newDate: Dayjs | null) => { + // Type assertion to Dayjs + if (newDate) { + setEndDate(newDate); + } }} + renderInput={(props) => } />
(undefined); - const [values, setValues] = useState(null); + const [values, setValues] = useState(null); const status = props.current_installation.status; const [ failedToCommunicateWithInstallation, @@ -77,7 +74,7 @@ function SalidomoInstallation(props: singleInstallationProps) { const fetchDataPeriodically = async () => { var timeperiodToSearch = 30; - let res; + let res: FetchResult> | undefined; let timestampToFetch; for (var i = 0; i < timeperiodToSearch; i += 1) { @@ -88,7 +85,7 @@ function SalidomoInstallation(props: singleInstallationProps) { console.log('timestamp to fetch is ' + timestampToFetch); try { - res = await fetchData(timestampToFetch, s3Credentials, true); + res = await fetchDataJson(timestampToFetch, s3Credentials, true); if (res !== FetchResult.notAvailable && res !== FetchResult.tryLater) { break; } @@ -114,15 +111,16 @@ function SalidomoInstallation(props: singleInstallationProps) { return false; } console.log(`Timestamp: ${timestamp}`); - console.log(res[timestamp]); + //console.log('object is' + res[timestamp]); // Set values asynchronously with delay - setValues( - extractValues({ - time: UnixTime.fromTicks(parseInt(timestamp, 10)), - value: res[timestamp] - }) - ); + setValues(res[timestamp]); + // setValues( + // extractValues({ + // time: UnixTime.fromTicks(parseInt(timestamp, 10)), + // value: res[timestamp] + // }) + // ); // Wait for 2 seconds before processing next timestamp await timeout(2000); } @@ -137,7 +135,7 @@ function SalidomoInstallation(props: singleInstallationProps) { try { console.log('Trying to fetch timestamp ' + timestampToFetch); - res = await fetchData(timestampToFetch, s3Credentials, true); + res = await fetchDataJson(timestampToFetch, s3Credentials, true); if ( res !== FetchResult.notAvailable && res !== FetchResult.tryLater diff --git a/typescript/frontend-marios2/src/content/dashboards/SalidomoInstallations/index.tsx b/typescript/frontend-marios2/src/content/dashboards/SalidomoInstallations/index.tsx index bc6ddfdb6..fe1fa07ec 100644 --- a/typescript/frontend-marios2/src/content/dashboards/SalidomoInstallations/index.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/SalidomoInstallations/index.tsx @@ -40,9 +40,7 @@ function SalidomoInstallationTabs() { } = useContext(InstallationsContext); const { product, setProduct } = useContext(ProductIdContext); - // const webSocketsContext = useContext(WebSocketContext); - // const { socket, openSocket, closeSocket } = webSocketsContext; - + //The following useEffect is for the current tab to be bold. Based on the location, we set the corresponding tab to be bold useEffect(() => { let path = location.pathname.split('/'); @@ -57,6 +55,7 @@ function SalidomoInstallationTabs() { }, [location]); useEffect(() => { + //The first time this component will be loaded, it needs to call the fetchAllSalidomoInstallations function from the InstallationsContextProvider if (salidomoInstallations.length === 0 && fetchedInstallations === false) { fetchAllSalidomoInstallations(); setFetchedInstallations(true); @@ -64,10 +63,12 @@ function SalidomoInstallationTabs() { }, [salidomoInstallations]); useEffect(() => { + //Since we know the ids of the installations we have access to, we need to open a web socket with the backend. if (salidomoInstallations && salidomoInstallations.length > 0) { if (!socket) { openSocket(1); - } else if (currentProduct == 0) { + } else if (currentProduct != 1) { + //If there is any other open websocket for another product, close it. closeSocket(); openSocket(1); } @@ -82,6 +83,7 @@ function SalidomoInstallationTabs() { setCurrentTab(value); }; + //Manually compute the path when the user clicks a tab const navigateToTabPath = (pathname: string, tab_value: string): string => { let pathlist = pathname.split('/'); let ret_path = ''; diff --git a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/FlatInstallationView.tsx b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/FlatInstallationView.tsx index 83f275cb6..a8563ee3c 100644 --- a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/FlatInstallationView.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/FlatInstallationView.tsx @@ -25,8 +25,6 @@ interface FlatInstallationViewProps { } const FlatInstallationView = (props: FlatInstallationViewProps) => { - // const webSocketContext = useContext(WebSocketContext); - // const { getSortedInstallations } = webSocketContext; const navigate = useNavigate(); const [selectedInstallation, setSelectedInstallation] = useState(-1); const currentLocation = useLocation(); @@ -57,7 +55,7 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => { routes.installation + `${installationID}` + '/' + - routes.information, + routes.live, { replace: true } diff --git a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/Installation.tsx b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/Installation.tsx index 97fb80954..b21b85423 100644 --- a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/Installation.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/Installation.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useEffect, useState } from 'react'; +import React, { useContext, useEffect, useRef, useState } from 'react'; import { Card, CircularProgress, @@ -20,6 +20,7 @@ import BuildIcon from '@mui/icons-material/Build'; import AccessContextProvider from '../../../contexts/AccessContextProvider'; import Access from '../ManageAccess/Access'; import InformationSodioHome from '../Information/InformationSodioHome'; +import CryptoJS from 'crypto-js'; interface singleInstallationProps { current_installation?: I_Installation; @@ -27,6 +28,9 @@ interface singleInstallationProps { } function SodioHomeInstallation(props: singleInstallationProps) { + if (props.current_installation == undefined) { + return null; + } const context = useContext(UserContext); const { currentUser } = context; const location = useLocation().pathname; @@ -40,17 +44,97 @@ function SodioHomeInstallation(props: singleInstallationProps) { ] = useState(0); const [connected, setConnected] = useState(true); const [loading, setLoading] = useState(true); - - if (props.current_installation == undefined) { - return null; - } - const [fetchFunctionCalled, setFetchFunctionCalled] = useState(false); + //In React, useRef creates a mutable object that persists across renders without triggering re-renders when its value changes. + //While fetching, we check the value of continueFetching, if its false, we break. This means that either the user changed tab or the object has been finished its execution (return) + const continueFetching = useRef(false); + function timeout(delay: number) { return new Promise((res) => setTimeout(res, delay)); } + const API_BASE_URL = + 'https://www.biwattpower.com/gateway/admin/open/device/currentEnergyFlowData/'; + + async function encryptAES_CBC(data: string, secretKey: string) { + const encoder = new TextEncoder(); + const keyBuffer = encoder.encode(secretKey.padEnd(32, ' ')); // Ensure 256-bit key + + const cryptoKey = await crypto.subtle.importKey( + 'raw', + keyBuffer, + { name: 'AES-CBC' }, + false, + ['encrypt'] + ); + + // Generate a random IV (initialization vector) + const iv = crypto.getRandomValues(new Uint8Array(16)); + const dataBuffer = encoder.encode( + data.padEnd(Math.ceil(data.length / 16) * 16, '\0') + ); + + const encryptedBuffer = await crypto.subtle.encrypt( + { name: 'AES-CBC', iv }, + cryptoKey, + dataBuffer + ); + + // Convert encrypted data + IV to Base64 + const encryptedArray = new Uint8Array(encryptedBuffer); + const combined = new Uint8Array(iv.length + encryptedArray.length); + combined.set(iv); + combined.set(encryptedArray, iv.length); + + return btoa(String.fromCharCode(...combined)); + } + + const fetchDataPeriodically = async () => { + while (continueFetching.current) { + //Fetch data from Bitwatt cloud + console.log('Fetching from Bitwatt cloud'); + + console.log(props.current_installation.serialNumber); + console.log(props.current_installation.s3WriteKey); + console.log(props.current_installation.s3WriteSecret); + + const timeStamp = Date.now().toString(); + + // Encrypt timestamp using AES-ECB with PKCS7 padding + const key = CryptoJS.enc.Utf8.parse( + props.current_installation.s3WriteSecret + ); + const encrypted = CryptoJS.AES.encrypt(timeStamp, key, { + mode: CryptoJS.mode.ECB, + padding: CryptoJS.pad.Pkcs7 + }).toString(); + + // Set headers + const headers = { + 'X-Signature': encrypted, + 'X-AccessKey': props.current_installation.s3WriteKey, + 'Content-Type': 'application/json' + }; + + // API URL + const url = `https://www.biwattpower.com/gateway/admin/open/device/currentEnergyFlowData/${props.current_installation.serialNumber}`; + + try { + const response = await fetch(url, { method: 'GET', headers }); + const result = await response.json(); + console.log('API Response:', result); + } catch (error) { + console.error('Request failed:', error); + } + + // Wait for 2 seconds before fetching again + await timeout(200000); + console.log('ssssssssssssssssssssssssssssssssssssss'); + } + setFetchFunctionCalled(false); + }; + useEffect(() => { let path = location.split('/'); setCurrentTab(path[path.length - 1]); @@ -62,6 +146,33 @@ function SodioHomeInstallation(props: singleInstallationProps) { } }, [status]); + useEffect(() => { + console.log(currentTab); + if (currentTab == 'live' || location.includes('batteryview')) { + //Fetch periodically if the tab is live or batteryview + if ( + currentTab == 'live' || + (location.includes('batteryview') && !location.includes('mainstats')) + ) { + if (!continueFetching.current) { + continueFetching.current = true; + //Call the function only one time. When the location and the currentTab change, this useEffect will be called 2 times + if (!fetchFunctionCalled) { + setFetchFunctionCalled(true); + fetchDataPeriodically(); + } + } + } + + return () => { + continueFetching.current = false; + }; + } else { + //If the tab is not live, pvview, batteryview or configuration, then stop fetching. + continueFetching.current = false; + } + }, [currentTab, location]); + return ( <> @@ -228,6 +339,18 @@ function SodioHomeInstallation(props: singleInstallationProps) { } /> +
+ // + } + /> + {/*} + element={} /> diff --git a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/index.tsx b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/index.tsx index 075cd518c..aa3771be4 100644 --- a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/index.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/index.tsx @@ -19,7 +19,7 @@ function SodioHomeInstallationTabs() { const context = useContext(UserContext); const { currentUser } = context; const tabList = [ - 'batteryview', + 'live', 'information', 'manage', 'overview', @@ -40,9 +40,6 @@ function SodioHomeInstallationTabs() { } = useContext(InstallationsContext); const { product, setProduct } = useContext(ProductIdContext); - // const webSocketsContext = useContext(WebSocketContext); - // const { socket, openSocket, closeSocket } = webSocketsContext; - useEffect(() => { let path = location.pathname.split('/'); @@ -90,6 +87,7 @@ function SodioHomeInstallationTabs() { ret_path += '/'; ret_path += pathlist[i]; } else { + //When finding the installation id (number), break and then add the tab_value ret_path += '/'; ret_path += pathlist[i]; ret_path += '/'; @@ -105,13 +103,8 @@ function SodioHomeInstallationTabs() { currentUser.userType == UserType.admin ? [ { - value: 'batteryview', - label: ( - - ) + value: 'live', + label: }, { value: 'overview', @@ -150,13 +143,8 @@ function SodioHomeInstallationTabs() { ] : [ { - value: 'batteryview', - label: ( - - ) + value: 'live', + label: }, { value: 'overview', @@ -186,13 +174,8 @@ function SodioHomeInstallationTabs() { icon: }, { - value: 'batteryview', - label: ( - - ) + value: 'live', + label: }, { value: 'overview', @@ -243,13 +226,8 @@ function SodioHomeInstallationTabs() { icon: }, { - value: 'batteryview', - label: ( - - ) + value: 'live', + label: }, { value: 'overview', diff --git a/typescript/frontend-marios2/src/contexts/InstallationsContextProvider.tsx b/typescript/frontend-marios2/src/contexts/InstallationsContextProvider.tsx index 1c9283218..1b1e7660d 100644 --- a/typescript/frontend-marios2/src/contexts/InstallationsContextProvider.tsx +++ b/typescript/frontend-marios2/src/contexts/InstallationsContextProvider.tsx @@ -41,18 +41,17 @@ const InstallationsContextProvider = ({ const [socket, setSocket] = useState(null); const [currentProduct, setcurrentProduct] = useState(0); + //Store pending updates and apply them in batches const pendingUpdates = useRef< Record - >({}); // To store pending updates + >({}); - const updateInstallationStatus = useCallback( - (product, id, status, testingMode) => { - // Buffer updates instead of applying them immediately - pendingUpdates.current[id] = { status: Number(status), testingMode }; // Ensure status is a number - }, - [] - ); + const updateInstallationStatus = useCallback((id, status, testingMode) => { + //Insert the incoming message to the pendingUpdates map + pendingUpdates.current[id] = { status: Number(status), testingMode }; // Ensure status is a number + }, []); + //This function will be called every 1 minute and it will update only the installations for which the status and/or the testingMOde has been changed const applyBatchUpdates = useCallback(() => { if (Object.keys(pendingUpdates.current).length === 0) return; // No updates to apply @@ -109,7 +108,7 @@ const InstallationsContextProvider = ({ setcurrentProduct(product); const tokenString = localStorage.getItem('token'); const token = tokenString !== null ? tokenString : ''; - const urlWithToken = `wss://stage.innov.energy/api/CreateWebSocket?authToken=${token}`; + const urlWithToken = `wss://monitor.innov.energy/api/CreateWebSocket?authToken=${token}`; const socket = new WebSocket(urlWithToken); @@ -124,7 +123,7 @@ const InstallationsContextProvider = ({ installationsToSend = sodiohomeInstallations; } - // Send the corresponding installation IDs + // Send the corresponding installation IDs to the backend socket.send( JSON.stringify( installationsToSend.map((installation) => installation.id) @@ -132,16 +131,6 @@ const InstallationsContextProvider = ({ ); }); - // socket.addEventListener('open', () => { - // socket.send( - // JSON.stringify( - // product === 1 - // ? salidomoInstallations.map((installation) => installation.id) - // : salimaxInstallations.map((installation) => installation.id) - // ) - // ); - // }); - // Periodically send ping messages to keep the connection alive const pingInterval = setInterval(() => { if (socket.readyState === WebSocket.OPEN) { @@ -152,8 +141,8 @@ const InstallationsContextProvider = ({ socket.addEventListener('message', (event) => { const message = JSON.parse(event.data); // Parse the JSON data if (message.id !== -1) { + //For each received message (except the first one which is a batch, call the updateInstallationStatus function in order to import the message to the pendingUpdates list updateInstallationStatus( - product, message.id, message.status, message.testingMode diff --git a/typescript/frontend-marios2/src/interfaces/Chart.tsx b/typescript/frontend-marios2/src/interfaces/Chart.tsx index 151f9ff08..8b03c78f7 100644 --- a/typescript/frontend-marios2/src/interfaces/Chart.tsx +++ b/typescript/frontend-marios2/src/interfaces/Chart.tsx @@ -1,7 +1,9 @@ import dayjs from 'dayjs'; import { fetchAggregatedData, - fetchData + fetchAggregatedDataJson, + fetchData, + fetchDataJson } from '../content/dashboards/Installations/fetchData'; import { FetchResult } from '../dataCache/dataCache'; import { I_S3Credentials } from './S3Types'; @@ -9,6 +11,7 @@ import { TimeSpan, UnixTime } from '../dataCache/time'; import { DataRecord } from '../dataCache/data'; import axiosConfig from '../Resources/axiosConfig'; import { AxiosError, AxiosResponse } from 'axios'; +import { JSONRecordData } from '../content/dashboards/Log/graph.util'; export interface chartInfoInterface { magnitude: number; @@ -68,18 +71,7 @@ export interface BatteryOverviewInterface { Current: chartInfoInterface; } -// We use this function in order to retrieve data for main stats. -//The data is of the following form: -//'Soc' : {name:'Soc',data:[ -// 'Node1': {name:'Node1', data: [[timestamp,value],[timestamp,value]]}, -// 'Node2': {name:'Node2', data: [[timestamp,value],[timestamp,value]]}, -// ]}, -//'Temperature' : {name:'Temperature',data:[ -// 'Node1': {name:'Node1', data: [[timestamp,value],[timestamp,value]]}, -// 'Node2': {name:'Node2', data: [[timestamp,value],[timestamp,value]]}, -// ]} - -export const transformInputToBatteryViewData = async ( +export const transformInputToBatteryViewDataJson = async ( s3Credentials: I_S3Credentials, id: number, product: number, @@ -93,24 +85,24 @@ export const transformInputToBatteryViewData = async ( const MAX_NUMBER = 9999999; const categories = ['Soc', 'Temperature', 'Power', 'Voltage', 'Current']; const pathCategories = [ - 'Soc', - 'Temperatures/Cells/Average', - 'Dc/Power', - 'Dc/Voltage', - 'Dc/Current' + '.Soc', + '.Temperatures.Cells.Average', + '.Dc.Power', + '.Dc.Voltage', + '.Dc.Current' ]; const pathsToSearch = [ - '/Battery/Devices/1/', - '/Battery/Devices/2/', - '/Battery/Devices/3/', - '/Battery/Devices/4/', - '/Battery/Devices/5/', - '/Battery/Devices/6/', - '/Battery/Devices/7/', - '/Battery/Devices/8/', - '/Battery/Devices/9/', - '/Battery/Devices/10/' + 'Battery.Devices.1', + 'Battery.Devices.2', + 'Battery.Devices.3', + 'Battery.Devices.4', + 'Battery.Devices.5', + 'Battery.Devices.6', + 'Battery.Devices.7', + 'Battery.Devices.8', + 'Battery.Devices.9', + 'Battery.Devices.10' ]; const pathsToSave = []; @@ -120,7 +112,7 @@ export const transformInputToBatteryViewData = async ( Temperature: { name: 'Temperature', data: [] }, Power: { name: 'Power', data: [] }, Voltage: { name: 'Voltage', data: [] }, - Current: { name: 'Voltage', data: [] } + Current: { name: 'Current', data: [] } }; const chartOverview: BatteryOverviewInterface = { @@ -132,7 +124,6 @@ export const transformInputToBatteryViewData = async ( }; let initialiation = true; - let timestampArray: number[] = []; let adjustedTimestampArray = []; const timestampPromises = []; @@ -153,7 +144,7 @@ export const transformInputToBatteryViewData = async ( for (var i = 0; i < timestampArray.length; i++) { timestampPromises.push( - fetchDataForOneTime( + fetchJsonDataForOneTime( UnixTime.fromTicks(timestampArray[i], true), s3Credentials ) @@ -170,7 +161,7 @@ export const transformInputToBatteryViewData = async ( adjustedTimestampArray.push(adjustedTimestamp); } - const results: Promise>>[] = + const results: Promise>>[] = await Promise.all(timestampPromises); for (let i = 0; i < results.length; i++) { @@ -180,8 +171,10 @@ export const transformInputToBatteryViewData = async ( const timestamp = Object.keys(results[i])[ Object.keys(results[i]).length - 1 ]; + const result = results[i][timestamp]; - const battery_nodes = result['/Config/Devices/BatteryNodes'].value + + const battery_nodes = result.Config.Devices.BatteryNodes.value .toString() .split(','); @@ -227,28 +220,31 @@ export const transformInputToBatteryViewData = async ( let category = categories[category_index]; for (let j = 0; j < pathsToSave.length; j++) { - let path = pathsToSearch[j] + pathCategories[category_index]; + let path = + pathsToSearch[j] + pathCategories[category_index] + '.value'; - if (result[path]) { - const value = result[path]; + //if (result[path]) { + const value = path + .split('.') + .reduce((o, key) => (o ? o[key] : undefined), result); - if (value.value < chartOverview[category].min) { - chartOverview[category].min = value.value; - } - - if (value.value > chartOverview[category].max) { - chartOverview[category].max = value.value; - } - chartData[category].data[pathsToSave[j]].data.push([ - adjustedTimestampArray[i], - value.value - ]); - } else { - // chartData[category].data[pathsToSave[j]].data.push([ - // adjustedTimestampArray[i], - // null - // ]); + if (value < chartOverview[category].min) { + chartOverview[category].min = value; } + + if (value > chartOverview[category].max) { + chartOverview[category].max = value; + } + chartData[category].data[pathsToSave[j]].data.push([ + adjustedTimestampArray[i], + value + ]); + // } else { + // // chartData[category].data[pathsToSave[j]].data.push([ + // // adjustedTimestampArray[i], + // // null + // // ]); + // } } } } @@ -288,6 +284,491 @@ export const transformInputToBatteryViewData = async ( }; }; +const fetchJsonDataForOneTime = async ( + startUnixTime: UnixTime, + s3Credentials: I_S3Credentials +): Promise>> => { + var timeperiodToSearch = 2; + let res; + let timestampToFetch; + + for (var i = 0; i < timeperiodToSearch; i++) { + timestampToFetch = startUnixTime.later(TimeSpan.fromSeconds(i)); + + try { + res = await fetchDataJson(timestampToFetch, s3Credentials); + + if (res !== FetchResult.notAvailable && res !== FetchResult.tryLater) { + //console.log('Successfully fetched ' + timestampToFetch); + return res; + } + } catch (err) { + console.error('Error fetching data:', err); + } + } + return null; +}; + +export const transformInputToAggregatedDataJson = async ( + s3Credentials: I_S3Credentials, + start_date: dayjs.Dayjs, + end_date: dayjs.Dayjs +): Promise<{ + chartAggregatedData: chartAggregatedDataInterface; + chartOverview: overviewInterface; + dateList: string[]; +}> => { + const data = {}; + const overviewData = {}; + const MAX_NUMBER = 9999999; + const dateList = []; + + let currentDay = start_date; + + const pathsToSearch = [ + 'MinSoc', + 'MaxSoc', + 'PvPower', + 'DischargingBatteryPower', + 'ChargingBatteryPower', + 'GridImportPower', + 'GridExportPower', + 'HeatingPower' + ]; + + const categories = [ + 'minsoc', + 'maxsoc', + 'pvProduction', + 'dcChargingPower', + 'heatingPower', + 'dcDischargingPower', + 'gridImportPower', + 'gridExportPower' + ]; + + const chartAggregatedData: chartAggregatedDataInterface = { + minsoc: { name: 'min SOC', data: [] }, + maxsoc: { name: 'max SOC', data: [] }, + pvProduction: { name: 'Pv Energy', data: [] }, + dcChargingPower: { name: 'Charging Battery Energy', data: [] }, + heatingPower: { name: 'Heating Energy', data: [] }, + dcDischargingPower: { name: 'Discharging Battery Energy', data: [] }, + gridImportPower: { name: 'Grid Import Energy', data: [] }, + gridExportPower: { name: 'Grid Export Energy', data: [] } + }; + + const chartOverview: overviewInterface = { + soc: { magnitude: 0, unit: '', min: 0, max: 0 }, + temperature: { magnitude: 0, unit: '', min: 0, max: 0 }, + dcPower: { magnitude: 0, unit: '', min: 0, max: 0 }, + dcPowerWithoutHeating: { magnitude: 0, unit: '', min: 0, max: 0 }, + gridPower: { magnitude: 0, unit: '', min: 0, max: 0 }, + pvProduction: { magnitude: 0, unit: '', min: 0, max: 0 }, + dcBusVoltage: { magnitude: 0, unit: '', min: 0, max: 0 }, + overview: { magnitude: 0, unit: '', min: 0, max: 0 }, + ACLoad: { magnitude: 0, unit: '', min: 0, max: 0 }, + DCLoad: { magnitude: 0, unit: '', min: 0, max: 0 } + }; + + pathsToSearch.forEach((path) => { + data[path] = []; + overviewData[path] = { + magnitude: 0, + unit: '', + min: MAX_NUMBER, + max: -MAX_NUMBER + }; + }); + + const timestampPromises = []; + + while (currentDay.isBefore(end_date)) { + timestampPromises.push( + fetchAggregatedDataJson(currentDay.format('YYYY-MM-DD'), s3Credentials) + ); + currentDay = currentDay.add(1, 'day'); + } + + const results = await Promise.all(timestampPromises); + currentDay = start_date; + + for (let i = 0; i < results.length; i++) { + const result = results[i]; + if ( + result === FetchResult.notAvailable || + result === FetchResult.tryLater + ) { + // Handle not available or try later case + } else { + dateList.push(currentDay.format('DD-MM')); + pathsToSearch.forEach((path) => { + const value = path + .split('.') + .reduce((o, key) => (o ? o[key] : undefined), result); + + if (value !== undefined) { + if (path === '.GridExportPower') { + result.GridExportPower = -value; + // result[path].value = -result[path].value; + } + if (value < overviewData[path].min) { + overviewData[path].min = value; + } + if (value > overviewData[path].max) { + overviewData[path].max = value; + } + data[path].push(value as number); + } + }); + } + currentDay = currentDay.add(1, 'day'); + } + + pathsToSearch.forEach((path) => { + let value = Math.max( + Math.abs(overviewData[path].max), + Math.abs(overviewData[path].min) + ); + let magnitude = 0; + + if (value < 0) { + value = -value; + } + while (value >= 1000) { + value /= 1000; + magnitude++; + } + overviewData[path].magnitude = magnitude; + }); + + let path = 'MinSoc'; + chartAggregatedData.minsoc.data = data[path]; + + path = 'MaxSoc'; + chartAggregatedData.maxsoc.data = data[path]; + + chartOverview.soc = { + unit: '(%)', + magnitude: overviewData[path].magnitude, + min: 0, + max: 100 + }; + + path = 'PvPower'; + chartAggregatedData.pvProduction.data = data[path]; + + chartOverview.pvProduction = { + magnitude: overviewData[path].magnitude, + unit: '(kWh)', + min: overviewData[path].min, + max: overviewData[path].max + }; + + path = 'ChargingBatteryPower'; + chartAggregatedData.dcChargingPower.data = data[path]; + + path = 'DischargingBatteryPower'; + chartAggregatedData.dcDischargingPower.data = data[path]; + + path = 'HeatingPower'; + chartAggregatedData.heatingPower.data = data[path]; + + chartOverview.dcPowerWithoutHeating = { + magnitude: Math.max( + overviewData['ChargingBatteryPower'].magnitude, + overviewData['DischargingBatteryPower'].magnitude + ), + unit: '(kWh)', + min: overviewData['DischargingBatteryPower'].min, + max: overviewData['ChargingBatteryPower'].max + }; + + chartOverview.dcPower = { + magnitude: Math.max( + overviewData['ChargingBatteryPower'].magnitude, + overviewData['HeatingPower'].magnitude, + overviewData['DischargingBatteryPower'].magnitude + ), + unit: '(kWh)', + min: overviewData['DischargingBatteryPower'].min, + max: + overviewData['ChargingBatteryPower'].max + + overviewData['HeatingPower'].max + }; + + path = 'GridImportPower'; + chartAggregatedData.gridImportPower.data = data[path]; + + path = 'GridExportPower'; + chartAggregatedData.gridExportPower.data = data[path]; + + chartOverview.gridPower = { + magnitude: Math.max( + overviewData['GridImportPower'].magnitude, + overviewData['GridExportPower'].magnitude + ), + unit: '(kWh)', + min: overviewData['GridExportPower'].min, + max: overviewData['GridImportPower'].max + }; + + chartOverview.overview = { + magnitude: 0, + unit: '(kWh)', + min: Math.min( + overviewData['GridImportPower'].min, + overviewData['GridExportPower'].min, + overviewData['PvPower'].min + ), + max: Math.max( + overviewData['GridImportPower'].max, + overviewData['GridExportPower'].max, + overviewData['PvPower'].max + ) + }; + + // console.log(chartAggregatedData); + + return { + chartAggregatedData: chartAggregatedData, + chartOverview: chartOverview, + dateList: dateList + }; +}; + +//UNCOMMENT THE FOLLOWING FOR CSV +// We use this function in order to retrieve data for main stats. +//The data is of the following form: +//'Soc' : {name:'Soc',data:[ +// 'Node1': {name:'Node1', data: [[timestamp,value],[timestamp,value]]}, +// 'Node2': {name:'Node2', data: [[timestamp,value],[timestamp,value]]}, +// ]}, +//'Temperature' : {name:'Temperature',data:[ +// 'Node1': {name:'Node1', data: [[timestamp,value],[timestamp,value]]}, +// 'Node2': {name:'Node2', data: [[timestamp,value],[timestamp,value]]}, +// ]} + +// export const transformInputToBatteryViewData = async ( +// s3Credentials: I_S3Credentials, +// id: number, +// product: number, +// start_time?: UnixTime, +// end_time?: UnixTime +// ): Promise<{ +// chartData: BatteryDataInterface; +// chartOverview: BatteryOverviewInterface; +// }> => { +// const prefixes = ['', 'k', 'M', 'G', 'T']; +// const MAX_NUMBER = 9999999; +// const categories = ['Soc', 'Temperature', 'Power', 'Voltage', 'Current']; +// const pathCategories = [ +// 'Soc', +// 'Temperatures/Cells/Average', +// 'Dc/Power', +// 'Dc/Voltage', +// 'Dc/Current' +// ]; +// +// const pathsToSearch = [ +// '/Battery/Devices/1/', +// '/Battery/Devices/2/', +// '/Battery/Devices/3/', +// '/Battery/Devices/4/', +// '/Battery/Devices/5/', +// '/Battery/Devices/6/', +// '/Battery/Devices/7/', +// '/Battery/Devices/8/', +// '/Battery/Devices/9/', +// '/Battery/Devices/10/' +// ]; +// +// const pathsToSave = []; +// +// const chartData: BatteryDataInterface = { +// Soc: { name: 'State Of Charge', data: [] }, +// Temperature: { name: 'Temperature', data: [] }, +// Power: { name: 'Power', data: [] }, +// Voltage: { name: 'Voltage', data: [] }, +// Current: { name: 'Voltage', data: [] } +// }; +// +// const chartOverview: BatteryOverviewInterface = { +// Soc: { magnitude: 0, unit: '', min: 0, max: 0 }, +// Temperature: { magnitude: 0, unit: '', min: 0, max: 0 }, +// Power: { magnitude: 0, unit: '', min: 0, max: 0 }, +// Voltage: { magnitude: 0, unit: '', min: 0, max: 0 }, +// Current: { magnitude: 0, unit: '', min: 0, max: 0 } +// }; +// +// let initialiation = true; +// +// let timestampArray: number[] = []; +// let adjustedTimestampArray = []; +// const timestampPromises = []; +// +// await axiosConfig +// .get( +// `/GetCsvTimestampsForInstallation?id=${id}&start=${start_time.ticks}&end=${end_time.ticks}` +// ) +// .then((res: AxiosResponse) => { +// timestampArray = res.data; +// }) +// .catch((err: AxiosError) => { +// if (err.response && err.response.status == 401) { +// //removeToken(); +// //navigate(routes.login); +// } +// }); +// +// for (var i = 0; i < timestampArray.length; i++) { +// timestampPromises.push( +// fetchDataForOneTime( +// UnixTime.fromTicks(timestampArray[i], true), +// s3Credentials +// ) +// ); +// +// const adjustedTimestamp = +// product == 0 +// ? new Date(timestampArray[i] * 1000) +// : new Date(timestampArray[i] * 100000); +// //Timezone offset is negative, so we convert the timestamp to the current zone by subtracting the corresponding offset +// adjustedTimestamp.setHours( +// adjustedTimestamp.getHours() - adjustedTimestamp.getTimezoneOffset() / 60 +// ); +// adjustedTimestampArray.push(adjustedTimestamp); +// } +// +// const results: Promise>>[] = +// await Promise.all(timestampPromises); +// +// for (let i = 0; i < results.length; i++) { +// if (results[i] == null) { +// // Handle not available or try later case +// } else { +// const timestamp = Object.keys(results[i])[ +// Object.keys(results[i]).length - 1 +// ]; +// const result = results[i][timestamp]; +// const battery_nodes = result['/Config/Devices/BatteryNodes'].value +// .toString() +// .split(','); +// +// //Initialize the chartData structure based on the node names extracted from the first result +// let old_length = pathsToSave.length; +// +// if (battery_nodes.length > old_length) { +// battery_nodes.forEach((node) => { +// if (!pathsToSave.includes('Node' + node)) { +// pathsToSave.push('Node' + node); +// } +// }); +// } +// +// if (initialiation) { +// initialiation = false; +// categories.forEach((category) => { +// chartData[category].data = []; +// chartOverview[category] = { +// magnitude: 0, +// unit: '', +// min: MAX_NUMBER, +// max: -MAX_NUMBER +// }; +// }); +// } +// +// if (battery_nodes.length > old_length) { +// categories.forEach((category) => { +// pathsToSave.forEach((path) => { +// if (pathsToSave.indexOf(path) >= old_length) { +// chartData[category].data[path] = { name: path, data: [] }; +// } +// }); +// }); +// } +// +// for ( +// let category_index = 0; +// category_index < pathCategories.length; +// category_index++ +// ) { +// let category = categories[category_index]; +// +// for (let j = 0; j < pathsToSave.length; j++) { +// let path = pathsToSearch[j] + pathCategories[category_index]; +// +// if (result[path]) { +// const value = result[path]; +// +// if (value.value < chartOverview[category].min) { +// chartOverview[category].min = value.value; +// } +// +// if (value.value > chartOverview[category].max) { +// chartOverview[category].max = value.value; +// } +// chartData[category].data[pathsToSave[j]].data.push([ +// adjustedTimestampArray[i], +// value.value +// ]); +// } else { +// // chartData[category].data[pathsToSave[j]].data.push([ +// // adjustedTimestampArray[i], +// // null +// // ]); +// } +// } +// } +// } +// } +// categories.forEach((category) => { +// let value = Math.max( +// Math.abs(chartOverview[category].max), +// Math.abs(chartOverview[category].min) +// ); +// let magnitude = 0; +// +// if (value < 0) { +// value = -value; +// } +// while (value >= 1000) { +// value /= 1000; +// magnitude++; +// } +// chartOverview[category].magnitude = magnitude; +// }); +// +// chartOverview.Soc.unit = '(%)'; +// chartOverview.Soc.min = 0; +// chartOverview.Soc.max = 100; +// chartOverview.Temperature.unit = '(°C)'; +// chartOverview.Power.unit = +// '(' + prefixes[chartOverview['Power'].magnitude] + 'W' + ')'; +// chartOverview.Voltage.unit = +// '(' + prefixes[chartOverview['Voltage'].magnitude] + 'V' + ')'; +// +// chartOverview.Current.unit = +// '(' + prefixes[chartOverview['Current'].magnitude] + 'A' + ')'; +// +// return { +// chartData: chartData, +// chartOverview: chartOverview +// }; +// }; + +// We use this function in order to retrieve data for main stats. +//The data is of the following form: +//'Soc' : {name:'Soc',data:[ +// 'Node1': {name:'Node1', data: [[timestamp,value],[timestamp,value]]}, +// 'Node2': {name:'Node2', data: [[timestamp,value],[timestamp,value]]}, +// ]}, +//'Temperature' : {name:'Temperature',data:[ +// 'Node1': {name:'Node1', data: [[timestamp,value],[timestamp,value]]}, +// 'Node2': {name:'Node2', data: [[timestamp,value],[timestamp,value]]}, +// ]} + const fetchDataForOneTime = async ( startUnixTime: UnixTime, s3Credentials: I_S3Credentials From 0592179e758ee66311c1fe7e7c994c7d5066b50e Mon Sep 17 00:00:00 2001 From: Yinyin Liu Date: Mon, 24 Feb 2025 12:11:31 +0100 Subject: [PATCH 3/5] create script to update files in all Venus and Cerbo --- firmware/update_all_installations.sh | 92 ++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100755 firmware/update_all_installations.sh diff --git a/firmware/update_all_installations.sh b/firmware/update_all_installations.sh new file mode 100755 index 000000000..8de55743c --- /dev/null +++ b/firmware/update_all_installations.sh @@ -0,0 +1,92 @@ +#!/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 + + # 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 + + # 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 + + 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" From 8b0b49fa3025562a10622e55659a188e7f22ecc4 Mon Sep 17 00:00:00 2001 From: Noe Date: Tue, 25 Feb 2025 08:57:36 +0100 Subject: [PATCH 4/5] Aggregator file supports json --- .../dbus-fzsonick-48tl/aggregator.py | 150 +++++++++++++----- .../dbus-fzsonick-48tl/aggregator.py | 146 ++++++++++++----- firmware/update_all_installations.sh | 31 ++++ 3 files changed, 241 insertions(+), 86 deletions(-) diff --git a/firmware/Cerbo_Release/CerboReleaseFiles/dbus-fzsonick-48tl/aggregator.py b/firmware/Cerbo_Release/CerboReleaseFiles/dbus-fzsonick-48tl/aggregator.py index ed1a9f3bc..1e6c2511d 100755 --- a/firmware/Cerbo_Release/CerboReleaseFiles/dbus-fzsonick-48tl/aggregator.py +++ b/firmware/Cerbo_Release/CerboReleaseFiles/dbus-fzsonick-48tl/aggregator.py @@ -13,10 +13,11 @@ import hmac import hashlib from threading import Thread, Event import config as cfg +import json -CSV_DIR = "/data/csv_files/" -HOURLY_DIR = "/data/csv_files/HourlyData" -DAILY_DIR = "/data/csv_files/DailyData" +JSON_DIR = "/data/json_files/" +HOURLY_DIR = "/data/json_files/HourlyData" +DAILY_DIR = "/data/json_files/DailyData" # S3 Credentials print("Start with the correct credentials") @@ -38,23 +39,24 @@ class AggregatedData: self.charging_battery_power = charging_battery_power self.heating_power = heating_power - def to_csv(self): - return ("/MinSoc;{};\n" - "/MaxSoc;{};\n" - "/DischargingBatteryPower;{};\n" - "/ChargingBatteryPower;{};\n" - "/HeatingPower;{};").format( - self.min_soc, self.max_soc, self.discharging_battery_power, self.charging_battery_power, self.heating_power) + def to_json(self): + return json.dumps({ + "MinSoc": self.min_soc, + "MaxSoc": self.max_soc, + "DischargingBatteryPower": self.discharging_battery_power, + "ChargingBatteryPower": self.charging_battery_power, + "HeatingPower": self.heating_power + }, separators=(',', ':')) def save(self, directory): timestamp = int(time.time()) if not os.path.exists(directory): os.makedirs(directory) - csv_path = os.path.join(directory, "{}.csv".format(timestamp)) - with open(csv_path, 'w') as file: - file.write(self.to_csv()) - print("Saved file to:", csv_path) - print("File content:\n", self.to_csv()) + json_path = os.path.join(directory, "{}.json".format(timestamp)) + with open(json_path, 'w') as file: + file.write(self.to_json()) + print("Saved file to:", json_path) + print("File content:\n", self.to_json()) @staticmethod def delete_data(directory): @@ -67,16 +69,16 @@ class AggregatedData: print("Deleted file: {}".format(file_path)) def push_to_s3(self, s3_config): - csv_data = self.to_csv() - compressed_csv = self.compress_csv_data(csv_data) + json_data = self.to_json() + compressed_json = self.compress_json_data(json_data) now = datetime.now() if now.hour == 0 and now.minute < 30: adjusted_date = now - timedelta(days=1) else: adjusted_date = now - s3_path = adjusted_date.strftime("%Y-%m-%d") + ".csv" - response = s3_config.create_put_request(s3_path, compressed_csv) + s3_path = adjusted_date.strftime("%Y-%m-%d") + ".json" + response = s3_config.create_put_request(s3_path, compressed_json) if response.status_code != 200: print("ERROR: PUT", response.text) return False @@ -84,10 +86,10 @@ class AggregatedData: return True @staticmethod - def compress_csv_data(csv_data): + def compress_json_data(json_data): memory_stream = io.BytesIO() 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() return base64.b64encode(compressed_bytes).decode('utf-8') @@ -150,7 +152,7 @@ class Aggregator: current_time = datetime.now() after_timestamp = datetime_to_timestamp(current_time - timedelta(hours=1)) 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") aggregated_data.save(HOURLY_DIR) except Exception as e: @@ -195,31 +197,55 @@ class Aggregator: @staticmethod def create_hourly_data(directory, after_timestamp, before_timestamp): node_data = {} + print("INSIDE HOURLY MANAGER") 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: - 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 + + data = json.load(file) + devices = data.get("Battery", {}).get("Devices", {}) + + for node_number, device_data in devices.items(): + + if node_number not in node_data: + node_data[node_number] = {'soc': [], 'discharge': [], 'charge': [], 'heating': []} + + value = device_data.get("Soc", {}).get("value", "N/A") + node_data[node_number]['soc'].append(float(value)) + + value = device_data.get("Dc", {}).get("Power", "N/A").get("value", "N/A") + value = float(value) + if value < 0: + node_data[node_number]['discharge'].append(value) + else: + node_data[node_number]['charge'].append(value) + value = device_data.get("HeatingPower", "N/A").get("value", "N/A") + 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: # No data collected, return default AggregatedData with zeros @@ -249,7 +275,45 @@ class Aggregator: @staticmethod 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 def is_file_within_time_range(filename, start_time, end_time): diff --git a/firmware/Venus_Release/VenusReleaseFiles/dbus-fzsonick-48tl/aggregator.py b/firmware/Venus_Release/VenusReleaseFiles/dbus-fzsonick-48tl/aggregator.py index 0f787b8ce..548437fb2 100755 --- a/firmware/Venus_Release/VenusReleaseFiles/dbus-fzsonick-48tl/aggregator.py +++ b/firmware/Venus_Release/VenusReleaseFiles/dbus-fzsonick-48tl/aggregator.py @@ -13,10 +13,11 @@ import hmac import hashlib from threading import Thread, Event import config as cfg +import json -CSV_DIR = "/data/csv_files/" -HOURLY_DIR = "/data/csv_files/HourlyData" -DAILY_DIR = "/data/csv_files/DailyData" +JSON_DIR = "/data/json_files/" +HOURLY_DIR = "/data/json_files/HourlyData" +DAILY_DIR = "/data/json_files/DailyData" print("start with the correct credentials") @@ -37,23 +38,24 @@ class AggregatedData: self.charging_battery_power = charging_battery_power self.heating_power = heating_power - def to_csv(self): - return ("/MinSoc;{};\n" - "/MaxSoc;{};\n" - "/DischargingBatteryPower;{};\n" - "/ChargingBatteryPower;{};\n" - "/HeatingPower;{};").format( - self.min_soc, self.max_soc, self.discharging_battery_power, self.charging_battery_power, self.heating_power) + def to_json(self): + return json.dumps({ + "MinSoc": self.min_soc, + "MaxSoc": self.max_soc, + "DischargingBatteryPower": self.discharging_battery_power, + "ChargingBatteryPower": self.charging_battery_power, + "HeatingPower": self.heating_power + }, separators=(',', ':')) def save(self, directory): timestamp = int(time.time()) if not os.path.exists(directory): os.makedirs(directory) - csv_path = os.path.join(directory, "{}.csv".format(timestamp)) - with open(csv_path, 'w') as file: - file.write(self.to_csv()) - print("Saved file to:", csv_path) - print("File content:\n", self.to_csv()) + json_path = os.path.join(directory, "{}.json".format(timestamp)) + with open(json_path, 'w') as file: + file.write(self.to_json()) + print("Saved file to:", json_path) + print("File content:\n", self.to_json()) @staticmethod def delete_data(directory): @@ -66,16 +68,16 @@ class AggregatedData: print("Deleted file: {}".format(file_path)) def push_to_s3(self, s3_config): - csv_data = self.to_csv() - compressed_csv = self.compress_csv_data(csv_data) + json_data = self.to_json() + compressed_json = self.compress_json_data(json_data) now = datetime.now() if now.hour == 0 and now.minute < 30: adjusted_date = now - timedelta(days=1) else: adjusted_date = now - s3_path = adjusted_date.strftime("%Y-%m-%d") + ".csv" - response = s3_config.create_put_request(s3_path, compressed_csv) + s3_path = adjusted_date.strftime("%Y-%m-%d") + ".json" + response = s3_config.create_put_request(s3_path, compressed_json) if response.status_code != 200: print("ERROR: PUT", response.text) return False @@ -83,10 +85,10 @@ class AggregatedData: return True @staticmethod - def compress_csv_data(csv_data): + def compress_json_data(json_data): memory_stream = io.BytesIO() 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() return base64.b64encode(compressed_bytes).decode('utf-8') @@ -152,7 +154,7 @@ class Aggregator: current_time = datetime.now() after_timestamp = datetime_to_timestamp(current_time - timedelta(hours=1)) 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") aggregated_data.save(HOURLY_DIR) except Exception as e: @@ -205,26 +207,49 @@ class Aggregator: 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: - 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 + data = json.load(file) + devices = data.get("Battery", {}).get("Devices", {}) + + for node_number, device_data in devices.items(): + + if node_number not in node_data: + node_data[node_number] = {'soc': [], 'discharge': [], 'charge': [], 'heating': []} + + value = device_data.get("Soc", {}).get("value", "N/A") + node_data[node_number]['soc'].append(float(value)) + + value = device_data.get("Dc", {}).get("Power", "N/A").get("value", "N/A") + value=float(value) + if value < 0: + node_data[node_number]['discharge'].append(value) + else: + node_data[node_number]['charge'].append(value) + value = device_data.get("HeatingPower", "N/A").get("value", "N/A") + 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: # No data collected, return default AggregatedData with zeros @@ -254,7 +279,42 @@ class Aggregator: @staticmethod 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 def is_file_within_time_range(filename, start_time, end_time): diff --git a/firmware/update_all_installations.sh b/firmware/update_all_installations.sh index 8de55743c..1ea7c0a92 100755 --- a/firmware/update_all_installations.sh +++ b/firmware/update_all_installations.sh @@ -38,6 +38,16 @@ deploy() { 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 @@ -51,6 +61,19 @@ deploy() { 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 @@ -58,6 +81,14 @@ deploy() { 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 From abeac4f49aba617bd8d6674b9fa95bf15c1cbfe1 Mon Sep 17 00:00:00 2001 From: Noe Date: Tue, 25 Feb 2025 10:23:37 +0100 Subject: [PATCH 5/5] Differentiate between Salimax and Salidomo (csv and json) so that the frontend supports both --- .../dashboards/BatteryView/BatteryView.tsx | 340 ++--- .../BatteryView/BatteryViewSalidomo.tsx | 493 +++++++ .../BatteryView/DetailedBatteryView.tsx | 704 ++++----- .../DetailedBatteryViewSalidomo.tsx | 1312 +++++++++++++++++ .../dashboards/BatteryView/MainStats.tsx | 8 +- .../BatteryView/MainStatsSalidomo.tsx | 812 ++++++++++ .../dashboards/Installations/Installation.tsx | 25 +- .../dashboards/Installations/index.tsx | 2 +- .../SalidomoInstallations/Installation.tsx | 6 +- .../SalidomoInstallations/index.tsx | 2 +- .../SodiohomeInstallations/Installation.tsx | 4 +- .../SodiohomeInstallations/index.tsx | 2 +- .../frontend-marios2/src/interfaces/Chart.tsx | 424 +++--- 13 files changed, 3270 insertions(+), 864 deletions(-) create mode 100644 typescript/frontend-marios2/src/content/dashboards/BatteryView/BatteryViewSalidomo.tsx create mode 100644 typescript/frontend-marios2/src/content/dashboards/BatteryView/DetailedBatteryViewSalidomo.tsx create mode 100644 typescript/frontend-marios2/src/content/dashboards/BatteryView/MainStatsSalidomo.tsx diff --git a/typescript/frontend-marios2/src/content/dashboards/BatteryView/BatteryView.tsx b/typescript/frontend-marios2/src/content/dashboards/BatteryView/BatteryView.tsx index 652504a90..4927b0e14 100644 --- a/typescript/frontend-marios2/src/content/dashboards/BatteryView/BatteryView.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/BatteryView/BatteryView.tsx @@ -11,6 +11,7 @@ import { TableRow, Typography } from '@mui/material'; +import { TopologyValues } from '../Log/graph.util'; import { Link, Route, @@ -18,17 +19,16 @@ import { useLocation, useNavigate } from 'react-router-dom'; +import Button from '@mui/material/Button'; +import { FormattedMessage } from 'react-intl'; import { I_S3Credentials } from '../../../interfaces/S3Types'; import routes from '../../../Resources/routes.json'; import CircularProgress from '@mui/material/CircularProgress'; -import { JSONRecordData } from '../Log/graph.util'; -import Button from '@mui/material/Button'; -import { FormattedMessage } from 'react-intl'; -import MainStats from './MainStats'; import DetailedBatteryView from './DetailedBatteryView'; +import MainStats from './MainStats'; interface BatteryViewProps { - values: JSONRecordData; + values: TopologyValues; s3Credentials: I_S3Credentials; installationId: number; productNum: number; @@ -39,15 +39,12 @@ function BatteryView(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 sortedBatteryView = + props.values != null + ? [...props.values.batteryView].sort((a, b) => b.BatteryId - a.BatteryId) + : []; const [loading, setLoading] = useState(sortedBatteryView.length == 0); @@ -55,13 +52,13 @@ function BatteryView(props: BatteryViewProps) { navigate(routes.mainstats); }; - // const findBatteryData = (batteryId: number) => { - // for (let i = 0; i < props.values.batteryView.length; i++) { - // if (props.values.batteryView[i].BatteryId == batteryId) { - // return props.values.batteryView[i]; - // } - // } - // }; + const findBatteryData = (batteryId: number) => { + for (let i = 0; i < props.values.batteryView.length; i++) { + if (props.values.batteryView[i].BatteryId == batteryId) { + return props.values.batteryView[i]; + } + } + }; useEffect(() => { if (sortedBatteryView.length == 0) { @@ -179,23 +176,20 @@ function BatteryView(props: BatteryViewProps) { > } /> - {Object.entries(props.values.Battery.Devices).map( - ([BatteryId, battery]) => ( - - } - /> - ) - )} + {props.values.batteryView.map((battery) => ( + + } + /> + ))} @@ -225,9 +219,9 @@ function BatteryView(props: BatteryViewProps) { - {sortedBatteryView.map(({ BatteryId, battery }) => ( + {sortedBatteryView.map((battery) => ( - {'Node ' + BatteryId} + {'Node ' + battery.BatteryId} - {battery.Dc.Power.value + ' W'} + {battery.Power.value + ' ' + battery.Power.unit} 57 + battery.Voltage.value < 44 || + battery.Voltage.value > 57 ? '#FF033E' : '#32CD32', - color: battery.Dc.Voltage.value ? 'inherit' : 'white' + color: + battery.Voltage.value === '' ? 'white' : 'inherit' }} > - {battery.Dc.Voltage.value + ' V'} + {battery.Voltage.value + ' ' + battery.Voltage.unit} - {battery.Soc.value + ' %'} + {battery.Soc.value + ' ' + battery.Soc.unit} 300 + battery.AverageTemperature.value > 300 ? '#FF033E' - : battery.Temperatures.Cells.Average.value > 280 + : battery.AverageTemperature.value > 280 ? '#ffbf00' - : battery.Temperatures.Cells.Average.value < 245 + : battery.AverageTemperature.value < 245 ? '#008FFB' : '#32CD32' }} > - {battery.Temperatures.Cells.Average.value + ' C'} + {battery.AverageTemperature.value + + ' ' + + battery.AverageTemperature.unit} - {/*{props.productNum === 0 && (*/} - {/* <>*/} - {/* */} - {/* {battery.Warnings.value === '' ? (*/} - {/* 'None'*/} - {/* ) : battery.Warnings.value.toString().split('-')*/} - {/* .length > 1 ? (*/} - {/* */} - {/* Multiple Warnings*/} - {/* */} - {/* ) : (*/} - {/* battery.Warnings.value*/} - {/* )}*/} - {/* */} - {/* */} - {/* {battery.Alarms.value === '' ? (*/} - {/* 'None'*/} - {/* ) : battery.Alarms.value.toString().split('-')*/} - {/* .length > 1 ? (*/} - {/* */} - {/* Multiple Alarms*/} - {/* */} - {/* ) : (*/} - {/* battery.Alarms.value*/} - {/* )}*/} - {/* */} - {/* */} - {/*)}*/} - - {props.productNum === 1 && ( - <> - + {battery.Warnings.value === '' ? ( + 'None' + ) : battery.Warnings.value.toString().split('-').length > + 1 ? ( + - {Number(battery.Warnings) === 0 ? ( - 'None' - ) : Number(battery.Warnings) === 1 ? ( - - New Warning - - ) : ( - - Multiple Warnings - - )} - - + ) : ( + battery.Warnings.value + )} + + + {battery.Alarms.value === '' ? ( + 'None' + ) : battery.Alarms.value.toString().split('-').length > + 1 ? ( + - {Number(battery.Alarms) === 0 ? ( - 'None' - ) : Number(battery.Alarms) === 1 ? ( - - New Alarm - - ) : ( - - Multiple Alarms - - )} - - - )} + Multiple Alarms + + ) : ( + battery.Alarms.value + )} + ))} diff --git a/typescript/frontend-marios2/src/content/dashboards/BatteryView/BatteryViewSalidomo.tsx b/typescript/frontend-marios2/src/content/dashboards/BatteryView/BatteryViewSalidomo.tsx new file mode 100644 index 000000000..9dad16594 --- /dev/null +++ b/typescript/frontend-marios2/src/content/dashboards/BatteryView/BatteryViewSalidomo.tsx @@ -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 && ( + + + + Unable to communicate with the installation + + + Please wait or refresh the page + + + )} + {loading && props.connected && ( + + + + Battery service is not available at the moment + + + Please wait or refresh the page + + + )} + + {!loading && props.connected && ( + + + + + + + + + + + + + } + /> + {Object.entries(props.values.Battery.Devices).map( + ([BatteryId, battery]) => ( + + } + /> + ) + )} + + + + + + + + Battery + Firmware + Power + Voltage + SoC + Temperature + Warnings + Alarms + + + + {sortedBatteryView.map(({ BatteryId, battery }) => ( + + + + {'Node ' + BatteryId} + + + + {battery.FwVersion.value} + + + {battery.Dc.Power.value + ' W'} + + 57 + ? '#FF033E' + : '#32CD32', + color: battery.Dc.Voltage.value ? 'inherit' : 'white' + }} + > + {battery.Dc.Voltage.value + ' V'} + + + {battery.Soc.value + ' %'} + + 300 + ? '#FF033E' + : battery.Temperatures.Cells.Average.value > 280 + ? '#ffbf00' + : battery.Temperatures.Cells.Average.value < 245 + ? '#008FFB' + : '#32CD32' + }} + > + {battery.Temperatures.Cells.Average.value + ' C'} + + + {/*{props.productNum === 0 && (*/} + {/* <>*/} + {/* */} + {/* {battery.Warnings.value === '' ? (*/} + {/* 'None'*/} + {/* ) : battery.Warnings.value.toString().split('-')*/} + {/* .length > 1 ? (*/} + {/* */} + {/* Multiple Warnings*/} + {/* */} + {/* ) : (*/} + {/* battery.Warnings.value*/} + {/* )}*/} + {/* */} + {/* */} + {/* {battery.Alarms.value === '' ? (*/} + {/* 'None'*/} + {/* ) : battery.Alarms.value.toString().split('-')*/} + {/* .length > 1 ? (*/} + {/* */} + {/* Multiple Alarms*/} + {/* */} + {/* ) : (*/} + {/* battery.Alarms.value*/} + {/* )}*/} + {/* */} + {/* */} + {/*)}*/} + + {props.productNum === 1 && ( + <> + + {Number(battery.Warnings) === 0 ? ( + 'None' + ) : Number(battery.Warnings) === 1 ? ( + + New Warning + + ) : ( + + Multiple Warnings + + )} + + + {Number(battery.Alarms) === 0 ? ( + 'None' + ) : Number(battery.Alarms) === 1 ? ( + + New Alarm + + ) : ( + + Multiple Alarms + + )} + + + )} + + ))} + +
+
+
+ )} + + ); +} + +export default BatteryViewSalidomo; diff --git a/typescript/frontend-marios2/src/content/dashboards/BatteryView/DetailedBatteryView.tsx b/typescript/frontend-marios2/src/content/dashboards/BatteryView/DetailedBatteryView.tsx index 1100ab672..f5edd711c 100644 --- a/typescript/frontend-marios2/src/content/dashboards/BatteryView/DetailedBatteryView.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/BatteryView/DetailedBatteryView.tsx @@ -16,7 +16,7 @@ import { TableRow, Typography } from '@mui/material'; -import { Device } from '../Log/graph.util'; +import { Battery } from '../Log/graph.util'; import { useNavigate } from 'react-router-dom'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import Button from '@mui/material/Button'; @@ -25,9 +25,8 @@ import { UserType } from '../../../interfaces/UserTypes'; import { UserContext } from '../../../contexts/userContext'; interface DetailedBatteryViewProps { - batteryId: number; s3Credentials: I_S3Credentials; - batteryData: Device; + batteryData: Battery; installationId: number; productNum: number; } @@ -36,7 +35,6 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { if (props.batteryData === null) { return null; } - const navigate = useNavigate(); const [openModalFirmwareUpdate, setOpenModalFirmwareUpdate] = useState(false); const [openModalResultFirmwareUpdate, setOpenModalResultFirmwareUpdate] = @@ -67,35 +65,35 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { }; const [GreenisBlinking, setGreenisBlinking] = useState( - props.batteryData.Leds.Green.value === 'Blinking' + props.batteryData.GreenLeds.value === 'Blinking' ); const [AmberisBlinking, setAmberisBlinking] = useState( - props.batteryData.Leds.Amber.value === 'Blinking' + props.batteryData.AmberLeds.value === 'Blinking' ); const [RedisBlinking, setRedisBlinking] = useState( - props.batteryData.Leds.Red.value === 'Blinking' + props.batteryData.RedLeds.value === 'Blinking' ); const [BlueisBlinking, setBlueisBlinking] = useState( - props.batteryData.Leds.Blue.value === 'Blinking' + props.batteryData.BlueLeds.value === 'Blinking' ); useEffect(() => { const intervalId = setInterval(() => { - if (props.batteryData.Leds.Amber.value === 'Blinking') { + if (props.batteryData.AmberLeds.value === 'Blinking') { setAmberisBlinking((prevIsBlinking) => !prevIsBlinking); } - if (props.batteryData.Leds.Red.value === 'Blinking') { + if (props.batteryData.RedLeds.value === 'Blinking') { setRedisBlinking((prevIsBlinking) => !prevIsBlinking); } - if (props.batteryData.Leds.Blue.value === 'Blinking') { + if (props.batteryData.BlueLeds.value === 'Blinking') { setBlueisBlinking((prevIsBlinking) => !prevIsBlinking); } - if (props.batteryData.Leds.Green.value === 'Blinking') { + if (props.batteryData.GreenLeds.value === 'Blinking') { setGreenisBlinking((prevIsBlinking) => !prevIsBlinking); } }, 500); // Blink every 500 milliseconds @@ -131,7 +129,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { const res = await axiosConfig .post( - `/UpdateFirmware?batteryNode=${props.batteryId.toString()}&installationId=${ + `/UpdateFirmware?batteryNode=${props.batteryData.BatteryId.toString()}&installationId=${ props.installationId }&version=${selectedVersion}` ) @@ -171,7 +169,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { try { // Start the job to generate the battery log const startRes = await axiosConfig.post( - `/StartDownloadBatteryLog?batteryNode=${props.batteryId.toString()}&installationId=${ + `/StartDownloadBatteryLog?batteryNode=${props.batteryData.BatteryId.toString()}&installationId=${ props.installationId }` ); @@ -637,7 +635,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { fontWeight: 'bold' }} > - {'Node ' + props.batteryId} + {'Node ' + props.batteryData.BatteryId}
@@ -645,11 +643,8 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { style={{ ...batteryStringStyle, backgroundColor: - props.batteryData.BatteryStrings.String1Active.value == - 'True' || - Number( - props.batteryData.BatteryStrings.String1Active.value - ) == 0 + props.batteryData.String1Active.value == 'True' || + Number(props.batteryData.String1Active.value) == 0 ? '#32CD32' : '#FF033E' }} @@ -658,11 +653,8 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { style={{ ...batteryStringStyle, backgroundColor: - props.batteryData.BatteryStrings.String2Active.value == - 'True' || - Number( - props.batteryData.BatteryStrings.String2Active.value - ) == 0 + props.batteryData.String2Active.value == 'True' || + Number(props.batteryData.String2Active.value) == 0 ? '#32CD32' : '#FF033E' }} @@ -671,11 +663,8 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { style={{ ...batteryStringStyle, backgroundColor: - props.batteryData.BatteryStrings.String3Active.value == - 'True' || - Number( - props.batteryData.BatteryStrings.String3Active.value - ) == 0 + props.batteryData.String3Active.value == 'True' || + Number(props.batteryData.String3Active.value) == 0 ? '#32CD32' : '#FF033E' }} @@ -684,11 +673,8 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { style={{ ...batteryStringStyle, backgroundColor: - props.batteryData.BatteryStrings.String4Active.value == - 'True' || - Number( - props.batteryData.BatteryStrings.String4Active.value - ) == 0 + props.batteryData.String4Active.value == 'True' || + Number(props.batteryData.String4Active.value) == 0 ? '#32CD32' : '#FF033E' }} @@ -697,11 +683,8 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { style={{ ...batteryStringStyle, backgroundColor: - props.batteryData.BatteryStrings.String5Active.value == - 'True' || - Number( - props.batteryData.BatteryStrings.String5Active.value - ) == 0 + props.batteryData.String5Active.value == 'True' || + Number(props.batteryData.String5Active.value) == 0 ? '#32CD32' : '#FF033E' }} @@ -716,7 +699,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { marginTop: '-10px', borderRadius: '50%', backgroundColor: - props.batteryData.Leds.Green.value === 'On' || + props.batteryData.GreenLeds.value === 'On' || GreenisBlinking ? 'green' : 'transparent' @@ -731,7 +714,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { marginTop: '10px', borderRadius: '50%', backgroundColor: - props.batteryData.Leds.Amber.value === 'On' || + props.batteryData.AmberLeds.value === 'On' || AmberisBlinking ? 'orange' : 'transparent' @@ -746,7 +729,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { marginTop: '10px', borderRadius: '50%', backgroundColor: - props.batteryData.Leds.Blue.value === 'On' || BlueisBlinking + props.batteryData.BlueLeds.value === 'On' || BlueisBlinking ? '#00ccff' : 'transparent' }} @@ -760,7 +743,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { marginTop: '10px', borderRadius: '50%', backgroundColor: - props.batteryData.Leds.Red.value === 'On' || RedisBlinking + props.batteryData.RedLeds.value === 'On' || RedisBlinking ? 'red' : 'transparent' }} @@ -820,7 +803,9 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { paddingRight: '12px' }} > - {props.batteryData.Dc.Voltage.value + ' V'} + {props.batteryData.Voltage.value + + ' ' + + props.batteryData.Voltage.unit} @@ -840,7 +825,9 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { paddingRight: '12px' }} > - {props.batteryData.Dc.Current.value + ' A'} + {props.batteryData.Current.value + + ' ' + + props.batteryData.Current.unit} @@ -860,7 +847,9 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { paddingRight: '12px' }} > - {props.batteryData.Dc.Power.value + ' W'} + {props.batteryData.Power.value + + ' ' + + props.batteryData.Power.unit} @@ -881,7 +870,9 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { paddingRight: '12px' }} > - {props.batteryData.BusCurrent.value + ' A'} + {props.batteryData.BusCurrent.value + + ' ' + + props.batteryData.BusCurrent.unit} @@ -901,7 +892,9 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { paddingRight: '12px' }} > - {props.batteryData.CellsCurrent.value + ' A'} + {props.batteryData.CellsCurrent.value + + ' ' + + props.batteryData.CellsCurrent.unit} @@ -921,7 +914,9 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { paddingRight: '12px' }} > - {props.batteryData.HeatingCurrent.value + ' A'} + {props.batteryData.HeatingCurrent.value + + ' ' + + props.batteryData.HeatingCurrent.unit} @@ -942,7 +937,9 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { paddingRight: '12px' }} > - {props.batteryData.Soc.value + ' %'} + {props.batteryData.Soc.value + + ' ' + + props.batteryData.Soc.unit} @@ -952,200 +949,200 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { {/*----------------------------------------------------------------------------------------------------------------------------------*/} - {/*{props.productNum === 0 && (*/} - {/* <>*/} - {/* */} - {/* */} - {/* */} - {/* Temperature*/} - {/* */} + {props.productNum === 0 && ( + <> + + + + Temperature + - {/* */} - {/* */} - {/* */} - {/* */} - {/* */} - {/* Heating*/} - {/* */} - {/* */} - {/* {props.batteryData.HeatingTemperature.value +*/} - {/* ' ' +*/} - {/* props.batteryData.HeatingTemperature.unit}*/} - {/* */} - {/* */} - {/* */} - {/* */} - {/* Board Temperature*/} - {/* */} - {/* */} - {/* {props.batteryData.BoardTemperature.value +*/} - {/* ' ' +*/} - {/* props.batteryData.BoardTemperature.unit}*/} - {/* */} - {/* */} - {/* */} - {/* */} - {/* Center Cells Temperature*/} - {/* */} - {/* */} - {/* {props.batteryData.AverageTemperature.value +*/} - {/* ' ' +*/} - {/* props.batteryData.AverageTemperature.unit}*/} - {/* */} - {/* */} - {/* */} - {/* */} - {/* Left Cells Temperature*/} - {/* */} - {/* */} - {/* {props.batteryData.LeftCellsTemperature.value +*/} - {/* ' ' +*/} - {/* props.batteryData.LeftCellsTemperature.unit}*/} - {/* */} - {/* */} - {/* */} - {/* */} - {/* Right Cells Temperature*/} - {/* */} - {/* */} - {/* {props.batteryData.RightCellsTemperature.value +*/} - {/* ' ' +*/} - {/* props.batteryData.RightCellsTemperature.unit}*/} - {/* */} - {/* */} - {/* */} - {/* */} - {/* Average Temperature*/} - {/* */} - {/* */} - {/* {props.batteryData.AverageTemperature.value +*/} - {/* ' ' +*/} - {/* props.batteryData.AverageTemperature.unit}*/} - {/* */} - {/* */} - {/* */} - {/* */} - {/* State*/} - {/* */} - {/* */} - {/* {props.batteryData.StateTemperature.value +*/} - {/* ' ' +*/} - {/* props.batteryData.StateTemperature.unit}*/} - {/* */} - {/* */} - {/* */} - {/*
*/} - {/* */} - {/*
*/} - {/*
*/} - {/* */} - {/*)}*/} + + + + + + Heating + + + {props.batteryData.HeatingTemperature.value + + ' ' + + props.batteryData.HeatingTemperature.unit} + + + + + Board Temperature + + + {props.batteryData.BoardTemperature.value + + ' ' + + props.batteryData.BoardTemperature.unit} + + + + + Center Cells Temperature + + + {props.batteryData.AverageTemperature.value + + ' ' + + props.batteryData.AverageTemperature.unit} + + + + + Left Cells Temperature + + + {props.batteryData.LeftCellsTemperature.value + + ' ' + + props.batteryData.LeftCellsTemperature.unit} + + + + + Right Cells Temperature + + + {props.batteryData.RightCellsTemperature.value + + ' ' + + props.batteryData.RightCellsTemperature.unit} + + + + + Average Temperature + + + {props.batteryData.AverageTemperature.value + + ' ' + + props.batteryData.AverageTemperature.unit} + + + + + State + + + {props.batteryData.StateTemperature.value + + ' ' + + props.batteryData.StateTemperature.unit} + + + +
+
+ +
+ + )} {/*----------------------------------------------------------------------------------------------------------------------------------*/} @@ -1196,9 +1193,9 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { paddingRight: '12px' }} > - {props.batteryData.IoStatus.ConnectedToDcBus.value - ? 'True' - : 'False'} + {props.batteryData.ConnectedToDcBus.value + + ' ' + + props.batteryData.ConnectedToDcBus.unit} @@ -1218,9 +1215,9 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { paddingRight: '12px' }} > - {props.batteryData.IoStatus.AlarmOutActive.value - ? 'True' - : 'False'} + {props.batteryData.AlarmOutActive.value + + ' ' + + props.batteryData.AlarmOutActive.unit} @@ -1240,9 +1237,9 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { paddingRight: '12px' }} > - {props.batteryData.IoStatus.InternalFanActive.value - ? 'True' - : 'False'} + {props.batteryData.InternalFanActive.value + + ' ' + + props.batteryData.InternalFanActive.unit} @@ -1262,9 +1259,9 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { paddingRight: '12px' }} > - {props.batteryData.IoStatus.VoltMeasurementAllowed.value - ? 'True' - : 'False'} + {props.batteryData.VoltMeasurementAllowed.value + + ' ' + + props.batteryData.VoltMeasurementAllowed.unit} @@ -1284,9 +1281,9 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { paddingRight: '12px' }} > - {props.batteryData.IoStatus.AuxRelayBus.value - ? 'True' - : 'False'} + {props.batteryData.AuxRelayBus.value + + ' ' + + props.batteryData.AuxRelayBus.unit} @@ -1306,9 +1303,9 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { paddingRight: '12px' }} > - {props.batteryData.IoStatus.RemoteStateActive.value - ? 'True' - : 'False'} + {props.batteryData.RemoteStateActive.value + + ' ' + + props.batteryData.RemoteStateActive.unit} @@ -1328,9 +1325,9 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { paddingRight: '12px' }} > - {props.batteryData.IoStatus.RiscActive.value - ? 'True' - : 'False'} + {props.batteryData.RiscActive.value + + ' ' + + props.batteryData.RiscActive.unit} @@ -1388,7 +1385,9 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { paddingRight: '12px' }} > - {props.batteryData.Eoc.value} + {props.batteryData.Eoc.value + + ' ' + + props.batteryData.Eoc.unit} @@ -1409,7 +1408,9 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { paddingRight: '12px' }} > - {props.batteryData.SerialNumber.value} + {props.batteryData.SerialNumber.value + + ' ' + + props.batteryData.SerialNumber.unit} @@ -1430,7 +1431,9 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { paddingRight: '12px' }} > - {props.batteryData.FwVersion.value} + {props.batteryData.FwVersion.value + + ' ' + + props.batteryData.FwVersion.unit} @@ -1450,33 +1453,35 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { paddingRight: '12px' }} > - {props.batteryData.TimeSinceTOC.value} + {props.batteryData.TimeSinceTOC.value + + ' ' + + props.batteryData.TimeSinceTOC.unit} - {/*{props.productNum === 0 && (*/} - {/* */} - {/* */} - {/* Calibration Charge Requested*/} - {/* */} - {/* */} - {/* {props.batteryData.CalibrationChargeRequested.value +*/} - {/* ' ' +*/} - {/* props.batteryData.CalibrationChargeRequested.unit}*/} - {/* */} - {/* */} - {/*)}*/} + {props.productNum === 0 && ( + + + Calibration Charge Requested + + + {props.batteryData.CalibrationChargeRequested.value + + ' ' + + props.batteryData.CalibrationChargeRequested.unit} + + + )} - {props.batteryData.MaxChargePower.value + ' W'} + {props.batteryData.MaxChargePower.value + + ' ' + + props.batteryData.MaxChargePower.unit} @@ -1514,7 +1521,9 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { paddingRight: '12px' }} > - {props.batteryData.MaxDischargePower.value + ' W'} + {props.batteryData.MaxDischargePower.value + + ' ' + + props.batteryData.MaxDischargePower.unit} @@ -1522,111 +1531,6 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) { - - {/*----------------------------------------------------------------------------------------------------------------------------------*/} - - {/**/} - {/* */} - {/* */} - {/* */} - {/* */} - {/* */} - {/* */} - {/* Green Led*/} - {/* */} - {/* */} - {/* {props.batteryData.GreenLeds.value}*/} - {/* */} - {/* */} - {/* */} - {/* */} - {/* Amber Led*/} - {/* */} - {/* */} - {/* {props.batteryData.AmberLeds.value}*/} - {/* */} - {/* */} - {/* */} - {/* */} - {/* Blue Led*/} - {/* */} - {/* */} - {/* {props.batteryData.BlueLeds.value}*/} - {/* */} - {/* */} - - {/* */} - {/* */} - {/* Red Led*/} - {/* */} - {/* */} - {/* {props.batteryData.RedLeds.value}*/} - {/* */} - {/* */} - {/* */} - {/*
*/} - {/* */} - {/* */} - {/*
*/} ); } diff --git a/typescript/frontend-marios2/src/content/dashboards/BatteryView/DetailedBatteryViewSalidomo.tsx b/typescript/frontend-marios2/src/content/dashboards/BatteryView/DetailedBatteryViewSalidomo.tsx new file mode 100644 index 000000000..e428a83e9 --- /dev/null +++ b/typescript/frontend-marios2/src/content/dashboards/BatteryView/DetailedBatteryViewSalidomo.tsx @@ -0,0 +1,1312 @@ +import React, { useContext, useEffect, useState } from 'react'; +import { I_S3Credentials } from '../../../interfaces/S3Types'; +import { + Box, + Card, + Grid, + IconButton, + MenuItem, + Modal, + Paper, + Select, + Table, + TableBody, + TableCell, + TableContainer, + TableRow, + Typography +} from '@mui/material'; +import { Device } from '../Log/graph.util'; +import { useNavigate } from 'react-router-dom'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import Button from '@mui/material/Button'; +import axiosConfig from '../../../Resources/axiosConfig'; +import { UserType } from '../../../interfaces/UserTypes'; +import { UserContext } from '../../../contexts/userContext'; + +interface DetailedBatteryViewProps { + batteryId: number; + s3Credentials: I_S3Credentials; + batteryData: Device; + installationId: number; + productNum: number; +} + +function DetailedBatteryViewSalidomo(props: DetailedBatteryViewProps) { + if (props.batteryData === null) { + return null; + } + + const navigate = useNavigate(); + const [openModalFirmwareUpdate, setOpenModalFirmwareUpdate] = useState(false); + const [openModalResultFirmwareUpdate, setOpenModalResultFirmwareUpdate] = + useState(false); + const [openModalDownloadBatteryLog, setOpenModalDownloadBatteryLog] = + useState(false); + const [ + openModalStartDownloadBatteryLog, + setOpenModalStartDownloadBatteryLog + ] = useState(false); + const [openModalError, setOpenModalError] = useState(false); + const [errorMessage, setErrorMessage] = useState(''); + + const [selectedVersion, setSelectedVersion] = useState( + props.batteryData.FwVersion.value + ); + + const handleBatteryViewButton = () => { + navigate(location.pathname.split('/').slice(0, -2).join('/')); + }; + + const handleUpdateFirmware = () => { + setOpenModalFirmwareUpdate(true); + }; + + const firmwareModalResultHandleOk = () => { + navigate(location.pathname.split('/').slice(0, -2).join('/')); + }; + + const [GreenisBlinking, setGreenisBlinking] = useState( + props.batteryData.Leds.Green.value === 'Blinking' + ); + + const [AmberisBlinking, setAmberisBlinking] = useState( + props.batteryData.Leds.Amber.value === 'Blinking' + ); + const [RedisBlinking, setRedisBlinking] = useState( + props.batteryData.Leds.Red.value === 'Blinking' + ); + + const [BlueisBlinking, setBlueisBlinking] = useState( + props.batteryData.Leds.Blue.value === 'Blinking' + ); + + useEffect(() => { + const intervalId = setInterval(() => { + if (props.batteryData.Leds.Amber.value === 'Blinking') { + setAmberisBlinking((prevIsBlinking) => !prevIsBlinking); + } + + if (props.batteryData.Leds.Red.value === 'Blinking') { + setRedisBlinking((prevIsBlinking) => !prevIsBlinking); + } + + if (props.batteryData.Leds.Blue.value === 'Blinking') { + setBlueisBlinking((prevIsBlinking) => !prevIsBlinking); + } + + if (props.batteryData.Leds.Green.value === 'Blinking') { + setGreenisBlinking((prevIsBlinking) => !prevIsBlinking); + } + }, 500); // Blink every 500 milliseconds + + // Cleanup the interval on component unmount + return () => clearInterval(intervalId); + }, []); + + const batteryStyle = { + borderRadius: '15px', + padding: '10px', + backgroundColor: 'lightgray', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + height: '150px' + }; + + const batteryStringStyle = { + flex: 1, + border: '1px solid #000', + height: '97%', + width: '30px', + borderRadius: '7px', + backgroundColor: '#bfbfbf' + }; + + const context = useContext(UserContext); + const { currentUser } = context; + + const FirmwareModalHandleProceed = async (e) => { + setOpenModalFirmwareUpdate(false); + + const res = await axiosConfig + .post( + `/UpdateFirmware?batteryNode=${props.batteryId.toString()}&installationId=${ + props.installationId + }&version=${selectedVersion}` + ) + .catch((err) => { + if (err.response) { + // setError(true); + // setLoading(false); + } + }); + + //if (res) { + setOpenModalResultFirmwareUpdate(true); + //} + }; + + const FirmwareModalHandleCancel = () => { + setOpenModalFirmwareUpdate(false); + }; + + const StartDownloadBatteryLogModalHandleOk = () => { + // stay in the current page which shows the single battery + setOpenModalStartDownloadBatteryLog(false); + }; + + const handleDownloadBmsLog = () => { + setOpenModalDownloadBatteryLog(true); + }; + + const DownloadBatteryLogModalHandleCancel = () => { + setOpenModalDownloadBatteryLog(false); + }; + + const DownloadBatteryLogModalHandleProceed = async () => { + setOpenModalDownloadBatteryLog(false); + setOpenModalStartDownloadBatteryLog(true); + + try { + // Start the job to generate the battery log + const startRes = await axiosConfig.post( + `/StartDownloadBatteryLog?batteryNode=${props.batteryId.toString()}&installationId=${ + props.installationId + }` + ); + + if (startRes.status === 200) { + const jobId = startRes.data; + + // Polling to check the job status + const checkJobStatus = async () => { + try { + const statusRes = await axiosConfig.get( + `/GetJobResult?jobId=${jobId}` + ); + + if (statusRes.status === 200) { + const jobStatus = statusRes.data.status; + + switch (jobStatus) { + case 'Completed': + return statusRes.data.fileName; // Return FileName upon completion + case 'Failed': + throw new Error('Job processing failed.'); + case 'Processing': + await new Promise((resolve) => setTimeout(resolve, 60000)); // Wait for 60 seconds before next check + return checkJobStatus(); + default: + throw new Error('Unknown download battery log job status.'); + } + } else { + throw new Error('Unexpected error occurred.'); + } + } catch (error) { + throw new Error('Failed to fetch job status.'); // Catch errors from status check + } + }; + + // Wait for job completion or failure + const fileName = await checkJobStatus(); + + // Once job is completed, download the file + const res = await axiosConfig.get( + `/DownloadBatteryLog?jobId=${jobId}`, + { + responseType: 'blob' + } + ); + + const finalFileName = fileName || 'unknown_file_name'; // Default filename if not received + console.log('Downloaded file name:', finalFileName); + + const url = window.URL.createObjectURL(new Blob([res.data])); + const a = document.createElement('a'); + a.style.display = 'none'; + a.href = url; + a.download = finalFileName; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + + // Delete the file after successful download + console.log('Deleted file name:', finalFileName); + await axiosConfig.delete(`/DeleteBatteryLog`, { + params: { fileName: finalFileName } + }); + } else { + console.error( + 'Failed to start downloading battery log in the backend.' + ); + } + } catch (error) { + console.error('Error:', error.message); + setErrorMessage('Download battery log failed, please try again.'); + setOpenModalError(true); + } finally { + setOpenModalStartDownloadBatteryLog(false); + } + }; + + const ErrorModalHandleOk = () => { + setOpenModalError(false); + }; + + return ( + <> + {openModalResultFirmwareUpdate && ( + + + + The firmware is getting updated. Please wait... + + +
+ +
+
+
+ )} + + {openModalFirmwareUpdate && ( + + + + Do you really want to update the firmware? + + + + This action requires the battery service to be stopped for around + 10-15 minutes. + + +
+ + +
+
+
+ )} + + {openModalStartDownloadBatteryLog && ( + + + + The battery log is getting downloaded. It will be saved in the + Downloads folder. Please wait... + + +
+ +
+
+
+ )} + + {openModalDownloadBatteryLog && ( + + + + Do you really want to download battery log? + + + + This action requires the battery service to be stopped for around + 10-15 minutes. + + +
+ + +
+
+
+ )} + + {openModalError && ( + + + + {errorMessage} + + +
+ +
+
+
+ )} + + + + + + + + {currentUser.userType == UserType.admin && ( + <> + + + + + )} + + + + + + + {'Node ' + props.batteryId} + + +
+
+
+
+
+
+ +
+
+ +
+ +
+ +
+
+
+ + + + + + + + + Battery Information + + + + + + + + Voltage + + + {props.batteryData.Dc.Voltage.value + ' V'} + + + + + Current + + + {props.batteryData.Dc.Current.value + ' A'} + + + + + Power + + + {props.batteryData.Dc.Power.value + ' W'} + + + + + + Bus Current + + + {props.batteryData.BusCurrent.value + ' A'} + + + + + Cells Current + + + {props.batteryData.CellsCurrent.value + ' A'} + + + + + Heating Current + + + {props.batteryData.HeatingCurrent.value + ' A'} + + + + + + Soc + + + {props.batteryData.Soc.value + ' %'} + + + +
+
+
+
+ + {/*----------------------------------------------------------------------------------------------------------------------------------*/} + + + + Io Status + + + + + + + Connected To Dc Bus + + + {props.batteryData.IoStatus.ConnectedToDcBus.value + ? 'True' + : 'False'} + + + + + Alarm Out Active + + + {props.batteryData.IoStatus.AlarmOutActive.value + ? 'True' + : 'False'} + + + + + Internal Fan Active + + + {props.batteryData.IoStatus.InternalFanActive.value + ? 'True' + : 'False'} + + + + + Volt Measurement Allowed + + + {props.batteryData.IoStatus.VoltMeasurementAllowed.value + ? 'True' + : 'False'} + + + + + Aux Relay Bus + + + {props.batteryData.IoStatus.AuxRelayBus.value + ? 'True' + : 'False'} + + + + + Remote State Active + + + {props.batteryData.IoStatus.RemoteStateActive.value + ? 'True' + : 'False'} + + + + + RiscActive + + + {props.batteryData.IoStatus.RiscActive.value + ? 'True' + : 'False'} + + + +
+
+
+
+ + {/*----------------------------------------------------------------------------------------------------------------------------------*/} + + + + + Specification + + + + + + + Eoc + + + {props.batteryData.Eoc.value} + + + + + + Serial Number + + + {props.batteryData.SerialNumber.value} + + + + + + Firmware Version + + + {props.batteryData.FwVersion.value} + + + + + Time Since TOC + + + {props.batteryData.TimeSinceTOC.value} + + + + + + Max Charge Power + + + {props.batteryData.MaxChargePower.value + ' W'} + + + + + Max Discharge Power + + + {props.batteryData.MaxDischargePower.value + ' W'} + + + +
+
+
+
+ + {/*----------------------------------------------------------------------------------------------------------------------------------*/} + + ); +} + +export default DetailedBatteryViewSalidomo; diff --git a/typescript/frontend-marios2/src/content/dashboards/BatteryView/MainStats.tsx b/typescript/frontend-marios2/src/content/dashboards/BatteryView/MainStats.tsx index 75c60f8fa..d771d967c 100644 --- a/typescript/frontend-marios2/src/content/dashboards/BatteryView/MainStats.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/BatteryView/MainStats.tsx @@ -16,7 +16,7 @@ import { getChartOptions } from '../Overview/chartOptions'; import { BatteryDataInterface, BatteryOverviewInterface, - transformInputToBatteryViewDataJson + transformInputToBatteryViewData } from '../../../interfaces/Chart'; import dayjs, { Dayjs } from 'dayjs'; import { TimeSpan, UnixTime } from '../../../dataCache/time'; @@ -96,7 +96,7 @@ function MainStats(props: MainStatsProps) { const resultPromise: Promise<{ chartData: BatteryDataInterface; chartOverview: BatteryOverviewInterface; - }> = transformInputToBatteryViewDataJson( + }> = transformInputToBatteryViewData( props.s3Credentials, props.id, product, @@ -192,7 +192,7 @@ function MainStats(props: MainStatsProps) { const resultPromise: Promise<{ chartData: BatteryDataInterface; chartOverview: BatteryOverviewInterface; - }> = transformInputToBatteryViewDataJson( + }> = transformInputToBatteryViewData( props.s3Credentials, props.id, product, @@ -254,7 +254,7 @@ function MainStats(props: MainStatsProps) { const resultPromise: Promise<{ chartData: BatteryDataInterface; chartOverview: BatteryOverviewInterface; - }> = transformInputToBatteryViewDataJson( + }> = transformInputToBatteryViewData( props.s3Credentials, props.id, product, diff --git a/typescript/frontend-marios2/src/content/dashboards/BatteryView/MainStatsSalidomo.tsx b/typescript/frontend-marios2/src/content/dashboards/BatteryView/MainStatsSalidomo.tsx new file mode 100644 index 000000000..fbe4fe728 --- /dev/null +++ b/typescript/frontend-marios2/src/content/dashboards/BatteryView/MainStatsSalidomo.tsx @@ -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 && ( + + + + Fetching data... + + + )} + {isErrorDateModalOpen && ( + {}}> + + + {dateSelectionError} + + + + + + )} + {isDateModalOpen && ( + + {}}> + + { + // Type assertion to Dayjs + if (newDate) { + setStartDate(newDate); + } + }} + renderInput={(props) => } + /> + + { + // Type assertion to Dayjs + if (newDate) { + setEndDate(newDate); + } + }} + renderInput={(props) => } + /> + +
+ + + +
+
+
+
+ )} + + {!loading && ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + { + startZoom(); + handleBeforeZoom(chartContext, options); + } + } + } + }} + series={generateSeries( + batteryViewDataArray[chartState].chartData, + 'Soc', + 'blue' + )} + type="line" + height={420} + /> + + + + + + + + + + + + + + + + + { + startZoom(); + handleBeforeZoom(chartContext, options); + } + } + } + }} + series={generateSeries( + batteryViewDataArray[chartState].chartData, + 'Temperature', + 'blue' + )} + type="line" + height={420} + /> + + + + + + + + + + + + + + + + + { + startZoom(); + handleBeforeZoom(chartContext, options); + } + } + } + }} + series={generateSeries( + batteryViewDataArray[chartState].chartData, + 'Power', + 'red' + )} + type="line" + height={420} + /> + + + + + + + + + + + + + + + + + { + startZoom(); + handleBeforeZoom(chartContext, options); + } + } + } + }} + series={generateSeries( + batteryViewDataArray[chartState].chartData, + 'Voltage', + 'orange' + )} + type="line" + height={420} + /> + + + + + + + + + + + + + + + + + { + startZoom(); + handleBeforeZoom(chartContext, options); + } + } + } + }} + series={generateSeries( + batteryViewDataArray[chartState].chartData, + 'Current', + 'orange' + )} + type="line" + height={420} + /> + + + + + )} + + ); +} + +export default MainStatsSalidomo; diff --git a/typescript/frontend-marios2/src/content/dashboards/Installations/Installation.tsx b/typescript/frontend-marios2/src/content/dashboards/Installations/Installation.tsx index 76532787a..b760cf5be 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Installations/Installation.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Installations/Installation.tsx @@ -30,6 +30,7 @@ import Information from '../Information/Information'; import { UserType } from '../../../interfaces/UserTypes'; import HistoryOfActions from '../History/History'; import PvView from '../PvView/PvView'; +import BatteryView from '../BatteryView/BatteryView'; interface singleInstallationProps { current_installation?: I_Installation; @@ -421,18 +422,18 @@ function Installation(props: singleInstallationProps) { } /> - {/**/} - {/* }*/} - {/*>*/} + + } + > tabList.includes(pathElement))); } }, [location]); diff --git a/typescript/frontend-marios2/src/content/dashboards/SalidomoInstallations/Installation.tsx b/typescript/frontend-marios2/src/content/dashboards/SalidomoInstallations/Installation.tsx index 3acc0b311..d5131e1d2 100644 --- a/typescript/frontend-marios2/src/content/dashboards/SalidomoInstallations/Installation.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/SalidomoInstallations/Installation.tsx @@ -16,7 +16,7 @@ import { fetchDataJson } from 'src/content/dashboards/Installations/fetchData'; import { Navigate, Route, Routes, useLocation } from 'react-router-dom'; import routes from '../../../Resources/routes.json'; import InformationSalidomo from '../Information/InformationSalidomo'; -import BatteryView from '../BatteryView/BatteryView'; +import BatteryViewSalidomo from '../BatteryView/BatteryViewSalidomo'; import Log from '../Log/Log'; import CancelIcon from '@mui/icons-material/Cancel'; import SalidomoOverview from '../Overview/salidomoOverview'; @@ -361,13 +361,13 @@ function SalidomoInstallation(props: singleInstallationProps) { + > } > diff --git a/typescript/frontend-marios2/src/content/dashboards/SalidomoInstallations/index.tsx b/typescript/frontend-marios2/src/content/dashboards/SalidomoInstallations/index.tsx index fe1fa07ec..d7b913b8d 100644 --- a/typescript/frontend-marios2/src/content/dashboards/SalidomoInstallations/index.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/SalidomoInstallations/index.tsx @@ -49,7 +49,7 @@ function SalidomoInstallationTabs() { } else if (path[path.length - 2] === 'tree') { setCurrentTab('tree'); } 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))); } }, [location]); diff --git a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/Installation.tsx b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/Installation.tsx index b21b85423..2adc8315b 100644 --- a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/Installation.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/Installation.tsx @@ -364,13 +364,13 @@ function SodioHomeInstallation(props: singleInstallationProps) { {/**/} + {/* >*/} {/* }*/} {/*>*/} diff --git a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/index.tsx b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/index.tsx index aa3771be4..190619c73 100644 --- a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/index.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/index.tsx @@ -48,7 +48,7 @@ function SodioHomeInstallationTabs() { } else if (path[path.length - 2] === 'tree') { setCurrentTab('tree'); } 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))); } }, [location]); diff --git a/typescript/frontend-marios2/src/interfaces/Chart.tsx b/typescript/frontend-marios2/src/interfaces/Chart.tsx index 8b03c78f7..8216d8feb 100644 --- a/typescript/frontend-marios2/src/interfaces/Chart.tsx +++ b/typescript/frontend-marios2/src/interfaces/Chart.tsx @@ -401,6 +401,7 @@ export const transformInputToAggregatedDataJson = async ( ) { // Handle not available or try later case } else { + console.log(result); dateList.push(currentDay.format('DD-MM')); pathsToSearch.forEach((path) => { const value = path @@ -410,7 +411,6 @@ export const transformInputToAggregatedDataJson = async ( if (value !== undefined) { if (path === '.GridExportPower') { result.GridExportPower = -value; - // result[path].value = -result[path].value; } if (value < overviewData[path].min) { overviewData[path].min = value; @@ -549,222 +549,222 @@ export const transformInputToAggregatedDataJson = async ( // 'Node2': {name:'Node2', data: [[timestamp,value],[timestamp,value]]}, // ]} -// export const transformInputToBatteryViewData = async ( -// s3Credentials: I_S3Credentials, -// id: number, -// product: number, -// start_time?: UnixTime, -// end_time?: UnixTime -// ): Promise<{ -// chartData: BatteryDataInterface; -// chartOverview: BatteryOverviewInterface; -// }> => { -// const prefixes = ['', 'k', 'M', 'G', 'T']; -// const MAX_NUMBER = 9999999; -// const categories = ['Soc', 'Temperature', 'Power', 'Voltage', 'Current']; -// const pathCategories = [ -// 'Soc', -// 'Temperatures/Cells/Average', -// 'Dc/Power', -// 'Dc/Voltage', -// 'Dc/Current' -// ]; -// -// const pathsToSearch = [ -// '/Battery/Devices/1/', -// '/Battery/Devices/2/', -// '/Battery/Devices/3/', -// '/Battery/Devices/4/', -// '/Battery/Devices/5/', -// '/Battery/Devices/6/', -// '/Battery/Devices/7/', -// '/Battery/Devices/8/', -// '/Battery/Devices/9/', -// '/Battery/Devices/10/' -// ]; -// -// const pathsToSave = []; -// -// const chartData: BatteryDataInterface = { -// Soc: { name: 'State Of Charge', data: [] }, -// Temperature: { name: 'Temperature', data: [] }, -// Power: { name: 'Power', data: [] }, -// Voltage: { name: 'Voltage', data: [] }, -// Current: { name: 'Voltage', data: [] } -// }; -// -// const chartOverview: BatteryOverviewInterface = { -// Soc: { magnitude: 0, unit: '', min: 0, max: 0 }, -// Temperature: { magnitude: 0, unit: '', min: 0, max: 0 }, -// Power: { magnitude: 0, unit: '', min: 0, max: 0 }, -// Voltage: { magnitude: 0, unit: '', min: 0, max: 0 }, -// Current: { magnitude: 0, unit: '', min: 0, max: 0 } -// }; -// -// let initialiation = true; -// -// let timestampArray: number[] = []; -// let adjustedTimestampArray = []; -// const timestampPromises = []; -// -// await axiosConfig -// .get( -// `/GetCsvTimestampsForInstallation?id=${id}&start=${start_time.ticks}&end=${end_time.ticks}` -// ) -// .then((res: AxiosResponse) => { -// timestampArray = res.data; -// }) -// .catch((err: AxiosError) => { -// if (err.response && err.response.status == 401) { -// //removeToken(); -// //navigate(routes.login); -// } -// }); -// -// for (var i = 0; i < timestampArray.length; i++) { -// timestampPromises.push( -// fetchDataForOneTime( -// UnixTime.fromTicks(timestampArray[i], true), -// s3Credentials -// ) -// ); -// -// const adjustedTimestamp = -// product == 0 -// ? new Date(timestampArray[i] * 1000) -// : new Date(timestampArray[i] * 100000); -// //Timezone offset is negative, so we convert the timestamp to the current zone by subtracting the corresponding offset -// adjustedTimestamp.setHours( -// adjustedTimestamp.getHours() - adjustedTimestamp.getTimezoneOffset() / 60 -// ); -// adjustedTimestampArray.push(adjustedTimestamp); -// } -// -// const results: Promise>>[] = -// await Promise.all(timestampPromises); -// -// for (let i = 0; i < results.length; i++) { -// if (results[i] == null) { -// // Handle not available or try later case -// } else { -// const timestamp = Object.keys(results[i])[ -// Object.keys(results[i]).length - 1 -// ]; -// const result = results[i][timestamp]; -// const battery_nodes = result['/Config/Devices/BatteryNodes'].value -// .toString() -// .split(','); -// -// //Initialize the chartData structure based on the node names extracted from the first result -// let old_length = pathsToSave.length; -// -// if (battery_nodes.length > old_length) { -// battery_nodes.forEach((node) => { -// if (!pathsToSave.includes('Node' + node)) { -// pathsToSave.push('Node' + node); -// } -// }); -// } -// -// if (initialiation) { -// initialiation = false; -// categories.forEach((category) => { -// chartData[category].data = []; -// chartOverview[category] = { -// magnitude: 0, -// unit: '', -// min: MAX_NUMBER, -// max: -MAX_NUMBER -// }; -// }); -// } -// -// if (battery_nodes.length > old_length) { -// categories.forEach((category) => { -// pathsToSave.forEach((path) => { -// if (pathsToSave.indexOf(path) >= old_length) { -// chartData[category].data[path] = { name: path, data: [] }; -// } -// }); -// }); -// } -// -// for ( -// let category_index = 0; -// category_index < pathCategories.length; -// category_index++ -// ) { -// let category = categories[category_index]; -// -// for (let j = 0; j < pathsToSave.length; j++) { -// let path = pathsToSearch[j] + pathCategories[category_index]; -// -// if (result[path]) { -// const value = result[path]; -// -// if (value.value < chartOverview[category].min) { -// chartOverview[category].min = value.value; -// } -// -// if (value.value > chartOverview[category].max) { -// chartOverview[category].max = value.value; -// } -// chartData[category].data[pathsToSave[j]].data.push([ -// adjustedTimestampArray[i], -// value.value -// ]); -// } else { -// // chartData[category].data[pathsToSave[j]].data.push([ -// // adjustedTimestampArray[i], -// // null -// // ]); -// } -// } -// } -// } -// } -// categories.forEach((category) => { -// let value = Math.max( -// Math.abs(chartOverview[category].max), -// Math.abs(chartOverview[category].min) -// ); -// let magnitude = 0; -// -// if (value < 0) { -// value = -value; -// } -// while (value >= 1000) { -// value /= 1000; -// magnitude++; -// } -// chartOverview[category].magnitude = magnitude; -// }); -// -// chartOverview.Soc.unit = '(%)'; -// chartOverview.Soc.min = 0; -// chartOverview.Soc.max = 100; -// chartOverview.Temperature.unit = '(°C)'; -// chartOverview.Power.unit = -// '(' + prefixes[chartOverview['Power'].magnitude] + 'W' + ')'; -// chartOverview.Voltage.unit = -// '(' + prefixes[chartOverview['Voltage'].magnitude] + 'V' + ')'; -// -// chartOverview.Current.unit = -// '(' + prefixes[chartOverview['Current'].magnitude] + 'A' + ')'; -// -// return { -// chartData: chartData, -// chartOverview: chartOverview -// }; -// }; +export const transformInputToBatteryViewData = async ( + s3Credentials: I_S3Credentials, + id: number, + product: number, + start_time?: UnixTime, + end_time?: UnixTime +): Promise<{ + chartData: BatteryDataInterface; + chartOverview: BatteryOverviewInterface; +}> => { + const prefixes = ['', 'k', 'M', 'G', 'T']; + const MAX_NUMBER = 9999999; + const categories = ['Soc', 'Temperature', 'Power', 'Voltage', 'Current']; + const pathCategories = [ + 'Soc', + 'Temperatures/Cells/Average', + 'Dc/Power', + 'Dc/Voltage', + 'Dc/Current' + ]; + + const pathsToSearch = [ + '/Battery/Devices/1/', + '/Battery/Devices/2/', + '/Battery/Devices/3/', + '/Battery/Devices/4/', + '/Battery/Devices/5/', + '/Battery/Devices/6/', + '/Battery/Devices/7/', + '/Battery/Devices/8/', + '/Battery/Devices/9/', + '/Battery/Devices/10/' + ]; + + const pathsToSave = []; + + const chartData: BatteryDataInterface = { + Soc: { name: 'State Of Charge', data: [] }, + Temperature: { name: 'Temperature', data: [] }, + Power: { name: 'Power', data: [] }, + Voltage: { name: 'Voltage', data: [] }, + Current: { name: 'Voltage', data: [] } + }; + + const chartOverview: BatteryOverviewInterface = { + Soc: { magnitude: 0, unit: '', min: 0, max: 0 }, + Temperature: { magnitude: 0, unit: '', min: 0, max: 0 }, + Power: { magnitude: 0, unit: '', min: 0, max: 0 }, + Voltage: { magnitude: 0, unit: '', min: 0, max: 0 }, + Current: { magnitude: 0, unit: '', min: 0, max: 0 } + }; + + let initialiation = true; + + let timestampArray: number[] = []; + let adjustedTimestampArray = []; + const timestampPromises = []; + + await axiosConfig + .get( + `/GetCsvTimestampsForInstallation?id=${id}&start=${start_time.ticks}&end=${end_time.ticks}` + ) + .then((res: AxiosResponse) => { + timestampArray = res.data; + }) + .catch((err: AxiosError) => { + if (err.response && err.response.status == 401) { + //removeToken(); + //navigate(routes.login); + } + }); + + for (var i = 0; i < timestampArray.length; i++) { + timestampPromises.push( + fetchDataForOneTime( + UnixTime.fromTicks(timestampArray[i], true), + s3Credentials + ) + ); + + const adjustedTimestamp = + product == 0 + ? new Date(timestampArray[i] * 1000) + : new Date(timestampArray[i] * 100000); + //Timezone offset is negative, so we convert the timestamp to the current zone by subtracting the corresponding offset + adjustedTimestamp.setHours( + adjustedTimestamp.getHours() - adjustedTimestamp.getTimezoneOffset() / 60 + ); + adjustedTimestampArray.push(adjustedTimestamp); + } + + const results: Promise>>[] = + await Promise.all(timestampPromises); + + for (let i = 0; i < results.length; i++) { + if (results[i] == null) { + // Handle not available or try later case + } else { + const timestamp = Object.keys(results[i])[ + Object.keys(results[i]).length - 1 + ]; + const result = results[i][timestamp]; + const battery_nodes = result['/Config/Devices/BatteryNodes'].value + .toString() + .split(','); + + //Initialize the chartData structure based on the node names extracted from the first result + let old_length = pathsToSave.length; + + if (battery_nodes.length > old_length) { + battery_nodes.forEach((node) => { + if (!pathsToSave.includes('Node' + node)) { + pathsToSave.push('Node' + node); + } + }); + } + + if (initialiation) { + initialiation = false; + categories.forEach((category) => { + chartData[category].data = []; + chartOverview[category] = { + magnitude: 0, + unit: '', + min: MAX_NUMBER, + max: -MAX_NUMBER + }; + }); + } + + if (battery_nodes.length > old_length) { + categories.forEach((category) => { + pathsToSave.forEach((path) => { + if (pathsToSave.indexOf(path) >= old_length) { + chartData[category].data[path] = { name: path, data: [] }; + } + }); + }); + } + + for ( + let category_index = 0; + category_index < pathCategories.length; + category_index++ + ) { + let category = categories[category_index]; + + for (let j = 0; j < pathsToSave.length; j++) { + let path = pathsToSearch[j] + pathCategories[category_index]; + + if (result[path]) { + const value = result[path]; + + if (value.value < chartOverview[category].min) { + chartOverview[category].min = value.value; + } + + if (value.value > chartOverview[category].max) { + chartOverview[category].max = value.value; + } + chartData[category].data[pathsToSave[j]].data.push([ + adjustedTimestampArray[i], + value.value + ]); + } else { + // chartData[category].data[pathsToSave[j]].data.push([ + // adjustedTimestampArray[i], + // null + // ]); + } + } + } + } + } + categories.forEach((category) => { + let value = Math.max( + Math.abs(chartOverview[category].max), + Math.abs(chartOverview[category].min) + ); + let magnitude = 0; + + if (value < 0) { + value = -value; + } + while (value >= 1000) { + value /= 1000; + magnitude++; + } + chartOverview[category].magnitude = magnitude; + }); + + chartOverview.Soc.unit = '(%)'; + chartOverview.Soc.min = 0; + chartOverview.Soc.max = 100; + chartOverview.Temperature.unit = '(°C)'; + chartOverview.Power.unit = + '(' + prefixes[chartOverview['Power'].magnitude] + 'W' + ')'; + chartOverview.Voltage.unit = + '(' + prefixes[chartOverview['Voltage'].magnitude] + 'V' + ')'; + + chartOverview.Current.unit = + '(' + prefixes[chartOverview['Current'].magnitude] + 'A' + ')'; + + return { + chartData: chartData, + chartOverview: chartOverview + }; +}; // We use this function in order to retrieve data for main stats. -//The data is of the following form: -//'Soc' : {name:'Soc',data:[ +// 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:[ +// 'Temperature' : {name:'Temperature',data:[ // 'Node1': {name:'Node1', data: [[timestamp,value],[timestamp,value]]}, // 'Node2': {name:'Node2', data: [[timestamp,value],[timestamp,value]]}, // ]}