Merge branch 'main' of 91.92.155.224:Innovenergy/Innovenergy_trunk
This commit is contained in:
commit
6d90b65bf6
|
@ -0,0 +1,25 @@
|
||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.5.002.0
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Backend", "Backend.csproj", "{161624D7-33B9-48B8-BA05-303DCFAEB03E}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{161624D7-33B9-48B8-BA05-303DCFAEB03E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{161624D7-33B9-48B8-BA05-303DCFAEB03E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{161624D7-33B9-48B8-BA05-303DCFAEB03E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{161624D7-33B9-48B8-BA05-303DCFAEB03E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
|
SolutionGuid = {A377D3C5-E56D-4A16-AC4B-F3B0BB4CFCCE}
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
|
@ -100,6 +100,24 @@ public class Controller : ControllerBase
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet(nameof(GetHistoryForInstallation))]
|
||||||
|
public ActionResult<IEnumerable<UserAction>> GetHistoryForInstallation(Int64 id, Token authToken)
|
||||||
|
{
|
||||||
|
var user = Db.GetSession(authToken)?.User;
|
||||||
|
if (user == null)
|
||||||
|
return Unauthorized();
|
||||||
|
|
||||||
|
var installation = Db.GetInstallationById(id);
|
||||||
|
|
||||||
|
if (installation is null || !user.HasAccessTo(installation))
|
||||||
|
return Unauthorized();
|
||||||
|
|
||||||
|
return Db.UserActions
|
||||||
|
.Where(action =>action.InstallationId == id)
|
||||||
|
.OrderByDescending(action => action.Timestamp)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet(nameof(GetAllWarningsForInstallation))]
|
[HttpGet(nameof(GetAllWarningsForInstallation))]
|
||||||
public ActionResult<IEnumerable<Warning>> GetAllWarningsForInstallation(Int64 id, Token authToken)
|
public ActionResult<IEnumerable<Warning>> GetAllWarningsForInstallation(Int64 id, Token authToken)
|
||||||
{
|
{
|
||||||
|
@ -553,11 +571,18 @@ public class Controller : ControllerBase
|
||||||
var session = Db.GetSession(authToken);
|
var session = Db.GetSession(authToken);
|
||||||
//Console.WriteLine(config.GridSetPoint);
|
//Console.WriteLine(config.GridSetPoint);
|
||||||
|
|
||||||
//var installationToUpdate = Db.GetInstallationById(installationId);
|
// Send configuration changes
|
||||||
|
var success = await session.SendInstallationConfig(installationId, config);
|
||||||
|
|
||||||
|
// Record configuration change
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
var actionSuccess = await session.RecordUserAction(installationId, config);
|
||||||
|
return actionSuccess?Ok():Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Unauthorized();
|
||||||
|
|
||||||
return await session.SendInstallationConfig(installationId, config)
|
|
||||||
? Ok()
|
|
||||||
: Unauthorized();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut(nameof(MoveFolder))]
|
[HttpPut(nameof(MoveFolder))]
|
||||||
|
|
|
@ -6,6 +6,10 @@ public class Configuration
|
||||||
public Double GridSetPoint { get; set; }
|
public Double GridSetPoint { get; set; }
|
||||||
public CalibrationChargeType CalibrationChargeState { get; set; }
|
public CalibrationChargeType CalibrationChargeState { get; set; }
|
||||||
public DateTime CalibrationChargeDate { get; set; }
|
public DateTime CalibrationChargeDate { get; set; }
|
||||||
|
public String GetConfigurationString()
|
||||||
|
{
|
||||||
|
return $"MinimumSoC: {MinimumSoC}, GridSetPoint: {GridSetPoint}, CalibrationChargeState: {CalibrationChargeState}, CalibrationChargeDate: {CalibrationChargeDate}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum CalibrationChargeType
|
public enum CalibrationChargeType
|
||||||
|
|
|
@ -75,14 +75,14 @@ public static class SessionMethods
|
||||||
|
|
||||||
await Task.Run(() =>
|
await Task.Run(() =>
|
||||||
{
|
{
|
||||||
Process process = new Process();
|
var process = new Process();
|
||||||
process.StartInfo.FileName = "/bin/bash";
|
process.StartInfo.FileName = "/bin/bash";
|
||||||
process.StartInfo.Arguments = $"{scriptPath} {vpnIp} {batteryNode} {version}";
|
process.StartInfo.Arguments = $"{scriptPath} {vpnIp} {batteryNode} {version}";
|
||||||
process.StartInfo.UseShellExecute = false;
|
process.StartInfo.UseShellExecute = false;
|
||||||
process.StartInfo.RedirectStandardOutput = true;
|
process.StartInfo.RedirectStandardOutput = true;
|
||||||
|
|
||||||
process.Start();
|
process.Start();
|
||||||
string output = process.StandardOutput.ReadToEnd();
|
var output = process.StandardOutput.ReadToEnd();
|
||||||
process.WaitForExit();
|
process.WaitForExit();
|
||||||
Console.WriteLine(output);
|
Console.WriteLine(output);
|
||||||
});
|
});
|
||||||
|
@ -102,6 +102,28 @@ public static class SessionMethods
|
||||||
&& await installation.SendConfig(configuration);
|
&& await installation.SendConfig(configuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async Task<Boolean> RecordUserAction(this Session? session, Int64 installationId, Configuration newConfiguration)
|
||||||
|
{
|
||||||
|
var user = session?.User;
|
||||||
|
var timestamp = DateTime.Now;
|
||||||
|
|
||||||
|
if (user is null || user.UserType == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Create a new UserAction object
|
||||||
|
var action = new UserAction
|
||||||
|
{
|
||||||
|
UserName = user.Name,
|
||||||
|
InstallationId = installationId,
|
||||||
|
Timestamp = timestamp,
|
||||||
|
Description = newConfiguration.GetConfigurationString()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save the configuration change to the database
|
||||||
|
Db.HandleAction(action);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public static Boolean Delete(this Session? session, Folder? folder)
|
public static Boolean Delete(this Session? session, Folder? folder)
|
||||||
{
|
{
|
||||||
var user = session?.User;
|
var user = session?.User;
|
||||||
|
@ -121,7 +143,7 @@ public static class SessionMethods
|
||||||
|
|
||||||
|
|
||||||
//Salimax installation
|
//Salimax installation
|
||||||
if (installation.Product==0)
|
if (installation.Product == 0)
|
||||||
{
|
{
|
||||||
return user is not null
|
return user is not null
|
||||||
&& user.UserType != 0
|
&& user.UserType != 0
|
||||||
|
@ -134,7 +156,7 @@ public static class SessionMethods
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (installation.Product==1)
|
if (installation.Product == 1)
|
||||||
{
|
{
|
||||||
return user is not null
|
return user is not null
|
||||||
&& user.UserType != 0
|
&& user.UserType != 0
|
||||||
|
@ -153,7 +175,7 @@ public static class SessionMethods
|
||||||
|
|
||||||
var original = Db.GetInstallationById(installation?.Id);
|
var original = Db.GetInstallationById(installation?.Id);
|
||||||
//Salimax installation
|
//Salimax installation
|
||||||
if (installation.Product==0)
|
if (installation.Product == 0)
|
||||||
{
|
{
|
||||||
|
|
||||||
return user is not null
|
return user is not null
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
using SQLite;
|
||||||
|
|
||||||
|
namespace InnovEnergy.App.Backend.DataTypes;
|
||||||
|
|
||||||
|
public class UserAction
|
||||||
|
{
|
||||||
|
[PrimaryKey, AutoIncrement]
|
||||||
|
public Int64 Id { get; set; } // Primary key for the table, auto-incremented
|
||||||
|
|
||||||
|
[Indexed]
|
||||||
|
public String UserName { get; set; } = null!;// User Name who made the configuration change
|
||||||
|
|
||||||
|
public Int64 InstallationId { get; set; } // Installation ID where the configuration change is made
|
||||||
|
|
||||||
|
public DateTime Timestamp { get; set; } // Timestamp of the configuration change
|
||||||
|
|
||||||
|
public String Description { get; set; } = null!;// Serialized string representing the new configuration
|
||||||
|
}
|
|
@ -62,6 +62,37 @@ public static partial class Db
|
||||||
return Insert(o2i);
|
return Insert(o2i);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Boolean Create(UserAction action)
|
||||||
|
{
|
||||||
|
return Insert(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void HandleAction(UserAction newAction)
|
||||||
|
{
|
||||||
|
//Find the total number of actions for this installation
|
||||||
|
var totalActions = UserActions.Count(action => action.InstallationId == newAction.InstallationId);
|
||||||
|
|
||||||
|
//If there are 100 actions, remove the one with the oldest timestamp
|
||||||
|
if (totalActions == 100)
|
||||||
|
{
|
||||||
|
var oldestAction =
|
||||||
|
UserActions.Where(action => action.InstallationId == newAction.InstallationId)
|
||||||
|
.OrderBy(action => action.Timestamp)
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
//Remove the old action
|
||||||
|
Delete(oldestAction);
|
||||||
|
|
||||||
|
//Add the new action
|
||||||
|
Create(newAction);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine("---------------Added the new Error to the database-----------------");
|
||||||
|
Create(newAction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void HandleError(Error newError,int installationId)
|
public static void HandleError(Error newError,int installationId)
|
||||||
{
|
{
|
||||||
//Find the total number of errors for this installation
|
//Find the total number of errors for this installation
|
||||||
|
|
|
@ -36,6 +36,7 @@ public static partial class Db
|
||||||
fileConnection.CreateTable<OrderNumber2Installation>();
|
fileConnection.CreateTable<OrderNumber2Installation>();
|
||||||
fileConnection.CreateTable<Error>();
|
fileConnection.CreateTable<Error>();
|
||||||
fileConnection.CreateTable<Warning>();
|
fileConnection.CreateTable<Warning>();
|
||||||
|
fileConnection.CreateTable<UserAction>();
|
||||||
|
|
||||||
return fileConnection;
|
return fileConnection;
|
||||||
//return CopyDbToMemory(fileConnection);
|
//return CopyDbToMemory(fileConnection);
|
||||||
|
@ -55,6 +56,7 @@ public static partial class Db
|
||||||
memoryConnection.CreateTable<OrderNumber2Installation>();
|
memoryConnection.CreateTable<OrderNumber2Installation>();
|
||||||
memoryConnection.CreateTable<Error>();
|
memoryConnection.CreateTable<Error>();
|
||||||
memoryConnection.CreateTable<Warning>();
|
memoryConnection.CreateTable<Warning>();
|
||||||
|
fileConnection.CreateTable<UserAction>();
|
||||||
|
|
||||||
//Copy all the existing tables from the disk to main memory
|
//Copy all the existing tables from the disk to main memory
|
||||||
fileConnection.Table<Session>().ForEach(memoryConnection.Insert);
|
fileConnection.Table<Session>().ForEach(memoryConnection.Insert);
|
||||||
|
@ -66,6 +68,7 @@ public static partial class Db
|
||||||
fileConnection.Table<OrderNumber2Installation>().ForEach(memoryConnection.Insert);
|
fileConnection.Table<OrderNumber2Installation>().ForEach(memoryConnection.Insert);
|
||||||
fileConnection.Table<Error>().ForEach(memoryConnection.Insert);
|
fileConnection.Table<Error>().ForEach(memoryConnection.Insert);
|
||||||
fileConnection.Table<Warning>().ForEach(memoryConnection.Insert);
|
fileConnection.Table<Warning>().ForEach(memoryConnection.Insert);
|
||||||
|
fileConnection.Table<UserAction>().ForEach(memoryConnection.Insert);
|
||||||
|
|
||||||
return memoryConnection;
|
return memoryConnection;
|
||||||
}
|
}
|
||||||
|
@ -85,6 +88,7 @@ public static partial class Db
|
||||||
public static TableQuery<OrderNumber2Installation> OrderNumber2Installation => Connection.Table<OrderNumber2Installation>();
|
public static TableQuery<OrderNumber2Installation> OrderNumber2Installation => Connection.Table<OrderNumber2Installation>();
|
||||||
public static TableQuery<Error> Errors => Connection.Table<Error>();
|
public static TableQuery<Error> Errors => Connection.Table<Error>();
|
||||||
public static TableQuery<Warning> Warnings => Connection.Table<Warning>();
|
public static TableQuery<Warning> Warnings => Connection.Table<Warning>();
|
||||||
|
public static TableQuery<UserAction> UserActions => Connection.Table<UserAction>();
|
||||||
|
|
||||||
public static void Init()
|
public static void Init()
|
||||||
{
|
{
|
||||||
|
@ -106,6 +110,7 @@ public static partial class Db
|
||||||
Connection.CreateTable<OrderNumber2Installation>();
|
Connection.CreateTable<OrderNumber2Installation>();
|
||||||
Connection.CreateTable<Error>();
|
Connection.CreateTable<Error>();
|
||||||
Connection.CreateTable<Warning>();
|
Connection.CreateTable<Warning>();
|
||||||
|
Connection.CreateTable<UserAction>();
|
||||||
});
|
});
|
||||||
|
|
||||||
//UpdateKeys();
|
//UpdateKeys();
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
using InnovEnergy.App.Backend.DataTypes;
|
using InnovEnergy.App.Backend.DataTypes;
|
||||||
using InnovEnergy.App.Backend.DataTypes.Methods;
|
using InnovEnergy.App.Backend.DataTypes.Methods;
|
||||||
using InnovEnergy.App.Backend.Relations;
|
using InnovEnergy.App.Backend.Relations;
|
||||||
|
using Microsoft.AspNetCore.Authentication.OAuth.Claims;
|
||||||
|
|
||||||
|
|
||||||
namespace InnovEnergy.App.Backend.Database;
|
namespace InnovEnergy.App.Backend.Database;
|
||||||
|
@ -49,6 +50,20 @@ public static partial class Db
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Boolean Delete(UserAction actionToDelete)
|
||||||
|
{
|
||||||
|
var deleteSuccess = RunTransaction(DeleteAction);
|
||||||
|
if (deleteSuccess)
|
||||||
|
BackupDatabase();
|
||||||
|
return deleteSuccess;
|
||||||
|
|
||||||
|
|
||||||
|
Boolean DeleteAction()
|
||||||
|
{
|
||||||
|
return UserActions.Delete(action => action.Id == actionToDelete.Id) >0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static Boolean Delete(Warning warningToDelete)
|
public static Boolean Delete(Warning warningToDelete)
|
||||||
{
|
{
|
||||||
var deleteSuccess = RunTransaction(DeleteWarning);
|
var deleteSuccess = RunTransaction(DeleteWarning);
|
||||||
|
|
|
@ -227,8 +227,8 @@ public static class Aggregator
|
||||||
Console.WriteLine($"Max SOC: {aggregatedData.MaxSoc}");
|
Console.WriteLine($"Max SOC: {aggregatedData.MaxSoc}");
|
||||||
Console.WriteLine($"Min SOC: {aggregatedData.MinSoc}");
|
Console.WriteLine($"Min SOC: {aggregatedData.MinSoc}");
|
||||||
|
|
||||||
Console.WriteLine($"ChargingBatteryPower: {aggregatedData.DischargingBatteryPower}");
|
Console.WriteLine($"DischargingBatteryBattery: {aggregatedData.DischargingBatteryPower}");
|
||||||
Console.WriteLine($"DischargingBatteryBattery: {aggregatedData.ChargingBatteryPower}");
|
Console.WriteLine($"ChargingBatteryPower: {aggregatedData.ChargingBatteryPower}");
|
||||||
|
|
||||||
Console.WriteLine($"SumGridExportPower: {aggregatedData.GridExportPower}");
|
Console.WriteLine($"SumGridExportPower: {aggregatedData.GridExportPower}");
|
||||||
Console.WriteLine($"SumGridImportPower: {aggregatedData.GridImportPower}");
|
Console.WriteLine($"SumGridImportPower: {aggregatedData.GridImportPower}");
|
||||||
|
|
|
@ -679,34 +679,35 @@ internal static class Program
|
||||||
// This is temporary for Wittman, but now it's for all Instalattion
|
// This is temporary for Wittman, but now it's for all Instalattion
|
||||||
await File.WriteAllTextAsync("/var/www/html/status.csv", csv.SplitLines().Where(l => !l.Contains("Secret")).JoinLines());
|
await File.WriteAllTextAsync("/var/www/html/status.csv", csv.SplitLines().Where(l => !l.Contains("Secret")).JoinLines());
|
||||||
|
|
||||||
|
var response = await request.PutAsync(new StringContent(csv));
|
||||||
|
|
||||||
// Compress CSV data to a byte array
|
// Compress CSV data to a byte array
|
||||||
byte[] compressedBytes;
|
// byte[] compressedBytes;
|
||||||
using (var memoryStream = new MemoryStream())
|
// using (var memoryStream = new MemoryStream())
|
||||||
{
|
// {
|
||||||
//Create a zip directory and put the compressed file inside
|
// //Create a zip directory and put the compressed file inside
|
||||||
using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true))
|
// using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true))
|
||||||
{
|
// {
|
||||||
var entry = archive.CreateEntry("data.csv", CompressionLevel.SmallestSize); // Add CSV data to the ZIP archive
|
// var entry = archive.CreateEntry("data.csv", CompressionLevel.SmallestSize); // Add CSV data to the ZIP archive
|
||||||
using (var entryStream = entry.Open())
|
// using (var entryStream = entry.Open())
|
||||||
using (var writer = new StreamWriter(entryStream))
|
// using (var writer = new StreamWriter(entryStream))
|
||||||
{
|
// {
|
||||||
writer.Write(csv);
|
// writer.Write(csv);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
compressedBytes = memoryStream.ToArray();
|
// compressedBytes = memoryStream.ToArray();
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// Encode the compressed byte array as a Base64 string
|
// // Encode the compressed byte array as a Base64 string
|
||||||
string base64String = Convert.ToBase64String(compressedBytes);
|
// string base64String = Convert.ToBase64String(compressedBytes);
|
||||||
|
//
|
||||||
// Create StringContent from Base64 string
|
// // Create StringContent from Base64 string
|
||||||
var stringContent = new StringContent(base64String, Encoding.UTF8, "application/base64");
|
// var stringContent = new StringContent(base64String, Encoding.UTF8, "application/base64");
|
||||||
|
//
|
||||||
// Upload the compressed data (ZIP archive) to S3
|
// // Upload the compressed data (ZIP archive) to S3
|
||||||
var response = await request.PutAsync(stringContent);
|
// var response = await request.PutAsync(stringContent);
|
||||||
|
//
|
||||||
if (response.StatusCode != 200)
|
if (response.StatusCode != 200)
|
||||||
{
|
{
|
||||||
Console.WriteLine("ERROR: PUT");
|
Console.WriteLine("ERROR: PUT");
|
||||||
|
|
|
@ -70,7 +70,8 @@ public record S3Config
|
||||||
// CanonicalizedResource;
|
// CanonicalizedResource;
|
||||||
|
|
||||||
|
|
||||||
contentType = "application/base64; charset=utf-8";
|
//contentType = "application/base64; charset=utf-8";
|
||||||
|
contentType = "text/plain; charset=utf-8";
|
||||||
|
|
||||||
var payload = $"{method}\n{md5Hash}\n{contentType}\n{date}\n/{bucket.Trim('/')}/{s3Path.Trim('/')}";
|
var payload = $"{method}\n{md5Hash}\n{contentType}\n{date}\n/{bucket.Trim('/')}/{s3Path.Trim('/')}";
|
||||||
using var hmacSha1 = new HMACSHA1(UTF8.GetBytes(s3Secret));
|
using var hmacSha1 = new HMACSHA1(UTF8.GetBytes(s3Secret));
|
||||||
|
|
|
@ -77,12 +77,13 @@ public class AmptDevices
|
||||||
Current = busCurrent
|
Current = busCurrent
|
||||||
};
|
};
|
||||||
|
|
||||||
// flatten the 2 strings of each SO into one array
|
// flatten the output strings of each SO into one array
|
||||||
var strings = soStati.SelectMany(GetStrings).ToArray(nStrings);
|
var strings = soStati.SelectMany(GetDc).ToArray(nStrings);
|
||||||
|
|
||||||
return new AmptStatus
|
return new AmptStatus
|
||||||
{
|
{
|
||||||
Dc = dc,
|
Dc = dc,
|
||||||
|
NbrOfStrings = nStringOptimizers,
|
||||||
Strings = strings,
|
Strings = strings,
|
||||||
DcWh = dailyOutputEnergy
|
DcWh = dailyOutputEnergy
|
||||||
};
|
};
|
||||||
|
@ -106,6 +107,16 @@ public class AmptDevices
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<DcBus> GetDc(StringOptimizerRegisters r)
|
||||||
|
{
|
||||||
|
// hardcoded: every SO has 2 strings (produced like this by AMPT)
|
||||||
|
|
||||||
|
yield return new()
|
||||||
|
{
|
||||||
|
Voltage = r.Voltage,
|
||||||
|
Current = r.Current,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private static IEnumerable<ModbusDevice<StringOptimizerRegisters>> StringOptimizers(ModbusClient modbusClient)
|
private static IEnumerable<ModbusDevice<StringOptimizerRegisters>> StringOptimizers(ModbusClient modbusClient)
|
||||||
{
|
{
|
||||||
|
|
|
@ -6,6 +6,7 @@ namespace InnovEnergy.Lib.Devices.AMPT;
|
||||||
public class AmptStatus : IMppt
|
public class AmptStatus : IMppt
|
||||||
{
|
{
|
||||||
public required DcBus Dc { get; init; }
|
public required DcBus Dc { get; init; }
|
||||||
|
public required UInt16 NbrOfStrings { get; init; }
|
||||||
public required IReadOnlyList<DcBus> Strings { get; init; }
|
public required IReadOnlyList<DcBus> Strings { get; init; }
|
||||||
public required Double DcWh { get; init; } //Daily integrated string output energy in Wh
|
public required Double DcWh { get; init; } //Daily integrated string output energy in Wh
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/*using InnovEnergy.Lib.Protocols.Modbus.Channels;
|
using InnovEnergy.Lib.Protocols.Modbus.Channels;
|
||||||
using InnovEnergy.Lib.Protocols.Modbus.Clients;
|
using InnovEnergy.Lib.Protocols.Modbus.Clients;
|
||||||
using InnovEnergy.Lib.Protocols.Modbus.Slaves;
|
using InnovEnergy.Lib.Protocols.Modbus.Slaves;
|
||||||
using InnovEnergy.Lib.Utils;
|
using InnovEnergy.Lib.Utils;
|
||||||
|
@ -49,83 +49,4 @@ public class Iem3KGridMeterDevice: ModbusDevice<Iem3KGridMeterRegisters>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}*/
|
|
||||||
|
|
||||||
using InnovEnergy.Lib.Protocols.Modbus.Channels;
|
|
||||||
using InnovEnergy.Lib.Protocols.Modbus.Clients;
|
|
||||||
using InnovEnergy.Lib.Protocols.Modbus.Slaves;
|
|
||||||
using InnovEnergy.Lib.Utils;
|
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace InnovEnergy.Lib.Devices.IEM3kGridMeter
|
|
||||||
{
|
|
||||||
public class Iem3KGridMeterDevice : ModbusDevice<Iem3KGridMeterRegisters>
|
|
||||||
{
|
|
||||||
private readonly string _hostname;
|
|
||||||
private readonly ushort _port;
|
|
||||||
private readonly byte _slaveId;
|
|
||||||
|
|
||||||
public Iem3KGridMeterDevice(string hostname, ushort port = 502, byte slaveId = 1)
|
|
||||||
: this(new TcpChannel(hostname, port), slaveId)
|
|
||||||
{
|
|
||||||
_hostname = hostname ?? throw new ArgumentNullException(nameof(hostname));
|
|
||||||
_port = port;
|
|
||||||
_slaveId = slaveId;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Iem3KGridMeterDevice(TcpChannel channel, byte slaveId = 1)
|
|
||||||
: base(new ModbusTcpClient(channel, slaveId))
|
|
||||||
{
|
|
||||||
_hostname = channel.Host;
|
|
||||||
_port = channel.Port;
|
|
||||||
_slaveId = slaveId;
|
|
||||||
Console.WriteLine($"Initializing Iem3KGridMeterDevice with channel: {channel.Host}:{channel.Port}");
|
|
||||||
}
|
|
||||||
|
|
||||||
public Iem3KGridMeterDevice(ModbusClient client)
|
|
||||||
: base(client)
|
|
||||||
{
|
|
||||||
if (client is ModbusTcpClient tcpClient)
|
|
||||||
{
|
|
||||||
_hostname = tcpClient.Channel.Host;
|
|
||||||
_port = tcpClient.Channel.Port;
|
|
||||||
_slaveId = tcpClient.SlaveId;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Invalid client type", nameof(client));
|
|
||||||
}
|
|
||||||
Console.WriteLine("Initializing Iem3KGridMeterDevice with ModbusClient");
|
|
||||||
}
|
|
||||||
|
|
||||||
public new Iem3KGridMeterRegisters? Read()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Attempting to read data from {_hostname}:{_port} with slaveId {_slaveId}");
|
|
||||||
return base.Read();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Failed to read data from {nameof(Iem3KGridMeterDevice)}: {ex.Message}");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public new void Write(Iem3KGridMeterRegisters registers)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
base.Write(registers);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Failed to write data to {nameof(Iem3KGridMeterDevice)}: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ using System.Net.Sockets;
|
||||||
using InnovEnergy.Lib.Protocols.Modbus.Protocol;
|
using InnovEnergy.Lib.Protocols.Modbus.Protocol;
|
||||||
using InnovEnergy.Lib.Utils.Net;
|
using InnovEnergy.Lib.Utils.Net;
|
||||||
|
|
||||||
/*namespace InnovEnergy.Lib.Protocols.Modbus.Channels;
|
namespace InnovEnergy.Lib.Protocols.Modbus.Channels;
|
||||||
|
|
||||||
public class TcpChannel : ConnectionChannel<TcpClient>
|
public class TcpChannel : ConnectionChannel<TcpClient>
|
||||||
{
|
{
|
||||||
|
@ -82,104 +82,4 @@ public class TcpChannel : ConnectionChannel<TcpClient>
|
||||||
var array = data.ToArray();
|
var array = data.ToArray();
|
||||||
tcpClient.GetStream().Write(array, 0, array.Length);
|
tcpClient.GetStream().Write(array, 0, array.Length);
|
||||||
}
|
}
|
||||||
}*/
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Net.Sockets;
|
|
||||||
|
|
||||||
namespace InnovEnergy.Lib.Protocols.Modbus.Channels
|
|
||||||
{
|
|
||||||
public class TcpChannel : Channel, IDisposable
|
|
||||||
{
|
|
||||||
public string Host { get; }
|
|
||||||
public ushort Port { get; }
|
|
||||||
|
|
||||||
private const int TimeoutMs = 500; // TODO: parametrize
|
|
||||||
private Socket? Socket { get; set; }
|
|
||||||
private byte[] Buffer { get; }
|
|
||||||
|
|
||||||
public TcpChannel(string hostname, ushort port)
|
|
||||||
{
|
|
||||||
Host = hostname ?? throw new ArgumentNullException(nameof(hostname));
|
|
||||||
Port = port;
|
|
||||||
Buffer = new byte[8192]; // Buffer size can be adjusted
|
|
||||||
}
|
|
||||||
|
|
||||||
public override IReadOnlyList<byte> Read(int nBytes)
|
|
||||||
{
|
|
||||||
if (Socket == null)
|
|
||||||
throw new InvalidOperationException("Socket is not connected.");
|
|
||||||
|
|
||||||
var buffer = new byte[nBytes];
|
|
||||||
int bytesRead = 0;
|
|
||||||
|
|
||||||
while (bytesRead < nBytes)
|
|
||||||
{
|
|
||||||
var read = Socket.Receive(buffer, bytesRead, nBytes - bytesRead, SocketFlags.None);
|
|
||||||
if (read == 0)
|
|
||||||
throw new Exception("Socket closed.");
|
|
||||||
|
|
||||||
bytesRead += read;
|
|
||||||
}
|
|
||||||
|
|
||||||
return buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Write(IReadOnlyList<byte> bytes)
|
|
||||||
{
|
|
||||||
if (Socket == null)
|
|
||||||
throw new InvalidOperationException("Socket is not connected.");
|
|
||||||
|
|
||||||
Socket.Send(bytes.ToArray(), SocketFlags.None);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Connect()
|
|
||||||
{
|
|
||||||
if (Socket != null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
Socket = new Socket(SocketType.Stream, ProtocolType.Tcp)
|
|
||||||
{
|
|
||||||
Blocking = true,
|
|
||||||
NoDelay = true,
|
|
||||||
LingerState = new LingerOption(false, 0),
|
|
||||||
ReceiveTimeout = TimeoutMs,
|
|
||||||
SendTimeout = TimeoutMs
|
|
||||||
};
|
|
||||||
|
|
||||||
var cts = new CancellationTokenSource();
|
|
||||||
cts.CancelAfter(TimeoutMs);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Socket.ConnectAsync(Host, Port).Wait(TimeoutMs);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
Socket = null;
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Disconnect()
|
|
||||||
{
|
|
||||||
if (Socket == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Socket.Close();
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
Socket = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Disconnect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ namespace InnovEnergy.Lib.Protocols.Modbus.Clients;
|
||||||
using UInt16s = IReadOnlyCollection<UInt16>;
|
using UInt16s = IReadOnlyCollection<UInt16>;
|
||||||
using Booleans = IReadOnlyCollection<Boolean>;
|
using Booleans = IReadOnlyCollection<Boolean>;
|
||||||
|
|
||||||
/*public class ModbusTcpClient : ModbusClient
|
public class ModbusTcpClient : ModbusClient
|
||||||
{
|
{
|
||||||
public const UInt16 DefaultPort = 502;
|
public const UInt16 DefaultPort = 502;
|
||||||
private UInt16 _Id;
|
private UInt16 _Id;
|
||||||
|
@ -184,171 +184,4 @@ using Booleans = IReadOnlyCollection<Boolean>;
|
||||||
return new MbData(rxFrm.RegistersRead.RawData, readAddress, Endian);
|
return new MbData(rxFrm.RegistersRead.RawData, readAddress, Endian);
|
||||||
}
|
}
|
||||||
|
|
||||||
}*/
|
|
||||||
|
|
||||||
public class ModbusTcpClient : ModbusClient
|
|
||||||
{
|
|
||||||
public const ushort DefaultPort = 502;
|
|
||||||
private ushort _Id;
|
|
||||||
public TcpChannel Channel { get; }
|
|
||||||
|
|
||||||
public ModbusTcpClient(TcpChannel channel, byte slaveId) : base(channel, slaveId)
|
|
||||||
{
|
|
||||||
Channel = channel;
|
|
||||||
Channel.Connect();
|
|
||||||
}
|
|
||||||
|
|
||||||
private ushort NextId() => unchecked(++_Id);
|
|
||||||
|
|
||||||
public override MbData ReadCoils(ushort readAddress, ushort nValues)
|
|
||||||
{
|
|
||||||
var id = NextId(); // TODO: check response id
|
|
||||||
|
|
||||||
var cmd = new ReadCoilsCommandFrame(SlaveId, readAddress, nValues);
|
|
||||||
var hdr = new MbapHeader(id, cmd.Data.Count);
|
|
||||||
var frm = new ModbusTcpFrame(hdr, cmd);
|
|
||||||
|
|
||||||
Channel.Write(frm.Data);
|
|
||||||
|
|
||||||
var hData = Channel.Read(MbapHeader.Size).ToArray();
|
|
||||||
var rxHdr = new MbapHeader(hData);
|
|
||||||
|
|
||||||
var rxFrm = Channel
|
|
||||||
.Read(rxHdr.FrameLength)
|
|
||||||
.ToArray()
|
|
||||||
.Apply(ReadCoilsResponseFrame.Parse)
|
|
||||||
.Apply(cmd.VerifyResponse);
|
|
||||||
|
|
||||||
return new MbData(rxFrm.Coils.RawData, readAddress, Endian);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override MbData ReadDiscreteInputs(ushort readAddress, ushort nValues)
|
|
||||||
{
|
|
||||||
var id = NextId(); // TODO: check response id
|
|
||||||
|
|
||||||
var cmd = new ReadDiscreteInputsCommandFrame(SlaveId, readAddress, nValues);
|
|
||||||
var hdr = new MbapHeader(id, cmd.Data.Count);
|
|
||||||
var frm = new ModbusTcpFrame(hdr, cmd);
|
|
||||||
|
|
||||||
Channel.Write(frm.Data);
|
|
||||||
|
|
||||||
var hData = Channel.Read(MbapHeader.Size).ToArray();
|
|
||||||
var rxHdr = new MbapHeader(hData);
|
|
||||||
|
|
||||||
var rxFrm = Channel
|
|
||||||
.Read(rxHdr.FrameLength)
|
|
||||||
.ToArray()
|
|
||||||
.Apply(ReadDiscreteInputsResponseFrame.Parse)
|
|
||||||
.Apply(cmd.VerifyResponse);
|
|
||||||
|
|
||||||
return new MbData(rxFrm.Inputs.RawData, readAddress, Endian);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override MbData ReadInputRegisters(ushort readAddress, ushort nValues)
|
|
||||||
{
|
|
||||||
var id = NextId(); // TODO: check response id
|
|
||||||
|
|
||||||
var cmd = new ReadInputRegistersCommandFrame(SlaveId, readAddress, nValues);
|
|
||||||
var hdr = new MbapHeader(id, cmd.Data.Count);
|
|
||||||
var frm = new ModbusTcpFrame(hdr, cmd);
|
|
||||||
|
|
||||||
Channel.Write(frm.Data);
|
|
||||||
|
|
||||||
var hData = Channel.Read(MbapHeader.Size).ToArray();
|
|
||||||
var rxHdr = new MbapHeader(hData);
|
|
||||||
|
|
||||||
var rxFrm = Channel
|
|
||||||
.Read(rxHdr.FrameLength)
|
|
||||||
.ToArray()
|
|
||||||
.Apply(ReadInputRegistersResponseFrame.Parse)
|
|
||||||
.Apply(cmd.VerifyResponse);
|
|
||||||
|
|
||||||
return new MbData(rxFrm.RegistersRead.RawData, readAddress, Endian);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override MbData ReadHoldingRegisters(ushort readAddress, ushort nValues)
|
|
||||||
{
|
|
||||||
var id = NextId(); // TODO: check response id
|
|
||||||
|
|
||||||
var cmd = new ReadHoldingRegistersCommandFrame(SlaveId, readAddress, nValues);
|
|
||||||
var hdr = new MbapHeader(id, cmd.Data.Count);
|
|
||||||
var frm = new ModbusTcpFrame(hdr, cmd);
|
|
||||||
|
|
||||||
Channel.Write(frm.Data);
|
|
||||||
|
|
||||||
var hData = Channel.Read(MbapHeader.Size).ToArray();
|
|
||||||
var rxHdr = new MbapHeader(hData);
|
|
||||||
|
|
||||||
var rxFrm = Channel
|
|
||||||
.Read(rxHdr.FrameLength)
|
|
||||||
.ToArray()
|
|
||||||
.Apply(ReadHoldingRegistersResponseFrame.Parse)
|
|
||||||
.Apply(cmd.VerifyResponse);
|
|
||||||
|
|
||||||
return new MbData(rxFrm.RegistersRead.RawData, readAddress, Endian);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override ushort WriteCoils(ushort writeAddress, Booleans coils)
|
|
||||||
{
|
|
||||||
var id = NextId(); // TODO: check response id
|
|
||||||
var cmd = new WriteCoilsCommandFrame(SlaveId, writeAddress, coils);
|
|
||||||
var hdr = new MbapHeader(id, cmd.Data.Count);
|
|
||||||
var frm = new ModbusTcpFrame(hdr, cmd);
|
|
||||||
|
|
||||||
Channel.Write(frm.Data);
|
|
||||||
|
|
||||||
var hData = Channel.Read(MbapHeader.Size).ToArray();
|
|
||||||
var rxHdr = new MbapHeader(hData);
|
|
||||||
|
|
||||||
var rxFrm = Channel
|
|
||||||
.Read(rxHdr.FrameLength)
|
|
||||||
.ToArray()
|
|
||||||
.Apply(WriteCoilsResponseFrame.Parse)
|
|
||||||
.Apply(cmd.VerifyResponse);
|
|
||||||
|
|
||||||
return rxFrm.NbWritten;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override ushort WriteRegisters(ushort writeAddress, UInt16s values)
|
|
||||||
{
|
|
||||||
var id = NextId(); // TODO: check response id
|
|
||||||
var cmd = new WriteRegistersCommandFrame(SlaveId, writeAddress, values);
|
|
||||||
var hdr = new MbapHeader(id, cmd.Data.Count);
|
|
||||||
var frm = new ModbusTcpFrame(hdr, cmd);
|
|
||||||
|
|
||||||
Channel.Write(frm.Data);
|
|
||||||
|
|
||||||
var hData = Channel.Read(MbapHeader.Size).ToArray();
|
|
||||||
var rxHdr = new MbapHeader(hData);
|
|
||||||
|
|
||||||
var rxFrm = Channel
|
|
||||||
.Read(rxHdr.FrameLength)
|
|
||||||
.ToArray()
|
|
||||||
.Apply(WriteRegistersResponseFrame.Parse)
|
|
||||||
.Apply(cmd.VerifyResponse);
|
|
||||||
|
|
||||||
return rxFrm.NbWritten;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override MbData ReadWriteRegisters(ushort readAddress, ushort nbToRead, ushort writeAddress, UInt16s registersToWrite)
|
|
||||||
{
|
|
||||||
var id = NextId(); // TODO: check response id
|
|
||||||
|
|
||||||
var cmd = new ReadWriteRegistersCommandFrame(SlaveId, readAddress, nbToRead, writeAddress, registersToWrite);
|
|
||||||
|
|
||||||
var hdr = new MbapHeader(id, cmd.Data.Count);
|
|
||||||
var frm = new ModbusTcpFrame(hdr, cmd);
|
|
||||||
|
|
||||||
Channel.Write(frm.Data);
|
|
||||||
|
|
||||||
var hData = Enumerable.ToArray(Channel.Read(MbapHeader.Size));
|
|
||||||
var rxHdr = new MbapHeader(hData);
|
|
||||||
|
|
||||||
var fData = Enumerable.ToArray(Channel.Read(rxHdr.FrameLength));
|
|
||||||
var rxFrm = fData
|
|
||||||
.Apply(ReadWriteRegistersResponseFrame.Parse)
|
|
||||||
.Apply(cmd.VerifyResponse);
|
|
||||||
|
|
||||||
return new MbData(rxFrm.RegistersRead.RawData, readAddress, Endian);
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -11,10 +11,12 @@
|
||||||
"overview": "overview",
|
"overview": "overview",
|
||||||
"manage": "manage",
|
"manage": "manage",
|
||||||
"batteryview": "batteryview",
|
"batteryview": "batteryview",
|
||||||
|
"pvview": "pvview",
|
||||||
"log": "log",
|
"log": "log",
|
||||||
"live": "live",
|
"live": "live",
|
||||||
"information": "information",
|
"information": "information",
|
||||||
"configuration": "configuration",
|
"configuration": "configuration",
|
||||||
|
"history": "history",
|
||||||
"mainstats": "mainstats",
|
"mainstats": "mainstats",
|
||||||
"detailed_view": "detailed_view/"
|
"detailed_view": "detailed_view/"
|
||||||
}
|
}
|
||||||
|
|
|
@ -246,8 +246,8 @@ function MainStats(props: MainStatsProps) {
|
||||||
chartOverview: BatteryOverviewInterface;
|
chartOverview: BatteryOverviewInterface;
|
||||||
}> = transformInputToBatteryViewData(
|
}> = transformInputToBatteryViewData(
|
||||||
props.s3Credentials,
|
props.s3Credentials,
|
||||||
UnixTime.fromTicks(startX),
|
UnixTime.fromTicks(startX).earlier(TimeSpan.fromHours(2)),
|
||||||
UnixTime.fromTicks(endX)
|
UnixTime.fromTicks(endX).earlier(TimeSpan.fromHours(2))
|
||||||
);
|
);
|
||||||
|
|
||||||
resultPromise
|
resultPromise
|
||||||
|
|
|
@ -15,7 +15,7 @@ import {
|
||||||
Typography,
|
Typography,
|
||||||
useTheme
|
useTheme
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import React, { useState } from 'react';
|
import React, { useContext, useState } from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import { Close as CloseIcon } from '@mui/icons-material';
|
import { Close as CloseIcon } from '@mui/icons-material';
|
||||||
|
@ -29,6 +29,7 @@ import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import axiosConfig from '../../../Resources/axiosConfig';
|
import axiosConfig from '../../../Resources/axiosConfig';
|
||||||
import utc from 'dayjs/plugin/utc';
|
import utc from 'dayjs/plugin/utc';
|
||||||
|
import { UserContext } from '../../../contexts/userContext';
|
||||||
|
|
||||||
interface ConfigurationProps {
|
interface ConfigurationProps {
|
||||||
values: TopologyValues;
|
values: TopologyValues;
|
||||||
|
@ -82,6 +83,8 @@ function Configuration(props: ConfigurationProps) {
|
||||||
const [updated, setUpdated] = useState(false);
|
const [updated, setUpdated] = useState(false);
|
||||||
const [dateSelectionError, setDateSelectionError] = useState('');
|
const [dateSelectionError, setDateSelectionError] = useState('');
|
||||||
const [isErrorDateModalOpen, setErrorDateModalOpen] = useState(false);
|
const [isErrorDateModalOpen, setErrorDateModalOpen] = useState(false);
|
||||||
|
const context = useContext(UserContext);
|
||||||
|
const { currentUser, setUser } = context;
|
||||||
|
|
||||||
const [formValues, setFormValues] = useState<ConfigurationValues>({
|
const [formValues, setFormValues] = useState<ConfigurationValues>({
|
||||||
minimumSoC: props.values.minimumSoC[0].value,
|
minimumSoC: props.values.minimumSoC[0].value,
|
||||||
|
@ -132,7 +135,6 @@ function Configuration(props: ConfigurationProps) {
|
||||||
.add(localOffset, 'minute')
|
.add(localOffset, 'minute')
|
||||||
.toDate()
|
.toDate()
|
||||||
};
|
};
|
||||||
|
|
||||||
// console.log('will send ', dayjs(formValues.calibrationChargeDate));
|
// console.log('will send ', dayjs(formValues.calibrationChargeDate));
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
|
@ -0,0 +1,308 @@
|
||||||
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
Card,
|
||||||
|
Container,
|
||||||
|
Divider,
|
||||||
|
Grid,
|
||||||
|
IconButton,
|
||||||
|
useTheme
|
||||||
|
} from '@mui/material';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import axiosConfig from '../../../Resources/axiosConfig';
|
||||||
|
import { AxiosError, AxiosResponse } from 'axios/index';
|
||||||
|
import routes from '../../../Resources/routes.json';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { TokenContext } from '../../../contexts/tokenContext';
|
||||||
|
import { Action } from '../../../interfaces/S3Types';
|
||||||
|
|
||||||
|
interface HistoryProps {
|
||||||
|
errorLoadingS3Data: boolean;
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function HistoryOfActions(props: HistoryProps) {
|
||||||
|
const theme = useTheme();
|
||||||
|
const searchParams = new URLSearchParams(location.search);
|
||||||
|
|
||||||
|
const [history, setHistory] = useState<Action[]>([]);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const tokencontext = useContext(TokenContext);
|
||||||
|
const { removeToken } = tokencontext;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
axiosConfig
|
||||||
|
.get(`/GetHistoryForInstallation?id=${props.id}`)
|
||||||
|
.then((res: AxiosResponse<Action[]>) => {
|
||||||
|
setHistory(res.data);
|
||||||
|
})
|
||||||
|
.catch((err: AxiosError) => {
|
||||||
|
if (err.response && err.response.status == 401) {
|
||||||
|
removeToken();
|
||||||
|
navigate(routes.login);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container maxWidth="xl">
|
||||||
|
<Grid container>
|
||||||
|
<Grid item xs={12} md={12}>
|
||||||
|
{history.length > 0 && (
|
||||||
|
<Card sx={{ marginTop: '10px' }}>
|
||||||
|
<Divider />
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: '40px',
|
||||||
|
marginBottom: '10px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
marginTop: '15px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
color="dimgrey"
|
||||||
|
fontWeight="bold"
|
||||||
|
fontSize="1rem"
|
||||||
|
gutterBottom
|
||||||
|
noWrap
|
||||||
|
>
|
||||||
|
<FormattedMessage id="user" defaultMessage="User" />
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
marginTop: '15px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
color="dimgrey"
|
||||||
|
fontWeight="bold"
|
||||||
|
fontSize="1rem"
|
||||||
|
gutterBottom
|
||||||
|
noWrap
|
||||||
|
>
|
||||||
|
<FormattedMessage id="date" defaultMessage="Date" />
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
marginTop: '15px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
color="dimgrey"
|
||||||
|
fontWeight="bold"
|
||||||
|
fontSize="1rem"
|
||||||
|
gutterBottom
|
||||||
|
noWrap
|
||||||
|
>
|
||||||
|
<FormattedMessage id="time" defaultMessage="Time" />
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
marginTop: '15px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
color="dimgrey"
|
||||||
|
fontWeight="bold"
|
||||||
|
fontSize="1rem"
|
||||||
|
gutterBottom
|
||||||
|
noWrap
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id="description"
|
||||||
|
defaultMessage="Description"
|
||||||
|
/>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Divider />
|
||||||
|
<div style={{ maxHeight: '400px', overflowY: 'auto' }}>
|
||||||
|
{history.map((action, index) => {
|
||||||
|
// 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();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Divider />
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
style={{
|
||||||
|
height: '40px',
|
||||||
|
marginBottom: '10px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
marginTop: '15px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
fontWeight="bold"
|
||||||
|
color="text.primary"
|
||||||
|
gutterBottom
|
||||||
|
noWrap
|
||||||
|
>
|
||||||
|
{action.userName}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
marginTop: '15px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
fontWeight="bold"
|
||||||
|
color="text.primary"
|
||||||
|
gutterBottom
|
||||||
|
noWrap
|
||||||
|
>
|
||||||
|
{datePart}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
marginTop: '15px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
fontWeight="bold"
|
||||||
|
color="text.primary"
|
||||||
|
gutterBottom
|
||||||
|
noWrap
|
||||||
|
>
|
||||||
|
{timePart}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 3,
|
||||||
|
display: 'flex',
|
||||||
|
marginTop: '15px',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
fontWeight="bold"
|
||||||
|
color="text.primary"
|
||||||
|
gutterBottom
|
||||||
|
noWrap
|
||||||
|
>
|
||||||
|
{action.description}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!props.errorLoadingS3Data && history.length == 0 && (
|
||||||
|
<Alert
|
||||||
|
severity="error"
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginTop: '20px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id="nohistory"
|
||||||
|
defaultMessage="There is no history of actions"
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
color="inherit"
|
||||||
|
size="small"
|
||||||
|
sx={{ marginLeft: '4px' }}
|
||||||
|
></IconButton>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} md={12} style={{ marginBottom: '20px' }}>
|
||||||
|
{props.errorLoadingS3Data && (
|
||||||
|
<Alert
|
||||||
|
severity="error"
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginTop: '20px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id="cannotloadloggingdata"
|
||||||
|
defaultMessage="Cannot load logging data"
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
color="inherit"
|
||||||
|
size="small"
|
||||||
|
sx={{ marginLeft: '4px' }}
|
||||||
|
></IconButton>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HistoryOfActions;
|
|
@ -23,6 +23,8 @@ import routes from '../../../Resources/routes.json';
|
||||||
import Information from '../Information/Information';
|
import Information from '../Information/Information';
|
||||||
import BatteryView from '../BatteryView/BatteryView';
|
import BatteryView from '../BatteryView/BatteryView';
|
||||||
import { UserType } from '../../../interfaces/UserTypes';
|
import { UserType } from '../../../interfaces/UserTypes';
|
||||||
|
import HistoryOfActions from '../History/History';
|
||||||
|
import PvView from '../PvView/PvView';
|
||||||
|
|
||||||
interface singleInstallationProps {
|
interface singleInstallationProps {
|
||||||
current_installation?: I_Installation;
|
current_installation?: I_Installation;
|
||||||
|
@ -123,6 +125,7 @@ function Installation(props: singleInstallationProps) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
currentTab == 'live' ||
|
currentTab == 'live' ||
|
||||||
|
currentTab == 'pvview' ||
|
||||||
currentTab == 'configuration' ||
|
currentTab == 'configuration' ||
|
||||||
location.includes('batteryview')
|
location.includes('batteryview')
|
||||||
) {
|
) {
|
||||||
|
@ -130,7 +133,8 @@ function Installation(props: singleInstallationProps) {
|
||||||
|
|
||||||
if (
|
if (
|
||||||
currentTab == 'live' ||
|
currentTab == 'live' ||
|
||||||
(location.includes('batteryview') && !location.includes('mainstats'))
|
(location.includes('batteryview') && !location.includes('mainstats')) ||
|
||||||
|
currentTab == 'pvview'
|
||||||
) {
|
) {
|
||||||
fetchDataPeriodically();
|
fetchDataPeriodically();
|
||||||
interval = setInterval(fetchDataPeriodically, 2000);
|
interval = setInterval(fetchDataPeriodically, 2000);
|
||||||
|
@ -143,6 +147,7 @@ function Installation(props: singleInstallationProps) {
|
||||||
return () => {
|
return () => {
|
||||||
if (
|
if (
|
||||||
currentTab == 'live' ||
|
currentTab == 'live' ||
|
||||||
|
currentTab == 'pvview' ||
|
||||||
(location.includes('batteryview') && !location.includes('mainstats'))
|
(location.includes('batteryview') && !location.includes('mainstats'))
|
||||||
) {
|
) {
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
|
@ -309,6 +314,14 @@ function Installation(props: singleInstallationProps) {
|
||||||
></BatteryView>
|
></BatteryView>
|
||||||
}
|
}
|
||||||
></Route>
|
></Route>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path={routes.pvview + '*'}
|
||||||
|
element={
|
||||||
|
<PvView values={values} connected={connected}></PvView>
|
||||||
|
}
|
||||||
|
></Route>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path={routes.overview}
|
path={routes.overview}
|
||||||
element={<Overview s3Credentials={s3Credentials}></Overview>}
|
element={<Overview s3Credentials={s3Credentials}></Overview>}
|
||||||
|
@ -342,6 +355,18 @@ function Installation(props: singleInstallationProps) {
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{currentUser.userType == UserType.admin && (
|
||||||
|
<Route
|
||||||
|
path={routes.history}
|
||||||
|
element={
|
||||||
|
<HistoryOfActions
|
||||||
|
errorLoadingS3Data={errorLoadingS3Data}
|
||||||
|
id={props.current_installation.id}
|
||||||
|
></HistoryOfActions>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{currentUser.userType == UserType.admin && (
|
{currentUser.userType == UserType.admin && (
|
||||||
<Route
|
<Route
|
||||||
path={routes.manage}
|
path={routes.manage}
|
||||||
|
|
|
@ -26,7 +26,9 @@ function InstallationTabs() {
|
||||||
'batteryview',
|
'batteryview',
|
||||||
'log',
|
'log',
|
||||||
'information',
|
'information',
|
||||||
'configuration'
|
'configuration',
|
||||||
|
'history',
|
||||||
|
'pvview'
|
||||||
];
|
];
|
||||||
|
|
||||||
const [currentTab, setCurrentTab] = useState<string>(undefined);
|
const [currentTab, setCurrentTab] = useState<string>(undefined);
|
||||||
|
@ -137,6 +139,19 @@ function InstallationTabs() {
|
||||||
defaultMessage="Configuration"
|
defaultMessage="Configuration"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'history',
|
||||||
|
label: (
|
||||||
|
<FormattedMessage
|
||||||
|
id="history"
|
||||||
|
defaultMessage="History Of Actions"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'pvview',
|
||||||
|
label: <FormattedMessage id="pvview" defaultMessage="Pv View" />
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
: currentUser.userType == UserType.partner
|
: currentUser.userType == UserType.partner
|
||||||
|
@ -158,6 +173,10 @@ function InstallationTabs() {
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: 'pvview',
|
||||||
|
label: <FormattedMessage id="pvview" defaultMessage="Pv View" />
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
value: 'information',
|
value: 'information',
|
||||||
|
@ -217,6 +236,10 @@ function InstallationTabs() {
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: 'pvview',
|
||||||
|
label: <FormattedMessage id="pvview" defaultMessage="Pv View" />
|
||||||
|
},
|
||||||
{
|
{
|
||||||
value: 'manage',
|
value: 'manage',
|
||||||
label: (
|
label: (
|
||||||
|
@ -248,6 +271,15 @@ function InstallationTabs() {
|
||||||
defaultMessage="Configuration"
|
defaultMessage="Configuration"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'history',
|
||||||
|
label: (
|
||||||
|
<FormattedMessage
|
||||||
|
id="history"
|
||||||
|
defaultMessage="History Of Actions"
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
: currentUser.userType == UserType.partner
|
: currentUser.userType == UserType.partner
|
||||||
|
@ -280,6 +312,10 @@ function InstallationTabs() {
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: 'pvview',
|
||||||
|
label: <FormattedMessage id="pvview" defaultMessage="Pv View" />
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
value: 'information',
|
value: 'information',
|
||||||
|
|
|
@ -35,6 +35,13 @@ export type ConfigurationValues = {
|
||||||
calibrationChargeDate: Date | null;
|
calibrationChargeDate: Date | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface Pv {
|
||||||
|
PvId: number;
|
||||||
|
Power: I_BoxDataValue;
|
||||||
|
Voltage: I_BoxDataValue;
|
||||||
|
Current: I_BoxDataValue;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Battery {
|
export interface Battery {
|
||||||
BatteryId: number;
|
BatteryId: number;
|
||||||
FwVersion: I_BoxDataValue;
|
FwVersion: I_BoxDataValue;
|
||||||
|
@ -79,6 +86,8 @@ export interface Battery {
|
||||||
MaxDischargePower: I_BoxDataValue;
|
MaxDischargePower: I_BoxDataValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PvKeys = ['PvId', 'Power', 'Voltage', 'Current'];
|
||||||
|
|
||||||
const BatteryKeys = [
|
const BatteryKeys = [
|
||||||
'BatteryId',
|
'BatteryId',
|
||||||
'FwVersion',
|
'FwVersion',
|
||||||
|
@ -163,11 +172,24 @@ export type TopologyValues = {
|
||||||
additionalCalibrationChargeDate: I_BoxDataValue[];
|
additionalCalibrationChargeDate: I_BoxDataValue[];
|
||||||
|
|
||||||
batteryView: Battery[];
|
batteryView: Battery[];
|
||||||
|
|
||||||
|
pvView: Pv[];
|
||||||
};
|
};
|
||||||
type TopologyPaths = { [key in keyof TopologyValues]: string[] };
|
type TopologyPaths = { [key in keyof TopologyValues]: string[] };
|
||||||
|
|
||||||
const batteryIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
const batteryIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||||
|
|
||||||
|
const pvIds = [
|
||||||
|
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,
|
||||||
|
23, 24, 25, 26, 27, 28, 29, 30
|
||||||
|
];
|
||||||
|
|
||||||
|
const PvPaths = [
|
||||||
|
'/PvOnDc/Strings/%id%/Power',
|
||||||
|
'/PvOnDc/Strings/%id%/Voltage',
|
||||||
|
'/PvOnDc/Strings/%id%/Current'
|
||||||
|
];
|
||||||
|
|
||||||
const batteryPaths = [
|
const batteryPaths = [
|
||||||
'/Battery/Devices/%id%/FwVersion',
|
'/Battery/Devices/%id%/FwVersion',
|
||||||
'/Battery/Devices/%id%/Dc/Power',
|
'/Battery/Devices/%id%/Dc/Power',
|
||||||
|
@ -290,6 +312,10 @@ export const topologyPaths: TopologyPaths = {
|
||||||
batteryPaths.map((path) => path.replace('%id%', id.toString()))
|
batteryPaths.map((path) => path.replace('%id%', id.toString()))
|
||||||
),
|
),
|
||||||
|
|
||||||
|
pvView: pvIds.flatMap((id) =>
|
||||||
|
PvPaths.map((path) => path.replace('%id%', id.toString()))
|
||||||
|
),
|
||||||
|
|
||||||
minimumSoC: ['/Config/MinSoc'],
|
minimumSoC: ['/Config/MinSoc'],
|
||||||
installedDcDcPower: ['/DcDc/SystemControl/NumberOfConnectedSlaves'],
|
installedDcDcPower: ['/DcDc/SystemControl/NumberOfConnectedSlaves'],
|
||||||
gridSetPoint: ['/Config/GridSetPoint'],
|
gridSetPoint: ['/Config/GridSetPoint'],
|
||||||
|
@ -317,15 +343,47 @@ export const extractValues = (
|
||||||
timeSeriesData: DataPoint
|
timeSeriesData: DataPoint
|
||||||
): TopologyValues | null => {
|
): TopologyValues | null => {
|
||||||
const extractedValues: TopologyValues = {} as TopologyValues;
|
const extractedValues: TopologyValues = {} as TopologyValues;
|
||||||
|
|
||||||
// console.log('timeSeriesData=', timeSeriesData);
|
|
||||||
|
|
||||||
for (const topologyKey of Object.keys(topologyPaths)) {
|
for (const topologyKey of Object.keys(topologyPaths)) {
|
||||||
//Each topologykey may have more than one paths (for example inverter)
|
//Each topologykey may have more than one paths (for example inverter)
|
||||||
const paths = topologyPaths[topologyKey];
|
const paths = topologyPaths[topologyKey];
|
||||||
let topologyValues: { unit: string; value: string | number }[] = [];
|
let topologyValues: { unit: string; value: string | number }[] = [];
|
||||||
|
|
||||||
if (topologyKey === 'batteryView') {
|
if (topologyKey === 'pvView') {
|
||||||
|
extractedValues[topologyKey] = [];
|
||||||
|
let pv_index = 0;
|
||||||
|
let pathIndex = 0;
|
||||||
|
|
||||||
|
while (pathIndex < paths.length) {
|
||||||
|
let pv = {};
|
||||||
|
let existingKeys = 0;
|
||||||
|
|
||||||
|
//We prepare a pv object for each node. We extract the number of nodes from the '/PvOnDc/NbrOfStrings' path.
|
||||||
|
//PvKeys[0] is the pv id.
|
||||||
|
pv[PvKeys[0]] = pv_index;
|
||||||
|
//Then, search all the remaining battery keys
|
||||||
|
for (let i = 1; i < PvKeys.length; i++) {
|
||||||
|
const path = paths[pathIndex];
|
||||||
|
if (timeSeriesData.value.hasOwnProperty(path)) {
|
||||||
|
existingKeys++;
|
||||||
|
|
||||||
|
pv[PvKeys[i]] = {
|
||||||
|
unit: timeSeriesData.value[path].unit.includes('~')
|
||||||
|
? timeSeriesData.value[path].unit.replace('~', '')
|
||||||
|
: timeSeriesData.value[path].unit,
|
||||||
|
value:
|
||||||
|
typeof timeSeriesData.value[path].value === 'string'
|
||||||
|
? timeSeriesData.value[path].value
|
||||||
|
: Number(timeSeriesData.value[path].value).toFixed(1)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
pathIndex++;
|
||||||
|
}
|
||||||
|
pv_index++;
|
||||||
|
if (existingKeys > 0) {
|
||||||
|
extractedValues[topologyKey].push(pv as Pv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (topologyKey === 'batteryView') {
|
||||||
extractedValues[topologyKey] = [];
|
extractedValues[topologyKey] = [];
|
||||||
const node_ids_from_csv = timeSeriesData.value[
|
const node_ids_from_csv = timeSeriesData.value[
|
||||||
'/Config/Devices/BatteryNodes'
|
'/Config/Devices/BatteryNodes'
|
||||||
|
|
|
@ -119,8 +119,8 @@ function Overview(props: OverviewProps) {
|
||||||
chartOverview: overviewInterface;
|
chartOverview: overviewInterface;
|
||||||
}> = transformInputToDailyData(
|
}> = transformInputToDailyData(
|
||||||
props.s3Credentials,
|
props.s3Credentials,
|
||||||
UnixTime.fromTicks(startX),
|
UnixTime.fromTicks(startX).earlier(TimeSpan.fromHours(2)),
|
||||||
UnixTime.fromTicks(endX)
|
UnixTime.fromTicks(endX).earlier(TimeSpan.fromHours(2))
|
||||||
);
|
);
|
||||||
|
|
||||||
let isComponentMounted = true;
|
let isComponentMounted = true;
|
||||||
|
|
|
@ -0,0 +1,187 @@
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Container,
|
||||||
|
Paper,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableContainer,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
Typography
|
||||||
|
} from '@mui/material';
|
||||||
|
import { TopologyValues } from '../Log/graph.util';
|
||||||
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
|
import routes from '../../../Resources/routes.json';
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
|
|
||||||
|
interface PvViewProps {
|
||||||
|
values: TopologyValues;
|
||||||
|
connected: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function PvView(props: PvViewProps) {
|
||||||
|
if (props.values === null && props.connected == true) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const currentLocation = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const sortedPvView =
|
||||||
|
props.values != null
|
||||||
|
? [...props.values.pvView].sort((a, b) => a.PvId - b.PvId)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(sortedPvView.length == 0);
|
||||||
|
|
||||||
|
const handleMainStatsButton = () => {
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (sortedPvView.length == 0) {
|
||||||
|
setLoading(true);
|
||||||
|
} else {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [sortedPvView]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!props.connected && (
|
||||||
|
<Container
|
||||||
|
maxWidth="xl"
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
height: '70vh'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CircularProgress size={60} style={{ color: '#ffc04d' }} />
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
style={{ color: 'black', fontWeight: 'bold' }}
|
||||||
|
mt={2}
|
||||||
|
>
|
||||||
|
Unable to communicate with the installation
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" style={{ color: 'black' }}>
|
||||||
|
Please wait or refresh the page
|
||||||
|
</Typography>
|
||||||
|
</Container>
|
||||||
|
)}
|
||||||
|
{loading && props.connected && (
|
||||||
|
<Container
|
||||||
|
maxWidth="xl"
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
height: '70vh'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CircularProgress size={60} style={{ color: '#ffc04d' }} />
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
style={{ color: 'black', fontWeight: 'bold' }}
|
||||||
|
mt={2}
|
||||||
|
>
|
||||||
|
Battery service is not available at the moment
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" style={{ color: 'black' }}>
|
||||||
|
Please wait or refresh the page
|
||||||
|
</Typography>
|
||||||
|
</Container>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!loading && props.connected && (
|
||||||
|
<Container maxWidth="xl">
|
||||||
|
<TableContainer
|
||||||
|
component={Paper}
|
||||||
|
sx={{
|
||||||
|
marginTop: '20px',
|
||||||
|
marginBottom: '20px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Table sx={{ minWidth: 250 }} aria-label="simple table">
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell align="center">Pv</TableCell>
|
||||||
|
<TableCell align="center">Power</TableCell>
|
||||||
|
<TableCell align="center">Voltage</TableCell>
|
||||||
|
<TableCell align="center">Current</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{sortedPvView.map((pv) => (
|
||||||
|
<TableRow
|
||||||
|
key={pv.PvId}
|
||||||
|
style={{
|
||||||
|
height: '10px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TableCell
|
||||||
|
component="th"
|
||||||
|
scope="row"
|
||||||
|
align="center"
|
||||||
|
sx={{ width: '10%', fontWeight: 'bold', color: 'black' }}
|
||||||
|
>
|
||||||
|
{'String ' + pv.PvId}
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell
|
||||||
|
sx={{
|
||||||
|
width: '10%',
|
||||||
|
textAlign: 'center',
|
||||||
|
backgroundColor:
|
||||||
|
pv.Current.value == 0 ? '#FF033E' : '#32CD32',
|
||||||
|
color: pv.Power.value === '' ? 'white' : 'inherit'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{pv.Power.value + ' ' + pv.Power.unit}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell
|
||||||
|
sx={{
|
||||||
|
width: '10%',
|
||||||
|
textAlign: 'center',
|
||||||
|
|
||||||
|
backgroundColor:
|
||||||
|
pv.Current.value == 0 ? '#FF033E' : '#32CD32',
|
||||||
|
color: pv.Voltage.value === '' ? 'white' : 'inherit'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{pv.Voltage.value + ' ' + pv.Voltage.unit}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell
|
||||||
|
sx={{
|
||||||
|
width: '10%',
|
||||||
|
textAlign: 'center',
|
||||||
|
backgroundColor:
|
||||||
|
pv.Current.value == 0 ? '#FF033E' : '#32CD32',
|
||||||
|
color: pv.Current.value === '' ? 'white' : 'inherit'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{pv.Current.value + ' ' + pv.Current.unit}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</Container>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PvView;
|
|
@ -58,6 +58,28 @@ function Installation(props: singleInstallationProps) {
|
||||||
|
|
||||||
const s3Credentials = { s3Bucket, ...S3data };
|
const s3Credentials = { s3Bucket, ...S3data };
|
||||||
|
|
||||||
|
const fetchDataOnlyOneTime = async () => {
|
||||||
|
var timeperiodToSearch = 70;
|
||||||
|
|
||||||
|
for (var i = timeperiodToSearch; i > 0; i -= 2) {
|
||||||
|
const now = UnixTime.now().earlier(TimeSpan.fromSeconds(i));
|
||||||
|
|
||||||
|
const res = await fetchData(now, s3Credentials);
|
||||||
|
|
||||||
|
if (res != FetchResult.notAvailable && res != FetchResult.tryLater) {
|
||||||
|
setConnected(true);
|
||||||
|
setFailedToCommunicateWithInstallation(0);
|
||||||
|
setValues(
|
||||||
|
extractValues({
|
||||||
|
time: now,
|
||||||
|
value: res
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const fetchDataPeriodically = async () => {
|
const fetchDataPeriodically = async () => {
|
||||||
const now = UnixTime.now().earlier(TimeSpan.fromSeconds(20));
|
const now = UnixTime.now().earlier(TimeSpan.fromSeconds(20));
|
||||||
|
|
||||||
|
@ -76,7 +98,7 @@ function Installation(props: singleInstallationProps) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
setFailedToCommunicateWithInstallation((prevCount) => {
|
setFailedToCommunicateWithInstallation((prevCount) => {
|
||||||
if (prevCount + 1 >= 3) {
|
if (prevCount + 1 >= 20) {
|
||||||
setConnected(false);
|
setConnected(false);
|
||||||
}
|
}
|
||||||
return prevCount + 1;
|
return prevCount + 1;
|
||||||
|
@ -87,19 +109,6 @@ function Installation(props: singleInstallationProps) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchDataOnlyOneTime = async () => {
|
|
||||||
let success = false;
|
|
||||||
const max_retransmissions = 3;
|
|
||||||
|
|
||||||
for (let i = 0; i < max_retransmissions; i++) {
|
|
||||||
success = await fetchDataPeriodically();
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
if (success) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let path = location.split('/');
|
let path = location.split('/');
|
||||||
|
|
||||||
|
@ -118,7 +127,7 @@ function Installation(props: singleInstallationProps) {
|
||||||
currentTab == 'live' ||
|
currentTab == 'live' ||
|
||||||
(location.includes('batteryview') && !location.includes('mainstats'))
|
(location.includes('batteryview') && !location.includes('mainstats'))
|
||||||
) {
|
) {
|
||||||
fetchDataPeriodically();
|
fetchDataOnlyOneTime();
|
||||||
interval = setInterval(fetchDataPeriodically, 2000);
|
interval = setInterval(fetchDataPeriodically, 2000);
|
||||||
}
|
}
|
||||||
if (currentTab == 'configuration' || location.includes('mainstats')) {
|
if (currentTab == 'configuration' || location.includes('mainstats')) {
|
||||||
|
|
|
@ -222,6 +222,7 @@ function Topology(props: TopologyProps) {
|
||||||
}}
|
}}
|
||||||
bottomBox={{
|
bottomBox={{
|
||||||
title: 'AC Loads',
|
title: 'AC Loads',
|
||||||
|
|
||||||
data: props.values.islandBusToLoadOnIslandBusConnection,
|
data: props.values.islandBusToLoadOnIslandBusConnection,
|
||||||
connected:
|
connected:
|
||||||
props.values.loadOnIslandBusBox[0].value.toString() !=
|
props.values.loadOnIslandBusBox[0].value.toString() !=
|
||||||
|
|
|
@ -16,3 +16,11 @@ export interface ErrorMessage {
|
||||||
deviceCreatedTheMessage: string;
|
deviceCreatedTheMessage: string;
|
||||||
seen: boolean;
|
seen: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Action {
|
||||||
|
id: number;
|
||||||
|
userName: string;
|
||||||
|
installationId: number;
|
||||||
|
timestamp: string;
|
||||||
|
description: String;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue