update daily data aggregator

This commit is contained in:
Noe 2023-12-18 16:09:43 +01:00
parent c36ebeeb6a
commit 85b248dc6f
5 changed files with 606 additions and 498 deletions

View File

@ -55,7 +55,6 @@ public static class Aggregator
Console.WriteLine("Waiting for " + timeUntilNextDay.TotalHours + " hours..."); Console.WriteLine("Waiting for " + timeUntilNextDay.TotalHours + " hours...");
Console.WriteLine("-----------------------------------------------------------------------------------------------------------------"); Console.WriteLine("-----------------------------------------------------------------------------------------------------------------");
// Wait until the next rounded hour // Wait until the next rounded hour
await Task.Delay(timeUntilNextDay); await Task.Delay(timeUntilNextDay);
@ -63,8 +62,15 @@ public static class Aggregator
{ {
try try
{ {
AggregatedData dailyAggregatedData = CreateAverage("HourlyData",DateTime.Now.AddDays(-1).ToUnixTime(),DateTime.Now.ToUnixTime()); var currentTime = DateTime.Now;
AggregatedData dailyAggregatedData = CreateAverage("HourlyData",currentTime.AddDays(-1).ToUnixTime(),currentTime.ToUnixTime());
dailyAggregatedData.Save("DailyData"); dailyAggregatedData.Save("DailyData");
if (await dailyAggregatedData.PushToS3())
{
DeleteHourlyData("HourlyData",currentTime.ToUnixTime());
dailyAggregatedData.DeleteDailyData("DailyData");
}
} }
catch (Exception e) catch (Exception e)
{ {
@ -74,6 +80,21 @@ public static class Aggregator
} }
} }
private static void DeleteHourlyData(String myDirectory, Int64 beforeTimestamp)
{
var csvFiles = Directory.GetFiles(myDirectory, "*.csv");
Console.WriteLine("Delete data before"+beforeTimestamp);
foreach (var csvFile in csvFiles)
{
if (IsFileWithinTimeRange(csvFile, 0, beforeTimestamp))
{
File.Delete(csvFile);
Console.WriteLine($"Deleted hourly data file: {csvFile}");
}
}
}
private static AggregatedData CreateAverage(String myDirectory, Int64 afterTimestamp, Int64 beforeTimestamp) private static AggregatedData CreateAverage(String myDirectory, Int64 afterTimestamp, Int64 beforeTimestamp)
{ {
// Get all CSV files in the specified directory // Get all CSV files in the specified directory

View File

@ -1,5 +1,7 @@
using System.Text.Json; using System.Text.Json;
using Flurl.Http;
using InnovEnergy.App.SaliMax.Devices; using InnovEnergy.App.SaliMax.Devices;
using InnovEnergy.App.SaliMax.SystemConfig;
using InnovEnergy.Lib.Units; using InnovEnergy.Lib.Units;
using InnovEnergy.Lib.Utils; using InnovEnergy.Lib.Utils;
using static System.Text.Json.JsonSerializer; using static System.Text.Json.JsonSerializer;
@ -14,6 +16,8 @@ public class AggregatedData
public required Double AvgPvPower { get; set; } public required Double AvgPvPower { get; set; }
public required Double BatteryPowerAverage { get; set; } public required Double BatteryPowerAverage { get; set; }
private readonly S3Config? _S3Config = Config.Load().S3;
public void Save(String directory) public void Save(String directory)
{ {
var date = DateTime.Now.ToUnixTime(); var date = DateTime.Now.ToUnixTime();
@ -39,6 +43,38 @@ public class AggregatedData
} }
} }
public void DeleteDailyData(String directory)
{
var csvFiles = Directory.GetFiles(directory, "*.csv");
foreach (var csvFile in csvFiles)
{
File.Delete(csvFile);
Console.WriteLine($"Deleted daily data file: {csvFile}");
}
}
public async Task<Boolean> PushToS3()
{
var csv = this.ToCsv();
if (_S3Config is null)
return false;
var s3Path = DateTime.Now.ToString("yyyy-MM-dd") + ".csv";
var request = _S3Config.CreatePutRequest(s3Path);
var response = await request.PutAsync(new StringContent(csv));
if (response.StatusCode != 200)
{
Console.WriteLine("ERROR: PUT");
var error = await response.GetStringAsync();
Console.WriteLine(error);
return false;
}
return true;
}
// public static HourlyData? Load(String dataFilePath) // public static HourlyData? Load(String dataFilePath)
// { // {
// try // try

View File

@ -19,7 +19,6 @@ using InnovEnergy.Lib.Devices.Trumpf.TruConvertDc.Control;
using InnovEnergy.Lib.Protocols.Modbus.Channels; using InnovEnergy.Lib.Protocols.Modbus.Channels;
using InnovEnergy.Lib.Units; using InnovEnergy.Lib.Units;
using InnovEnergy.Lib.Utils; using InnovEnergy.Lib.Utils;
using System.Text.Json;
using InnovEnergy.App.SaliMax.AggregationService; using InnovEnergy.App.SaliMax.AggregationService;
using InnovEnergy.App.SaliMax.DataTypes; using InnovEnergy.App.SaliMax.DataTypes;
using static InnovEnergy.Lib.Devices.Trumpf.SystemControl.DataTypes.SystemConfig; using static InnovEnergy.Lib.Devices.Trumpf.SystemControl.DataTypes.SystemConfig;
@ -36,21 +35,21 @@ internal static class Program
private static readonly IReadOnlyList<Byte> BatteryNodes; private static readonly IReadOnlyList<Byte> BatteryNodes;
private static readonly Channel TruConvertAcChannel ; private static readonly Channel TruConvertAcChannel;
private static readonly Channel TruConvertDcChannel ; private static readonly Channel TruConvertDcChannel;
private static readonly Channel GridMeterChannel ; private static readonly Channel GridMeterChannel;
private static readonly Channel IslandBusLoadChannel; private static readonly Channel IslandBusLoadChannel;
private static readonly Channel PvOnDc ; private static readonly Channel PvOnDc;
private static readonly Channel PvOnAcGrid ; private static readonly Channel PvOnAcGrid;
private static readonly Channel PvOnAcIsland ; private static readonly Channel PvOnAcIsland;
private static readonly Channel RelaysChannel ; private static readonly Channel RelaysChannel;
private static readonly Channel BatteriesChannel ; private static readonly Channel BatteriesChannel;
private const String VpnServerIp = "10.2.0.11"; private const String VpnServerIp = "10.2.0.11";
private static Boolean _subscribedToQueue = false; private static Boolean _subscribedToQueue = false;
private static Boolean _subscribeToQueueForTheFirstTime = false; private static Boolean _subscribeToQueueForTheFirstTime = false;
private static SalimaxAlarmState _prevSalimaxState = SalimaxAlarmState.Green; private static SalimaxAlarmState _prevSalimaxState = SalimaxAlarmState.Green;
private static Int32 _heartBitInterval = 0; private static Int32 _heartBitInterval = 0;
static Program() static Program()
{ {
@ -58,31 +57,31 @@ internal static class Program
var d = config.Devices; var d = config.Devices;
Channel CreateChannel(SalimaxDevice device) => device.DeviceState == DeviceState.Disabled Channel CreateChannel(SalimaxDevice device) => device.DeviceState == DeviceState.Disabled
? new NullChannel() ? new NullChannel()
: new TcpChannel(device); : new TcpChannel(device);
TruConvertAcChannel = CreateChannel(d.TruConvertAcIp); TruConvertAcChannel = CreateChannel(d.TruConvertAcIp);
TruConvertDcChannel = CreateChannel(d.TruConvertDcIp); TruConvertDcChannel = CreateChannel(d.TruConvertDcIp);
GridMeterChannel = CreateChannel(d.GridMeterIp); GridMeterChannel = CreateChannel(d.GridMeterIp);
IslandBusLoadChannel = CreateChannel(d.IslandBusLoadMeterIp); IslandBusLoadChannel = CreateChannel(d.IslandBusLoadMeterIp);
PvOnDc = CreateChannel(d.PvOnDc); PvOnDc = CreateChannel(d.PvOnDc);
PvOnAcGrid = CreateChannel(d.PvOnAcGrid); PvOnAcGrid = CreateChannel(d.PvOnAcGrid);
PvOnAcIsland = CreateChannel(d.PvOnAcIsland); PvOnAcIsland = CreateChannel(d.PvOnAcIsland);
RelaysChannel = CreateChannel(d.RelaysIp); RelaysChannel = CreateChannel(d.RelaysIp);
BatteriesChannel = CreateChannel(d.BatteryIp); BatteriesChannel = CreateChannel(d.BatteryIp);
BatteryNodes = config BatteryNodes = config
.Devices .Devices
.BatteryNodes .BatteryNodes
.Select(n => n.ConvertTo<Byte>()) .Select(n => n.ConvertTo<Byte>())
.ToArray(config.Devices.BatteryNodes.Length); .ToArray(config.Devices.BatteryNodes.Length);
} }
public static async Task Main(String[] args) public static async Task Main(String[] args)
{ {
//Do not await //Do not await
Aggregator.HourlyDataAggregationManager().ContinueWith(t=>t.Exception.WriteLine(), TaskContinuationOptions.OnlyOnFaulted).SupressAwaitWarning(); Aggregator.HourlyDataAggregationManager().ContinueWith(t => t.Exception.WriteLine(), TaskContinuationOptions.OnlyOnFaulted).SupressAwaitWarning();
Aggregator.DailyDataAggregationManager().ContinueWith(t=>t.Exception.WriteLine(), TaskContinuationOptions.OnlyOnFaulted).SupressAwaitWarning(); Aggregator.DailyDataAggregationManager().ContinueWith(t => t.Exception.WriteLine(), TaskContinuationOptions.OnlyOnFaulted).SupressAwaitWarning();
MiddlewareAgent.InitializeCommunicationToMiddleware(); MiddlewareAgent.InitializeCommunicationToMiddleware();
while (true) while (true)
{ {
@ -98,7 +97,6 @@ internal static class Program
} }
private static async Task Run() private static async Task Run()
{ {
"Starting SaliMax".LogInfo(); "Starting SaliMax".LogInfo();
@ -106,68 +104,68 @@ internal static class Program
Watchdog.NotifyReady(); Watchdog.NotifyReady();
var battery48TlDevices = BatteryNodes var battery48TlDevices = BatteryNodes
.Select(n => new Battery48TlDevice(BatteriesChannel, n)) .Select(n => new Battery48TlDevice(BatteriesChannel, n))
.ToList(); .ToList();
var batteryDevices = new Battery48TlDevices(battery48TlDevices); var batteryDevices = new Battery48TlDevices(battery48TlDevices);
var acDcDevices = new TruConvertAcDcDevices(TruConvertAcChannel); var acDcDevices = new TruConvertAcDcDevices(TruConvertAcChannel);
var dcDcDevices = new TruConvertDcDcDevices(TruConvertDcChannel); var dcDcDevices = new TruConvertDcDcDevices(TruConvertDcChannel);
var gridMeterDevice = new EmuMeterDevice(GridMeterChannel); var gridMeterDevice = new EmuMeterDevice(GridMeterChannel);
var acIslandLoadMeter = new EmuMeterDevice(IslandBusLoadChannel); var acIslandLoadMeter = new EmuMeterDevice(IslandBusLoadChannel);
var pvOnDcDevice = new AmptDevices(PvOnDc); var pvOnDcDevice = new AmptDevices(PvOnDc);
var pvOnAcGridDevice = new AmptDevices(PvOnAcGrid); var pvOnAcGridDevice = new AmptDevices(PvOnAcGrid);
var pvOnAcIslandDevice = new AmptDevices(PvOnAcIsland); var pvOnAcIslandDevice = new AmptDevices(PvOnAcIsland);
var saliMaxRelaysDevice = new RelaysDevice(RelaysChannel); var saliMaxRelaysDevice = new RelaysDevice(RelaysChannel);
StatusRecord ReadStatus() StatusRecord ReadStatus()
{ {
var config = Config.Load(); var config = Config.Load();
var devices = config.Devices; var devices = config.Devices;
var acDc = acDcDevices.Read(); var acDc = acDcDevices.Read();
var dcDc = dcDcDevices.Read(); var dcDc = dcDcDevices.Read();
var relays = saliMaxRelaysDevice.Read(); var relays = saliMaxRelaysDevice.Read();
var loadOnAcIsland = acIslandLoadMeter.Read(); var loadOnAcIsland = acIslandLoadMeter.Read();
var gridMeter = gridMeterDevice.Read(); var gridMeter = gridMeterDevice.Read();
var pvOnDc = pvOnDcDevice.Read(); var pvOnDc = pvOnDcDevice.Read();
var battery = batteryDevices.Read(); var battery = batteryDevices.Read();
var pvOnAcGrid = pvOnAcGridDevice.Read(); var pvOnAcGrid = pvOnAcGridDevice.Read();
var pvOnAcIsland = pvOnAcIslandDevice.Read(); var pvOnAcIsland = pvOnAcIslandDevice.Read();
var gridBusToIslandBus = Topology.CalculateGridBusToIslandBusPower(pvOnAcIsland, loadOnAcIsland, acDc); var gridBusToIslandBus = Topology.CalculateGridBusToIslandBusPower(pvOnAcIsland, loadOnAcIsland, acDc);
var gridBusLoad = devices.LoadOnAcGrid.DeviceState == DeviceState.Disabled var gridBusLoad = devices.LoadOnAcGrid.DeviceState == DeviceState.Disabled
? new AcPowerDevice { Power = 0 } ? new AcPowerDevice { Power = 0 }
: Topology.CalculateGridBusLoad(gridMeter, pvOnAcGrid, gridBusToIslandBus); : Topology.CalculateGridBusLoad(gridMeter, pvOnAcGrid, gridBusToIslandBus);
var dcLoad = devices.LoadOnDc.DeviceState == DeviceState.Disabled var dcLoad = devices.LoadOnDc.DeviceState == DeviceState.Disabled
? new DcPowerDevice { Power = 0 } ? new DcPowerDevice { Power = 0 }
: Topology.CalculateDcLoad(acDc, pvOnDc, dcDc); : Topology.CalculateDcLoad(acDc, pvOnDc, dcDc);
var acDcToDcLink = devices.LoadOnDc.DeviceState == DeviceState.Disabled ? var acDcToDcLink = devices.LoadOnDc.DeviceState == DeviceState.Disabled
Topology.CalculateAcDcToDcLink(pvOnDc, dcDc, acDc) ? Topology.CalculateAcDcToDcLink(pvOnDc, dcDc, acDc)
: new DcPowerDevice{ Power = acDc.Dc.Power}; : new DcPowerDevice { Power = acDc.Dc.Power };
return new StatusRecord return new StatusRecord
{ {
AcDc = acDc, AcDc = acDc,
DcDc = dcDc, DcDc = dcDc,
Battery = battery, Battery = battery,
Relays = relays, Relays = relays,
GridMeter = gridMeter, GridMeter = gridMeter,
PvOnAcGrid = pvOnAcGrid, PvOnAcGrid = pvOnAcGrid,
PvOnAcIsland = pvOnAcIsland, PvOnAcIsland = pvOnAcIsland,
PvOnDc = pvOnDc, PvOnDc = pvOnDc,
AcGridToAcIsland = gridBusToIslandBus, AcGridToAcIsland = gridBusToIslandBus,
AcDcToDcLink = acDcToDcLink, AcDcToDcLink = acDcToDcLink,
LoadOnAcGrid = gridBusLoad, LoadOnAcGrid = gridBusLoad,
LoadOnAcIsland = loadOnAcIsland, LoadOnAcIsland = loadOnAcIsland,
LoadOnDc = dcLoad, LoadOnDc = dcLoad,
StateMachine = StateMachine.Default, StateMachine = StateMachine.Default,
EssControl = EssControl.Default, EssControl = EssControl.Default,
Log = new SystemLog { SalimaxAlarmState = SalimaxAlarmState.Green, Message = null }, //TODO: Put real stuff Log = new SystemLog { SalimaxAlarmState = SalimaxAlarmState.Green, Message = null }, //TODO: Put real stuff
Config = config // load from disk every iteration, so config can be changed while running Config = config // load from disk every iteration, so config can be changed while running
}; };
} }
@ -185,11 +183,11 @@ internal static class Program
while (true) while (true)
{ {
await Observable await Observable
.Interval(UpdateInterval) .Interval(UpdateInterval)
.Select(_ => RunIteration()) .Select(_ => RunIteration())
.SelectMany(r => UploadCsv(r, DateTime.Now.Round(UpdateInterval))) .SelectMany(r => UploadCsv(r, DateTime.Now.Round(UpdateInterval)))
.SelectError() .SelectError()
.ToTask(); .ToTask();
} }
@ -201,7 +199,7 @@ internal static class Program
var currentSalimaxState = GetSalimaxStateAlarm(record); var currentSalimaxState = GetSalimaxStateAlarm(record);
SendSalimaxStateAlarm(currentSalimaxState,record); SendSalimaxStateAlarm(currentSalimaxState, record);
record.ControlConstants(); record.ControlConstants();
record.ControlSystemState(); record.ControlSystemState();
@ -223,7 +221,7 @@ internal static class Program
record.CreateTopologyTextBlock().WriteLine(); record.CreateTopologyTextBlock().WriteLine();
(record.Relays is null ? "No relay Data available" : record.Relays.FiWarning ? "Alert: Fi Warning Detected" : "No Fi Warning Detected").WriteLine(); (record.Relays is null ? "No relay Data available" : record.Relays.FiWarning ? "Alert: Fi Warning Detected" : "No Fi Warning Detected").WriteLine();
(record.Relays is null ? "No relay Data available" : record.Relays.FiError ? "Alert: Fi Error Detected" : "No Fi Error Detected") .WriteLine(); (record.Relays is null ? "No relay Data available" : record.Relays.FiError ? "Alert: Fi Error Detected" : "No Fi Error Detected").WriteLine();
//record.ApplyConfigFile(minSoc:22, gridSetPoint:1); //record.ApplyConfigFile(minSoc:22, gridSetPoint:1);
@ -245,11 +243,12 @@ internal static class Program
_heartBitInterval++; _heartBitInterval++;
//When the controller boots, it tries to subscribe to the queue //When the controller boots, it tries to subscribe to the queue
if (_subscribeToQueueForTheFirstTime==false) if (_subscribeToQueueForTheFirstTime == false)
{ {
_subscribeToQueueForTheFirstTime = true; _subscribeToQueueForTheFirstTime = true;
_subscribedToQueue = RabbitMqManager.SubscribeToQueue(currentSalimaxState, s3Bucket, VpnServerIp); _subscribedToQueue = RabbitMqManager.SubscribeToQueue(currentSalimaxState, s3Bucket, VpnServerIp);
} }
//If already subscribed to the queue and the status has been changed, update the queue //If already subscribed to the queue and the status has been changed, update the queue
if (_subscribedToQueue && currentSalimaxState.Status != _prevSalimaxState) if (_subscribedToQueue && currentSalimaxState.Status != _prevSalimaxState)
{ {
@ -257,7 +256,7 @@ internal static class Program
if (s3Bucket != null) if (s3Bucket != null)
RabbitMqManager.InformMiddleware(currentSalimaxState); RabbitMqManager.InformMiddleware(currentSalimaxState);
} }
else if (_subscribedToQueue && _heartBitInterval>=15) else if (_subscribedToQueue && _heartBitInterval >= 15)
{ {
//Send a heartbit to the backend //Send a heartbit to the backend
Console.WriteLine("----------------------------------------Sending Heartbit----------------------------------------"); Console.WriteLine("----------------------------------------Sending Heartbit----------------------------------------");
@ -274,16 +273,15 @@ internal static class Program
{ {
record.ApplyConfigFile(config); record.ApplyConfigFile(config);
} }
} }
private static StatusMessage GetSalimaxStateAlarm(StatusRecord record) private static StatusMessage GetSalimaxStateAlarm(StatusRecord record)
{ {
var alarmCondition = record.DetectAlarmStates(); var alarmCondition = record.DetectAlarmStates();
var s3Bucket = Config.Load().S3?.Bucket; var s3Bucket = Config.Load().S3?.Bucket;
var alarmList = new List<AlarmOrWarning>(); var alarmList = new List<AlarmOrWarning>();
var warningList = new List<AlarmOrWarning>(); var warningList = new List<AlarmOrWarning>();
if (alarmCondition is not null) if (alarmCondition is not null)
{ {
@ -298,147 +296,146 @@ internal static class Program
}); });
} }
foreach (var alarm in record.AcDc.Alarms) foreach (var alarm in record.AcDc.Alarms)
{ {
alarmList.Add(new AlarmOrWarning alarmList.Add(new AlarmOrWarning
{ {
Date = DateTime.Now.ToString("yyyy-MM-dd"), Date = DateTime.Now.ToString("yyyy-MM-dd"),
Time = DateTime.Now.ToString("HH:mm:ss"), Time = DateTime.Now.ToString("HH:mm:ss"),
CreatedBy = "AcDc", CreatedBy = "AcDc",
Description = alarm.ToString() Description = alarm.ToString()
}); });
} }
foreach (var alarm in record.DcDc.Alarms) foreach (var alarm in record.DcDc.Alarms)
{ {
alarmList.Add(new AlarmOrWarning alarmList.Add(new AlarmOrWarning
{ {
Date = DateTime.Now.ToString("yyyy-MM-dd"), Date = DateTime.Now.ToString("yyyy-MM-dd"),
Time = DateTime.Now.ToString("HH:mm:ss"), Time = DateTime.Now.ToString("HH:mm:ss"),
CreatedBy = "DcDc", CreatedBy = "DcDc",
Description = alarm.ToString() Description = alarm.ToString()
}); });
} }
if (record.Battery != null) if (record.Battery != null)
{ {
var i = 0; var i = 0;
foreach (var battery in record.Battery.Devices) foreach (var battery in record.Battery.Devices)
{ {
i++; i++;
foreach (var alarm in battery.Alarms) foreach (var alarm in battery.Alarms)
{ {
alarmList.Add(new AlarmOrWarning alarmList.Add(new AlarmOrWarning
{ {
Date = DateTime.Now.ToString("yyyy-MM-dd"), Date = DateTime.Now.ToString("yyyy-MM-dd"),
Time = DateTime.Now.ToString("HH:mm:ss"), Time = DateTime.Now.ToString("HH:mm:ss"),
CreatedBy = "Battery" + i, CreatedBy = "Battery" + i,
Description = alarm Description = alarm
}); });
} }
} }
}
} foreach (var warning in record.AcDc.Warnings)
{
warningList.Add(new AlarmOrWarning
{
Date = DateTime.Now.ToString("yyyy-MM-dd"),
Time = DateTime.Now.ToString("HH:mm:ss"),
CreatedBy = "AcDc",
Description = warning.ToString()
});
}
foreach (var warning in record.AcDc.Warnings) foreach (var warning in record.DcDc.Warnings)
{ {
warningList.Add(new AlarmOrWarning warningList.Add(new AlarmOrWarning
{ {
Date = DateTime.Now.ToString("yyyy-MM-dd"), Date = DateTime.Now.ToString("yyyy-MM-dd"),
Time = DateTime.Now.ToString("HH:mm:ss"), Time = DateTime.Now.ToString("HH:mm:ss"),
CreatedBy = "AcDc", CreatedBy = "DcDc",
Description = warning.ToString() Description = warning.ToString()
}); });
} }
foreach (var warning in record.DcDc.Warnings) if (record.Battery != null)
{ {
warningList.Add(new AlarmOrWarning foreach (var warning in record.Battery.Warnings)
{ {
Date = DateTime.Now.ToString("yyyy-MM-dd"), warningList.Add(new AlarmOrWarning
Time = DateTime.Now.ToString("HH:mm:ss"), {
CreatedBy = "DcDc", Date = DateTime.Now.ToString("yyyy-MM-dd"),
Description = warning.ToString() Time = DateTime.Now.ToString("HH:mm:ss"),
}); CreatedBy = "Battery",
} Description = warning
});
}
}
if (record.Battery != null) var salimaxAlarmsState = (record.Battery is not null && record.Battery.Warnings.Any())
{ | record.AcDc.Warnings.Any()
foreach (var warning in record.Battery.Warnings) | record.AcDc.SystemControl.Warnings.Any()
{ | record.DcDc.Warnings.Any()
warningList.Add(new AlarmOrWarning ? SalimaxAlarmState.Orange
{ : SalimaxAlarmState.Green; // this will be replaced by LedState
Date = DateTime.Now.ToString("yyyy-MM-dd"),
Time = DateTime.Now.ToString("HH:mm:ss"),
CreatedBy = "Battery",
Description = warning
});
}
}
var salimaxAlarmsState = (record.Battery is not null && record.Battery.Warnings.Any()) salimaxAlarmsState = (record.Battery is not null && record.Battery.Alarms.Any())
| record.AcDc.Warnings.Any() | record.AcDc.Alarms.Any()
| record.AcDc.SystemControl.Warnings.Any() | record.AcDc.SystemControl.Alarms.Any()
| record.DcDc.Warnings.Any() | record.DcDc.Alarms.Any()
? SalimaxAlarmState.Orange | alarmCondition is not null
: SalimaxAlarmState.Green; // this will be replaced by LedState ? SalimaxAlarmState.Red
: salimaxAlarmsState; // this will be replaced by LedState
salimaxAlarmsState = (record.Battery is not null && record.Battery.Alarms.Any()) int.TryParse(s3Bucket?.Split("-")[0], out var installationId);
| record.AcDc.Alarms.Any()
| record.AcDc.SystemControl.Alarms.Any()
| record.DcDc.Alarms.Any()
| alarmCondition is not null
? SalimaxAlarmState.Red
: salimaxAlarmsState; // this will be replaced by LedState
int.TryParse(s3Bucket?.Split("-")[0], out var installationId); var returnedStatus = new StatusMessage
{
InstallationId = installationId,
Status = salimaxAlarmsState,
Type = MessageType.AlarmOrWarning,
Alarms = alarmList,
Warnings = warningList
};
var returnedStatus = new StatusMessage return returnedStatus;
{
InstallationId = installationId,
Status = salimaxAlarmsState,
Type = MessageType.AlarmOrWarning,
Alarms = alarmList,
Warnings = warningList
};
return returnedStatus;
} }
private static String? DetectAlarmStates(this StatusRecord r) => r.Relays switch private static String? DetectAlarmStates(this StatusRecord r) => r.Relays switch
{ {
{ K2ConnectIslandBusToGridBus: false, K2IslandBusIsConnectedToGridBus: true } => " Contradiction: R0 is opening the K2 but the K2 is still close ", { K2ConnectIslandBusToGridBus: false, K2IslandBusIsConnectedToGridBus: true } => " Contradiction: R0 is opening the K2 but the K2 is still close ",
{ K1GridBusIsConnectedToGrid : false, K2IslandBusIsConnectedToGridBus: true } => " Contradiction: K1 is open but the K2 is still close ", { K1GridBusIsConnectedToGrid : false, K2IslandBusIsConnectedToGridBus: true } => " Contradiction: K1 is open but the K2 is still close ",
{ FiError: true, K2IslandBusIsConnectedToGridBus: true } => " Contradiction: Fi error occured but the K2 is still close ", { FiError: true, K2IslandBusIsConnectedToGridBus: true } => " Contradiction: Fi error occured but the K2 is still close ",
_ => null _ => null
}; };
private static void ControlConstants(this StatusRecord r) private static void ControlConstants(this StatusRecord r)
{ {
var inverters = r.AcDc.Devices; var inverters = r.AcDc.Devices;
var dcDevices = r.DcDc.Devices; var dcDevices = r.DcDc.Devices;
var configFile = r.Config; var configFile = r.Config;
var devicesConfig = r.AcDc.Devices.All(d => d.Control.Ac.GridType == GridType.GridTied400V50Hz) ? configFile.GridTie : configFile.IslandMode; // TODO if any of the grid tie mode var devicesConfig = r.AcDc.Devices.All(d => d.Control.Ac.GridType == GridType.GridTied400V50Hz) ? configFile.GridTie : configFile.IslandMode; // TODO if any of the grid tie mode
inverters.ForEach(d => d.Control.Dc.MaxVoltage = devicesConfig.AcDc.MaxDcLinkVoltage); inverters.ForEach(d => d.Control.Dc.MaxVoltage = devicesConfig.AcDc.MaxDcLinkVoltage);
inverters.ForEach(d => d.Control.Dc.MinVoltage = devicesConfig.AcDc.MinDcLinkVoltage); inverters.ForEach(d => d.Control.Dc.MinVoltage = devicesConfig.AcDc.MinDcLinkVoltage);
inverters.ForEach(d => d.Control.Dc.ReferenceVoltage = devicesConfig.AcDc.ReferenceDcLinkVoltage); inverters.ForEach(d => d.Control.Dc.ReferenceVoltage = devicesConfig.AcDc.ReferenceDcLinkVoltage);
inverters.ForEach(d => d.Control.Dc.PrechargeConfig = DcPrechargeConfig.PrechargeDcWithInternal); inverters.ForEach(d => d.Control.Dc.PrechargeConfig = DcPrechargeConfig.PrechargeDcWithInternal);
dcDevices.ForEach(d => d.Control.DroopControl.UpperVoltage = devicesConfig.DcDc.UpperDcLinkVoltage); dcDevices.ForEach(d => d.Control.DroopControl.UpperVoltage = devicesConfig.DcDc.UpperDcLinkVoltage);
dcDevices.ForEach(d => d.Control.DroopControl.LowerVoltage = devicesConfig.DcDc.LowerDcLinkVoltage); dcDevices.ForEach(d => d.Control.DroopControl.LowerVoltage = devicesConfig.DcDc.LowerDcLinkVoltage);
dcDevices.ForEach(d => d.Control.DroopControl.ReferenceVoltage = devicesConfig.DcDc.ReferenceDcLinkVoltage); dcDevices.ForEach(d => d.Control.DroopControl.ReferenceVoltage = devicesConfig.DcDc.ReferenceDcLinkVoltage);
dcDevices.ForEach(d => d.Control.CurrentControl.MaxBatteryChargingCurrent = configFile.MaxBatteryChargingCurrent); dcDevices.ForEach(d => d.Control.CurrentControl.MaxBatteryChargingCurrent = configFile.MaxBatteryChargingCurrent);
dcDevices.ForEach(d => d.Control.CurrentControl.MaxBatteryDischargingCurrent = configFile.MaxBatteryDischargingCurrent); dcDevices.ForEach(d => d.Control.CurrentControl.MaxBatteryDischargingCurrent = configFile.MaxBatteryDischargingCurrent);
dcDevices.ForEach(d => d.Control.MaxDcPower = configFile.MaxDcPower); dcDevices.ForEach(d => d.Control.MaxDcPower = configFile.MaxDcPower);
dcDevices.ForEach(d => d.Control.VoltageLimits.MaxBatteryVoltage = configFile.MaxChargeBatteryVoltage); dcDevices.ForEach(d => d.Control.VoltageLimits.MaxBatteryVoltage = configFile.MaxChargeBatteryVoltage);
dcDevices.ForEach(d => d.Control.VoltageLimits.MinBatteryVoltage = configFile.MinDischargeBatteryVoltage); dcDevices.ForEach(d => d.Control.VoltageLimits.MinBatteryVoltage = configFile.MinDischargeBatteryVoltage);
dcDevices.ForEach(d => d.Control.ControlMode = DcControlMode.VoltageDroop); dcDevices.ForEach(d => d.Control.ControlMode = DcControlMode.VoltageDroop);
r.DcDc.ResetAlarms(); r.DcDc.ResetAlarms();
r.AcDc.ResetAlarms(); r.AcDc.ResetAlarms();
@ -451,8 +448,8 @@ internal static class Program
var nInverters = record.AcDc.Devices.Count; var nInverters = record.AcDc.Devices.Count;
var powerPerInverterPhase = nInverters > 0 var powerPerInverterPhase = nInverters > 0
? essControl.PowerSetpoint / nInverters / 3 ? essControl.PowerSetpoint / nInverters / 3
: 0; : 0;
record.AcDc.Devices.ForEach(d => record.AcDc.Devices.ForEach(d =>
{ {
@ -468,53 +465,51 @@ internal static class Program
if (sc is null) if (sc is null)
return; return;
sc.ReferenceFrame = ReferenceFrame.Consumer; sc.ReferenceFrame = ReferenceFrame.Consumer;
sc.SystemConfig = AcDcAndDcDc; sc.SystemConfig = AcDcAndDcDc;
#if DEBUG #if DEBUG
sc.CommunicationTimeout = TimeSpan.FromMinutes(2); sc.CommunicationTimeout = TimeSpan.FromMinutes(2);
#else #else
sc.CommunicationTimeout = TimeSpan.FromSeconds(20); sc.CommunicationTimeout = TimeSpan.FromSeconds(20);
#endif #endif
sc.PowerSetPointActivation = PowerSetPointActivation.Immediate; sc.PowerSetPointActivation = PowerSetPointActivation.Immediate;
sc.UseSlaveIdForAddressing = true; sc.UseSlaveIdForAddressing = true;
sc.SlaveErrorHandling = SlaveErrorHandling.Relaxed; sc.SlaveErrorHandling = SlaveErrorHandling.Relaxed;
sc.SubSlaveErrorHandling = SubSlaveErrorHandling.Off; sc.SubSlaveErrorHandling = SubSlaveErrorHandling.Off;
sc.ResetAlarmsAndWarnings = true; sc.ResetAlarmsAndWarnings = true;
} }
private static void ApplyDcDcDefaultSettings(this SystemControlRegisters? sc) private static void ApplyDcDcDefaultSettings(this SystemControlRegisters? sc)
{ {
if (sc is null) if (sc is null)
return; return;
sc.SystemConfig = DcDcOnly; sc.SystemConfig = DcDcOnly;
#if DEBUG #if DEBUG
sc.CommunicationTimeout = TimeSpan.FromMinutes(2); sc.CommunicationTimeout = TimeSpan.FromMinutes(2);
#else #else
sc.CommunicationTimeout = TimeSpan.FromSeconds(20); sc.CommunicationTimeout = TimeSpan.FromSeconds(20);
#endif #endif
sc.PowerSetPointActivation = PowerSetPointActivation.Immediate; sc.PowerSetPointActivation = PowerSetPointActivation.Immediate;
sc.UseSlaveIdForAddressing = true; sc.UseSlaveIdForAddressing = true;
sc.SlaveErrorHandling = SlaveErrorHandling.Relaxed; sc.SlaveErrorHandling = SlaveErrorHandling.Relaxed;
sc.SubSlaveErrorHandling = SubSlaveErrorHandling.Off; sc.SubSlaveErrorHandling = SubSlaveErrorHandling.Off;
sc.ResetAlarmsAndWarnings = true;
sc.ResetAlarmsAndWarnings = true;
} }
private static async Task<Boolean> UploadCsv(StatusRecord status, DateTime timeStamp) private static async Task<Boolean> UploadCsv(StatusRecord status, DateTime timeStamp)
{ {
var s3Config = status.Config.S3; var s3Config = status.Config.S3;
var csv = status.ToCsv().LogInfo(); var csv = status.ToCsv().LogInfo();
if (s3Config is null) if (s3Config is null)
return false; return false;
var s3Path = timeStamp.ToUnixTime() + ".csv"; var s3Path = timeStamp.ToUnixTime() + ".csv";
var request = s3Config.CreatePutRequest(s3Path); var request = s3Config.CreatePutRequest(s3Path);
var response = await request.PutAsync(new StringContent(csv)); var response = await request.PutAsync(new StringContent(csv));
// This is temporary for Wittman // This is temporary for Wittman
@ -532,9 +527,8 @@ internal static class Program
private static void ApplyConfigFile(this StatusRecord status, Configuration? config) private static void ApplyConfigFile(this StatusRecord status, Configuration? config)
{ {
status.Config.MinSoc = config.MinimumSoC; status.Config.MinSoc = config.MinimumSoC;
status.Config.GridSetPoint = config.GridSetPoint * 1000; // converted from kW to W status.Config.GridSetPoint = config.GridSetPoint * 1000; // converted from kW to W
status.Config.ForceCalibrationCharge = config.ForceCalibrationCharge; status.Config.ForceCalibrationCharge = config.ForceCalibrationCharge;
} }
} }

View File

@ -3,125 +3,230 @@
import { ApexOptions } from 'apexcharts'; import { ApexOptions } from 'apexcharts';
import { chartInfoInterface } from 'src/interfaces/Chart'; import { chartInfoInterface } from 'src/interfaces/Chart';
import { findPower, formatPowerForGraph } from 'src/Resources/formatPower'; import { findPower, formatPowerForGraph } from 'src/Resources/formatPower';
import { addHours, format } from 'date-fns';
export const getChartOptions = (chartInfo: chartInfoInterface): ApexOptions => { export const getChartOptions = (
// Custom datetime formatter for GMT+2 chartInfo: chartInfoInterface,
const customDatetimeFormatter = (timestamp, options) => { type: string
const gmtDate = new Date(timestamp); // Convert Unix timestamp to milliseconds ): ApexOptions => {
const gmtPlus2Date = addHours(gmtDate, 4); // Add 2 hours to convert to GMT+2 const chartOptions: ApexOptions =
type == 'daily'
? {
chart: {
id: 'area-datetime',
toolbar: {
show: false
},
type: 'area',
height: 350,
zoom: {
autoScaleYaxis: false
}
},
// Use the specified options to format the date and time dataLabels: {
const year = format(gmtDate, 'yyyy'); enabled: false
const month = format(gmtDate, "MMM 'yy"); },
const day = format(gmtDate, 'dd MMM');
const hour = format(gmtDate, 'HH:mm');
const minute = format(gmtDate, 'mm');
// Return the formatted date and time based on the provided options fill: {
return ` ${hour}:${minute}`; type: 'gradient',
}; gradient: {
shadeIntensity: 1,
opacityFrom: 0.7,
opacityTo: 0.9,
stops: [0, 100]
}
},
//colors: ['#FF5733', '#3498db'],
colors: ['#3498db', '#2ecc71'],
//colors: ['#1abc9c', '#e91e63'],
xaxis: {
type: 'datetime',
labels: {
datetimeFormatter: {
year: 'yyyy',
month: "MMM 'yy",
day: 'dd MMM',
hour: 'HH:mm',
minute: 'mm'
}
}
},
stroke: {
curve: 'smooth',
width: 2
},
yaxis: {
min:
chartInfo.min >= 0
? 0
: chartInfo.max <= 0
? Math.ceil(chartInfo.min / findPower(chartInfo.min).value) *
findPower(chartInfo.min).value
: undefined,
max:
chartInfo.min >= 0
? Math.ceil(chartInfo.max / findPower(chartInfo.max).value) *
findPower(chartInfo.max).value
: chartInfo.max <= 0
? 0
: undefined,
title: {
text: chartInfo.unit,
style: {
fontSize: '12px'
},
offsetY: -160,
offsetX: 25,
rotate: 0
},
labels: {
formatter: function (value: number) {
return formatPowerForGraph(
value,
Math.max(Math.abs(chartInfo.max), Math.abs(chartInfo.min))
).value.toString();
}
}
},
const chartOptions: ApexOptions = { tooltip: {
chart: { x: {
id: 'area-datetime', format: 'dd MMM HH:mm:ss'
toolbar: { },
show: false y: {
}, formatter: function (val, opts) {
type: 'area', return (
height: 350, formatPowerForGraph(
zoom: { val,
autoScaleYaxis: false Math.max(Math.abs(chartInfo.max), Math.abs(chartInfo.min))
} ).value.toFixed(2) +
}, ' ' +
// markers: { chartInfo.unit
// size: 1, );
// strokeColors: 'black' }
// }, }
dataLabels: { }
enabled: false
},
fill: {
type: 'gradient',
gradient: {
shadeIntensity: 1,
opacityFrom: 0.7,
opacityTo: 0.9,
stops: [0, 100]
}
},
//colors: ['#FF5733', '#3498db'],
colors: ['#3498db', '#2ecc71'],
//colors: ['#1abc9c', '#e91e63'],
xaxis: {
type: 'datetime',
labels: {
datetimeFormatter: {
year: 'yyyy',
month: "MMM 'yy",
day: 'dd MMM',
hour: 'HH:mm',
minute: 'mm'
} }
} : type == 'monthly'
}, ? {
stroke: { chart: {
curve: 'smooth', height: 350,
width: 2 type: 'bar'
}, },
yaxis: { plotOptions: {
min: bar: {
chartInfo.min >= 0 borderRadius: 10,
? 0 dataLabels: {
: chartInfo.max <= 0 position: 'top' // top, center, bottom
? Math.ceil(chartInfo.min / findPower(chartInfo.min).value) * }
findPower(chartInfo.min).value }
: undefined, },
max: dataLabels: {
chartInfo.min >= 0 enabled: true,
? Math.ceil(chartInfo.max / findPower(chartInfo.max).value) * formatter: function (val) {
findPower(chartInfo.max).value return val + '%';
: chartInfo.max <= 0 },
? 0 offsetY: -20,
: undefined, style: {
title: { fontSize: '12px',
text: chartInfo.unit, colors: ['#304758']
style: { }
fontSize: '12px' },
},
offsetY: -160,
offsetX: 25,
rotate: 0
},
labels: {
formatter: function (value: number) {
return formatPowerForGraph(
value,
Math.max(Math.abs(chartInfo.max), Math.abs(chartInfo.min))
).value.toString();
}
}
},
tooltip: { xaxis: {
x: { categories: [
format: 'dd MMM HH:mm:ss' 'Jan',
}, 'Feb',
y: { 'Mar',
formatter: function (val, opts) { 'Apr',
return ( 'May',
formatPowerForGraph( 'Jun',
val, 'Jul',
Math.max(Math.abs(chartInfo.max), Math.abs(chartInfo.min)) 'Aug',
).value.toFixed(2) + 'Sep',
' ' + 'Oct',
chartInfo.unit 'Nov',
); 'Dec'
],
position: 'bottom',
axisBorder: {
show: false
},
axisTicks: {
show: false
},
tooltip: {
enabled: true
}
},
yaxis: {
axisBorder: {
show: false
},
axisTicks: {
show: false
},
labels: {
show: false,
formatter: function (val) {
return val + '%';
}
}
}
} }
} : {
} chart: {
}; height: 350,
type: 'bar'
},
plotOptions: {
bar: {
borderRadius: 10,
dataLabels: {
position: 'top' // top, center, bottom
}
}
},
dataLabels: {
enabled: true,
formatter: function (val) {
return val + '%';
},
offsetY: -20,
style: {
fontSize: '12px',
colors: ['#304758']
}
},
xaxis: {
categories: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
position: 'bottom',
axisBorder: {
show: false
},
axisTicks: {
show: false
},
tooltip: {
enabled: true
}
},
yaxis: {
axisBorder: {
show: false
},
axisTicks: {
show: false
},
labels: {
show: false,
formatter: function (val) {
return val + '%';
}
}
}
};
return chartOptions; return chartOptions;
}; };

View File

@ -1,11 +1,4 @@
import { import { Box, Card, Container, Grid, Typography } from '@mui/material';
Box,
Card,
Container,
Grid,
Typography,
useTheme
} from '@mui/material';
import ReactApexChart from 'react-apexcharts'; import ReactApexChart from 'react-apexcharts';
import React, { useEffect, useMemo, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import DataCache from 'src/dataCache/dataCache'; import DataCache from 'src/dataCache/dataCache';
@ -25,7 +18,6 @@ import { chartDataInterface, overviewInterface } from 'src/interfaces/Chart';
import { fetchData } from 'src/content/dashboards/Installations/fetchData'; import { fetchData } from 'src/content/dashboards/Installations/fetchData';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { ApexOptions } from 'apexcharts';
const prefixes = ['', 'k', 'M', 'G', 'T']; const prefixes = ['', 'k', 'M', 'G', 'T'];
const MAX_NUMBER = 9999999; const MAX_NUMBER = 9999999;
@ -35,7 +27,6 @@ interface OverviewProps {
} }
function Overview(props: OverviewProps) { function Overview(props: OverviewProps) {
const theme = useTheme();
const numOfPointsToFetch = 100; const numOfPointsToFetch = 100;
const [timeRange, setTimeRange] = useState( const [timeRange, setTimeRange] = useState(
createTimes( createTimes(
@ -80,6 +71,9 @@ function Overview(props: OverviewProps) {
}, []); }, []);
const times$ = useMemo(() => new BehaviorSubject(timeRange), []); const times$ = useMemo(() => new BehaviorSubject(timeRange), []);
const [dailyData, setDailyData] = useState(true);
const [weeklyData, setWeeklyData] = useState(false);
const [monthlyData, setMonthlyData] = useState(false);
const transformToGraphData = ( const transformToGraphData = (
input: RecordSeries input: RecordSeries
@ -157,12 +151,10 @@ function Overview(props: OverviewProps) {
Math.abs(overviewData[path].max), Math.abs(overviewData[path].max),
Math.abs(overviewData[path].min) Math.abs(overviewData[path].min)
); );
let negative = false;
let magnitude = 0; let magnitude = 0;
if (value < 0) { if (value < 0) {
value = -value; value = -value;
negative = true;
} }
while (value >= 1000) { while (value >= 1000) {
value /= 1000; value /= 1000;
@ -289,6 +281,9 @@ function Overview(props: OverviewProps) {
}; };
const handle24HourData = () => { const handle24HourData = () => {
setDailyData(true);
setWeeklyData(false);
setMonthlyData(false);
const times = createTimes( const times = createTimes(
UnixTime.now().rangeBefore(TimeSpan.fromDays(1)), UnixTime.now().rangeBefore(TimeSpan.fromDays(1)),
numOfPointsToFetch numOfPointsToFetch
@ -298,21 +293,28 @@ function Overview(props: OverviewProps) {
}; };
const handleWeekData = () => { const handleWeekData = () => {
const times = createTimes( setDailyData(false);
UnixTime.now().rangeBefore(TimeSpan.fromWeeks(1)), setWeeklyData(true);
numOfPointsToFetch setMonthlyData(false);
); //fetchData(12312,props.s3Credentials);
cache.getSeries(times); // const times = createTimes(
times$.next(times); // UnixTime.now().rangeBefore(TimeSpan.fromWeeks(1)),
// numOfPointsToFetch
// );
// cache.getSeries(times);
// times$.next(times);
}; };
const handleMonthData = () => { const handleMonthData = () => {
const times = createTimes( setDailyData(false);
UnixTime.now().rangeBefore(TimeSpan.fromWeeks(4)), setWeeklyData(false);
numOfPointsToFetch setMonthlyData(true);
); // const times = createTimes(
cache.getSeries(times); // UnixTime.now().rangeBefore(TimeSpan.fromWeeks(4)),
times$.next(times); // numOfPointsToFetch
// );
// cache.getSeries(times);
// times$.next(times);
}; };
const series = [ const series = [
@ -322,73 +324,6 @@ function Overview(props: OverviewProps) {
} }
]; ];
const state: ApexOptions = {
chart: {
height: 350,
type: 'bar'
},
plotOptions: {
bar: {
borderRadius: 10,
dataLabels: {
position: 'top' // top, center, bottom
}
}
},
dataLabels: {
enabled: true,
formatter: function (val) {
return val + '%';
},
offsetY: -20,
style: {
fontSize: '12px',
colors: ['#304758']
}
},
xaxis: {
categories: [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec'
],
position: 'bottom',
axisBorder: {
show: false
},
axisTicks: {
show: false
},
tooltip: {
enabled: true
}
},
yaxis: {
axisBorder: {
show: false
},
axisTicks: {
show: false
},
labels: {
show: false,
formatter: function (val) {
return val + '%';
}
}
}
};
const renderGraphs = () => { const renderGraphs = () => {
return ( return (
<Container maxWidth="xl"> <Container maxWidth="xl">
@ -475,26 +410,43 @@ function Overview(props: OverviewProps) {
></Box> ></Box>
</Box> </Box>
<ReactApexChart {dailyData && (
options={state} <ReactApexChart
series={series} options={{
type="bar" ...getChartOptions(chartOverview.soc, 'daily'),
height={350} chart: {
/> events: {
beforeZoom: handleBeforeZoom
}
}
}}
series={chartData.soc}
type="area"
height={350}
/>
)}
{/*<ReactApexChart*/} {weeklyData && (
{/* options={{*/} <ReactApexChart
{/* ...getChartOptions(chartOverview.soc),*/} options={{
{/* chart: {*/} ...getChartOptions(chartOverview.soc, 'weekly')
{/* events: {*/} }}
{/* beforeZoom: handleBeforeZoom*/} series={series}
{/* }*/} type="bar"
{/* }*/} height={350}
{/* }}*/} />
{/* series={chartData.soc}*/} )}
{/* type="area"*/}
{/* height={350}*/} {monthlyData && (
{/*/>*/} <ReactApexChart
options={{
...getChartOptions(chartOverview.soc, 'monthly')
}}
series={series}
type="bar"
height={350}
/>
)}
</Card> </Card>
</Grid> </Grid>
<Grid item md={6} xs={12}> <Grid item md={6} xs={12}>
@ -532,7 +484,7 @@ function Overview(props: OverviewProps) {
<div onDoubleClick={handleDoubleClick}> <div onDoubleClick={handleDoubleClick}>
<ReactApexChart <ReactApexChart
options={{ options={{
...getChartOptions(chartOverview.temperature), ...getChartOptions(chartOverview.temperature, 'daily'),
chart: { chart: {
events: { events: {
beforeZoom: handleBeforeZoom beforeZoom: handleBeforeZoom
@ -590,7 +542,7 @@ function Overview(props: OverviewProps) {
<div onDoubleClick={handleDoubleClick}> <div onDoubleClick={handleDoubleClick}>
<ReactApexChart <ReactApexChart
options={{ options={{
...getChartOptions(chartOverview.pvProduction), ...getChartOptions(chartOverview.pvProduction, 'daily'),
chart: { chart: {
events: { events: {
beforeZoom: handleBeforeZoom beforeZoom: handleBeforeZoom
@ -639,7 +591,7 @@ function Overview(props: OverviewProps) {
<div onDoubleClick={handleDoubleClick}> <div onDoubleClick={handleDoubleClick}>
<ReactApexChart <ReactApexChart
options={{ options={{
...getChartOptions(chartOverview.gridPower), ...getChartOptions(chartOverview.gridPower, 'daily'),
chart: { chart: {
events: { events: {
beforeZoom: handleBeforeZoom beforeZoom: handleBeforeZoom
@ -696,7 +648,7 @@ function Overview(props: OverviewProps) {
<div onDoubleClick={handleDoubleClick}> <div onDoubleClick={handleDoubleClick}>
<ReactApexChart <ReactApexChart
options={{ options={{
...getChartOptions(chartOverview.dcPower), ...getChartOptions(chartOverview.dcPower, 'daily'),
chart: { chart: {
events: { events: {
beforeZoom: handleBeforeZoom beforeZoom: handleBeforeZoom
@ -745,7 +697,7 @@ function Overview(props: OverviewProps) {
<div onDoubleClick={handleDoubleClick}> <div onDoubleClick={handleDoubleClick}>
<ReactApexChart <ReactApexChart
options={{ options={{
...getChartOptions(chartOverview.dcBusVoltage), ...getChartOptions(chartOverview.dcBusVoltage, 'daily'),
chart: { chart: {
events: { events: {
beforeZoom: handleBeforeZoom beforeZoom: handleBeforeZoom