Compare commits

..

7 Commits

38 changed files with 697 additions and 198 deletions

View File

@ -200,6 +200,8 @@ public class Controller : ControllerBase
bucketPath = "s3://" + installation.S3BucketId + "-3e5b3069-214a-43ee-8d85-57d72000c19d/" + startTimestamp;
else if (installation.Product == (int)ProductType.SodioHome)
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
bucketPath = "s3://" + installation.S3BucketId + "-c0436b6a-d276-4cd8-9c44-1eae86cf5d0e/" + startTimestamp;
Console.WriteLine("Fetching data for "+startTimestamp);
@ -537,16 +539,29 @@ public class Controller : ControllerBase
public ActionResult<IEnumerable<Installation>> GetAllSodioHomeInstallations(Token authToken)
{
var user = Db.GetSession(authToken)?.User;
if (user is null)
return Unauthorized();
return user
.AccessibleInstallations(product:(int)ProductType.SodioHome)
.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))]
public ActionResult<IEnumerable<Folder>> GetAllFolders(Token authToken)
@ -1513,6 +1528,7 @@ public class Controller : ControllerBase
0 => config.GetConfigurationSalimax(), // Salimax
3 => config.GetConfigurationSodistoreMax(), // SodiStoreMax
2 => config.GetConfigurationSodistoreHome(), // SodiStoreHome
4 => config.GetConfigurationSodistoreGrid(), // SodistoreGrid
_ => config.GetConfigurationString() // fallback
};
@ -1719,6 +1735,17 @@ public class Controller : ControllerBase
"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);
}
}

View File

@ -48,6 +48,12 @@ public class Configuration
$"BatteriesCount: {BatteriesCount}, ClusterNumber: {ClusterNumber}, PvNumber: {PvNumber}, ControlPermission:{ControlPermission}, "+
$"SinexcelTimeChargeandDischargePower: {TimeChargeandDischargePower}, SinexcelStartTimeChargeandDischargeDayandTime: {StartTimeChargeandDischargeDayandTime}, SinexcelStopTimeChargeandDischargeDayandTime: {StopTimeChargeandDischargeDayandTime}";
}
// TODO: SodistoreGrid — update configuration fields when defined
public string GetConfigurationSodistoreGrid()
{
return "";
}
}
public enum CalibrationChargeType

View File

@ -7,7 +7,8 @@ public enum ProductType
Salimax = 0,
Salidomo = 1,
SodioHome =2,
SodiStoreMax=3
SodiStoreMax=3,
SodistoreGrid=4
}
public enum StatusType

View File

@ -145,6 +145,7 @@ public static class ExoCmd
const String method = "iam-role";
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.SodistoreGrid?Db.Installations.Count(f => f.Product == (int)ProductType.SodistoreGrid) + installation.Name:
Db.Installations.Count(f => f.Product == (int)ProductType.Salidomo) + installation.Name;
@ -263,42 +264,70 @@ public static class ExoCmd
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 method = $"access-key/{installation.S3Key}";
var unixtime = DateTimeOffset.UtcNow.ToUnixTimeSeconds()+60;
var authheader = "credential="+S3Credentials.Key+",expires="+unixtime+",signature="+BuildSignature("DELETE", method, unixtime);
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("EXO2-HMAC-SHA256", authheader);
var response = await client.DeleteAsync(url);
return response.IsSuccessStatusCode;
try
{
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)
{
//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 method = $"access-key/{installation.S3WriteKey}";
var unixtime = DateTimeOffset.UtcNow.ToUnixTimeSeconds()+60;
var authheader = "credential="+S3Credentials.Key+",expires="+unixtime+",signature="+BuildSignature("DELETE", method, unixtime);
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("EXO2-HMAC-SHA256", authheader);
var response = await client.DeleteAsync(url);
return response.IsSuccessStatusCode;
try
{
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)
@ -320,6 +349,7 @@ public static class ExoCmd
const String method = "iam-role";
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.SodistoreGrid?Db.Installations.Count(f => f.Product == (int)ProductType.SodistoreGrid) + installation.Name:
Db.Installations.Count(f => f.Product == (int)ProductType.Salidomo) + installation.Name;
var contentString = $$"""
@ -371,10 +401,35 @@ public static class ExoCmd
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!);
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;
}
}

View File

@ -10,6 +10,7 @@ public static class InstallationMethods
private static readonly String BucketNameSalt = "3e5b3069-214a-43ee-8d85-57d72000c19d";
private static readonly String SalidomoBucketNameSalt = "c0436b6a-d276-4cd8-9c44-1eae86cf5d0e";
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)
{
@ -17,12 +18,17 @@ public static class InstallationMethods
{
return $"{installation.S3BucketId}-{BucketNameSalt}";
}
if (installation.Product == (int)ProductType.SodioHome)
{
return $"{installation.S3BucketId}-{SodioHomeBucketNameSalt}";
}
if (installation.Product == (int)ProductType.SodistoreGrid)
{
return $"{installation.S3BucketId}-{SodistoreGridBucketNameSalt}";
}
return $"{installation.S3BucketId}-{SalidomoBucketNameSalt}";
}

View File

@ -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
&& user.UserType != 0
@ -295,9 +295,9 @@ public static class SessionMethods
.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
&& installation is not null
&& original is not null
@ -305,7 +305,7 @@ public static class SessionMethods
&& user.HasAccessTo(installation)
&& installation
.WithParentOf(original) // prevent moving
.Apply(Db.Update);
.Apply(Db.Update);
}
@ -324,23 +324,22 @@ public static class SessionMethods
{
var user = session?.User;
if (user is not null
&& installation is not null
&& user.UserType != 0)
{
return
Db.Delete(installation)
&& await installation.RevokeReadKey()
&& await installation.RevokeWriteKey()
&& await installation.RemoveReadRole()
&& await installation.RemoveWriteRole()
&& await installation.DeleteBucket();
}
if (user is null || installation is null || user.UserType == 0)
return false;
return false;
// Try all Exoscale operations independently (don't short-circuit)
var readKeyOk = await installation.RevokeReadKey();
var writeKeyOk = await installation.RevokeWriteKey();
var readRoleOk = await installation.RemoveReadRole();
var writeRoleOk = await installation.RemoveWriteRole();
var bucketOk = await installation.PurgeAndDeleteBucket();
if (!readKeyOk || !writeKeyOk || !readRoleOk || !writeRoleOk || !bucketOk)
Console.WriteLine($"[Delete] Partial Exoscale cleanup for installation {installation.Id}: " +
$"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)

View File

@ -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 = '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();
CleanupSessions().SupressAwaitWarning();
DeleteSnapshots().SupressAwaitWarning();

View File

@ -102,10 +102,28 @@ public static partial class Db
InstallationAccess.Delete(i => i.InstallationId == installation.Id);
if (installation.Product == (int)ProductType.Salimax)
{
//For Salimax, delete the OrderNumber2Installation entries associated with this 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;
}
}

View File

@ -1,94 +1,157 @@
using System.Diagnostics;
using InnovEnergy.App.Backend.Database;
using InnovEnergy.App.Backend.DataTypes;
using InnovEnergy.Lib.Utils;
using InnovEnergy.App.Backend.DataTypes.Methods;
using InnovEnergy.Lib.S3Utils;
using InnovEnergy.Lib.S3Utils.DataTypes;
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()
{
string configPath = "/home/ubuntu/.s3cfg";
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}");
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);
Console.WriteLine("Deleting data for timestamp prefix: " + timestamps_to_delete);
try
{
ProcessStartInfo startInfo = new ProcessStartInfo
_cleanupTimer = new Timer(
_ =>
{
FileName = "s3cmd",
Arguments = $"--config {configPath} rm {bucketPath}",
RedirectStandardOutput = true,
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--;
}
}
try
{
CleanupAllInstallations().GetAwaiter().GetResult();
}
Console.WriteLine("FINISHED DELETING S3 DATA FOR ALL INSTALLATIONS\n");
await Task.Delay(TimeSpan.FromDays(1));
}
catch (Exception ex)
{
Console.Error.WriteLine($"[S3Cleanup] Scheduler error: {ex.Message}");
}
},
null,
next - now,
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;
}
}

View File

@ -38,7 +38,7 @@ public static class Program
WebsocketManager.MonitorInstallationTable().SupressAwaitWarning();
// Task.Run(() => DeleteOldDataFromS3.DeleteOldData());
DeleteOldDataFromS3.StartScheduler();
builder.Services.AddControllers();
builder.Services.AddProblemDetails(setup =>

View File

@ -16,6 +16,7 @@ public class Session : Relation<String, Int64>
public Boolean AccessToSalidomo { get; set; } = false;
public Boolean AccessToSodistoreMax { get; set; } = false;
public Boolean AccessToSodioHome { get; set; } = false;
public Boolean AccessToSodistoreGrid { get; set; } = false;
[Ignore] public Boolean Valid => DateTime.Now - LastSeen <=MaxAge ;
// Private backing field
@ -49,7 +50,8 @@ public class Session : Relation<String, Int64>
AccessToSalidomo = user.AccessibleInstallations(product: (int)ProductType.Salidomo).ToList().Count > 0;
AccessToSodistoreMax = user.AccessibleInstallations(product: (int)ProductType.SodiStoreMax).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("AccessToSodistoreMax" +user.AccessibleInstallations(product: (int)ProductType.SodiStoreMax).ToList().Count);
Console.WriteLine("sodio" + user.AccessibleInstallations(product: (int)ProductType.SodioHome).ToList().Count);

View File

@ -100,6 +100,11 @@ public static class RabbitMqManager
monitorLink =
$"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
{
monitorLink =
@ -126,7 +131,7 @@ public static class RabbitMqManager
Console.WriteLine("Send replace battery email to the support team for installation "+installationId);
string recipient = "support@innov.energy";
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"+
$"Installation Name: {installation.InstallationName}\n"+
$"\n"+
@ -138,7 +143,7 @@ public static class RabbitMqManager
$"\n"+
$"Thank you for your great support:)";
//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
Db.HandleError(newError, installationId);

View File

@ -30,7 +30,8 @@ public static class WebsocketManager
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.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);

View File

@ -201,10 +201,65 @@ public static class S3
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)
{
var request = new DeleteBucketRequest { BucketName = bucketName };
var response = await region
.GetS3Client()
.DeleteBucketAsync(request);

View File

@ -35,7 +35,8 @@ function App() {
setAccessToSalimax,
setAccessToSalidomo,
setAccessToSodiohome,
setAccessToSodistore
setAccessToSodistore,
setAccessToSodistoreGrid
} = useContext(ProductIdContext);
const [language, setLanguage] = useState<string>(
@ -102,12 +103,15 @@ function App() {
setAccessToSalidomo(response.data.accessToSalidomo);
setAccessToSodiohome(response.data.accessToSodioHome);
setAccessToSodistore(response.data.accessToSodistoreMax);
setAccessToSodistoreGrid(response.data.accessToSodistoreGrid);
if (response.data.accessToSalimax) {
navigate(routes.installations);
} else if (response.data.accessToSalidomo) {
navigate(routes.salidomo_installations);
} else if (response.data.accessToSodistoreMax) {
navigate(routes.sodistore_installations);
} else if (response.data.accessToSodistoreGrid) {
navigate(routes.sodistoregrid_installations);
} else {
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={'*'}

View File

@ -4,6 +4,7 @@
"salidomo_installations": "/salidomo_installations/",
"sodistore_installations": "/sodistore_installations/",
"sodiohome_installations": "/sodiohome_installations/",
"sodistoregrid_installations": "/sodistoregrid_installations/",
"installation": "installation/",
"login": "/login/",
"forgotPassword": "/forgotPassword/",

View File

@ -98,7 +98,7 @@ function Logo() {
const theme = useTheme();
return (
<TooltipWrapper title="InnovEnergy" arrow>
<TooltipWrapper title="inesco Energy" arrow>
<LogoWrapper to="/overview">
<Badge
sx={{

View File

@ -40,7 +40,8 @@ function Login() {
setAccessToSalimax,
setAccessToSalidomo,
setAccessToSodiohome,
setAccessToSodistore
setAccessToSodistore,
setAccessToSodistoreGrid
} = useContext(ProductIdContext);
const navigate = useNavigate();
@ -84,6 +85,7 @@ function Login() {
setAccessToSalidomo(response.data.accessToSalidomo);
setAccessToSodiohome(response.data.accessToSodioHome);
setAccessToSodistore(response.data.accessToSodistoreMax);
setAccessToSodistoreGrid(response.data.accessToSodistoreGrid);
if (rememberMe) {
cookies.set('rememberedUsername', username, { path: '/' });
@ -95,6 +97,8 @@ function Login() {
navigate(routes.salidomo_installations);
} else if (response.data.accessToSodistoreMax) {
navigate(routes.sodistore_installations);
} else if (response.data.accessToSodistoreGrid) {
navigate(routes.sodistoregrid_installations);
} else {
navigate(routes.sodiohome_installations);
}

View File

@ -245,7 +245,7 @@ function BatteryView(props: BatteryViewProps) {
) : (
<TableCell align="center">Max Cell Voltage</TableCell>
)}
{product === 3 && (
{(product === 3 || product === 4) && (
<TableCell align="center">Voltage Difference</TableCell>
)}
</TableRow>
@ -469,7 +469,7 @@ function BatteryView(props: BatteryViewProps) {
</TableCell>
</>
)}
{product === 3 && (
{(product === 3 || product === 4) && (
<>
{(() => {
const cellVoltagesString =
@ -524,7 +524,7 @@ function BatteryView(props: BatteryViewProps) {
})()}
</>
)}
{product === 3 && (
{(product === 3 || product === 4) && (
<>
{(() => {
const cellVoltagesString =

View File

@ -128,7 +128,7 @@ function Information(props: InformationProps) {
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 350,
width: 450,
bgcolor: 'background.paper',
borderRadius: 4,
boxShadow: 24,
@ -143,7 +143,37 @@ function Information(props: InformationProps) {
gutterBottom
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>
<div
@ -328,9 +358,12 @@ function Information(props: InformationProps) {
label="S3 Bucket Name"
name="s3bucketname"
value={
product === 0 || product == 3
formValues.product === 0 || formValues.product == 3
? formValues.s3BucketId +
'-3e5b3069-214a-43ee-8d85-57d72000c19d'
: formValues.product == 4
? formValues.s3BucketId +
'-5109c126-e141-43ab-8658-f3c44c838ae8'
: formValues.s3BucketId +
'-e7b9a240-3c5d-4d2e-a019-6d8b1f7b73fa'
}

View File

@ -119,7 +119,7 @@ function InformationSalidomo(props: InformationSalidomoProps) {
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 350,
width: 450,
bgcolor: 'background.paper',
borderRadius: 4,
boxShadow: 24,
@ -134,7 +134,37 @@ function InformationSalidomo(props: InformationSalidomoProps) {
gutterBottom
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>
<div

View File

@ -193,7 +193,7 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 350,
width: 450,
bgcolor: 'background.paper',
borderRadius: 4,
boxShadow: 24,
@ -208,7 +208,37 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
gutterBottom
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>
<div

View File

@ -57,9 +57,13 @@ function Installation(props: singleInstallationProps) {
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 =
props.current_installation.s3BucketId.toString() +
'-3e5b3069-214a-43ee-8d85-57d72000c19d';
props.current_installation.s3BucketId.toString() + '-' + s3BucketSalt;
const s3Credentials = { s3Bucket, ...S3data };
@ -427,12 +431,15 @@ function Installation(props: singleInstallationProps) {
}
></Route>
<Route
path={routes.pvview + '/*'}
element={
<PvView values={values} connected={connected}></PvView>
}
></Route>
{/* TODO: SodistoreGrid — PV View excluded, add back when data path is ready */}
{props.current_installation.product !== 4 && (
<Route
path={routes.pvview + '/*'}
element={
<PvView values={values} connected={connected}></PvView>
}
></Route>
)}
<Route
path={routes.overview}
@ -447,11 +454,28 @@ function Installation(props: singleInstallationProps) {
<Route
path={routes.live}
element={
<Topology
values={values}
connected={connected}
loading={loading}
></Topology>
props.current_installation.product === 4 ? (
// TODO: SodistoreGrid — implement actual topology layout
<Container
maxWidth="xl"
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
path={routes.configuration}
element={
<Configuration
values={values}
id={props.current_installation.id}
></Configuration>
props.current_installation.product === 4 ? (
// TODO: SodistoreGrid — implement actual configuration
<Container
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>
)
}
/>
)}

View File

@ -39,11 +39,18 @@ function InstallationTabs(props: InstallationTabsProps) {
const {
salimax_or_sodistore_Installations,
sodistoreGridInstallations,
fetchAllInstallations,
socket,
openSocket,
closeSocket
} = 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);
useEffect(() => {
@ -93,7 +100,10 @@ function InstallationTabs(props: InstallationTabsProps) {
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
? [
{
@ -204,7 +214,8 @@ function InstallationTabs(props: InstallationTabsProps) {
<FormattedMessage id="information" defaultMessage="Information" />
)
}
];
]
).filter((tab) => !(hidePvView && tab.value === 'pvview'));
const tabs =
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">
<TabsContainerWrapper>
@ -384,7 +400,7 @@ function InstallationTabs(props: InstallationTabsProps) {
textColor="primary"
indicatorColor="primary"
>
{tabs.map((tab) => (
{filteredTabs.map((tab) => (
<Tab
key={tab.value}
value={tab.value}
@ -415,7 +431,7 @@ function InstallationTabs(props: InstallationTabsProps) {
<Grid item xs={12}>
<Box p={4}>
<InstallationSearch
installations={salimax_or_sodistore_Installations}
installations={installations}
/>
</Box>
</Grid>
@ -428,6 +444,10 @@ function InstallationTabs(props: InstallationTabsProps) {
element={
props.product === 0 ? (
<Navigate to={routes.installations + routes.list} />
) : props.product === 4 ? (
<Navigate
to={routes.sodistoregrid_installations + routes.list}
/>
) : (
<Navigate
to={routes.sodistore_installations + routes.list}
@ -441,7 +461,7 @@ function InstallationTabs(props: InstallationTabsProps) {
</Container>
<Footer />
</>
) : salimax_or_sodistore_Installations.length === 1 ? (
) : installations.length === 1 ? (
<>
<Container maxWidth="xl" sx={{ marginTop: '20px' }} className="mainframe">
<TabsContainerWrapper>
@ -480,7 +500,7 @@ function InstallationTabs(props: InstallationTabsProps) {
<Box p={4}>
<Installation
current_installation={
salimax_or_sodistore_Installations[0]
installations[0]
}
type="installation"
></Installation>

View File

@ -112,11 +112,11 @@ function SodioHomeInstallation(props: singleInstallationProps) {
}
const fetchDataPeriodically = async () => {
var timeperiodToSearch = 200;
var timeperiodToSearch = 350;
let res;
let timestampToFetch;
for (var i = 0; i < timeperiodToSearch; i += 2) {
for (var i = 0; i < timeperiodToSearch; i += 30) {
if (!continueFetching.current) {
return false;
}
@ -184,13 +184,13 @@ function SodioHomeInstallation(props: singleInstallationProps) {
};
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 timestampToFetch;
// Search from NOW backward to find the most recent data
// Step by 10 seconds - balances between finding files quickly and reducing 404s
for (var i = 0; i < timeperiodToSearch; i += 10) {
// Step by 50 seconds - data is uploaded every ~150s, so finer steps are wasteful
for (var i = 0; i < timeperiodToSearch; i += 50) {
timestampToFetch = UnixTime.now().earlier(TimeSpan.fromSeconds(i));
try {
res = await fetchDataJson(timestampToFetch, s3Credentials, false);
@ -264,7 +264,7 @@ function SodioHomeInstallation(props: singleInstallationProps) {
const configRefreshInterval = setInterval(() => {
console.log('Refreshing configuration data from S3...');
fetchDataForOneTime();
}, 15000); // Refresh every 15 seconds
}, 60000); // Refresh every 60 seconds (data uploads every ~150s)
return () => {
continueFetching.current = false;

View File

@ -143,9 +143,9 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
const submittedAt = pendingConfig.submittedAt || 0;
const timeSinceSubmit = Date.now() - submittedAt;
// Within 150 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
if (timeSinceSubmit < 150000) {
// Within 300 seconds of submit: use localStorage (waiting for S3 sync)
// This covers two full S3 upload cycles (150 sec × 2) to ensure new file is available
if (timeSinceSubmit < 300000) {
// Check if S3 now matches - if so, sync is complete
const s3MatchesPending =
s3Values.controlPermission === pendingConfig.values.controlPermission &&
@ -247,7 +247,7 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
setLoading(false);
// 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({
values: formValues,
submittedAt: Date.now()

View File

@ -159,7 +159,10 @@ function FormattedBullet({ text }: { text: string }) {
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) ──────────────────────────────────────────
@ -545,7 +548,7 @@ function WeeklySection({ installationId, latestMonthlyPeriodEnd }: { installatio
</Box>
{report.dailyData.map((d, i) => {
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;
return (
<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>}
</Typography>
<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>
</Box>
<Box sx={{ display: 'flex', gap: '2px', height: 18 }}>
@ -737,7 +740,7 @@ function MonthlySection({
? intl.formatMessage({ id: 'generatingMonthly', defaultMessage: 'Generating...' })
: intl.formatMessage(
{ 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>
@ -752,7 +755,7 @@ function MonthlySection({
<AggregatedSection
reports={reports}
type="monthly"
labelFn={(r: MonthlyReport) => `${MONTH_NAMES[r.month]} ${r.year}`}
labelFn={(r: MonthlyReport) => `${getMonthName(r.month, intl.locale)} ${r.year}`}
countLabelId="weeksAggregated"
countFn={(r: MonthlyReport) => r.weekCount}
sendEndpoint="/SendMonthlyReportEmail"

View File

@ -65,7 +65,12 @@ function TreeInformation(props: TreeInformationProps) {
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;
@ -282,7 +287,7 @@ function TreeInformation(props: TreeInformationProps) {
>
{ProductTypes.map((type) => (
<MenuItem key={type} value={type}>
{type}
{ProductDisplayNames[type] || type}
</MenuItem>
))}
</Select>
@ -323,7 +328,8 @@ function TreeInformation(props: TreeInformationProps) {
)}
{openModalInstallation &&
(product == 'Salimax' ||
product == 'SodistoreMax') && (
product == 'SodistoreMax' ||
product == 'SodistoreGrid') && (
<InstallationForm
cancel={handleFormCancel}
submit={handleInstallationFormSubmit}

View File

@ -80,7 +80,7 @@ function InstallationTree() {
key={installation.id}
path={routes.installation + installation.id + '/*'}
element={
installation.product == 0 || installation.product == 3 ? (
installation.product == 0 || installation.product == 3 || installation.product == 4 ? (
<Installation
key={installation.id}
current_installation={installation}

View File

@ -115,7 +115,7 @@ function Status500() {
<Container maxWidth="sm">
<Box textAlign="center">
<TypographyPrimary variant="h1" sx={{ my: 2 }}>
InnovEnergy{' '}
inesco Energy{' '}
</TypographyPrimary>
<TypographySecondary
variant="h4"

View File

@ -31,6 +31,9 @@ const InstallationsContextProvider = ({
const [sodiohomeInstallations, setSodiohomeInstallations] = useState<
I_Installation[]
>([]);
const [sodistoreGridInstallations, setSodistoreGridInstallations] = useState<
I_Installation[]
>([]);
const [foldersAndInstallations, setFoldersAndInstallations] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
@ -89,16 +92,31 @@ const InstallationsContextProvider = ({
: installation;
});
const updatedSodistoreGrid = sodistoreGridInstallations.map(
(installation) => {
const update = pendingUpdates.current[installation.id];
return update
? {
...installation,
status: update.status,
testingMode: update.testingMode
}
: installation;
}
);
setSalidomoInstallations(updatedSalidomo);
setSalimax_Or_Sodistore_Installations(updatedSalimax);
setSodiohomeInstallations(updatedSodiohome);
setSodistoreGridInstallations(updatedSodistoreGrid);
// Clear the pending updates after applying
pendingUpdates.current = {};
}, [
salidomoInstallations,
salimax_or_sodistore_Installations,
sodiohomeInstallations
sodiohomeInstallations,
sodistoreGridInstallations
]);
useEffect(() => {
@ -172,6 +190,8 @@ const InstallationsContextProvider = ({
setSalidomoInstallations(res.data);
} else if (product === 0 || product === 3) {
setSalimax_Or_Sodistore_Installations(res.data);
} else if (product === 4) {
setSodistoreGridInstallations(res.data);
}
if (open_socket) {
@ -390,6 +410,7 @@ const InstallationsContextProvider = ({
salimax_or_sodistore_Installations,
salidomoInstallations,
sodiohomeInstallations,
sodistoreGridInstallations,
foldersAndInstallations,
fetchAllInstallations,
fetchAllFoldersAndInstallations,
@ -416,6 +437,7 @@ const InstallationsContextProvider = ({
salimax_or_sodistore_Installations,
salidomoInstallations,
sodiohomeInstallations,
sodistoreGridInstallations,
foldersAndInstallations,
loading,
error,

View File

@ -9,10 +9,12 @@ interface ProductIdContextType {
accessToSalidomo: boolean;
accessToSodiohome: boolean;
accessToSodistore: boolean;
accessToSodistoreGrid: boolean;
setAccessToSalimax: (access: boolean) => void;
setAccessToSalidomo: (access: boolean) => void;
setAccessToSodiohome: (access: boolean) => void;
setAccessToSodistore: (access: boolean) => void;
setAccessToSodistoreGrid: (access: boolean) => void;
}
// Create the context.
@ -43,6 +45,10 @@ export const ProductIdContextProvider = ({
const storedValue = localStorage.getItem('accessToSodistore');
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<number>(
// productMapping[Object.keys(productMapping).find((key) => location.includes(key)) || ''] || -1
@ -52,6 +58,8 @@ export const ProductIdContextProvider = ({
return 1;
} else if (location.includes('sodiohome')) {
return 2;
} else if (location.includes('sodistoregrid')) {
return 4;
} else {
return 0;
}
@ -80,6 +88,10 @@ export const ProductIdContextProvider = ({
setAccessToSodistore(access);
localStorage.setItem('accessToSodistore', JSON.stringify(access));
};
const changeAccessSodistoreGrid = (access: boolean) => {
setAccessToSodistoreGrid(access);
localStorage.setItem('accessToSodistoreGrid', JSON.stringify(access));
};
return (
<ProductIdContext.Provider
@ -90,10 +102,12 @@ export const ProductIdContextProvider = ({
accessToSalidomo,
accessToSodiohome,
accessToSodistore,
accessToSodistoreGrid,
setAccessToSalimax: changeAccessSalimax,
setAccessToSalidomo: changeAccessSalidomo,
setAccessToSodiohome: changeAccessSodiohome,
setAccessToSodistore: changeAccessSodistore
setAccessToSodistore: changeAccessSodistore,
setAccessToSodistoreGrid: changeAccessSodistoreGrid
}}
>
{children}

View File

@ -89,8 +89,9 @@ export const transformInputToBatteryViewDataJson = async (
const categories = isSodioHome
? ['Soc', 'Power', 'Voltage', 'Current', 'Soh']
: ['Soc', 'Temperature', 'Power', 'Voltage', 'Current'];
// TODO: SodistoreGrid — update data paths when installation data format is finalized
const pathCategories =
product === 3
product === 3 || product === 4
? [
'.BatteryDeligreenDataRecord.Soc',
'.BatteryDeligreenDataRecord.TemperaturesList.EnvironmentTemperature',
@ -167,7 +168,7 @@ export const transformInputToBatteryViewDataJson = async (
);
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] * 100000);
//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) {
battery_nodes.forEach((node) => {
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)) {
pathsToSave.push('Node' + node_number);
}

View File

@ -71,6 +71,9 @@
"status": "Status",
"live": "Live Daten",
"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",
"successfullyUpdated": "Erfolgreich aktualisiert",
"grantAccess": "Zugriff gewähren",
@ -155,6 +158,7 @@
"generatingMonthly": "Wird generiert...",
"generatingYearly": "Wird generiert...",
"thisMonthWeeklyReports": "Wöchentliche Berichte dieses Monats",
"recentWeeklyReports": "Letzte Wochenberichte",
"ai_analyzing": "KI analysiert...",
"ai_show_details": "Details anzeigen",
"ai_show_less": "Weniger anzeigen",

View File

@ -53,6 +53,9 @@
"status": "Status",
"live": "Live View",
"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",
"successfullyUpdated": "Successfully updated",
"grantAccess": "Grant Access",
@ -137,6 +140,7 @@
"generatingMonthly": "Generating...",
"generatingYearly": "Generating...",
"thisMonthWeeklyReports": "This Month's Weekly Reports",
"recentWeeklyReports": "Recent Weekly Reports",
"ai_analyzing": "AI is analyzing...",
"ai_show_details": "Show details",
"ai_show_less": "Show less",

View File

@ -65,6 +65,9 @@
"status": "Statut",
"live": "Diffusion en direct",
"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",
"successfullyUpdated": "Mise à jour réussie",
"grantAccess": "Accorder l'accès",
@ -149,6 +152,7 @@
"generatingMonthly": "Génération en cours...",
"generatingYearly": "Génération en cours...",
"thisMonthWeeklyReports": "Rapports hebdomadaires de ce mois",
"recentWeeklyReports": "Derniers rapports hebdomadaires",
"ai_analyzing": "L'IA analyse...",
"ai_show_details": "Afficher les détails",
"ai_show_less": "Afficher moins",

View File

@ -53,6 +53,9 @@
"status": "Stato",
"live": "Vista in diretta",
"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",
"successfullyUpdated": "Aggiornamento riuscito",
"grantAccess": "Concedi accesso",
@ -160,6 +163,7 @@
"generatingMonthly": "Generazione in corso...",
"generatingYearly": "Generazione in corso...",
"thisMonthWeeklyReports": "Rapporti settimanali di questo mese",
"recentWeeklyReports": "Ultimi rapporti settimanali",
"ai_analyzing": "L'IA sta analizzando...",
"ai_show_details": "Mostra dettagli",
"ai_show_less": "Mostra meno",

View File

@ -168,7 +168,8 @@ function SidebarMenu() {
accessToSalimax,
accessToSodistore,
accessToSalidomo,
accessToSodiohome
accessToSodiohome,
accessToSodistoreGrid
} = useContext(ProductIdContext);
return (
@ -213,7 +214,7 @@ function SidebarMenu() {
<Box sx={{ marginTop: '3px' }}>
<FormattedMessage
id="sodistore"
defaultMessage="SodistoreMax"
defaultMessage="Sodistore Max"
/>
</Box>
</Button>
@ -242,6 +243,27 @@ function SidebarMenu() {
</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 && (
<List component="div">
<ListItem component="div">