Compare commits
7 Commits
a86dc963b2
...
4ac1bc78ab
| Author | SHA1 | Date |
|---|---|---|
|
|
4ac1bc78ab | |
|
|
f82190afc1 | |
|
|
584abe5b53 | |
|
|
7aacddd761 | |
|
|
7df4842980 | |
|
|
79f695f9b4 | |
|
|
25b961dc93 |
|
|
@ -200,6 +200,8 @@ public class Controller : ControllerBase
|
||||||
bucketPath = "s3://" + installation.S3BucketId + "-3e5b3069-214a-43ee-8d85-57d72000c19d/" + startTimestamp;
|
bucketPath = "s3://" + installation.S3BucketId + "-3e5b3069-214a-43ee-8d85-57d72000c19d/" + startTimestamp;
|
||||||
else if (installation.Product == (int)ProductType.SodioHome)
|
else if (installation.Product == (int)ProductType.SodioHome)
|
||||||
bucketPath = "s3://" + installation.S3BucketId + "-e7b9a240-3c5d-4d2e-a019-6d8b1f7b73fa/" + startTimestamp;
|
bucketPath = "s3://" + installation.S3BucketId + "-e7b9a240-3c5d-4d2e-a019-6d8b1f7b73fa/" + startTimestamp;
|
||||||
|
else if (installation.Product == (int)ProductType.SodistoreGrid)
|
||||||
|
bucketPath = "s3://" + installation.S3BucketId + "-5109c126-e141-43ab-8658-f3c44c838ae8/" + startTimestamp;
|
||||||
else
|
else
|
||||||
bucketPath = "s3://" + installation.S3BucketId + "-c0436b6a-d276-4cd8-9c44-1eae86cf5d0e/" + startTimestamp;
|
bucketPath = "s3://" + installation.S3BucketId + "-c0436b6a-d276-4cd8-9c44-1eae86cf5d0e/" + startTimestamp;
|
||||||
Console.WriteLine("Fetching data for "+startTimestamp);
|
Console.WriteLine("Fetching data for "+startTimestamp);
|
||||||
|
|
@ -546,6 +548,19 @@ public class Controller : ControllerBase
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet(nameof(GetAllSodistoreGridInstallations))]
|
||||||
|
public ActionResult<IEnumerable<Installation>> GetAllSodistoreGridInstallations(Token authToken)
|
||||||
|
{
|
||||||
|
var user = Db.GetSession(authToken)?.User;
|
||||||
|
|
||||||
|
if (user is null)
|
||||||
|
return Unauthorized();
|
||||||
|
|
||||||
|
return user
|
||||||
|
.AccessibleInstallations(product:(int)ProductType.SodistoreGrid)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[HttpGet(nameof(GetAllFolders))]
|
[HttpGet(nameof(GetAllFolders))]
|
||||||
|
|
@ -1513,6 +1528,7 @@ public class Controller : ControllerBase
|
||||||
0 => config.GetConfigurationSalimax(), // Salimax
|
0 => config.GetConfigurationSalimax(), // Salimax
|
||||||
3 => config.GetConfigurationSodistoreMax(), // SodiStoreMax
|
3 => config.GetConfigurationSodistoreMax(), // SodiStoreMax
|
||||||
2 => config.GetConfigurationSodistoreHome(), // SodiStoreHome
|
2 => config.GetConfigurationSodistoreHome(), // SodiStoreHome
|
||||||
|
4 => config.GetConfigurationSodistoreGrid(), // SodistoreGrid
|
||||||
_ => config.GetConfigurationString() // fallback
|
_ => config.GetConfigurationString() // fallback
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1719,6 +1735,17 @@ public class Controller : ControllerBase
|
||||||
"AlarmKnowledgeBaseChecked.cs");
|
"AlarmKnowledgeBaseChecked.cs");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet(nameof(DryRunS3Cleanup))]
|
||||||
|
public async Task<ActionResult<String>> DryRunS3Cleanup(Token authToken, long? installationId = null)
|
||||||
|
{
|
||||||
|
var user = Db.GetSession(authToken)?.User;
|
||||||
|
if (user == null)
|
||||||
|
return Unauthorized();
|
||||||
|
|
||||||
|
var result = await DeleteOldData.DeleteOldDataFromS3.DryRun(installationId);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,12 @@ public class Configuration
|
||||||
$"BatteriesCount: {BatteriesCount}, ClusterNumber: {ClusterNumber}, PvNumber: {PvNumber}, ControlPermission:{ControlPermission}, "+
|
$"BatteriesCount: {BatteriesCount}, ClusterNumber: {ClusterNumber}, PvNumber: {PvNumber}, ControlPermission:{ControlPermission}, "+
|
||||||
$"SinexcelTimeChargeandDischargePower: {TimeChargeandDischargePower}, SinexcelStartTimeChargeandDischargeDayandTime: {StartTimeChargeandDischargeDayandTime}, SinexcelStopTimeChargeandDischargeDayandTime: {StopTimeChargeandDischargeDayandTime}";
|
$"SinexcelTimeChargeandDischargePower: {TimeChargeandDischargePower}, SinexcelStartTimeChargeandDischargeDayandTime: {StartTimeChargeandDischargeDayandTime}, SinexcelStopTimeChargeandDischargeDayandTime: {StopTimeChargeandDischargeDayandTime}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: SodistoreGrid — update configuration fields when defined
|
||||||
|
public string GetConfigurationSodistoreGrid()
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum CalibrationChargeType
|
public enum CalibrationChargeType
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@ public enum ProductType
|
||||||
Salimax = 0,
|
Salimax = 0,
|
||||||
Salidomo = 1,
|
Salidomo = 1,
|
||||||
SodioHome =2,
|
SodioHome =2,
|
||||||
SodiStoreMax=3
|
SodiStoreMax=3,
|
||||||
|
SodistoreGrid=4
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum StatusType
|
public enum StatusType
|
||||||
|
|
|
||||||
|
|
@ -145,6 +145,7 @@ public static class ExoCmd
|
||||||
const String method = "iam-role";
|
const String method = "iam-role";
|
||||||
String rolename = installation.Product==(int)ProductType.Salimax?Db.Installations.Count(f => f.Product == (int)ProductType.Salimax) + installation.Name:
|
String rolename = installation.Product==(int)ProductType.Salimax?Db.Installations.Count(f => f.Product == (int)ProductType.Salimax) + installation.Name:
|
||||||
installation.Product==(int)ProductType.SodiStoreMax?Db.Installations.Count(f => f.Product == (int)ProductType.SodiStoreMax) + installation.Name:
|
installation.Product==(int)ProductType.SodiStoreMax?Db.Installations.Count(f => f.Product == (int)ProductType.SodiStoreMax) + installation.Name:
|
||||||
|
installation.Product==(int)ProductType.SodistoreGrid?Db.Installations.Count(f => f.Product == (int)ProductType.SodistoreGrid) + installation.Name:
|
||||||
Db.Installations.Count(f => f.Product == (int)ProductType.Salidomo) + installation.Name;
|
Db.Installations.Count(f => f.Product == (int)ProductType.Salidomo) + installation.Name;
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -263,8 +264,6 @@ public static class ExoCmd
|
||||||
|
|
||||||
public static async Task<Boolean> RevokeReadKey(this Installation installation)
|
public static async Task<Boolean> RevokeReadKey(this Installation installation)
|
||||||
{
|
{
|
||||||
//Check exoscale documentation https://openapi-v2.exoscale.com/topic/topic-api-request-signature
|
|
||||||
|
|
||||||
var url = $"https://api-ch-dk-2.exoscale.com/v2/access-key/{installation.S3Key}";
|
var url = $"https://api-ch-dk-2.exoscale.com/v2/access-key/{installation.S3Key}";
|
||||||
var method = $"access-key/{installation.S3Key}";
|
var method = $"access-key/{installation.S3Key}";
|
||||||
|
|
||||||
|
|
@ -277,14 +276,28 @@ public static class ExoCmd
|
||||||
client.DefaultRequestHeaders.Authorization =
|
client.DefaultRequestHeaders.Authorization =
|
||||||
new AuthenticationHeaderValue("EXO2-HMAC-SHA256", authheader);
|
new AuthenticationHeaderValue("EXO2-HMAC-SHA256", authheader);
|
||||||
|
|
||||||
var response = await client.DeleteAsync(url);
|
try
|
||||||
return response.IsSuccessStatusCode;
|
{
|
||||||
|
var response = await client.DeleteAsync(url);
|
||||||
|
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Successfully revoked read key for installation {installation.Id}.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"Failed to revoke read key. HTTP Status: {response.StatusCode}. Response: {await response.Content.ReadAsStringAsync()}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Error occurred while revoking read key: {ex.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<Boolean> RevokeWriteKey(this Installation installation)
|
public static async Task<Boolean> RevokeWriteKey(this Installation installation)
|
||||||
{
|
{
|
||||||
//Check exoscale documentation https://openapi-v2.exoscale.com/topic/topic-api-request-signature
|
|
||||||
|
|
||||||
var url = $"https://api-ch-dk-2.exoscale.com/v2/access-key/{installation.S3WriteKey}";
|
var url = $"https://api-ch-dk-2.exoscale.com/v2/access-key/{installation.S3WriteKey}";
|
||||||
var method = $"access-key/{installation.S3WriteKey}";
|
var method = $"access-key/{installation.S3WriteKey}";
|
||||||
|
|
||||||
|
|
@ -297,8 +310,24 @@ public static class ExoCmd
|
||||||
client.DefaultRequestHeaders.Authorization =
|
client.DefaultRequestHeaders.Authorization =
|
||||||
new AuthenticationHeaderValue("EXO2-HMAC-SHA256", authheader);
|
new AuthenticationHeaderValue("EXO2-HMAC-SHA256", authheader);
|
||||||
|
|
||||||
var response = await client.DeleteAsync(url);
|
try
|
||||||
return response.IsSuccessStatusCode;
|
{
|
||||||
|
var response = await client.DeleteAsync(url);
|
||||||
|
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Successfully revoked write key for installation {installation.Id}.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"Failed to revoke write key. HTTP Status: {response.StatusCode}. Response: {await response.Content.ReadAsStringAsync()}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Error occurred while revoking write key: {ex.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<(String, String)> CreateWriteKey(this Installation installation)
|
public static async Task<(String, String)> CreateWriteKey(this Installation installation)
|
||||||
|
|
@ -320,6 +349,7 @@ public static class ExoCmd
|
||||||
const String method = "iam-role";
|
const String method = "iam-role";
|
||||||
String rolename = installation.Product==(int)ProductType.Salimax?Db.Installations.Count(f => f.Product == (int)ProductType.Salimax) + installation.Name:
|
String rolename = installation.Product==(int)ProductType.Salimax?Db.Installations.Count(f => f.Product == (int)ProductType.Salimax) + installation.Name:
|
||||||
installation.Product==(int)ProductType.SodiStoreMax?Db.Installations.Count(f => f.Product == (int)ProductType.SodiStoreMax) + installation.Name:
|
installation.Product==(int)ProductType.SodiStoreMax?Db.Installations.Count(f => f.Product == (int)ProductType.SodiStoreMax) + installation.Name:
|
||||||
|
installation.Product==(int)ProductType.SodistoreGrid?Db.Installations.Count(f => f.Product == (int)ProductType.SodistoreGrid) + installation.Name:
|
||||||
Db.Installations.Count(f => f.Product == (int)ProductType.Salidomo) + installation.Name;
|
Db.Installations.Count(f => f.Product == (int)ProductType.Salidomo) + installation.Name;
|
||||||
|
|
||||||
var contentString = $$"""
|
var contentString = $$"""
|
||||||
|
|
@ -371,10 +401,35 @@ public static class ExoCmd
|
||||||
return await s3Region.PutBucket(installation.BucketName()) != null;
|
return await s3Region.PutBucket(installation.BucketName()) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<Boolean> DeleteBucket(this Installation installation)
|
public static async Task<Boolean> PurgeAndDeleteBucket(this Installation installation)
|
||||||
{
|
{
|
||||||
var s3Region = new S3Region($"https://{installation.S3Region}.{installation.S3Provider}", S3Credentials!);
|
var s3Region = new S3Region($"https://{installation.S3Region}.{installation.S3Provider}", S3Credentials!);
|
||||||
return await s3Region.DeleteBucket(installation.BucketName()) ;
|
var bucket = s3Region.Bucket(installation.BucketName());
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var purged = await bucket.PurgeBucket();
|
||||||
|
if (!purged)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Failed to purge bucket {installation.BucketName()} for installation {installation.Id}.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var deleted = await s3Region.DeleteBucket(installation.BucketName());
|
||||||
|
if (!deleted)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Failed to delete bucket {installation.BucketName()} for installation {installation.Id}.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"Successfully purged and deleted bucket {installation.BucketName()} for installation {installation.Id}.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Error occurred while purging/deleting bucket {installation.BucketName()}: {ex.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ public static class InstallationMethods
|
||||||
private static readonly String BucketNameSalt = "3e5b3069-214a-43ee-8d85-57d72000c19d";
|
private static readonly String BucketNameSalt = "3e5b3069-214a-43ee-8d85-57d72000c19d";
|
||||||
private static readonly String SalidomoBucketNameSalt = "c0436b6a-d276-4cd8-9c44-1eae86cf5d0e";
|
private static readonly String SalidomoBucketNameSalt = "c0436b6a-d276-4cd8-9c44-1eae86cf5d0e";
|
||||||
private static readonly String SodioHomeBucketNameSalt = "e7b9a240-3c5d-4d2e-a019-6d8b1f7b73fa";
|
private static readonly String SodioHomeBucketNameSalt = "e7b9a240-3c5d-4d2e-a019-6d8b1f7b73fa";
|
||||||
|
private static readonly String SodistoreGridBucketNameSalt = "5109c126-e141-43ab-8658-f3c44c838ae8";
|
||||||
|
|
||||||
public static String BucketName(this Installation installation)
|
public static String BucketName(this Installation installation)
|
||||||
{
|
{
|
||||||
|
|
@ -23,6 +24,11 @@ public static class InstallationMethods
|
||||||
return $"{installation.S3BucketId}-{SodioHomeBucketNameSalt}";
|
return $"{installation.S3BucketId}-{SodioHomeBucketNameSalt}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (installation.Product == (int)ProductType.SodistoreGrid)
|
||||||
|
{
|
||||||
|
return $"{installation.S3BucketId}-{SodistoreGridBucketNameSalt}";
|
||||||
|
}
|
||||||
|
|
||||||
return $"{installation.S3BucketId}-{SalidomoBucketNameSalt}";
|
return $"{installation.S3BucketId}-{SalidomoBucketNameSalt}";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -239,7 +239,7 @@ public static class SessionMethods
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (installation.Product == (int)ProductType.SodiStoreMax || installation.Product == (int)ProductType.SodioHome)
|
if (installation.Product == (int)ProductType.SodiStoreMax || installation.Product == (int)ProductType.SodioHome || installation.Product == (int)ProductType.SodistoreGrid)
|
||||||
{
|
{
|
||||||
return user is not null
|
return user is not null
|
||||||
&& user.UserType != 0
|
&& user.UserType != 0
|
||||||
|
|
@ -295,7 +295,7 @@ public static class SessionMethods
|
||||||
.Apply(Db.Update);
|
.Apply(Db.Update);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (installation.Product == (int)ProductType.SodiStoreMax)
|
if (installation.Product == (int)ProductType.SodiStoreMax || installation.Product == (int)ProductType.SodistoreGrid)
|
||||||
{
|
{
|
||||||
|
|
||||||
return user is not null
|
return user is not null
|
||||||
|
|
@ -324,23 +324,22 @@ public static class SessionMethods
|
||||||
{
|
{
|
||||||
var user = session?.User;
|
var user = session?.User;
|
||||||
|
|
||||||
if (user is not null
|
if (user is null || installation is null || user.UserType == 0)
|
||||||
&& installation is not null
|
return false;
|
||||||
&& user.UserType != 0)
|
|
||||||
{
|
|
||||||
|
|
||||||
return
|
// Try all Exoscale operations independently (don't short-circuit)
|
||||||
Db.Delete(installation)
|
var readKeyOk = await installation.RevokeReadKey();
|
||||||
&& await installation.RevokeReadKey()
|
var writeKeyOk = await installation.RevokeWriteKey();
|
||||||
&& await installation.RevokeWriteKey()
|
var readRoleOk = await installation.RemoveReadRole();
|
||||||
&& await installation.RemoveReadRole()
|
var writeRoleOk = await installation.RemoveWriteRole();
|
||||||
&& await installation.RemoveWriteRole()
|
var bucketOk = await installation.PurgeAndDeleteBucket();
|
||||||
&& await installation.DeleteBucket();
|
|
||||||
|
|
||||||
}
|
if (!readKeyOk || !writeKeyOk || !readRoleOk || !writeRoleOk || !bucketOk)
|
||||||
|
Console.WriteLine($"[Delete] Partial Exoscale cleanup for installation {installation.Id}: " +
|
||||||
return false;
|
$"readKey={readKeyOk}, writeKey={writeKeyOk}, readRole={readRoleOk}, writeRole={writeRoleOk}, bucket={bucketOk}");
|
||||||
|
|
||||||
|
// Always delete from DB (best-effort — admin wants it gone)
|
||||||
|
return Db.Delete(installation);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Boolean Create(this Session? session, User newUser)
|
public static Boolean Create(this Session? session, User newUser)
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,11 @@ public static partial class Db
|
||||||
Connection.Execute("UPDATE User SET Language = 'fr' WHERE Language = 'french'");
|
Connection.Execute("UPDATE User SET Language = 'fr' WHERE Language = 'french'");
|
||||||
Connection.Execute("UPDATE User SET Language = 'it' WHERE Language = 'italian'");
|
Connection.Execute("UPDATE User SET Language = 'it' WHERE Language = 'italian'");
|
||||||
|
|
||||||
|
// One-time migration: rebrand to inesco Energy
|
||||||
|
Connection.Execute("UPDATE Folder SET Name = 'inesco Energy' WHERE Name = 'InnovEnergy'");
|
||||||
|
Connection.Execute("UPDATE Folder SET Name = 'Sodistore Max Installations' WHERE Name = 'SodistoreMax Installations'");
|
||||||
|
Connection.Execute("UPDATE User SET Name = 'inesco Energy Master Admin' WHERE Name = 'InnovEnergy Master Admin'");
|
||||||
|
|
||||||
//UpdateKeys();
|
//UpdateKeys();
|
||||||
CleanupSessions().SupressAwaitWarning();
|
CleanupSessions().SupressAwaitWarning();
|
||||||
DeleteSnapshots().SupressAwaitWarning();
|
DeleteSnapshots().SupressAwaitWarning();
|
||||||
|
|
|
||||||
|
|
@ -102,10 +102,28 @@ public static partial class Db
|
||||||
InstallationAccess.Delete(i => i.InstallationId == installation.Id);
|
InstallationAccess.Delete(i => i.InstallationId == installation.Id);
|
||||||
if (installation.Product == (int)ProductType.Salimax)
|
if (installation.Product == (int)ProductType.Salimax)
|
||||||
{
|
{
|
||||||
//For Salimax, delete the OrderNumber2Installation entries associated with this installation id.
|
|
||||||
OrderNumber2Installation.Delete(i => i.InstallationId == installation.Id);
|
OrderNumber2Installation.Delete(i => i.InstallationId == installation.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean up AI insight cache entries linked to this installation's reports
|
||||||
|
var weeklyIds = WeeklyReports .Where(r => r.InstallationId == installation.Id).Select(r => r.Id).ToList();
|
||||||
|
var monthlyIds = MonthlyReports.Where(r => r.InstallationId == installation.Id).Select(r => r.Id).ToList();
|
||||||
|
var yearlyIds = YearlyReports .Where(r => r.InstallationId == installation.Id).Select(r => r.Id).ToList();
|
||||||
|
|
||||||
|
foreach (var id in weeklyIds) AiInsightCaches.Delete(c => c.ReportType == "weekly" && c.ReportId == id);
|
||||||
|
foreach (var id in monthlyIds) AiInsightCaches.Delete(c => c.ReportType == "monthly" && c.ReportId == id);
|
||||||
|
foreach (var id in yearlyIds) AiInsightCaches.Delete(c => c.ReportType == "yearly" && c.ReportId == id);
|
||||||
|
|
||||||
|
// Clean up energy records, report summaries, errors, warnings, and user actions
|
||||||
|
DailyRecords .Delete(r => r.InstallationId == installation.Id);
|
||||||
|
HourlyRecords .Delete(r => r.InstallationId == installation.Id);
|
||||||
|
WeeklyReports .Delete(r => r.InstallationId == installation.Id);
|
||||||
|
MonthlyReports.Delete(r => r.InstallationId == installation.Id);
|
||||||
|
YearlyReports .Delete(r => r.InstallationId == installation.Id);
|
||||||
|
Errors .Delete(e => e.InstallationId == installation.Id);
|
||||||
|
Warnings .Delete(w => w.InstallationId == installation.Id);
|
||||||
|
UserActions .Delete(a => a.InstallationId == installation.Id);
|
||||||
|
|
||||||
return Installations.Delete(i => i.Id == installation.Id) > 0;
|
return Installations.Delete(i => i.Id == installation.Id) > 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,94 +1,157 @@
|
||||||
using System.Diagnostics;
|
|
||||||
using InnovEnergy.App.Backend.Database;
|
using InnovEnergy.App.Backend.Database;
|
||||||
using InnovEnergy.App.Backend.DataTypes;
|
using InnovEnergy.App.Backend.DataTypes.Methods;
|
||||||
using InnovEnergy.Lib.Utils;
|
using InnovEnergy.Lib.S3Utils;
|
||||||
|
using InnovEnergy.Lib.S3Utils.DataTypes;
|
||||||
|
|
||||||
namespace InnovEnergy.App.Backend.DeleteOldData;
|
namespace InnovEnergy.App.Backend.DeleteOldData;
|
||||||
|
|
||||||
public class DeleteOldDataFromS3
|
public static class DeleteOldDataFromS3
|
||||||
{
|
{
|
||||||
|
private static Timer? _cleanupTimer;
|
||||||
|
|
||||||
public static void DeleteFrom(Installation installation, int timestamps_to_delete)
|
public static void StartScheduler()
|
||||||
{
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var next = new DateTime(now.Year, now.Month, now.Day, 3, 0, 0, DateTimeKind.Utc);
|
||||||
|
if (next <= now) next = next.AddDays(1);
|
||||||
|
|
||||||
string configPath = "/home/ubuntu/.s3cfg";
|
_cleanupTimer = new Timer(
|
||||||
string bucketPath = installation.Product ==(int)ProductType.Salidomo ? $"s3://{installation.S3BucketId}-c0436b6a-d276-4cd8-9c44-1eae86cf5d0e/{timestamps_to_delete}*" : $"s3://{installation.S3BucketId}-3e5b3069-214a-43ee-8d85-57d72000c19d/{timestamps_to_delete}*" ;
|
_ =>
|
||||||
|
|
||||||
//Console.WriteLine($"Deleting old data from {bucketPath}");
|
|
||||||
|
|
||||||
Console.WriteLine("Deleting data for timestamp prefix: " + timestamps_to_delete);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
ProcessStartInfo startInfo = new ProcessStartInfo
|
|
||||||
{
|
{
|
||||||
FileName = "s3cmd",
|
try
|
||||||
Arguments = $"--config {configPath} rm {bucketPath}",
|
{
|
||||||
RedirectStandardOutput = true,
|
CleanupAllInstallations().GetAwaiter().GetResult();
|
||||||
RedirectStandardError = true,
|
|
||||||
UseShellExecute = false,
|
|
||||||
CreateNoWindow = true
|
|
||||||
};
|
|
||||||
|
|
||||||
using Process process = new Process { StartInfo = startInfo };
|
|
||||||
|
|
||||||
process.OutputDataReceived += (sender, e) =>
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(e.Data))
|
|
||||||
Console.WriteLine("[s3cmd] " + e.Data);
|
|
||||||
};
|
|
||||||
|
|
||||||
process.ErrorDataReceived += (sender, e) =>
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(e.Data))
|
|
||||||
Console.WriteLine("[s3cmd-ERR] " + e.Data);
|
|
||||||
};
|
|
||||||
|
|
||||||
process.Start();
|
|
||||||
process.BeginOutputReadLine();
|
|
||||||
process.BeginErrorReadLine();
|
|
||||||
process.WaitForExit();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine("Exception occurred during deletion: " + ex.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task DeleteOldData()
|
|
||||||
{
|
|
||||||
while (true){
|
|
||||||
var installations = Db.Installations.ToList();
|
|
||||||
foreach (var installation in installations){
|
|
||||||
Console.WriteLine("DELETE S3 DATA FOR INSTALLATION "+installation.Name);
|
|
||||||
long oneYearAgoTimestamp = DateTimeOffset.UtcNow.AddYears(-1).ToUnixTimeSeconds();
|
|
||||||
|
|
||||||
Console.WriteLine("delete data before "+oneYearAgoTimestamp);
|
|
||||||
for (int lastDigit=4;lastDigit>=0; lastDigit--)
|
|
||||||
{
|
|
||||||
int timestamps_to_delete = int.Parse(oneYearAgoTimestamp.ToString().Substring(0, lastDigit+1));
|
|
||||||
timestamps_to_delete--;
|
|
||||||
Console.WriteLine(timestamps_to_delete);
|
|
||||||
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
if (timestamps_to_delete % 10 == 0)
|
|
||||||
{
|
|
||||||
Console.WriteLine("delete " + timestamps_to_delete + "*");
|
|
||||||
DeleteFrom(installation,timestamps_to_delete);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Console.WriteLine("delete " + timestamps_to_delete + "*");
|
|
||||||
DeleteFrom(installation,timestamps_to_delete);
|
|
||||||
timestamps_to_delete--;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Console.WriteLine("FINISHED DELETING S3 DATA FOR ALL INSTALLATIONS\n");
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[S3Cleanup] Scheduler error: {ex.Message}");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
next - now,
|
||||||
|
TimeSpan.FromDays(1)
|
||||||
|
);
|
||||||
|
|
||||||
await Task.Delay(TimeSpan.FromDays(1));
|
Console.WriteLine($"[S3Cleanup] Scheduled daily at 03:00 UTC, first run in {(next - now).TotalHours:F1}h");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task CleanupAllInstallations()
|
||||||
|
{
|
||||||
|
var cutoffTimestamp = DateTimeOffset.UtcNow.AddYears(-1).ToUnixTimeSeconds();
|
||||||
|
var cutoffKey = cutoffTimestamp.ToString();
|
||||||
|
var installations = Db.Installations.ToList();
|
||||||
|
|
||||||
|
Console.WriteLine($"[S3Cleanup] Starting cleanup for {installations.Count} installations, cutoff: {cutoffKey}");
|
||||||
|
|
||||||
|
foreach (var installation in installations)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var s3Region = new S3Region(
|
||||||
|
$"https://{installation.S3Region}.{installation.S3Provider}",
|
||||||
|
ExoCmd.S3Credentials
|
||||||
|
);
|
||||||
|
var bucket = s3Region.Bucket(installation.BucketName());
|
||||||
|
|
||||||
|
Console.WriteLine($"[S3Cleanup] Processing {installation.Name} (bucket: {bucket.Name})");
|
||||||
|
var deleted = await DeleteObjectsBefore(bucket, cutoffKey);
|
||||||
|
Console.WriteLine($"[S3Cleanup] {installation.Name}: deleted {deleted} objects");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[S3Cleanup] Failed for {installation.Name}: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine("[S3Cleanup] Finished cleanup for all installations");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<string> DryRun(long? installationId = null)
|
||||||
|
{
|
||||||
|
var cutoffTimestamp = DateTimeOffset.UtcNow.AddYears(-1).ToUnixTimeSeconds();
|
||||||
|
var cutoffKey = cutoffTimestamp.ToString();
|
||||||
|
var allInstallations = Db.Installations.ToList();
|
||||||
|
var installations = installationId.HasValue
|
||||||
|
? allInstallations.Where(i => i.Id == installationId.Value).ToList()
|
||||||
|
: allInstallations;
|
||||||
|
var results = new List<string>();
|
||||||
|
|
||||||
|
results.Add($"Cutoff: {cutoffKey} ({DateTimeOffset.FromUnixTimeSeconds(cutoffTimestamp):yyyy-MM-dd HH:mm:ss} UTC)");
|
||||||
|
results.Add($"Installations: {installations.Count} (of {allInstallations.Count} total)");
|
||||||
|
results.Add("");
|
||||||
|
|
||||||
|
foreach (var installation in installations)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var s3Region = new S3Region(
|
||||||
|
$"https://{installation.S3Region}.{installation.S3Provider}",
|
||||||
|
ExoCmd.S3Credentials
|
||||||
|
);
|
||||||
|
var bucket = s3Region.Bucket(installation.BucketName());
|
||||||
|
|
||||||
|
var sampleKeys = new List<string>();
|
||||||
|
var hasOldData = false;
|
||||||
|
|
||||||
|
await foreach (var obj in bucket.ListObjects())
|
||||||
|
{
|
||||||
|
if (string.Compare(obj.Path, cutoffKey, StringComparison.Ordinal) >= 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
hasOldData = true;
|
||||||
|
if (sampleKeys.Count < 5)
|
||||||
|
sampleKeys.Add(obj.Path);
|
||||||
|
else
|
||||||
|
break; // only need a sample, not full count
|
||||||
|
}
|
||||||
|
|
||||||
|
results.Add($"{installation.Name} (bucket: {bucket.Name})");
|
||||||
|
results.Add($" Has old data: {(hasOldData ? "YES" : "NO")}");
|
||||||
|
if (sampleKeys.Count > 0)
|
||||||
|
results.Add($" Sample keys: {string.Join(", ", sampleKeys)}");
|
||||||
|
results.Add("");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
results.Add($"{installation.Name}: ERROR - {ex.Message}");
|
||||||
|
results.Add("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Join("\n", results);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<int> DeleteObjectsBefore(S3Bucket bucket, string cutoffKey)
|
||||||
|
{
|
||||||
|
var totalDeleted = 0;
|
||||||
|
var keysToDelete = new List<string>();
|
||||||
|
|
||||||
|
await foreach (var obj in bucket.ListObjects())
|
||||||
|
{
|
||||||
|
if (string.Compare(obj.Path, cutoffKey, StringComparison.Ordinal) >= 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
keysToDelete.Add(obj.Path);
|
||||||
|
|
||||||
|
if (keysToDelete.Count >= 1000)
|
||||||
|
{
|
||||||
|
if (await bucket.DeleteObjects(keysToDelete))
|
||||||
|
totalDeleted += keysToDelete.Count;
|
||||||
|
else
|
||||||
|
Console.Error.WriteLine($"[S3Cleanup] Failed to delete batch of {keysToDelete.Count} objects from {bucket.Name}");
|
||||||
|
|
||||||
|
keysToDelete.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keysToDelete.Count > 0)
|
||||||
|
{
|
||||||
|
if (await bucket.DeleteObjects(keysToDelete))
|
||||||
|
totalDeleted += keysToDelete.Count;
|
||||||
|
else
|
||||||
|
Console.Error.WriteLine($"[S3Cleanup] Failed to delete batch of {keysToDelete.Count} objects from {bucket.Name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalDeleted;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -38,7 +38,7 @@ public static class Program
|
||||||
WebsocketManager.MonitorInstallationTable().SupressAwaitWarning();
|
WebsocketManager.MonitorInstallationTable().SupressAwaitWarning();
|
||||||
|
|
||||||
|
|
||||||
// Task.Run(() => DeleteOldDataFromS3.DeleteOldData());
|
DeleteOldDataFromS3.StartScheduler();
|
||||||
|
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
builder.Services.AddProblemDetails(setup =>
|
builder.Services.AddProblemDetails(setup =>
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ public class Session : Relation<String, Int64>
|
||||||
public Boolean AccessToSalidomo { get; set; } = false;
|
public Boolean AccessToSalidomo { get; set; } = false;
|
||||||
public Boolean AccessToSodistoreMax { get; set; } = false;
|
public Boolean AccessToSodistoreMax { get; set; } = false;
|
||||||
public Boolean AccessToSodioHome { get; set; } = false;
|
public Boolean AccessToSodioHome { get; set; } = false;
|
||||||
|
public Boolean AccessToSodistoreGrid { get; set; } = false;
|
||||||
[Ignore] public Boolean Valid => DateTime.Now - LastSeen <=MaxAge ;
|
[Ignore] public Boolean Valid => DateTime.Now - LastSeen <=MaxAge ;
|
||||||
|
|
||||||
// Private backing field
|
// Private backing field
|
||||||
|
|
@ -49,6 +50,7 @@ public class Session : Relation<String, Int64>
|
||||||
AccessToSalidomo = user.AccessibleInstallations(product: (int)ProductType.Salidomo).ToList().Count > 0;
|
AccessToSalidomo = user.AccessibleInstallations(product: (int)ProductType.Salidomo).ToList().Count > 0;
|
||||||
AccessToSodistoreMax = user.AccessibleInstallations(product: (int)ProductType.SodiStoreMax).ToList().Count > 0;
|
AccessToSodistoreMax = user.AccessibleInstallations(product: (int)ProductType.SodiStoreMax).ToList().Count > 0;
|
||||||
AccessToSodioHome = user.AccessibleInstallations(product: (int)ProductType.SodioHome).ToList().Count > 0;
|
AccessToSodioHome = user.AccessibleInstallations(product: (int)ProductType.SodioHome).ToList().Count > 0;
|
||||||
|
AccessToSodistoreGrid = user.AccessibleInstallations(product: (int)ProductType.SodistoreGrid).ToList().Count > 0;
|
||||||
|
|
||||||
Console.WriteLine("salimax" +user.AccessibleInstallations(product: (int)ProductType.Salimax).ToList().Count);
|
Console.WriteLine("salimax" +user.AccessibleInstallations(product: (int)ProductType.Salimax).ToList().Count);
|
||||||
Console.WriteLine("AccessToSodistoreMax" +user.AccessibleInstallations(product: (int)ProductType.SodiStoreMax).ToList().Count);
|
Console.WriteLine("AccessToSodistoreMax" +user.AccessibleInstallations(product: (int)ProductType.SodiStoreMax).ToList().Count);
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,11 @@ public static class RabbitMqManager
|
||||||
monitorLink =
|
monitorLink =
|
||||||
$"https://monitor.inesco.energy/sodistore_installations/list/installation/{installation.S3BucketId}/batteryview";
|
$"https://monitor.inesco.energy/sodistore_installations/list/installation/{installation.S3BucketId}/batteryview";
|
||||||
}
|
}
|
||||||
|
else if (installation.Product == (int)ProductType.SodistoreGrid)
|
||||||
|
{
|
||||||
|
monitorLink =
|
||||||
|
$"https://monitor.inesco.energy/sodistoregrid_installations/list/installation/{installation.S3BucketId}/batteryview";
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
monitorLink =
|
monitorLink =
|
||||||
|
|
@ -126,7 +131,7 @@ public static class RabbitMqManager
|
||||||
Console.WriteLine("Send replace battery email to the support team for installation "+installationId);
|
Console.WriteLine("Send replace battery email to the support team for installation "+installationId);
|
||||||
string recipient = "support@innov.energy";
|
string recipient = "support@innov.energy";
|
||||||
string subject = $"Battery Alarm from {installation.InstallationName}: 2 or more strings broken";
|
string subject = $"Battery Alarm from {installation.InstallationName}: 2 or more strings broken";
|
||||||
string text = $"Dear InnovEnergy Support Team,\n" +
|
string text = $"Dear inesco Energy Support Team,\n" +
|
||||||
$"\n"+
|
$"\n"+
|
||||||
$"Installation Name: {installation.InstallationName}\n"+
|
$"Installation Name: {installation.InstallationName}\n"+
|
||||||
$"\n"+
|
$"\n"+
|
||||||
|
|
@ -138,7 +143,7 @@ public static class RabbitMqManager
|
||||||
$"\n"+
|
$"\n"+
|
||||||
$"Thank you for your great support:)";
|
$"Thank you for your great support:)";
|
||||||
//Disable this function now
|
//Disable this function now
|
||||||
//Mailer.Send("InnovEnergy Support Team", recipient, subject, text);
|
//Mailer.Send("inesco Energy Support Team", recipient, subject, text);
|
||||||
}
|
}
|
||||||
//Create a new error and add it to the database
|
//Create a new error and add it to the database
|
||||||
Db.HandleError(newError, installationId);
|
Db.HandleError(newError, installationId);
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,8 @@ public static class WebsocketManager
|
||||||
if ((installationConnection.Value.Product == (int)ProductType.Salimax && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(2)) ||
|
if ((installationConnection.Value.Product == (int)ProductType.Salimax && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(2)) ||
|
||||||
(installationConnection.Value.Product == (int)ProductType.Salidomo && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(60)) ||
|
(installationConnection.Value.Product == (int)ProductType.Salidomo && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(60)) ||
|
||||||
(installationConnection.Value.Product == (int)ProductType.SodioHome && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(4)) ||
|
(installationConnection.Value.Product == (int)ProductType.SodioHome && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(4)) ||
|
||||||
(installationConnection.Value.Product == (int)ProductType.SodiStoreMax && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(2))
|
(installationConnection.Value.Product == (int)ProductType.SodiStoreMax && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(2)) ||
|
||||||
|
(installationConnection.Value.Product == (int)ProductType.SodistoreGrid && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(2))
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
Console.WriteLine("Installation ID is " + installationConnection.Key);
|
Console.WriteLine("Installation ID is " + installationConnection.Key);
|
||||||
|
|
|
||||||
|
|
@ -201,6 +201,61 @@ public static class S3
|
||||||
return response.HttpStatusCode == HttpStatusCode.OK;
|
return response.HttpStatusCode == HttpStatusCode.OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async Task<Boolean> PurgeBucket(this S3Bucket bucket)
|
||||||
|
{
|
||||||
|
var client = bucket.Region.GetS3Client();
|
||||||
|
var totalDeleted = 0;
|
||||||
|
|
||||||
|
Console.WriteLine($"[PurgeBucket] Starting purge of bucket {bucket.Name}");
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var listResponse = await client.ListObjectsV2Async(new ListObjectsV2Request
|
||||||
|
{
|
||||||
|
BucketName = bucket.Name,
|
||||||
|
MaxKeys = 1000
|
||||||
|
});
|
||||||
|
|
||||||
|
if (listResponse.S3Objects.Count == 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[PurgeBucket] Completed purge of bucket {bucket.Name}: {totalDeleted} objects deleted");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var deleteResponse = await client.DeleteObjectsAsync(new DeleteObjectsRequest
|
||||||
|
{
|
||||||
|
BucketName = bucket.Name,
|
||||||
|
Objects = listResponse.S3Objects
|
||||||
|
.Select(o => new KeyVersion { Key = o.Key })
|
||||||
|
.ToList()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (deleteResponse.HttpStatusCode != HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[PurgeBucket] Failed at batch after {totalDeleted} objects deleted from bucket {bucket.Name}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
totalDeleted += listResponse.S3Objects.Count;
|
||||||
|
|
||||||
|
if (totalDeleted % 10000 == 0)
|
||||||
|
Console.WriteLine($"[PurgeBucket] Progress: {totalDeleted} objects deleted from bucket {bucket.Name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<Boolean> DeleteObjects(this S3Bucket bucket, IReadOnlyList<String> keys)
|
||||||
|
{
|
||||||
|
if (keys.Count == 0) return true;
|
||||||
|
|
||||||
|
var response = await bucket.Region.GetS3Client().DeleteObjectsAsync(new DeleteObjectsRequest
|
||||||
|
{
|
||||||
|
BucketName = bucket.Name,
|
||||||
|
Objects = keys.Select(k => new KeyVersion { Key = k }).ToList()
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.HttpStatusCode == HttpStatusCode.OK;
|
||||||
|
}
|
||||||
|
|
||||||
public static async Task<Boolean> DeleteBucket(this S3Region region, String bucketName)
|
public static async Task<Boolean> DeleteBucket(this S3Region region, String bucketName)
|
||||||
{
|
{
|
||||||
var request = new DeleteBucketRequest { BucketName = bucketName };
|
var request = new DeleteBucketRequest { BucketName = bucketName };
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,8 @@ function App() {
|
||||||
setAccessToSalimax,
|
setAccessToSalimax,
|
||||||
setAccessToSalidomo,
|
setAccessToSalidomo,
|
||||||
setAccessToSodiohome,
|
setAccessToSodiohome,
|
||||||
setAccessToSodistore
|
setAccessToSodistore,
|
||||||
|
setAccessToSodistoreGrid
|
||||||
} = useContext(ProductIdContext);
|
} = useContext(ProductIdContext);
|
||||||
|
|
||||||
const [language, setLanguage] = useState<string>(
|
const [language, setLanguage] = useState<string>(
|
||||||
|
|
@ -102,12 +103,15 @@ function App() {
|
||||||
setAccessToSalidomo(response.data.accessToSalidomo);
|
setAccessToSalidomo(response.data.accessToSalidomo);
|
||||||
setAccessToSodiohome(response.data.accessToSodioHome);
|
setAccessToSodiohome(response.data.accessToSodioHome);
|
||||||
setAccessToSodistore(response.data.accessToSodistoreMax);
|
setAccessToSodistore(response.data.accessToSodistoreMax);
|
||||||
|
setAccessToSodistoreGrid(response.data.accessToSodistoreGrid);
|
||||||
if (response.data.accessToSalimax) {
|
if (response.data.accessToSalimax) {
|
||||||
navigate(routes.installations);
|
navigate(routes.installations);
|
||||||
} else if (response.data.accessToSalidomo) {
|
} else if (response.data.accessToSalidomo) {
|
||||||
navigate(routes.salidomo_installations);
|
navigate(routes.salidomo_installations);
|
||||||
} else if (response.data.accessToSodistoreMax) {
|
} else if (response.data.accessToSodistoreMax) {
|
||||||
navigate(routes.sodistore_installations);
|
navigate(routes.sodistore_installations);
|
||||||
|
} else if (response.data.accessToSodistoreGrid) {
|
||||||
|
navigate(routes.sodistoregrid_installations);
|
||||||
} else {
|
} else {
|
||||||
navigate(routes.sodiohome_installations);
|
navigate(routes.sodiohome_installations);
|
||||||
}
|
}
|
||||||
|
|
@ -215,6 +219,15 @@ function App() {
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path={routes.sodistoregrid_installations + '*'}
|
||||||
|
element={
|
||||||
|
<AccessContextProvider>
|
||||||
|
<InstallationTabs product={4} />
|
||||||
|
</AccessContextProvider>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<Route path={routes.users + '*'} element={<Users />} />
|
<Route path={routes.users + '*'} element={<Users />} />
|
||||||
<Route
|
<Route
|
||||||
path={'*'}
|
path={'*'}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
"salidomo_installations": "/salidomo_installations/",
|
"salidomo_installations": "/salidomo_installations/",
|
||||||
"sodistore_installations": "/sodistore_installations/",
|
"sodistore_installations": "/sodistore_installations/",
|
||||||
"sodiohome_installations": "/sodiohome_installations/",
|
"sodiohome_installations": "/sodiohome_installations/",
|
||||||
|
"sodistoregrid_installations": "/sodistoregrid_installations/",
|
||||||
"installation": "installation/",
|
"installation": "installation/",
|
||||||
"login": "/login/",
|
"login": "/login/",
|
||||||
"forgotPassword": "/forgotPassword/",
|
"forgotPassword": "/forgotPassword/",
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@ function Logo() {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipWrapper title="InnovEnergy" arrow>
|
<TooltipWrapper title="inesco Energy" arrow>
|
||||||
<LogoWrapper to="/overview">
|
<LogoWrapper to="/overview">
|
||||||
<Badge
|
<Badge
|
||||||
sx={{
|
sx={{
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,8 @@ function Login() {
|
||||||
setAccessToSalimax,
|
setAccessToSalimax,
|
||||||
setAccessToSalidomo,
|
setAccessToSalidomo,
|
||||||
setAccessToSodiohome,
|
setAccessToSodiohome,
|
||||||
setAccessToSodistore
|
setAccessToSodistore,
|
||||||
|
setAccessToSodistoreGrid
|
||||||
} = useContext(ProductIdContext);
|
} = useContext(ProductIdContext);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
|
@ -84,6 +85,7 @@ function Login() {
|
||||||
setAccessToSalidomo(response.data.accessToSalidomo);
|
setAccessToSalidomo(response.data.accessToSalidomo);
|
||||||
setAccessToSodiohome(response.data.accessToSodioHome);
|
setAccessToSodiohome(response.data.accessToSodioHome);
|
||||||
setAccessToSodistore(response.data.accessToSodistoreMax);
|
setAccessToSodistore(response.data.accessToSodistoreMax);
|
||||||
|
setAccessToSodistoreGrid(response.data.accessToSodistoreGrid);
|
||||||
|
|
||||||
if (rememberMe) {
|
if (rememberMe) {
|
||||||
cookies.set('rememberedUsername', username, { path: '/' });
|
cookies.set('rememberedUsername', username, { path: '/' });
|
||||||
|
|
@ -95,6 +97,8 @@ function Login() {
|
||||||
navigate(routes.salidomo_installations);
|
navigate(routes.salidomo_installations);
|
||||||
} else if (response.data.accessToSodistoreMax) {
|
} else if (response.data.accessToSodistoreMax) {
|
||||||
navigate(routes.sodistore_installations);
|
navigate(routes.sodistore_installations);
|
||||||
|
} else if (response.data.accessToSodistoreGrid) {
|
||||||
|
navigate(routes.sodistoregrid_installations);
|
||||||
} else {
|
} else {
|
||||||
navigate(routes.sodiohome_installations);
|
navigate(routes.sodiohome_installations);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -245,7 +245,7 @@ function BatteryView(props: BatteryViewProps) {
|
||||||
) : (
|
) : (
|
||||||
<TableCell align="center">Max Cell Voltage</TableCell>
|
<TableCell align="center">Max Cell Voltage</TableCell>
|
||||||
)}
|
)}
|
||||||
{product === 3 && (
|
{(product === 3 || product === 4) && (
|
||||||
<TableCell align="center">Voltage Difference</TableCell>
|
<TableCell align="center">Voltage Difference</TableCell>
|
||||||
)}
|
)}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|
@ -469,7 +469,7 @@ function BatteryView(props: BatteryViewProps) {
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{product === 3 && (
|
{(product === 3 || product === 4) && (
|
||||||
<>
|
<>
|
||||||
{(() => {
|
{(() => {
|
||||||
const cellVoltagesString =
|
const cellVoltagesString =
|
||||||
|
|
@ -524,7 +524,7 @@ function BatteryView(props: BatteryViewProps) {
|
||||||
})()}
|
})()}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{product === 3 && (
|
{(product === 3 || product === 4) && (
|
||||||
<>
|
<>
|
||||||
{(() => {
|
{(() => {
|
||||||
const cellVoltagesString =
|
const cellVoltagesString =
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,7 @@ function Information(props: InformationProps) {
|
||||||
top: '50%',
|
top: '50%',
|
||||||
left: '50%',
|
left: '50%',
|
||||||
transform: 'translate(-50%, -50%)',
|
transform: 'translate(-50%, -50%)',
|
||||||
width: 350,
|
width: 450,
|
||||||
bgcolor: 'background.paper',
|
bgcolor: 'background.paper',
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
boxShadow: 24,
|
boxShadow: 24,
|
||||||
|
|
@ -143,7 +143,37 @@ function Information(props: InformationProps) {
|
||||||
gutterBottom
|
gutterBottom
|
||||||
sx={{ fontWeight: 'bold' }}
|
sx={{ fontWeight: 'bold' }}
|
||||||
>
|
>
|
||||||
Do you want to delete this installation?
|
<FormattedMessage
|
||||||
|
id="confirmDeleteInstallation"
|
||||||
|
defaultMessage="Do you want to delete this installation?"
|
||||||
|
/>
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{
|
||||||
|
mt: 1,
|
||||||
|
p: 1,
|
||||||
|
bgcolor: '#f5f5f5',
|
||||||
|
borderRadius: 1,
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
wordBreak: 'break-all',
|
||||||
|
userSelect: 'all',
|
||||||
|
width: '100%',
|
||||||
|
textAlign: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.s3Credentials.s3Bucket}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ mt: 1, fontSize: '0.75rem', color: 'text.secondary', textAlign: 'center' }}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id="deleteInstallationWarning"
|
||||||
|
defaultMessage="Please note the bucket name above. Purging S3 data may take several minutes. If deletion fails, you can manually remove the bucket from Exoscale."
|
||||||
|
/>
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
@ -328,9 +358,12 @@ function Information(props: InformationProps) {
|
||||||
label="S3 Bucket Name"
|
label="S3 Bucket Name"
|
||||||
name="s3bucketname"
|
name="s3bucketname"
|
||||||
value={
|
value={
|
||||||
product === 0 || product == 3
|
formValues.product === 0 || formValues.product == 3
|
||||||
? formValues.s3BucketId +
|
? formValues.s3BucketId +
|
||||||
'-3e5b3069-214a-43ee-8d85-57d72000c19d'
|
'-3e5b3069-214a-43ee-8d85-57d72000c19d'
|
||||||
|
: formValues.product == 4
|
||||||
|
? formValues.s3BucketId +
|
||||||
|
'-5109c126-e141-43ab-8658-f3c44c838ae8'
|
||||||
: formValues.s3BucketId +
|
: formValues.s3BucketId +
|
||||||
'-e7b9a240-3c5d-4d2e-a019-6d8b1f7b73fa'
|
'-e7b9a240-3c5d-4d2e-a019-6d8b1f7b73fa'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,7 @@ function InformationSalidomo(props: InformationSalidomoProps) {
|
||||||
top: '50%',
|
top: '50%',
|
||||||
left: '50%',
|
left: '50%',
|
||||||
transform: 'translate(-50%, -50%)',
|
transform: 'translate(-50%, -50%)',
|
||||||
width: 350,
|
width: 450,
|
||||||
bgcolor: 'background.paper',
|
bgcolor: 'background.paper',
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
boxShadow: 24,
|
boxShadow: 24,
|
||||||
|
|
@ -134,7 +134,37 @@ function InformationSalidomo(props: InformationSalidomoProps) {
|
||||||
gutterBottom
|
gutterBottom
|
||||||
sx={{ fontWeight: 'bold' }}
|
sx={{ fontWeight: 'bold' }}
|
||||||
>
|
>
|
||||||
Do you want to delete this installation?
|
<FormattedMessage
|
||||||
|
id="confirmDeleteInstallation"
|
||||||
|
defaultMessage="Do you want to delete this installation?"
|
||||||
|
/>
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{
|
||||||
|
mt: 1,
|
||||||
|
p: 1,
|
||||||
|
bgcolor: '#f5f5f5',
|
||||||
|
borderRadius: 1,
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
wordBreak: 'break-all',
|
||||||
|
userSelect: 'all',
|
||||||
|
width: '100%',
|
||||||
|
textAlign: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.s3Credentials.s3Bucket}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ mt: 1, fontSize: '0.75rem', color: 'text.secondary', textAlign: 'center' }}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id="deleteInstallationWarning"
|
||||||
|
defaultMessage="Please note the bucket name above. Purging S3 data may take several minutes. If deletion fails, you can manually remove the bucket from Exoscale."
|
||||||
|
/>
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -193,7 +193,7 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
|
||||||
top: '50%',
|
top: '50%',
|
||||||
left: '50%',
|
left: '50%',
|
||||||
transform: 'translate(-50%, -50%)',
|
transform: 'translate(-50%, -50%)',
|
||||||
width: 350,
|
width: 450,
|
||||||
bgcolor: 'background.paper',
|
bgcolor: 'background.paper',
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
boxShadow: 24,
|
boxShadow: 24,
|
||||||
|
|
@ -208,7 +208,37 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
|
||||||
gutterBottom
|
gutterBottom
|
||||||
sx={{ fontWeight: 'bold' }}
|
sx={{ fontWeight: 'bold' }}
|
||||||
>
|
>
|
||||||
Do you want to delete this installation?
|
<FormattedMessage
|
||||||
|
id="confirmDeleteInstallation"
|
||||||
|
defaultMessage="Do you want to delete this installation?"
|
||||||
|
/>
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{
|
||||||
|
mt: 1,
|
||||||
|
p: 1,
|
||||||
|
bgcolor: '#f5f5f5',
|
||||||
|
borderRadius: 1,
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
wordBreak: 'break-all',
|
||||||
|
userSelect: 'all',
|
||||||
|
width: '100%',
|
||||||
|
textAlign: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.s3Credentials.s3Bucket}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ mt: 1, fontSize: '0.75rem', color: 'text.secondary', textAlign: 'center' }}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id="deleteInstallationWarning"
|
||||||
|
defaultMessage="Please note the bucket name above. Purging S3 data may take several minutes. If deletion fails, you can manually remove the bucket from Exoscale."
|
||||||
|
/>
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -57,9 +57,13 @@ function Installation(props: singleInstallationProps) {
|
||||||
s3BucketId: props.current_installation.s3BucketId
|
s3BucketId: props.current_installation.s3BucketId
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: SodistoreGrid — uses its own bucket salt
|
||||||
|
const s3BucketSalt =
|
||||||
|
props.current_installation.product === 4
|
||||||
|
? '5109c126-e141-43ab-8658-f3c44c838ae8'
|
||||||
|
: '3e5b3069-214a-43ee-8d85-57d72000c19d';
|
||||||
const s3Bucket =
|
const s3Bucket =
|
||||||
props.current_installation.s3BucketId.toString() +
|
props.current_installation.s3BucketId.toString() + '-' + s3BucketSalt;
|
||||||
'-3e5b3069-214a-43ee-8d85-57d72000c19d';
|
|
||||||
|
|
||||||
const s3Credentials = { s3Bucket, ...S3data };
|
const s3Credentials = { s3Bucket, ...S3data };
|
||||||
|
|
||||||
|
|
@ -427,12 +431,15 @@ function Installation(props: singleInstallationProps) {
|
||||||
}
|
}
|
||||||
></Route>
|
></Route>
|
||||||
|
|
||||||
<Route
|
{/* TODO: SodistoreGrid — PV View excluded, add back when data path is ready */}
|
||||||
path={routes.pvview + '/*'}
|
{props.current_installation.product !== 4 && (
|
||||||
element={
|
<Route
|
||||||
<PvView values={values} connected={connected}></PvView>
|
path={routes.pvview + '/*'}
|
||||||
}
|
element={
|
||||||
></Route>
|
<PvView values={values} connected={connected}></PvView>
|
||||||
|
}
|
||||||
|
></Route>
|
||||||
|
)}
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path={routes.overview}
|
path={routes.overview}
|
||||||
|
|
@ -447,11 +454,28 @@ function Installation(props: singleInstallationProps) {
|
||||||
<Route
|
<Route
|
||||||
path={routes.live}
|
path={routes.live}
|
||||||
element={
|
element={
|
||||||
<Topology
|
props.current_installation.product === 4 ? (
|
||||||
values={values}
|
// TODO: SodistoreGrid — implement actual topology layout
|
||||||
connected={connected}
|
<Container
|
||||||
loading={loading}
|
maxWidth="xl"
|
||||||
></Topology>
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
height: '40vh'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="body1" color="text.secondary">
|
||||||
|
Live view coming soon
|
||||||
|
</Typography>
|
||||||
|
</Container>
|
||||||
|
) : (
|
||||||
|
<Topology
|
||||||
|
values={values}
|
||||||
|
connected={connected}
|
||||||
|
loading={loading}
|
||||||
|
></Topology>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
@ -470,10 +494,27 @@ function Installation(props: singleInstallationProps) {
|
||||||
<Route
|
<Route
|
||||||
path={routes.configuration}
|
path={routes.configuration}
|
||||||
element={
|
element={
|
||||||
<Configuration
|
props.current_installation.product === 4 ? (
|
||||||
values={values}
|
// TODO: SodistoreGrid — implement actual configuration
|
||||||
id={props.current_installation.id}
|
<Container
|
||||||
></Configuration>
|
maxWidth="xl"
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
height: '40vh'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="body1" color="text.secondary">
|
||||||
|
Configuration not yet available
|
||||||
|
</Typography>
|
||||||
|
</Container>
|
||||||
|
) : (
|
||||||
|
<Configuration
|
||||||
|
values={values}
|
||||||
|
id={props.current_installation.id}
|
||||||
|
></Configuration>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -39,11 +39,18 @@ function InstallationTabs(props: InstallationTabsProps) {
|
||||||
|
|
||||||
const {
|
const {
|
||||||
salimax_or_sodistore_Installations,
|
salimax_or_sodistore_Installations,
|
||||||
|
sodistoreGridInstallations,
|
||||||
fetchAllInstallations,
|
fetchAllInstallations,
|
||||||
socket,
|
socket,
|
||||||
openSocket,
|
openSocket,
|
||||||
closeSocket
|
closeSocket
|
||||||
} = useContext(InstallationsContext);
|
} = useContext(InstallationsContext);
|
||||||
|
|
||||||
|
// Use the correct installations array based on product
|
||||||
|
const installations =
|
||||||
|
props.product === 4
|
||||||
|
? sodistoreGridInstallations
|
||||||
|
: salimax_or_sodistore_Installations;
|
||||||
const { product, setProduct } = useContext(ProductIdContext);
|
const { product, setProduct } = useContext(ProductIdContext);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -93,7 +100,10 @@ function InstallationTabs(props: InstallationTabsProps) {
|
||||||
return ret_path;
|
return ret_path;
|
||||||
};
|
};
|
||||||
|
|
||||||
const singleInstallationTabs =
|
// TODO: SodistoreGrid — PV View excluded for product 4, add back when data path is ready
|
||||||
|
const hidePvView = props.product === 4;
|
||||||
|
|
||||||
|
const singleInstallationTabs = (
|
||||||
currentUser.userType == UserType.admin
|
currentUser.userType == UserType.admin
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
|
|
@ -204,7 +214,8 @@ function InstallationTabs(props: InstallationTabsProps) {
|
||||||
<FormattedMessage id="information" defaultMessage="Information" />
|
<FormattedMessage id="information" defaultMessage="Information" />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
];
|
]
|
||||||
|
).filter((tab) => !(hidePvView && tab.value === 'pvview'));
|
||||||
|
|
||||||
const tabs =
|
const tabs =
|
||||||
currentTab != 'list' &&
|
currentTab != 'list' &&
|
||||||
|
|
@ -372,7 +383,12 @@ function InstallationTabs(props: InstallationTabsProps) {
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
return salimax_or_sodistore_Installations.length > 1 ? (
|
// Filter out PV View for SodistoreGrid
|
||||||
|
const filteredTabs = hidePvView
|
||||||
|
? tabs.filter((tab) => tab.value !== 'pvview')
|
||||||
|
: tabs;
|
||||||
|
|
||||||
|
return installations.length > 1 ? (
|
||||||
<>
|
<>
|
||||||
<Container maxWidth="xl" sx={{ marginTop: '20px' }} className="mainframe">
|
<Container maxWidth="xl" sx={{ marginTop: '20px' }} className="mainframe">
|
||||||
<TabsContainerWrapper>
|
<TabsContainerWrapper>
|
||||||
|
|
@ -384,7 +400,7 @@ function InstallationTabs(props: InstallationTabsProps) {
|
||||||
textColor="primary"
|
textColor="primary"
|
||||||
indicatorColor="primary"
|
indicatorColor="primary"
|
||||||
>
|
>
|
||||||
{tabs.map((tab) => (
|
{filteredTabs.map((tab) => (
|
||||||
<Tab
|
<Tab
|
||||||
key={tab.value}
|
key={tab.value}
|
||||||
value={tab.value}
|
value={tab.value}
|
||||||
|
|
@ -415,7 +431,7 @@ function InstallationTabs(props: InstallationTabsProps) {
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Box p={4}>
|
<Box p={4}>
|
||||||
<InstallationSearch
|
<InstallationSearch
|
||||||
installations={salimax_or_sodistore_Installations}
|
installations={installations}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
@ -428,6 +444,10 @@ function InstallationTabs(props: InstallationTabsProps) {
|
||||||
element={
|
element={
|
||||||
props.product === 0 ? (
|
props.product === 0 ? (
|
||||||
<Navigate to={routes.installations + routes.list} />
|
<Navigate to={routes.installations + routes.list} />
|
||||||
|
) : props.product === 4 ? (
|
||||||
|
<Navigate
|
||||||
|
to={routes.sodistoregrid_installations + routes.list}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Navigate
|
<Navigate
|
||||||
to={routes.sodistore_installations + routes.list}
|
to={routes.sodistore_installations + routes.list}
|
||||||
|
|
@ -441,7 +461,7 @@ function InstallationTabs(props: InstallationTabsProps) {
|
||||||
</Container>
|
</Container>
|
||||||
<Footer />
|
<Footer />
|
||||||
</>
|
</>
|
||||||
) : salimax_or_sodistore_Installations.length === 1 ? (
|
) : installations.length === 1 ? (
|
||||||
<>
|
<>
|
||||||
<Container maxWidth="xl" sx={{ marginTop: '20px' }} className="mainframe">
|
<Container maxWidth="xl" sx={{ marginTop: '20px' }} className="mainframe">
|
||||||
<TabsContainerWrapper>
|
<TabsContainerWrapper>
|
||||||
|
|
@ -480,7 +500,7 @@ function InstallationTabs(props: InstallationTabsProps) {
|
||||||
<Box p={4}>
|
<Box p={4}>
|
||||||
<Installation
|
<Installation
|
||||||
current_installation={
|
current_installation={
|
||||||
salimax_or_sodistore_Installations[0]
|
installations[0]
|
||||||
}
|
}
|
||||||
type="installation"
|
type="installation"
|
||||||
></Installation>
|
></Installation>
|
||||||
|
|
|
||||||
|
|
@ -112,11 +112,11 @@ function SodioHomeInstallation(props: singleInstallationProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchDataPeriodically = async () => {
|
const fetchDataPeriodically = async () => {
|
||||||
var timeperiodToSearch = 200;
|
var timeperiodToSearch = 350;
|
||||||
let res;
|
let res;
|
||||||
let timestampToFetch;
|
let timestampToFetch;
|
||||||
|
|
||||||
for (var i = 0; i < timeperiodToSearch; i += 2) {
|
for (var i = 0; i < timeperiodToSearch; i += 30) {
|
||||||
if (!continueFetching.current) {
|
if (!continueFetching.current) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -184,13 +184,13 @@ function SodioHomeInstallation(props: singleInstallationProps) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchDataForOneTime = async () => {
|
const fetchDataForOneTime = async () => {
|
||||||
var timeperiodToSearch = 300; // 5 minutes to cover ~4 upload cycles
|
var timeperiodToSearch = 300; // 5 minutes to cover ~2 upload cycles (150s each)
|
||||||
let res;
|
let res;
|
||||||
let timestampToFetch;
|
let timestampToFetch;
|
||||||
|
|
||||||
// Search from NOW backward to find the most recent data
|
// Search from NOW backward to find the most recent data
|
||||||
// Step by 10 seconds - balances between finding files quickly and reducing 404s
|
// Step by 50 seconds - data is uploaded every ~150s, so finer steps are wasteful
|
||||||
for (var i = 0; i < timeperiodToSearch; i += 10) {
|
for (var i = 0; i < timeperiodToSearch; i += 50) {
|
||||||
timestampToFetch = UnixTime.now().earlier(TimeSpan.fromSeconds(i));
|
timestampToFetch = UnixTime.now().earlier(TimeSpan.fromSeconds(i));
|
||||||
try {
|
try {
|
||||||
res = await fetchDataJson(timestampToFetch, s3Credentials, false);
|
res = await fetchDataJson(timestampToFetch, s3Credentials, false);
|
||||||
|
|
@ -264,7 +264,7 @@ function SodioHomeInstallation(props: singleInstallationProps) {
|
||||||
const configRefreshInterval = setInterval(() => {
|
const configRefreshInterval = setInterval(() => {
|
||||||
console.log('Refreshing configuration data from S3...');
|
console.log('Refreshing configuration data from S3...');
|
||||||
fetchDataForOneTime();
|
fetchDataForOneTime();
|
||||||
}, 15000); // Refresh every 15 seconds
|
}, 60000); // Refresh every 60 seconds (data uploads every ~150s)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
continueFetching.current = false;
|
continueFetching.current = false;
|
||||||
|
|
|
||||||
|
|
@ -143,9 +143,9 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
|
||||||
const submittedAt = pendingConfig.submittedAt || 0;
|
const submittedAt = pendingConfig.submittedAt || 0;
|
||||||
const timeSinceSubmit = Date.now() - submittedAt;
|
const timeSinceSubmit = Date.now() - submittedAt;
|
||||||
|
|
||||||
// Within 150 seconds of submit: use localStorage (waiting for S3 sync)
|
// Within 300 seconds of submit: use localStorage (waiting for S3 sync)
|
||||||
// This covers two full S3 upload cycles (75 sec × 2) to ensure new file is available
|
// This covers two full S3 upload cycles (150 sec × 2) to ensure new file is available
|
||||||
if (timeSinceSubmit < 150000) {
|
if (timeSinceSubmit < 300000) {
|
||||||
// Check if S3 now matches - if so, sync is complete
|
// Check if S3 now matches - if so, sync is complete
|
||||||
const s3MatchesPending =
|
const s3MatchesPending =
|
||||||
s3Values.controlPermission === pendingConfig.values.controlPermission &&
|
s3Values.controlPermission === pendingConfig.values.controlPermission &&
|
||||||
|
|
@ -247,7 +247,7 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
// Save submitted values to localStorage for optimistic UI update
|
// Save submitted values to localStorage for optimistic UI update
|
||||||
// This ensures the form shows correct values even before S3 syncs (up to 75 sec delay)
|
// This ensures the form shows correct values even before S3 syncs (up to 150 sec delay)
|
||||||
localStorage.setItem(pendingConfigKey, JSON.stringify({
|
localStorage.setItem(pendingConfigKey, JSON.stringify({
|
||||||
values: formValues,
|
values: formValues,
|
||||||
submittedAt: Date.now()
|
submittedAt: Date.now()
|
||||||
|
|
|
||||||
|
|
@ -159,7 +159,10 @@ function FormattedBullet({ text }: { text: string }) {
|
||||||
return <>{parts}</>;
|
return <>{parts}</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MONTH_NAMES = ['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
function getMonthName(month: number, locale: string): string {
|
||||||
|
const date = new Date(2000, month - 1, 1);
|
||||||
|
return date.toLocaleDateString(locale, { month: 'long' });
|
||||||
|
}
|
||||||
|
|
||||||
// ── Email Bar (shared) ──────────────────────────────────────────
|
// ── Email Bar (shared) ──────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -545,7 +548,7 @@ function WeeklySection({ installationId, latestMonthlyPeriodEnd }: { installatio
|
||||||
</Box>
|
</Box>
|
||||||
{report.dailyData.map((d, i) => {
|
{report.dailyData.map((d, i) => {
|
||||||
const dt = new Date(d.date);
|
const dt = new Date(d.date);
|
||||||
const dayLabel = dt.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' });
|
const dayLabel = dt.toLocaleDateString(intl.locale, { weekday: 'short', month: 'short', day: 'numeric' });
|
||||||
const isCurrentWeek = report.dailyData.length > 7 ? i >= report.dailyData.length - 7 : true;
|
const isCurrentWeek = report.dailyData.length > 7 ? i >= report.dailyData.length - 7 : true;
|
||||||
return (
|
return (
|
||||||
<Box key={d.date} sx={{ mb: 1.5, opacity: isCurrentWeek ? 1 : 0.6 }}>
|
<Box key={d.date} sx={{ mb: 1.5, opacity: isCurrentWeek ? 1 : 0.6 }}>
|
||||||
|
|
@ -555,7 +558,7 @@ function WeeklySection({ installationId, latestMonthlyPeriodEnd }: { installatio
|
||||||
{!isCurrentWeek && <span style={{ color: '#999', marginLeft: 4 }}><FormattedMessage id="prevWeek" defaultMessage="(prev week)" /></span>}
|
{!isCurrentWeek && <span style={{ color: '#999', marginLeft: 4 }}><FormattedMessage id="prevWeek" defaultMessage="(prev week)" /></span>}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="caption" sx={{ color: '#888', fontSize: '11px' }}>
|
<Typography variant="caption" sx={{ color: '#888', fontSize: '11px' }}>
|
||||||
PV {d.pvProduction.toFixed(1)} | Load {d.loadConsumption.toFixed(1)} | Grid {d.gridImport.toFixed(1)} kWh
|
{intl.formatMessage({ id: 'pvProduction' })} {d.pvProduction.toFixed(1)} | {intl.formatMessage({ id: 'consumption' })} {d.loadConsumption.toFixed(1)} | {intl.formatMessage({ id: 'gridImport' })} {d.gridImport.toFixed(1)} kWh
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ display: 'flex', gap: '2px', height: 18 }}>
|
<Box sx={{ display: 'flex', gap: '2px', height: 18 }}>
|
||||||
|
|
@ -737,7 +740,7 @@ function MonthlySection({
|
||||||
? intl.formatMessage({ id: 'generatingMonthly', defaultMessage: 'Generating...' })
|
? intl.formatMessage({ id: 'generatingMonthly', defaultMessage: 'Generating...' })
|
||||||
: intl.formatMessage(
|
: intl.formatMessage(
|
||||||
{ id: 'generateMonth', defaultMessage: 'Generate {month} {year} ({count} weeks)' },
|
{ id: 'generateMonth', defaultMessage: 'Generate {month} {year} ({count} weeks)' },
|
||||||
{ month: MONTH_NAMES[p.month], year: p.year, count: p.weekCount }
|
{ month: getMonthName(p.month, intl.locale), year: p.year, count: p.weekCount }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -752,7 +755,7 @@ function MonthlySection({
|
||||||
<AggregatedSection
|
<AggregatedSection
|
||||||
reports={reports}
|
reports={reports}
|
||||||
type="monthly"
|
type="monthly"
|
||||||
labelFn={(r: MonthlyReport) => `${MONTH_NAMES[r.month]} ${r.year}`}
|
labelFn={(r: MonthlyReport) => `${getMonthName(r.month, intl.locale)} ${r.year}`}
|
||||||
countLabelId="weeksAggregated"
|
countLabelId="weeksAggregated"
|
||||||
countFn={(r: MonthlyReport) => r.weekCount}
|
countFn={(r: MonthlyReport) => r.weekCount}
|
||||||
sendEndpoint="/SendMonthlyReportEmail"
|
sendEndpoint="/SendMonthlyReportEmail"
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,12 @@ function TreeInformation(props: TreeInformationProps) {
|
||||||
setProduct(e.target.value); // Directly update the product state
|
setProduct(e.target.value); // Directly update the product state
|
||||||
};
|
};
|
||||||
|
|
||||||
const ProductTypes = ['Salimax', 'Salidomo', 'SodistoreHome', 'SodistoreMax'];
|
const ProductTypes = ['Salimax', 'Salidomo', 'SodistoreHome', 'SodistoreMax', 'SodistoreGrid'];
|
||||||
|
const ProductDisplayNames: Record<string, string> = {
|
||||||
|
'SodistoreHome': 'Sodistore Home',
|
||||||
|
'SodistoreMax': 'Sodistore Max',
|
||||||
|
'SodistoreGrid': 'Sodistore Grid'
|
||||||
|
};
|
||||||
|
|
||||||
const isMobile = window.innerWidth <= 1490;
|
const isMobile = window.innerWidth <= 1490;
|
||||||
|
|
||||||
|
|
@ -282,7 +287,7 @@ function TreeInformation(props: TreeInformationProps) {
|
||||||
>
|
>
|
||||||
{ProductTypes.map((type) => (
|
{ProductTypes.map((type) => (
|
||||||
<MenuItem key={type} value={type}>
|
<MenuItem key={type} value={type}>
|
||||||
{type}
|
{ProductDisplayNames[type] || type}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
|
|
@ -323,7 +328,8 @@ function TreeInformation(props: TreeInformationProps) {
|
||||||
)}
|
)}
|
||||||
{openModalInstallation &&
|
{openModalInstallation &&
|
||||||
(product == 'Salimax' ||
|
(product == 'Salimax' ||
|
||||||
product == 'SodistoreMax') && (
|
product == 'SodistoreMax' ||
|
||||||
|
product == 'SodistoreGrid') && (
|
||||||
<InstallationForm
|
<InstallationForm
|
||||||
cancel={handleFormCancel}
|
cancel={handleFormCancel}
|
||||||
submit={handleInstallationFormSubmit}
|
submit={handleInstallationFormSubmit}
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ function InstallationTree() {
|
||||||
key={installation.id}
|
key={installation.id}
|
||||||
path={routes.installation + installation.id + '/*'}
|
path={routes.installation + installation.id + '/*'}
|
||||||
element={
|
element={
|
||||||
installation.product == 0 || installation.product == 3 ? (
|
installation.product == 0 || installation.product == 3 || installation.product == 4 ? (
|
||||||
<Installation
|
<Installation
|
||||||
key={installation.id}
|
key={installation.id}
|
||||||
current_installation={installation}
|
current_installation={installation}
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,7 @@ function Status500() {
|
||||||
<Container maxWidth="sm">
|
<Container maxWidth="sm">
|
||||||
<Box textAlign="center">
|
<Box textAlign="center">
|
||||||
<TypographyPrimary variant="h1" sx={{ my: 2 }}>
|
<TypographyPrimary variant="h1" sx={{ my: 2 }}>
|
||||||
InnovEnergy{' '}
|
inesco Energy{' '}
|
||||||
</TypographyPrimary>
|
</TypographyPrimary>
|
||||||
<TypographySecondary
|
<TypographySecondary
|
||||||
variant="h4"
|
variant="h4"
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,9 @@ const InstallationsContextProvider = ({
|
||||||
const [sodiohomeInstallations, setSodiohomeInstallations] = useState<
|
const [sodiohomeInstallations, setSodiohomeInstallations] = useState<
|
||||||
I_Installation[]
|
I_Installation[]
|
||||||
>([]);
|
>([]);
|
||||||
|
const [sodistoreGridInstallations, setSodistoreGridInstallations] = useState<
|
||||||
|
I_Installation[]
|
||||||
|
>([]);
|
||||||
const [foldersAndInstallations, setFoldersAndInstallations] = useState([]);
|
const [foldersAndInstallations, setFoldersAndInstallations] = useState([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
|
|
@ -89,16 +92,31 @@ const InstallationsContextProvider = ({
|
||||||
: installation;
|
: installation;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const updatedSodistoreGrid = sodistoreGridInstallations.map(
|
||||||
|
(installation) => {
|
||||||
|
const update = pendingUpdates.current[installation.id];
|
||||||
|
return update
|
||||||
|
? {
|
||||||
|
...installation,
|
||||||
|
status: update.status,
|
||||||
|
testingMode: update.testingMode
|
||||||
|
}
|
||||||
|
: installation;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
setSalidomoInstallations(updatedSalidomo);
|
setSalidomoInstallations(updatedSalidomo);
|
||||||
setSalimax_Or_Sodistore_Installations(updatedSalimax);
|
setSalimax_Or_Sodistore_Installations(updatedSalimax);
|
||||||
setSodiohomeInstallations(updatedSodiohome);
|
setSodiohomeInstallations(updatedSodiohome);
|
||||||
|
setSodistoreGridInstallations(updatedSodistoreGrid);
|
||||||
|
|
||||||
// Clear the pending updates after applying
|
// Clear the pending updates after applying
|
||||||
pendingUpdates.current = {};
|
pendingUpdates.current = {};
|
||||||
}, [
|
}, [
|
||||||
salidomoInstallations,
|
salidomoInstallations,
|
||||||
salimax_or_sodistore_Installations,
|
salimax_or_sodistore_Installations,
|
||||||
sodiohomeInstallations
|
sodiohomeInstallations,
|
||||||
|
sodistoreGridInstallations
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -172,6 +190,8 @@ const InstallationsContextProvider = ({
|
||||||
setSalidomoInstallations(res.data);
|
setSalidomoInstallations(res.data);
|
||||||
} else if (product === 0 || product === 3) {
|
} else if (product === 0 || product === 3) {
|
||||||
setSalimax_Or_Sodistore_Installations(res.data);
|
setSalimax_Or_Sodistore_Installations(res.data);
|
||||||
|
} else if (product === 4) {
|
||||||
|
setSodistoreGridInstallations(res.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (open_socket) {
|
if (open_socket) {
|
||||||
|
|
@ -390,6 +410,7 @@ const InstallationsContextProvider = ({
|
||||||
salimax_or_sodistore_Installations,
|
salimax_or_sodistore_Installations,
|
||||||
salidomoInstallations,
|
salidomoInstallations,
|
||||||
sodiohomeInstallations,
|
sodiohomeInstallations,
|
||||||
|
sodistoreGridInstallations,
|
||||||
foldersAndInstallations,
|
foldersAndInstallations,
|
||||||
fetchAllInstallations,
|
fetchAllInstallations,
|
||||||
fetchAllFoldersAndInstallations,
|
fetchAllFoldersAndInstallations,
|
||||||
|
|
@ -416,6 +437,7 @@ const InstallationsContextProvider = ({
|
||||||
salimax_or_sodistore_Installations,
|
salimax_or_sodistore_Installations,
|
||||||
salidomoInstallations,
|
salidomoInstallations,
|
||||||
sodiohomeInstallations,
|
sodiohomeInstallations,
|
||||||
|
sodistoreGridInstallations,
|
||||||
foldersAndInstallations,
|
foldersAndInstallations,
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,12 @@ interface ProductIdContextType {
|
||||||
accessToSalidomo: boolean;
|
accessToSalidomo: boolean;
|
||||||
accessToSodiohome: boolean;
|
accessToSodiohome: boolean;
|
||||||
accessToSodistore: boolean;
|
accessToSodistore: boolean;
|
||||||
|
accessToSodistoreGrid: boolean;
|
||||||
setAccessToSalimax: (access: boolean) => void;
|
setAccessToSalimax: (access: boolean) => void;
|
||||||
setAccessToSalidomo: (access: boolean) => void;
|
setAccessToSalidomo: (access: boolean) => void;
|
||||||
setAccessToSodiohome: (access: boolean) => void;
|
setAccessToSodiohome: (access: boolean) => void;
|
||||||
setAccessToSodistore: (access: boolean) => void;
|
setAccessToSodistore: (access: boolean) => void;
|
||||||
|
setAccessToSodistoreGrid: (access: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the context.
|
// Create the context.
|
||||||
|
|
@ -43,6 +45,10 @@ export const ProductIdContextProvider = ({
|
||||||
const storedValue = localStorage.getItem('accessToSodistore');
|
const storedValue = localStorage.getItem('accessToSodistore');
|
||||||
return storedValue === 'true';
|
return storedValue === 'true';
|
||||||
});
|
});
|
||||||
|
const [accessToSodistoreGrid, setAccessToSodistoreGrid] = useState(() => {
|
||||||
|
const storedValue = localStorage.getItem('accessToSodistoreGrid');
|
||||||
|
return storedValue === 'true';
|
||||||
|
});
|
||||||
// const [product, setProduct] = useState(location.includes('salidomo') ? 1 : 0);
|
// const [product, setProduct] = useState(location.includes('salidomo') ? 1 : 0);
|
||||||
// const [product, setProduct] = useState<number>(
|
// const [product, setProduct] = useState<number>(
|
||||||
// productMapping[Object.keys(productMapping).find((key) => location.includes(key)) || ''] || -1
|
// productMapping[Object.keys(productMapping).find((key) => location.includes(key)) || ''] || -1
|
||||||
|
|
@ -52,6 +58,8 @@ export const ProductIdContextProvider = ({
|
||||||
return 1;
|
return 1;
|
||||||
} else if (location.includes('sodiohome')) {
|
} else if (location.includes('sodiohome')) {
|
||||||
return 2;
|
return 2;
|
||||||
|
} else if (location.includes('sodistoregrid')) {
|
||||||
|
return 4;
|
||||||
} else {
|
} else {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
@ -80,6 +88,10 @@ export const ProductIdContextProvider = ({
|
||||||
setAccessToSodistore(access);
|
setAccessToSodistore(access);
|
||||||
localStorage.setItem('accessToSodistore', JSON.stringify(access));
|
localStorage.setItem('accessToSodistore', JSON.stringify(access));
|
||||||
};
|
};
|
||||||
|
const changeAccessSodistoreGrid = (access: boolean) => {
|
||||||
|
setAccessToSodistoreGrid(access);
|
||||||
|
localStorage.setItem('accessToSodistoreGrid', JSON.stringify(access));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProductIdContext.Provider
|
<ProductIdContext.Provider
|
||||||
|
|
@ -90,10 +102,12 @@ export const ProductIdContextProvider = ({
|
||||||
accessToSalidomo,
|
accessToSalidomo,
|
||||||
accessToSodiohome,
|
accessToSodiohome,
|
||||||
accessToSodistore,
|
accessToSodistore,
|
||||||
|
accessToSodistoreGrid,
|
||||||
setAccessToSalimax: changeAccessSalimax,
|
setAccessToSalimax: changeAccessSalimax,
|
||||||
setAccessToSalidomo: changeAccessSalidomo,
|
setAccessToSalidomo: changeAccessSalidomo,
|
||||||
setAccessToSodiohome: changeAccessSodiohome,
|
setAccessToSodiohome: changeAccessSodiohome,
|
||||||
setAccessToSodistore: changeAccessSodistore
|
setAccessToSodistore: changeAccessSodistore,
|
||||||
|
setAccessToSodistoreGrid: changeAccessSodistoreGrid
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
||||||
|
|
@ -89,8 +89,9 @@ export const transformInputToBatteryViewDataJson = async (
|
||||||
const categories = isSodioHome
|
const categories = isSodioHome
|
||||||
? ['Soc', 'Power', 'Voltage', 'Current', 'Soh']
|
? ['Soc', 'Power', 'Voltage', 'Current', 'Soh']
|
||||||
: ['Soc', 'Temperature', 'Power', 'Voltage', 'Current'];
|
: ['Soc', 'Temperature', 'Power', 'Voltage', 'Current'];
|
||||||
|
// TODO: SodistoreGrid — update data paths when installation data format is finalized
|
||||||
const pathCategories =
|
const pathCategories =
|
||||||
product === 3
|
product === 3 || product === 4
|
||||||
? [
|
? [
|
||||||
'.BatteryDeligreenDataRecord.Soc',
|
'.BatteryDeligreenDataRecord.Soc',
|
||||||
'.BatteryDeligreenDataRecord.TemperaturesList.EnvironmentTemperature',
|
'.BatteryDeligreenDataRecord.TemperaturesList.EnvironmentTemperature',
|
||||||
|
|
@ -167,7 +168,7 @@ export const transformInputToBatteryViewDataJson = async (
|
||||||
);
|
);
|
||||||
|
|
||||||
const adjustedTimestamp =
|
const adjustedTimestamp =
|
||||||
product == 0 || product == 2 || product == 3
|
product == 0 || product == 2 || product == 3 || product == 4
|
||||||
? new Date(timestampArray[i] * 1000)
|
? new Date(timestampArray[i] * 1000)
|
||||||
: new Date(timestampArray[i] * 100000);
|
: new Date(timestampArray[i] * 100000);
|
||||||
//Timezone offset is negative, so we convert the timestamp to the current zone by subtracting the corresponding offset
|
//Timezone offset is negative, so we convert the timestamp to the current zone by subtracting the corresponding offset
|
||||||
|
|
@ -270,7 +271,7 @@ export const transformInputToBatteryViewDataJson = async (
|
||||||
if (battery_nodes.length > old_length) {
|
if (battery_nodes.length > old_length) {
|
||||||
battery_nodes.forEach((node) => {
|
battery_nodes.forEach((node) => {
|
||||||
const node_number =
|
const node_number =
|
||||||
product == 3 ? Number(node) + 1 : Number(node) - 1;
|
product == 3 || product == 4 ? Number(node) + 1 : Number(node) - 1;
|
||||||
if (!pathsToSave.includes('Node' + node_number)) {
|
if (!pathsToSave.includes('Node' + node_number)) {
|
||||||
pathsToSave.push('Node' + node_number);
|
pathsToSave.push('Node' + node_number);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,9 @@
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"live": "Live Daten",
|
"live": "Live Daten",
|
||||||
"deleteInstallation": "Installation löschen",
|
"deleteInstallation": "Installation löschen",
|
||||||
|
"confirmDeleteInstallation": "Möchten Sie diese Installation löschen?",
|
||||||
|
"bucketLabel": "Bucket",
|
||||||
|
"deleteInstallationWarning": "Bitte notieren Sie den Bucket-Namen oben. Das Löschen der S3-Daten kann mehrere Minuten dauern. Überprüfen Sie nach dem Löschen in Exoscale, ob der Bucket entfernt wurde. Falls nicht, leeren und löschen Sie den Bucket manuell.",
|
||||||
"errorOccured": "Ein Fehler ist aufgetreten",
|
"errorOccured": "Ein Fehler ist aufgetreten",
|
||||||
"successfullyUpdated": "Erfolgreich aktualisiert",
|
"successfullyUpdated": "Erfolgreich aktualisiert",
|
||||||
"grantAccess": "Zugriff gewähren",
|
"grantAccess": "Zugriff gewähren",
|
||||||
|
|
@ -155,6 +158,7 @@
|
||||||
"generatingMonthly": "Wird generiert...",
|
"generatingMonthly": "Wird generiert...",
|
||||||
"generatingYearly": "Wird generiert...",
|
"generatingYearly": "Wird generiert...",
|
||||||
"thisMonthWeeklyReports": "Wöchentliche Berichte dieses Monats",
|
"thisMonthWeeklyReports": "Wöchentliche Berichte dieses Monats",
|
||||||
|
"recentWeeklyReports": "Letzte Wochenberichte",
|
||||||
"ai_analyzing": "KI analysiert...",
|
"ai_analyzing": "KI analysiert...",
|
||||||
"ai_show_details": "Details anzeigen",
|
"ai_show_details": "Details anzeigen",
|
||||||
"ai_show_less": "Weniger anzeigen",
|
"ai_show_less": "Weniger anzeigen",
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,9 @@
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"live": "Live View",
|
"live": "Live View",
|
||||||
"deleteInstallation": "Delete Installation",
|
"deleteInstallation": "Delete Installation",
|
||||||
|
"confirmDeleteInstallation": "Do you want to delete this installation?",
|
||||||
|
"bucketLabel": "Bucket",
|
||||||
|
"deleteInstallationWarning": "Please note the bucket name above. Purging S3 data may take several minutes. After deletion, verify in Exoscale that the bucket has been removed. If not, purge and delete the bucket manually.",
|
||||||
"errorOccured": "An error has occurred",
|
"errorOccured": "An error has occurred",
|
||||||
"successfullyUpdated": "Successfully updated",
|
"successfullyUpdated": "Successfully updated",
|
||||||
"grantAccess": "Grant Access",
|
"grantAccess": "Grant Access",
|
||||||
|
|
@ -137,6 +140,7 @@
|
||||||
"generatingMonthly": "Generating...",
|
"generatingMonthly": "Generating...",
|
||||||
"generatingYearly": "Generating...",
|
"generatingYearly": "Generating...",
|
||||||
"thisMonthWeeklyReports": "This Month's Weekly Reports",
|
"thisMonthWeeklyReports": "This Month's Weekly Reports",
|
||||||
|
"recentWeeklyReports": "Recent Weekly Reports",
|
||||||
"ai_analyzing": "AI is analyzing...",
|
"ai_analyzing": "AI is analyzing...",
|
||||||
"ai_show_details": "Show details",
|
"ai_show_details": "Show details",
|
||||||
"ai_show_less": "Show less",
|
"ai_show_less": "Show less",
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,9 @@
|
||||||
"status": "Statut",
|
"status": "Statut",
|
||||||
"live": "Diffusion en direct",
|
"live": "Diffusion en direct",
|
||||||
"deleteInstallation": "Supprimer l'installation",
|
"deleteInstallation": "Supprimer l'installation",
|
||||||
|
"confirmDeleteInstallation": "Voulez-vous supprimer cette installation ?",
|
||||||
|
"bucketLabel": "Bucket",
|
||||||
|
"deleteInstallationWarning": "Veuillez noter le nom du bucket ci-dessus. La purge des données S3 peut prendre plusieurs minutes. Après la suppression, vérifiez dans Exoscale que le bucket a bien été supprimé. Sinon, purgez et supprimez le bucket manuellement.",
|
||||||
"errorOccured": "Une erreur s'est produite",
|
"errorOccured": "Une erreur s'est produite",
|
||||||
"successfullyUpdated": "Mise à jour réussie",
|
"successfullyUpdated": "Mise à jour réussie",
|
||||||
"grantAccess": "Accorder l'accès",
|
"grantAccess": "Accorder l'accès",
|
||||||
|
|
@ -149,6 +152,7 @@
|
||||||
"generatingMonthly": "Génération en cours...",
|
"generatingMonthly": "Génération en cours...",
|
||||||
"generatingYearly": "Génération en cours...",
|
"generatingYearly": "Génération en cours...",
|
||||||
"thisMonthWeeklyReports": "Rapports hebdomadaires de ce mois",
|
"thisMonthWeeklyReports": "Rapports hebdomadaires de ce mois",
|
||||||
|
"recentWeeklyReports": "Derniers rapports hebdomadaires",
|
||||||
"ai_analyzing": "L'IA analyse...",
|
"ai_analyzing": "L'IA analyse...",
|
||||||
"ai_show_details": "Afficher les détails",
|
"ai_show_details": "Afficher les détails",
|
||||||
"ai_show_less": "Afficher moins",
|
"ai_show_less": "Afficher moins",
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,9 @@
|
||||||
"status": "Stato",
|
"status": "Stato",
|
||||||
"live": "Vista in diretta",
|
"live": "Vista in diretta",
|
||||||
"deleteInstallation": "Elimina installazione",
|
"deleteInstallation": "Elimina installazione",
|
||||||
|
"confirmDeleteInstallation": "Vuoi eliminare questa installazione?",
|
||||||
|
"bucketLabel": "Bucket",
|
||||||
|
"deleteInstallationWarning": "Prendi nota del nome del bucket qui sopra. L'eliminazione dei dati S3 potrebbe richiedere diversi minuti. Dopo l'eliminazione, verifica in Exoscale che il bucket sia stato rimosso. In caso contrario, svuota ed elimina il bucket manualmente.",
|
||||||
"errorOccured": "Si è verificato un errore",
|
"errorOccured": "Si è verificato un errore",
|
||||||
"successfullyUpdated": "Aggiornamento riuscito",
|
"successfullyUpdated": "Aggiornamento riuscito",
|
||||||
"grantAccess": "Concedi accesso",
|
"grantAccess": "Concedi accesso",
|
||||||
|
|
@ -160,6 +163,7 @@
|
||||||
"generatingMonthly": "Generazione in corso...",
|
"generatingMonthly": "Generazione in corso...",
|
||||||
"generatingYearly": "Generazione in corso...",
|
"generatingYearly": "Generazione in corso...",
|
||||||
"thisMonthWeeklyReports": "Rapporti settimanali di questo mese",
|
"thisMonthWeeklyReports": "Rapporti settimanali di questo mese",
|
||||||
|
"recentWeeklyReports": "Ultimi rapporti settimanali",
|
||||||
"ai_analyzing": "L'IA sta analizzando...",
|
"ai_analyzing": "L'IA sta analizzando...",
|
||||||
"ai_show_details": "Mostra dettagli",
|
"ai_show_details": "Mostra dettagli",
|
||||||
"ai_show_less": "Mostra meno",
|
"ai_show_less": "Mostra meno",
|
||||||
|
|
|
||||||
|
|
@ -168,7 +168,8 @@ function SidebarMenu() {
|
||||||
accessToSalimax,
|
accessToSalimax,
|
||||||
accessToSodistore,
|
accessToSodistore,
|
||||||
accessToSalidomo,
|
accessToSalidomo,
|
||||||
accessToSodiohome
|
accessToSodiohome,
|
||||||
|
accessToSodistoreGrid
|
||||||
} = useContext(ProductIdContext);
|
} = useContext(ProductIdContext);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -213,7 +214,7 @@ function SidebarMenu() {
|
||||||
<Box sx={{ marginTop: '3px' }}>
|
<Box sx={{ marginTop: '3px' }}>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="sodistore"
|
id="sodistore"
|
||||||
defaultMessage="SodistoreMax"
|
defaultMessage="Sodistore Max"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -242,6 +243,27 @@ function SidebarMenu() {
|
||||||
</List>
|
</List>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{accessToSodistoreGrid && (
|
||||||
|
<List component="div">
|
||||||
|
<ListItem component="div">
|
||||||
|
<Button
|
||||||
|
disableRipple
|
||||||
|
component={RouterLink}
|
||||||
|
onClick={closeSidebar}
|
||||||
|
to="/sodistoregrid_installations"
|
||||||
|
startIcon={<BrightnessLowTwoToneIcon />}
|
||||||
|
>
|
||||||
|
<Box sx={{ marginTop: '3px' }}>
|
||||||
|
<FormattedMessage
|
||||||
|
id="sodistoregrid"
|
||||||
|
defaultMessage="Sodistore Grid"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Button>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
)}
|
||||||
|
|
||||||
{accessToSodiohome && (
|
{accessToSodiohome && (
|
||||||
<List component="div">
|
<List component="div">
|
||||||
<ListItem component="div">
|
<ListItem component="div">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue