Compare commits

...

16 Commits

Author SHA1 Message Date
Yinyin Liu 812962ace0 sodistore home as default loggin page 2026-03-26 10:31:32 +01:00
Yinyin Liu cc1ec216ee fix main stats of sinexcel battery view 2026-03-26 10:27:54 +01:00
Yinyin Liu 68d057b919 fix main stats of battery view of growatt 2026-03-26 10:21:53 +01:00
Yinyin Liu dfa2f89295 comment last week in Overview and detailed battery view of sodistore home 2026-03-26 10:15:25 +01:00
Yinyin Liu c70cacf179 improve AI and data privacy disclaim 2026-03-26 10:08:59 +01:00
Yinyin Liu 1683ab9b9a make inesco energy as the default naming 2026-03-26 09:33:35 +01:00
Yinyin Liu fd35248b72 reorder products order showing on monitor 2026-03-26 09:17:40 +01:00
Yinyin Liu 41031b3b87 the default Installation Setup details in Information tab of Sodistore Pro should be not expanded 2026-03-26 09:11:13 +01:00
Yinyin Liu 8c58ce45f6 add DC or AC Coupled option for Sodistore Home and Sodistore Pro 2026-03-26 09:08:49 +01:00
Yinyin Liu 2681248bdc fix tree path issues of all products 2026-03-26 08:52:43 +01:00
Yinyin Liu 3521da7a1d add sodistore pro as a new product 2026-03-26 08:05:49 +01:00
Yinyin Liu d59027a277 email the assignee when a ticket is assigend 2026-03-24 15:47:30 +01:00
Yinyin Liu 4bb35c6951 frontend bug fix 2026-03-24 14:38:23 +01:00
Yinyin Liu 0657a5fb82 update sodistore home information tab based on weekly meeting's feedback 2026-03-24 13:42:36 +01:00
Yinyin Liu baaabbecd0 add AI and cookies usage acknowledgement 2026-03-24 12:35:50 +01:00
Yinyin Liu 730f337502 restructure sodistore home Information tab 2026-03-23 13:19:14 +01:00
57 changed files with 1870 additions and 649 deletions

View File

@ -202,6 +202,8 @@ public class Controller : ControllerBase
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) else if (installation.Product == (int)ProductType.SodistoreGrid)
bucketPath = "s3://" + installation.S3BucketId + "-5109c126-e141-43ab-8658-f3c44c838ae8/" + startTimestamp; bucketPath = "s3://" + installation.S3BucketId + "-5109c126-e141-43ab-8658-f3c44c838ae8/" + startTimestamp;
else if (installation.Product == (int)ProductType.SodistorePro)
bucketPath = "s3://" + installation.S3BucketId + "-325c9373-9025-4a8d-bf5a-f9eedf1f155c/" + 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);
@ -815,9 +817,10 @@ public class Controller : ControllerBase
if (installation is null || !user.HasAccessTo(installation)) if (installation is null || !user.HasAccessTo(installation))
return Unauthorized(); return Unauthorized();
// AI diagnostics are scoped to SodistoreHome and SodiStoreMax only // AI diagnostics are scoped to SodistoreHome, SodiStoreMax, and SodistorePro only
if (installation.Product != (int)ProductType.SodioHome && if (installation.Product != (int)ProductType.SodioHome &&
installation.Product != (int)ProductType.SodiStoreMax) installation.Product != (int)ProductType.SodiStoreMax &&
installation.Product != (int)ProductType.SodistorePro)
return BadRequest("AI diagnostics not available for this product."); return BadRequest("AI diagnostics not available for this product.");
var result = await DiagnosticService.DiagnoseAsync(installationId, errorDescription, user.Language ?? "en"); var result = await DiagnosticService.DiagnoseAsync(installationId, errorDescription, user.Language ?? "en");
@ -2134,6 +2137,31 @@ public class Controller : ControllerBase
}); });
} }
var assigneeChanged = ticket.AssigneeId != existing.AssigneeId
&& ticket.AssigneeId.HasValue;
if (assigneeChanged)
{
var assignee = Db.GetUserById(ticket.AssigneeId);
Db.Create(new TicketTimelineEvent
{
TicketId = ticket.Id,
EventType = (Int32)TimelineEventType.Assigned,
Description = $"Ticket assigned to {assignee?.Name ?? "unknown"}.",
ActorType = (Int32)TimelineActorType.Human,
ActorId = user.Id,
CreatedAt = DateTime.UtcNow
});
if (assignee is not null)
_ = Task.Run(async () =>
{
try { await assignee.SendTicketAssignedEmail(ticket); }
catch (Exception ex) { Console.WriteLine($"Failed to send ticket assignment email: {ex}"); }
});
}
return Db.Update(ticket) ? ticket : StatusCode(500, "Update failed."); return Db.Update(ticket) ? ticket : StatusCode(500, "Update failed.");
} }
@ -2309,4 +2337,17 @@ public class Controller : ControllerBase
return Ok(); return Ok();
} }
[HttpPut(nameof(AcknowledgeTerms))]
public ActionResult AcknowledgeTerms(Int32 version, Token authToken)
{
var session = Db.GetSession(authToken);
if (session is null) return Unauthorized();
var user = Db.GetUserById(session.User.Id);
if (user is null) return Unauthorized();
user.AcknowledgedTermsVersion = version;
return Db.Update(user) ? Ok() : StatusCode(500);
}
} }

View File

@ -8,7 +8,8 @@ public enum ProductType
Salidomo = 1, Salidomo = 1,
SodioHome =2, SodioHome =2,
SodiStoreMax=3, SodiStoreMax=3,
SodistoreGrid=4 SodistoreGrid=4,
SodistorePro=5
} }
public enum StatusType public enum StatusType
@ -27,6 +28,13 @@ public class Installation : TreeNode
public String Location { get; set; } = ""; public String Location { get; set; } = "";
public String Region { get; set; } = ""; public String Region { get; set; } = "";
public String Country { get; set; } = ""; public String Country { get; set; } = "";
public String Street { get; set; } = "";
public String PostCode { get; set; } = "";
public String City { get; set; } = "";
public String Canton { get; set; } = "";
public String DistributionPartner { get; set; } = "";
public String InverterFirmwareVersion { get; set; } = "";
public String BatteryFirmwareVersion { get; set; } = "";
public String VpnIp { get; set; } = ""; public String VpnIp { get; set; } = "";
public String InstallationName { get; set; } = ""; public String InstallationName { get; set; } = "";
@ -49,7 +57,11 @@ public class Installation : TreeNode
public int BatteryClusterNumber { get; set; } = 0; public int BatteryClusterNumber { get; set; } = 0;
public int BatteryNumber { get; set; } = 0; public int BatteryNumber { get; set; } = 0;
public string BatterySerialNumbers { get; set; } = ""; public string BatterySerialNumbers { get; set; } = "";
public string PvStringsPerInverter { get; set; } = "";
public string InstallationModel { get; set; } = "";
public string ExternalEms { get; set; } = "No";
public string CouplingType { get; set; } = "DC";
[Ignore] [Ignore]
public String OrderNumbers { get; set; } public String OrderNumbers { get; set; }
public String VrmLink { get; set; } = ""; public String VrmLink { get; set; } = "";

View File

@ -146,6 +146,7 @@ public static class ExoCmd
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: installation.Product==(int)ProductType.SodistoreGrid?Db.Installations.Count(f => f.Product == (int)ProductType.SodistoreGrid) + installation.Name:
installation.Product==(int)ProductType.SodistorePro?Db.Installations.Count(f => f.Product == (int)ProductType.SodistorePro) + installation.Name:
Db.Installations.Count(f => f.Product == (int)ProductType.Salidomo) + installation.Name; Db.Installations.Count(f => f.Product == (int)ProductType.Salidomo) + installation.Name;
@ -350,6 +351,7 @@ public static class ExoCmd
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: installation.Product==(int)ProductType.SodistoreGrid?Db.Installations.Count(f => f.Product == (int)ProductType.SodistoreGrid) + installation.Name:
installation.Product==(int)ProductType.SodistorePro?Db.Installations.Count(f => f.Product == (int)ProductType.SodistorePro) + 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 = $$"""

View File

@ -11,6 +11,7 @@ public static class InstallationMethods
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"; private static readonly String SodistoreGridBucketNameSalt = "5109c126-e141-43ab-8658-f3c44c838ae8";
private static readonly String SodistoreProBucketNameSalt = "325c9373-9025-4a8d-bf5a-f9eedf1f155c";
public static String BucketName(this Installation installation) public static String BucketName(this Installation installation)
{ {
@ -29,6 +30,11 @@ public static class InstallationMethods
return $"{installation.S3BucketId}-{SodistoreGridBucketNameSalt}"; return $"{installation.S3BucketId}-{SodistoreGridBucketNameSalt}";
} }
if (installation.Product == (int)ProductType.SodistorePro)
{
return $"{installation.S3BucketId}-{SodistoreProBucketNameSalt}";
}
return $"{installation.S3BucketId}-{SalidomoBucketNameSalt}"; 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 || installation.Product == (int)ProductType.SodistoreGrid) if (installation.Product == (int)ProductType.SodiStoreMax || installation.Product == (int)ProductType.SodioHome || installation.Product == (int)ProductType.SodistoreGrid || installation.Product == (int)ProductType.SodistorePro)
{ {
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 || installation.Product == (int)ProductType.SodistoreGrid) if (installation.Product == (int)ProductType.SodiStoreMax || installation.Product == (int)ProductType.SodistoreGrid || installation.Product == (int)ProductType.SodistorePro)
{ {
return user is not null return user is not null

View File

@ -243,22 +243,22 @@ public static class UserMethods
var (subject, body) = (user.Language ?? "en") switch var (subject, body) = (user.Language ?? "en") switch
{ {
"de" => ( "de" => (
"Passwort Ihres Inesco Energy Kontos zurücksetzen", "Passwort Ihres inesco energy Kontos zurücksetzen",
$"Sehr geehrte/r {user.Name}\n" + $"Sehr geehrte/r {user.Name}\n" +
$"Um Ihr Passwort zurückzusetzen, öffnen Sie bitte diesen Link: {resetLink}?token={encodedToken}" $"Um Ihr Passwort zurückzusetzen, öffnen Sie bitte diesen Link: {resetLink}?token={encodedToken}"
), ),
"fr" => ( "fr" => (
"Réinitialisation du mot de passe de votre compte Inesco Energy", "Réinitialisation du mot de passe de votre compte inesco energy",
$"Cher/Chère {user.Name}\n" + $"Cher/Chère {user.Name}\n" +
$"Pour réinitialiser votre mot de passe, veuillez ouvrir ce lien : {resetLink}?token={encodedToken}" $"Pour réinitialiser votre mot de passe, veuillez ouvrir ce lien : {resetLink}?token={encodedToken}"
), ),
"it" => ( "it" => (
"Reimposta la password del tuo account Inesco Energy", "Reimposta la password del tuo account inesco energy",
$"Gentile {user.Name}\n" + $"Gentile {user.Name}\n" +
$"Per reimpostare la password, apra questo link: {resetLink}?token={encodedToken}" $"Per reimpostare la password, apra questo link: {resetLink}?token={encodedToken}"
), ),
_ => ( _ => (
"Reset the password of your Inesco Energy Account", "Reset the password of your inesco energy Account",
$"Dear {user.Name}\n" + $"Dear {user.Name}\n" +
$"To reset your password please open this link: {resetLink}?token={encodedToken}" $"To reset your password please open this link: {resetLink}?token={encodedToken}"
) )
@ -274,28 +274,89 @@ public static class UserMethods
var (subject, body) = (user.Language ?? "en") switch var (subject, body) = (user.Language ?? "en") switch
{ {
"de" => ( "de" => (
"Ihr neues Inesco Energy Konto", "Ihr neues inesco energy Konto",
$"Sehr geehrte/r {user.Name}\n" + $"Sehr geehrte/r {user.Name}\n" +
$"Um Ihr Passwort festzulegen und sich bei Ihrem Inesco Energy Konto anzumelden, öffnen Sie bitte diesen Link: {resetLink}" $"Um Ihr Passwort festzulegen und sich bei Ihrem inesco energy Konto anzumelden, öffnen Sie bitte diesen Link: {resetLink}"
), ),
"fr" => ( "fr" => (
"Votre nouveau compte Inesco Energy", "Votre nouveau compte inesco energy",
$"Cher/Chère {user.Name}\n" + $"Cher/Chère {user.Name}\n" +
$"Pour définir votre mot de passe et vous connecter à votre compte Inesco Energy, veuillez ouvrir ce lien : {resetLink}" $"Pour définir votre mot de passe et vous connecter à votre compte inesco energy, veuillez ouvrir ce lien : {resetLink}"
), ),
"it" => ( "it" => (
"Il tuo nuovo account Inesco Energy", "Il tuo nuovo account inesco energy",
$"Gentile {user.Name}\n" + $"Gentile {user.Name}\n" +
$"Per impostare la password e accedere al suo account Inesco Energy, apra questo link: {resetLink}" $"Per impostare la password e accedere al suo account inesco energy, apra questo link: {resetLink}"
), ),
_ => ( _ => (
"Your new Inesco Energy Account", "Your new inesco energy Account",
$"Dear {user.Name}\n" + $"Dear {user.Name}\n" +
$"To set your password and log in to your Inesco Energy Account open this link: {resetLink}" $"To set your password and log in to your inesco energy Account open this link: {resetLink}"
) )
}; };
return user.SendEmail(subject, body); return user.SendEmail(subject, body);
} }
public static Task SendTicketAssignedEmail(this User user, Ticket ticket)
{
var ticketLink = $"https://monitor.inesco.energy/tickets/{ticket.Id}";
var priority = (TicketPriority)ticket.Priority;
var category = (TicketCategory)ticket.Category;
var (subject, body) = (user.Language ?? "en") switch
{
"de" => (
$"inesco energy Ticket #{ticket.Id} wurde Ihnen zugewiesen",
$"Sehr geehrte/r {user.Name},\n\n" +
$"Ein Ticket wurde Ihnen zugewiesen:\n\n" +
$"Ticket: #{ticket.Id}\n" +
$"Betreff: {ticket.Subject}\n" +
$"Priorität: {priority}\n" +
$"Kategorie: {category}\n\n" +
$"Beschreibung:\n{ticket.Description}\n\n" +
$"Öffnen Sie das Ticket hier: {ticketLink}\n\n" +
"Mit freundlichen Grüssen\ninesco energy Monitor"
),
"fr" => (
$"inesco energy Le ticket #{ticket.Id} vous a été attribué",
$"Cher/Chère {user.Name},\n\n" +
$"Un ticket vous a été attribué :\n\n" +
$"Ticket : #{ticket.Id}\n" +
$"Objet : {ticket.Subject}\n" +
$"Priorité : {priority}\n" +
$"Catégorie : {category}\n\n" +
$"Description :\n{ticket.Description}\n\n" +
$"Ouvrir le ticket : {ticketLink}\n\n" +
"Cordialement,\ninesco energy Monitor"
),
"it" => (
$"inesco energy Il ticket #{ticket.Id} le è stato assegnato",
$"Gentile {user.Name},\n\n" +
$"Le è stato assegnato un ticket:\n\n" +
$"Ticket: #{ticket.Id}\n" +
$"Oggetto: {ticket.Subject}\n" +
$"Priorità: {priority}\n" +
$"Categoria: {category}\n\n" +
$"Descrizione:\n{ticket.Description}\n\n" +
$"Aprire il ticket: {ticketLink}\n\n" +
"Cordiali saluti,\ninesco energy Monitor"
),
_ => (
$"inesco energy Ticket #{ticket.Id} has been assigned to you",
$"Dear {user.Name},\n\n" +
$"A ticket has been assigned to you:\n\n" +
$"Ticket: #{ticket.Id}\n" +
$"Subject: {ticket.Subject}\n" +
$"Priority: {priority}\n" +
$"Category: {category}\n\n" +
$"Description:\n{ticket.Description}\n\n" +
$"Open the ticket: {ticketLink}\n\n" +
"Best regards,\ninesco energy Monitor"
)
};
return user.SendEmail(subject, body);
}
} }

View File

@ -11,6 +11,7 @@ public class User : TreeNode
public Boolean MustResetPassword { get; set; } = false; public Boolean MustResetPassword { get; set; } = false;
public String? Password { get; set; } = null!; public String? Password { get; set; } = null!;
public String Language { get; set; } = "en"; public String Language { get; set; } = "en";
public Int32? AcknowledgedTermsVersion { get; set; }
[Unique] [Unique]
public override String Name { get; set; } = null!; public override String Name { get; set; } = null!;

View File

@ -83,10 +83,12 @@ 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 // One-time migration: rebrand to inesco energy
Connection.Execute("UPDATE Folder SET Name = 'inesco Energy' WHERE Name = 'InnovEnergy'"); Connection.Execute("UPDATE Folder SET Name = 'inesco energy' WHERE Name = 'InnovEnergy'");
Connection.Execute("UPDATE Folder SET Name = 'inesco energy' WHERE Name = 'inesco Energy'");
Connection.Execute("UPDATE Folder SET Name = 'Sodistore Max Installations' WHERE Name = 'SodistoreMax Installations'"); 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'"); Connection.Execute("UPDATE User SET Name = 'inesco energy Master Admin' WHERE Name = 'InnovEnergy Master Admin'");
Connection.Execute("UPDATE User SET Name = 'inesco energy Master Admin' WHERE Name = 'inesco Energy Master Admin'");
//UpdateKeys(); //UpdateKeys();
CleanupSessions().SupressAwaitWarning(); CleanupSessions().SupressAwaitWarning();
@ -130,6 +132,11 @@ public static partial class Db
fileConnection.CreateTable<TicketAiDiagnosis>(); fileConnection.CreateTable<TicketAiDiagnosis>();
fileConnection.CreateTable<TicketTimelineEvent>(); fileConnection.CreateTable<TicketTimelineEvent>();
// Migrate new columns: set defaults for existing rows where NULL or empty
fileConnection.Execute("UPDATE Installation SET ExternalEms = 'No' WHERE ExternalEms IS NULL OR ExternalEms = ''");
fileConnection.Execute("UPDATE Installation SET InstallationModel = '' WHERE InstallationModel IS NULL");
fileConnection.Execute("UPDATE Installation SET PvStringsPerInverter = '' WHERE PvStringsPerInverter IS NULL");
return fileConnection; return fileConnection;
//return CopyDbToMemory(fileConnection); //return CopyDbToMemory(fileConnection);
} }

View File

@ -3,6 +3,6 @@
"SmtpUsername" : "no-reply@inesco.ch", "SmtpUsername" : "no-reply@inesco.ch",
"SmtpPassword" : "1ci4vi%+bfccIp", "SmtpPassword" : "1ci4vi%+bfccIp",
"SmtpPort" : 587, "SmtpPort" : 587,
"SenderName" : "Inesco Energy", "SenderName" : "inesco energy",
"SenderAddress" : "no-reply@inesco.ch" "SenderAddress" : "no-reply@inesco.ch"
} }

View File

@ -17,6 +17,7 @@ public class Session : Relation<String, Int64>
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; public Boolean AccessToSodistoreGrid { get; set; } = false;
public Boolean AccessToSodistorePro { 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
@ -51,6 +52,7 @@ public class Session : Relation<String, Int64>
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; AccessToSodistoreGrid = user.AccessibleInstallations(product: (int)ProductType.SodistoreGrid).ToList().Count > 0;
AccessToSodistorePro = user.AccessibleInstallations(product: (int)ProductType.SodistorePro).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);

View File

@ -1223,7 +1223,7 @@ textarea:focus{outline:none;border-color:#3498db}
<div id="app"></div> <div id="app"></div>
<div class="nav" id="nav"></div> <div class="nav" id="nav"></div>
<div class="thankyou">Vielen Dank für Ihre Zeit Ihr Beitrag macht einen echten Unterschied für unsere Kunden. 🙏</div> <div class="thankyou">Vielen Dank für Ihre Zeit Ihr Beitrag macht einen echten Unterschied für unsere Kunden. 🙏</div>
<div style="text-align:center;padding-bottom:24px;font-size:11px;color:#bbb">inesco Energy Monitor</div> <div style="text-align:center;padding-bottom:24px;font-size:11px;color:#bbb">inesco energy Monitor</div>
<script> <script>
var ALARMS = %%ALARMS_JSON%%; var ALARMS = %%ALARMS_JSON%%;
var SUBMIT_URL = "%%SUBMIT_URL%%"; var SUBMIT_URL = "%%SUBMIT_URL%%";
@ -1473,7 +1473,7 @@ render();
<p style="margin-bottom:0;font-size:13px;color:#555">Vielen Dank für Ihre Zeit Ihr Beitrag macht einen echten Unterschied für unsere Kunden. 🙏</p> <p style="margin-bottom:0;font-size:13px;color:#555">Vielen Dank für Ihre Zeit Ihr Beitrag macht einen echten Unterschied für unsere Kunden. 🙏</p>
</td></tr> </td></tr>
<tr><td style="background:#ecf0f1;padding:12px 28px;text-align:center;font-size:11px;color:#888;border-top:1px solid #ddd"> <tr><td style="background:#ecf0f1;padding:12px 28px;text-align:center;font-size:11px;color:#888;border-top:1px solid #ddd">
inesco Energy Monitor inesco energy Monitor
</td></tr> </td></tr>
</table></td></tr></table></body></html> </table></td></tr></table></body></html>
"""; """;
@ -1545,7 +1545,7 @@ render();
<p>Hallo <strong>{name}</strong>,</p> <p>Hallo <strong>{name}</strong>,</p>
<p style="margin-top:12px">Kurze Erinnerung die heutige Alarmprüfung <strong>(Stapel {batch.BatchNumber})</strong> schließt um <strong>8:00 Uhr morgen früh</strong>. Es dauert nur 10 Minuten!</p> <p style="margin-top:12px">Kurze Erinnerung die heutige Alarmprüfung <strong>(Stapel {batch.BatchNumber})</strong> schließt um <strong>8:00 Uhr morgen früh</strong>. Es dauert nur 10 Minuten!</p>
<p style="margin:20px 0"><a href="{reviewUrl}" style="background:#3498db;color:#fff;text-decoration:none;padding:10px 24px;border-radius:6px;font-size:13px;display:inline-block">Überprüfung abschließen </a></p> <p style="margin:20px 0"><a href="{reviewUrl}" style="background:#3498db;color:#fff;text-decoration:none;padding:10px 24px;border-radius:6px;font-size:13px;display:inline-block">Überprüfung abschließen </a></p>
<p style="font-size:11px;color:#bbb">inesco Energy Monitor</p> <p style="font-size:11px;color:#bbb">inesco energy Monitor</p>
</body></html> </body></html>
"""; """;
await SendEmailAsync(email, subject, html); await SendEmailAsync(email, subject, html);
@ -1645,7 +1645,7 @@ render();
<table style="border-collapse:collapse;width:100%"> <table style="border-collapse:collapse;width:100%">
{beforeAfterRows} {beforeAfterRows}
</table> </table>
<p style="margin-top:18px;font-size:11px;color:#bbb">inesco Energy Monitor</p> <p style="margin-top:18px;font-size:11px;color:#bbb">inesco energy Monitor</p>
</body></html> </body></html>
"""; """;

View File

@ -50,7 +50,7 @@ public static class DailyIngestionService
Console.WriteLine($"[DailyIngestion] Starting ingestion for all SodioHome installations..."); Console.WriteLine($"[DailyIngestion] Starting ingestion for all SodioHome installations...");
var installations = Db.Installations var installations = Db.Installations
.Where(i => i.Product == (Int32)ProductType.SodioHome && i.Device != 3) // Skip Growatt (device=3) .Where(i => (i.Product == (Int32)ProductType.SodioHome || i.Product == (Int32)ProductType.SodistorePro) && i.Device != 3) // Skip Growatt (device=3)
.ToList(); .ToList();
foreach (var installation in installations) foreach (var installation in installations)

View File

@ -106,7 +106,7 @@ public static class ReportAggregationService
Console.WriteLine("[ReportAggregation] Running Monday weekly report generation..."); Console.WriteLine("[ReportAggregation] Running Monday weekly report generation...");
var installations = Db.Installations var installations = Db.Installations
.Where(i => i.Product == (Int32)ProductType.SodioHome && i.Device != 3) // Skip Growatt (device=3) .Where(i => (i.Product == (Int32)ProductType.SodioHome || i.Product == (Int32)ProductType.SodistorePro) && i.Device != 3) // Skip Growatt (device=3)
.ToList(); .ToList();
var generated = 0; var generated = 0;
@ -364,12 +364,15 @@ public static class ReportAggregationService
var installationName = installation?.Name ?? $"Installation {installationId}"; var installationName = installation?.Name ?? $"Installation {installationId}";
var monthName = new DateTime(year, month, 1).ToString("MMMM yyyy"); var monthName = new DateTime(year, month, 1).ToString("MMMM yyyy");
var weatherCity = !string.IsNullOrWhiteSpace(installation?.City) ? installation.City : installation?.Location;
var weatherRegion = !string.IsNullOrWhiteSpace(installation?.Canton) ? installation.Canton : installation?.Region;
var aiInsight = await GenerateMonthlyAiInsightAsync( var aiInsight = await GenerateMonthlyAiInsightAsync(
installationName, monthName, days.Count, installationName, monthName, days.Count,
totalPv, totalConsump, totalGridIn, totalGridOut, totalPv, totalConsump, totalGridIn, totalGridOut,
totalBattChg, totalBattDis, energySaved, savingsCHF, totalBattChg, totalBattDis, energySaved, savingsCHF,
selfSufficiency, batteryEff, language, selfSufficiency, batteryEff, language,
installation?.Location, installation?.Country, installation?.Region); weatherCity, installation?.Country, weatherRegion);
var monthlySummary = new MonthlyReportSummary var monthlySummary = new MonthlyReportSummary
{ {
@ -591,6 +594,8 @@ public static class ReportAggregationService
var installationName = installation?.Name var installationName = installation?.Name
?? $"Installation {report.InstallationId}"; ?? $"Installation {report.InstallationId}";
var monthName = new DateTime(report.Year, report.Month, 1).ToString("MMMM yyyy"); var monthName = new DateTime(report.Year, report.Month, 1).ToString("MMMM yyyy");
var weatherCity = !string.IsNullOrWhiteSpace(installation?.City) ? installation.City : installation?.Location;
var weatherRegion = !string.IsNullOrWhiteSpace(installation?.Canton) ? installation.Canton : installation?.Region;
return GetOrGenerateInsightAsync("monthly", report.Id, language, return GetOrGenerateInsightAsync("monthly", report.Id, language,
() => GenerateMonthlyAiInsightAsync( () => GenerateMonthlyAiInsightAsync(
installationName, monthName, report.WeekCount, installationName, monthName, report.WeekCount,
@ -599,7 +604,7 @@ public static class ReportAggregationService
report.TotalBatteryCharged, report.TotalBatteryDischarged, report.TotalBatteryCharged, report.TotalBatteryDischarged,
report.TotalEnergySaved, report.TotalSavingsCHF, report.TotalEnergySaved, report.TotalSavingsCHF,
report.SelfSufficiencyPercent, report.BatteryEfficiencyPercent, language, report.SelfSufficiencyPercent, report.BatteryEfficiencyPercent, language,
installation?.Location, installation?.Country, installation?.Region)); weatherCity, installation?.Country, weatherRegion));
} }
/// <summary>Cached-or-generated AI insight for a stored YearlyReportSummary.</summary> /// <summary>Cached-or-generated AI insight for a stored YearlyReportSummary.</summary>

View File

@ -120,7 +120,7 @@ public static class ReportEmailService
GridIn: "Netz Ein", GridIn: "Netz Ein",
GridOut: "Netz Aus", GridOut: "Netz Aus",
BattInOut: "Batt. Laden/Entl.", BattInOut: "Batt. Laden/Entl.",
Footer: "Erstellt von <strong style=\"color:#666\">inesco Energy Monitor</strong>", Footer: "Erstellt von <strong style=\"color:#666\">inesco energy Monitor</strong>",
FooterLink: "Detaillierte Berichte ansehen auf monitor.inesco.energy" FooterLink: "Detaillierte Berichte ansehen auf monitor.inesco.energy"
), ),
"fr" => new EmailStrings( "fr" => new EmailStrings(
@ -151,7 +151,7 @@ public static class ReportEmailService
GridIn: "Réseau Ent.", GridIn: "Réseau Ent.",
GridOut: "Réseau Sor.", GridOut: "Réseau Sor.",
BattInOut: "Batt. Ch./Déch.", BattInOut: "Batt. Ch./Déch.",
Footer: "Généré par <strong style=\"color:#666\">inesco Energy Monitor</strong>", Footer: "Généré par <strong style=\"color:#666\">inesco energy Monitor</strong>",
FooterLink: "Consultez vos rapports détaillés sur monitor.inesco.energy" FooterLink: "Consultez vos rapports détaillés sur monitor.inesco.energy"
), ),
"it" => new EmailStrings( "it" => new EmailStrings(
@ -182,7 +182,7 @@ public static class ReportEmailService
GridIn: "Rete Ent.", GridIn: "Rete Ent.",
GridOut: "Rete Usc.", GridOut: "Rete Usc.",
BattInOut: "Batt. Car./Sc.", BattInOut: "Batt. Car./Sc.",
Footer: "Generato da <strong style=\"color:#666\">inesco Energy Monitor</strong>", Footer: "Generato da <strong style=\"color:#666\">inesco energy Monitor</strong>",
FooterLink: "Visualizza i tuoi report dettagliati su monitor.inesco.energy" FooterLink: "Visualizza i tuoi report dettagliati su monitor.inesco.energy"
), ),
_ => new EmailStrings( _ => new EmailStrings(
@ -213,7 +213,7 @@ public static class ReportEmailService
GridIn: "Grid In", GridIn: "Grid In",
GridOut: "Grid Out", GridOut: "Grid Out",
BattInOut: "Batt. Ch./Dis.", BattInOut: "Batt. Ch./Dis.",
Footer: "Generated by <strong style=\"color:#666\">inesco Energy Monitor</strong>", Footer: "Generated by <strong style=\"color:#666\">inesco energy Monitor</strong>",
FooterLink: "View your detailed reports at monitor.inesco.energy" FooterLink: "View your detailed reports at monitor.inesco.energy"
) )
}; };
@ -340,7 +340,7 @@ public static class ReportEmailService
<!-- Header --> <!-- Header -->
<tr> <tr>
<td style=""background:#2c3e50;padding:24px 30px;color:#ffffff""> <td style=""background:#2c3e50;padding:24px 30px;color:#ffffff"">
<img src=""{LogoBase64}"" alt=""inesco Energy"" style=""height:36px;margin-bottom:12px"" /> <img src=""{LogoBase64}"" alt=""inesco energy"" style=""height:36px;margin-bottom:12px"" />
<div style=""font-size:20px;font-weight:bold"">{s.Title}</div> <div style=""font-size:20px;font-weight:bold"">{s.Title}</div>
<div style=""font-size:14px;margin-top:6px;opacity:0.9"">{r.InstallationName}</div> <div style=""font-size:14px;margin-top:6px;opacity:0.9"">{r.InstallationName}</div>
<div style=""font-size:13px;margin-top:2px;opacity:0.7"">{r.PeriodStart} {r.PeriodEnd}</div> <div style=""font-size:13px;margin-top:2px;opacity:0.7"">{r.PeriodStart} {r.PeriodEnd}</div>
@ -546,56 +546,56 @@ public static class ReportEmailService
"Kennzahl", "Gesamt", "PV-Produktion", "Verbrauch", "Netzbezug", "Netzeinspeisung", "Batterie Laden / Entladen", "Kennzahl", "Gesamt", "PV-Produktion", "Verbrauch", "Netzbezug", "Netzeinspeisung", "Batterie Laden / Entladen",
"Energie gespart", "Solar + Batterie, nicht vom Netz", "Geschätzte Ersparnis", "bei 0.39 CHF/kWh", "Energie gespart", "Solar + Batterie, nicht vom Netz", "Geschätzte Ersparnis", "bei 0.39 CHF/kWh",
"Energieunabhängigkeit", "aus eigenem Solar + Batterie System", "Batterie-Eff.", "Entladung vs. Ladung", "Energieunabhängigkeit", "aus eigenem Solar + Batterie System", "Batterie-Eff.", "Entladung vs. Ladung",
"Tage aggregiert", "Erstellt von <strong style=\"color:#666\">inesco Energy Monitor</strong>", "Tage aggregiert", "Erstellt von <strong style=\"color:#666\">inesco energy Monitor</strong>",
"Detaillierte Berichte ansehen auf monitor.inesco.energy"), "Detaillierte Berichte ansehen auf monitor.inesco.energy"),
("de", "yearly") => new AggregatedEmailStrings( ("de", "yearly") => new AggregatedEmailStrings(
"Jährlicher Leistungsbericht", "Jährliche Erkenntnisse", "Jährliche Zusammenfassung", "Ihre Ersparnisse dieses Jahr", "Jährlicher Leistungsbericht", "Jährliche Erkenntnisse", "Jährliche Zusammenfassung", "Ihre Ersparnisse dieses Jahr",
"Kennzahl", "Gesamt", "PV-Produktion", "Verbrauch", "Netzbezug", "Netzeinspeisung", "Batterie Laden / Entladen", "Kennzahl", "Gesamt", "PV-Produktion", "Verbrauch", "Netzbezug", "Netzeinspeisung", "Batterie Laden / Entladen",
"Energie gespart", "Solar + Batterie, nicht vom Netz", "Geschätzte Ersparnis", "bei 0.39 CHF/kWh", "Energie gespart", "Solar + Batterie, nicht vom Netz", "Geschätzte Ersparnis", "bei 0.39 CHF/kWh",
"Energieunabhängigkeit", "aus eigenem Solar + Batterie System", "Batterie-Eff.", "Entladung vs. Ladung", "Energieunabhängigkeit", "aus eigenem Solar + Batterie System", "Batterie-Eff.", "Entladung vs. Ladung",
"Monate aggregiert", "Erstellt von <strong style=\"color:#666\">inesco Energy Monitor</strong>", "Monate aggregiert", "Erstellt von <strong style=\"color:#666\">inesco energy Monitor</strong>",
"Detaillierte Berichte ansehen auf monitor.inesco.energy"), "Detaillierte Berichte ansehen auf monitor.inesco.energy"),
("fr", "monthly") => new AggregatedEmailStrings( ("fr", "monthly") => new AggregatedEmailStrings(
"Rapport de performance mensuel", "Aperçus du mois", "Résumé du mois", "Vos économies ce mois", "Rapport de performance mensuel", "Aperçus du mois", "Résumé du mois", "Vos économies ce mois",
"Indicateur", "Total", "Production PV", "Consommation", "Import réseau", "Export réseau", "Batterie Charge / Décharge", "Indicateur", "Total", "Production PV", "Consommation", "Import réseau", "Export réseau", "Batterie Charge / Décharge",
"Énergie économisée", "solaire + batterie, non achetée au réseau", "Économies estimées", "à 0.39 CHF/kWh", "Énergie économisée", "solaire + batterie, non achetée au réseau", "Économies estimées", "à 0.39 CHF/kWh",
"Indépendance énergétique", "de votre système solaire + batterie", "Eff. batterie", "décharge vs charge", "Indépendance énergétique", "de votre système solaire + batterie", "Eff. batterie", "décharge vs charge",
"jours agrégés", "Généré par <strong style=\"color:#666\">inesco Energy Monitor</strong>", "jours agrégés", "Généré par <strong style=\"color:#666\">inesco energy Monitor</strong>",
"Consultez vos rapports détaillés sur monitor.inesco.energy"), "Consultez vos rapports détaillés sur monitor.inesco.energy"),
("fr", "yearly") => new AggregatedEmailStrings( ("fr", "yearly") => new AggregatedEmailStrings(
"Rapport de performance annuel", "Aperçus de l'année", "Résumé de l'année", "Vos économies cette année", "Rapport de performance annuel", "Aperçus de l'année", "Résumé de l'année", "Vos économies cette année",
"Indicateur", "Total", "Production PV", "Consommation", "Import réseau", "Export réseau", "Batterie Charge / Décharge", "Indicateur", "Total", "Production PV", "Consommation", "Import réseau", "Export réseau", "Batterie Charge / Décharge",
"Énergie économisée", "solaire + batterie, non achetée au réseau", "Économies estimées", "à 0.39 CHF/kWh", "Énergie économisée", "solaire + batterie, non achetée au réseau", "Économies estimées", "à 0.39 CHF/kWh",
"Indépendance énergétique", "de votre système solaire + batterie", "Eff. batterie", "décharge vs charge", "Indépendance énergétique", "de votre système solaire + batterie", "Eff. batterie", "décharge vs charge",
"mois agrégés", "Généré par <strong style=\"color:#666\">inesco Energy Monitor</strong>", "mois agrégés", "Généré par <strong style=\"color:#666\">inesco energy Monitor</strong>",
"Consultez vos rapports détaillés sur monitor.inesco.energy"), "Consultez vos rapports détaillés sur monitor.inesco.energy"),
("it", "monthly") => new AggregatedEmailStrings( ("it", "monthly") => new AggregatedEmailStrings(
"Rapporto mensile delle prestazioni", "Approfondimenti mensili", "Riepilogo mensile", "I tuoi risparmi questo mese", "Rapporto mensile delle prestazioni", "Approfondimenti mensili", "Riepilogo mensile", "I tuoi risparmi questo mese",
"Metrica", "Totale", "Produzione PV", "Consumo", "Import dalla rete", "Export nella rete", "Batteria Carica / Scarica", "Metrica", "Totale", "Produzione PV", "Consumo", "Import dalla rete", "Export nella rete", "Batteria Carica / Scarica",
"Energia risparmiata", "solare + batteria, non acquistata dalla rete", "Risparmio stimato", "a 0.39 CHF/kWh", "Energia risparmiata", "solare + batteria, non acquistata dalla rete", "Risparmio stimato", "a 0.39 CHF/kWh",
"Indipendenza energetica", "dal proprio impianto solare + batteria", "Eff. batteria", "scarica vs carica", "Indipendenza energetica", "dal proprio impianto solare + batteria", "Eff. batteria", "scarica vs carica",
"giorni aggregati", "Generato da <strong style=\"color:#666\">inesco Energy Monitor</strong>", "giorni aggregati", "Generato da <strong style=\"color:#666\">inesco energy Monitor</strong>",
"Visualizza i tuoi report dettagliati su monitor.inesco.energy"), "Visualizza i tuoi report dettagliati su monitor.inesco.energy"),
("it", "yearly") => new AggregatedEmailStrings( ("it", "yearly") => new AggregatedEmailStrings(
"Rapporto annuale delle prestazioni", "Approfondimenti annuali", "Riepilogo annuale", "I tuoi risparmi quest'anno", "Rapporto annuale delle prestazioni", "Approfondimenti annuali", "Riepilogo annuale", "I tuoi risparmi quest'anno",
"Metrica", "Totale", "Produzione PV", "Consumo", "Import dalla rete", "Export nella rete", "Batteria Carica / Scarica", "Metrica", "Totale", "Produzione PV", "Consumo", "Import dalla rete", "Export nella rete", "Batteria Carica / Scarica",
"Energia risparmiata", "solare + batteria, non acquistata dalla rete", "Risparmio stimato", "a 0.39 CHF/kWh", "Energia risparmiata", "solare + batteria, non acquistata dalla rete", "Risparmio stimato", "a 0.39 CHF/kWh",
"Indipendenza energetica", "dal proprio impianto solare + batteria", "Eff. batteria", "scarica vs carica", "Indipendenza energetica", "dal proprio impianto solare + batteria", "Eff. batteria", "scarica vs carica",
"mesi aggregati", "Generato da <strong style=\"color:#666\">inesco Energy Monitor</strong>", "mesi aggregati", "Generato da <strong style=\"color:#666\">inesco energy Monitor</strong>",
"Visualizza i tuoi report dettagliati su monitor.inesco.energy"), "Visualizza i tuoi report dettagliati su monitor.inesco.energy"),
(_, "monthly") => new AggregatedEmailStrings( (_, "monthly") => new AggregatedEmailStrings(
"Monthly Performance Report", "Monthly Insights", "Monthly Summary", "Your Savings This Month", "Monthly Performance Report", "Monthly Insights", "Monthly Summary", "Your Savings This Month",
"Metric", "Total", "PV Production", "Consumption", "Grid Import", "Grid Export", "Battery Charge / Discharge", "Metric", "Total", "PV Production", "Consumption", "Grid Import", "Grid Export", "Battery Charge / Discharge",
"Energy Saved", "solar + battery, not bought from grid", "Est. Money Saved", "at 0.39 CHF/kWh", "Energy Saved", "solar + battery, not bought from grid", "Est. Money Saved", "at 0.39 CHF/kWh",
"Energy Independence", "from your own solar + battery system", "Battery Eff.", "discharge vs charge", "Energy Independence", "from your own solar + battery system", "Battery Eff.", "discharge vs charge",
"days aggregated", "Generated by <strong style=\"color:#666\">inesco Energy Monitor</strong>", "days aggregated", "Generated by <strong style=\"color:#666\">inesco energy Monitor</strong>",
"View your detailed reports at monitor.inesco.energy"), "View your detailed reports at monitor.inesco.energy"),
_ => new AggregatedEmailStrings( _ => new AggregatedEmailStrings(
"Annual Performance Report", "Annual Insights", "Annual Summary", "Your Savings This Year", "Annual Performance Report", "Annual Insights", "Annual Summary", "Your Savings This Year",
"Metric", "Total", "PV Production", "Consumption", "Grid Import", "Grid Export", "Battery Charge / Discharge", "Metric", "Total", "PV Production", "Consumption", "Grid Import", "Grid Export", "Battery Charge / Discharge",
"Energy Saved", "solar + battery, not bought from grid", "Est. Money Saved", "at 0.39 CHF/kWh", "Energy Saved", "solar + battery, not bought from grid", "Est. Money Saved", "at 0.39 CHF/kWh",
"Energy Independence", "from your own solar + battery system", "Battery Eff.", "discharge vs charge", "Energy Independence", "from your own solar + battery system", "Battery Eff.", "discharge vs charge",
"months aggregated", "Generated by <strong style=\"color:#666\">inesco Energy Monitor</strong>", "months aggregated", "Generated by <strong style=\"color:#666\">inesco energy Monitor</strong>",
"View your detailed reports at monitor.inesco.energy") "View your detailed reports at monitor.inesco.energy")
}; };
@ -637,7 +637,7 @@ public static class ReportEmailService
<!-- Header --> <!-- Header -->
<tr> <tr>
<td style=""background:#2c3e50;padding:24px 30px;color:#ffffff""> <td style=""background:#2c3e50;padding:24px 30px;color:#ffffff"">
<img src=""{LogoBase64}"" alt=""inesco Energy"" style=""height:36px;margin-bottom:12px"" /> <img src=""{LogoBase64}"" alt=""inesco energy"" style=""height:36px;margin-bottom:12px"" />
<div style=""font-size:20px;font-weight:bold"">{dailyTitle}</div> <div style=""font-size:20px;font-weight:bold"">{dailyTitle}</div>
<div style=""font-size:14px;margin-top:6px;opacity:0.9"">{installationName}</div> <div style=""font-size:14px;margin-top:6px;opacity:0.9"">{installationName}</div>
<div style=""font-size:13px;margin-top:2px;opacity:0.7"">{record.Date}</div> <div style=""font-size:13px;margin-top:2px;opacity:0.7"">{record.Date}</div>
@ -723,7 +723,7 @@ public static class ReportEmailService
<!-- Header --> <!-- Header -->
<tr> <tr>
<td style=""background:#2c3e50;padding:24px 30px;color:#ffffff""> <td style=""background:#2c3e50;padding:24px 30px;color:#ffffff"">
<img src=""{LogoBase64}"" alt=""inesco Energy"" style=""height:36px;margin-bottom:12px"" /> <img src=""{LogoBase64}"" alt=""inesco energy"" style=""height:36px;margin-bottom:12px"" />
<div style=""font-size:20px;font-weight:bold"">{s.Title}</div> <div style=""font-size:20px;font-weight:bold"">{s.Title}</div>
<div style=""font-size:14px;margin-top:6px;opacity:0.9"">{installationName}</div> <div style=""font-size:14px;margin-top:6px;opacity:0.9"">{installationName}</div>
<div style=""font-size:13px;margin-top:2px;opacity:0.7"">{periodStart} {periodEnd}</div> <div style=""font-size:13px;margin-top:2px;opacity:0.7"">{periodStart} {periodEnd}</div>

View File

@ -179,9 +179,9 @@ public static class WeeklyReportService
// 4. Get installation location for weather forecast // 4. Get installation location for weather forecast
var installation = Db.GetInstallationById(installationId); var installation = Db.GetInstallationById(installationId);
var location = installation?.Location; var location = !string.IsNullOrWhiteSpace(installation?.City) ? installation.City : installation?.Location;
var country = installation?.Country; var country = installation?.Country;
var region = installation?.Region; var region = !string.IsNullOrWhiteSpace(installation?.Canton) ? installation.Canton : installation?.Region;
Console.WriteLine($"[WeeklyReportService] Installation {installationId}: Location='{location}', Region='{region}', Country='{country}', HourlyRecords={currentHourlyData.Count}"); Console.WriteLine($"[WeeklyReportService] Installation {installationId}: Location='{location}', Region='{region}', Country='{country}', HourlyRecords={currentHourlyData.Count}");
return await GenerateReportFromDataAsync( return await GenerateReportFromDataAsync(

View File

@ -105,6 +105,11 @@ public static class RabbitMqManager
monitorLink = monitorLink =
$"https://monitor.inesco.energy/sodistoregrid_installations/list/installation/{installation.S3BucketId}/batteryview"; $"https://monitor.inesco.energy/sodistoregrid_installations/list/installation/{installation.S3BucketId}/batteryview";
} }
else if (installation.Product == (int)ProductType.SodistorePro)
{
monitorLink =
$"https://monitor.inesco.energy/sodistorepro_installations/list/installation/{installation.S3BucketId}/batteryview";
}
else else
{ {
monitorLink = monitorLink =
@ -131,7 +136,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.Name}: 2 or more strings broken"; string subject = $"Battery Alarm from {installation.Name}: 2 or more strings broken";
string text = $"Dear inesco Energy Support Team,\n" + string text = $"Dear inesco energy Support Team,\n" +
$"\n"+ $"\n"+
$"Installation Name: {installation.Name}\n"+ $"Installation Name: {installation.Name}\n"+
$"\n"+ $"\n"+
@ -143,7 +148,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("inesco Energy 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);

View File

@ -31,7 +31,8 @@ public static class WebsocketManager
(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)) (installationConnection.Value.Product == (int)ProductType.SodistoreGrid && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(2)) ||
(installationConnection.Value.Product == (int)ProductType.SodistorePro && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(4))
) )
{ {
Console.WriteLine("Installation ID is " + installationConnection.Key); Console.WriteLine("Installation ID is " + installationConnection.Key);

View File

@ -13,7 +13,7 @@
href="https://fonts.googleapis.com/css2?family=Inter:ital,wght@0,400&display=swap" href="https://fonts.googleapis.com/css2?family=Inter:ital,wght@0,400&display=swap"
rel="stylesheet" rel="stylesheet"
/> />
<title>Inesco Energy</title> <title>inesco energy</title>
</head> </head>
<body> <body>

View File

@ -34,15 +34,41 @@ function App() {
const searchParams = new URLSearchParams(location.search); const searchParams = new URLSearchParams(location.search);
const username = searchParams.get('username'); const username = searchParams.get('username');
const { const {
accessToSalimax,
accessToSodiohome,
accessToSodistore,
accessToSodistoreGrid,
accessToSodistorePro,
setAccessToSalimax, setAccessToSalimax,
setAccessToSalidomo, setAccessToSalidomo,
setAccessToSodiohome, setAccessToSodiohome,
setAccessToSodistore, setAccessToSodistore,
setAccessToSodistoreGrid setAccessToSodistoreGrid,
setAccessToSodistorePro
} = useContext(ProductIdContext); } = useContext(ProductIdContext);
const defaultRoute = accessToSodiohome
? routes.sodiohome_installations
: accessToSodistorePro
? routes.sodistorepro_installations
: accessToSodistoreGrid
? routes.sodistoregrid_installations
: accessToSodistore
? routes.sodistore_installations
: accessToSalimax
? routes.installations
: routes.salidomo_installations;
const detectBrowserLanguage = (): string => {
const browserLang = navigator.language?.toLowerCase() || '';
if (browserLang.startsWith('de')) return 'de';
if (browserLang.startsWith('fr')) return 'fr';
if (browserLang.startsWith('it')) return 'it';
return 'en';
};
const [language, setLanguage] = useState<string>( const [language, setLanguage] = useState<string>(
() => localStorage.getItem('language') || currentUser?.language || 'en' () => localStorage.getItem('language') || currentUser?.language || detectBrowserLanguage()
); );
const onSelectLanguage = (lang: string) => { const onSelectLanguage = (lang: string) => {
@ -94,11 +120,19 @@ function App() {
const Login = Loader(lazy(() => import('src/components/login'))); const Login = Loader(lazy(() => import('src/components/login')));
const Users = Loader(lazy(() => import('src/content/dashboards/Users'))); const Users = Loader(lazy(() => import('src/content/dashboards/Users')));
const loginToResetPassword = () => { useEffect(() => {
if (!username || token) return;
axiosConfigWithoutToken axiosConfigWithoutToken
.post('/Login', null, { params: { username, password: '' } }) .post('/Login', null, { params: { username, password: '' } })
.then((response) => { .then((response) => {
if (response.data && response.data.token) { if (response.data && response.data.token) {
// Clear the username param from URL to prevent re-login loops
const url = new URL(window.location.href);
url.searchParams.delete('username');
url.searchParams.delete('reset');
window.history.replaceState({}, '', url.pathname);
setNewToken(response.data.token); setNewToken(response.data.token);
setUser(response.data.user); setUser(response.data.user);
setAccessToSalimax(response.data.accessToSalimax); setAccessToSalimax(response.data.accessToSalimax);
@ -106,25 +140,11 @@ function App() {
setAccessToSodiohome(response.data.accessToSodioHome); setAccessToSodiohome(response.data.accessToSodioHome);
setAccessToSodistore(response.data.accessToSodistoreMax); setAccessToSodistore(response.data.accessToSodistoreMax);
setAccessToSodistoreGrid(response.data.accessToSodistoreGrid); setAccessToSodistoreGrid(response.data.accessToSodistoreGrid);
if (response.data.accessToSalimax) { setAccessToSodistorePro(response.data.accessToSodistorePro);
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);
}
} }
}) })
.catch(() => {}); .catch(() => {});
}; }, [username]);
if (username) {
loginToResetPassword();
}
if (!token) { if (!token) {
return ( return (
@ -158,8 +178,14 @@ function App() {
if (token && currentUser?.mustResetPassword) { if (token && currentUser?.mustResetPassword) {
return ( return (
<ThemeProvider> <ThemeProvider>
<CssBaseline /> <IntlProvider
<SetNewPassword></SetNewPassword> messages={getTranslations()}
locale={language}
defaultLocale="en"
>
<CssBaseline />
<SetNewPassword></SetNewPassword>
</IntlProvider>
</ThemeProvider> </ThemeProvider>
); );
} }
@ -177,11 +203,11 @@ function App() {
<Routes> <Routes>
<Route <Route
path={''} path={''}
element={<Navigate to={routes.installations}></Navigate>} element={<Navigate to={defaultRoute}></Navigate>}
></Route> ></Route>
<Route <Route
path={'login'} path={'login'}
element={<Navigate to={routes.installations}></Navigate>} element={<Navigate to={defaultRoute}></Navigate>}
></Route> ></Route>
<Route <Route
path="/" path="/"
@ -228,6 +254,15 @@ function App() {
} }
/> />
<Route
path={routes.sodistorepro_installations + '*'}
element={
<AccessContextProvider>
<SodioHomeInstallationTabs product={5} />
</AccessContextProvider>
}
/>
<Route <Route
path={routes.sodistoregrid_installations + '*'} path={routes.sodistoregrid_installations + '*'}
element={ element={
@ -241,7 +276,7 @@ function App() {
<Route path={routes.users + '*'} element={<Users />} /> <Route path={routes.users + '*'} element={<Users />} />
<Route <Route
path={'*'} path={'*'}
element={<Navigate to={routes.installations}></Navigate>} element={<Navigate to={defaultRoute}></Navigate>}
></Route> ></Route>
<Route path="ResetPassword" element={<ResetPassword />}></Route> <Route path="ResetPassword" element={<ResetPassword />}></Route>
</Route> </Route>

View File

@ -5,6 +5,7 @@
"sodistore_installations": "/sodistore_installations/", "sodistore_installations": "/sodistore_installations/",
"sodiohome_installations": "/sodiohome_installations/", "sodiohome_installations": "/sodiohome_installations/",
"sodistoregrid_installations": "/sodistoregrid_installations/", "sodistoregrid_installations": "/sodistoregrid_installations/",
"sodistorepro_installations": "/sodistorepro_installations/",
"installation": "installation/", "installation": "installation/",
"login": "/login/", "login": "/login/",
"forgotPassword": "/forgotPassword/", "forgotPassword": "/forgotPassword/",

View File

@ -0,0 +1,95 @@
import React from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
Typography,
Divider,
Box
} from '@mui/material';
import { FormattedMessage } from 'react-intl';
export const CURRENT_TERMS_VERSION = 2;
interface AcknowledgementDialogProps {
open: boolean;
onAcknowledge: () => void;
}
const AcknowledgementDialog: React.FC<AcknowledgementDialogProps> = ({
open,
onAcknowledge
}) => {
return (
<Dialog open={open} maxWidth="sm" fullWidth>
<DialogTitle>
<FormattedMessage
id="terms_dialog_title"
defaultMessage="Welcome to inesco energy"
/>
</DialogTitle>
<DialogContent dividers>
<Box mb={2}>
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
<FormattedMessage
id="terms_data_heading"
defaultMessage="Your Data"
/>
</Typography>
<Typography variant="body2">
<FormattedMessage
id="terms_data_body"
defaultMessage="Your installation data is securely stored in Switzerland. We do not share your data with third parties."
/>
</Typography>
</Box>
<Divider />
<Box my={2}>
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
<FormattedMessage
id="terms_ai_heading"
defaultMessage="AI-Powered Insights"
/>
</Typography>
<Typography variant="body2">
<FormattedMessage
id="terms_ai_body"
defaultMessage="We use an AI service hosted in the EU to provide diagnostics and insights for your installations. AI-generated results are recommendations and should be verified by qualified personnel."
/>
</Typography>
</Box>
<Divider />
<Box mt={2}>
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
<FormattedMessage
id="terms_cookies_heading"
defaultMessage="Browser Storage"
/>
</Typography>
<Typography variant="body2">
<FormattedMessage
id="terms_cookies_body"
defaultMessage="This platform stores login and preference settings in your browser to keep you signed in and remember your language choice."
/>
</Typography>
</Box>
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={onAcknowledge}>
<FormattedMessage
id="terms_acknowledge_button"
defaultMessage="I understand"
/>
</Button>
</DialogActions>
</Dialog>
);
};
export default AcknowledgementDialog;

View File

@ -0,0 +1,110 @@
import React from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
Typography,
Divider,
Box
} from '@mui/material';
import { FormattedMessage } from 'react-intl';
interface DataPrivacyDialogProps {
open: boolean;
onClose: () => void;
}
const DataPrivacyDialog: React.FC<DataPrivacyDialogProps> = ({
open,
onClose
}) => {
return (
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
<DialogTitle>
<FormattedMessage
id="privacy_dialog_title"
defaultMessage="Data & Privacy"
/>
</DialogTitle>
<DialogContent dividers>
<Box mb={2}>
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
<FormattedMessage
id="privacy_data_heading"
defaultMessage="Where is my data stored?"
/>
</Typography>
<Typography variant="body2">
<FormattedMessage
id="privacy_data_body"
defaultMessage="Your installation data is stored on servers in Switzerland. Only authorized inesco energy personnel can access your data for support purposes."
/>
</Typography>
</Box>
<Divider />
<Box my={2}>
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
<FormattedMessage
id="privacy_ai_heading"
defaultMessage="How is AI used?"
/>
</Typography>
<Typography variant="body2">
<FormattedMessage
id="privacy_ai_body"
defaultMessage="We use an AI service hosted in the European Union to analyze your installation data and provide diagnostic insights. The AI processes technical data such as battery readings and error codes. AI recommendations should always be verified by qualified personnel."
/>
</Typography>
</Box>
<Divider />
<Box my={2}>
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
<FormattedMessage
id="privacy_browser_heading"
defaultMessage="What does my browser store?"
/>
</Typography>
<Typography variant="body2">
<FormattedMessage
id="privacy_browser_body"
defaultMessage="Your browser stores your login session to keep you signed in, and your language and theme preferences. No tracking or advertising cookies are used."
/>
</Typography>
</Box>
<Divider />
<Box mt={2}>
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
<FormattedMessage
id="privacy_access_heading"
defaultMessage="Who has access to my data?"
/>
</Typography>
<Typography variant="body2">
<FormattedMessage
id="privacy_access_body"
defaultMessage="Your data is not shared with third parties. It is used solely to operate the platform and provide you with insights about your installations."
/>
</Typography>
</Box>
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={onClose}>
<FormattedMessage
id="privacy_close_button"
defaultMessage="Close"
/>
</Button>
</DialogActions>
</Dialog>
);
};
export default DataPrivacyDialog;

View File

@ -18,7 +18,7 @@ function Footer() {
> >
<Box> <Box>
<Typography variant="subtitle1"> <Typography variant="subtitle1">
&copy; 2025 - Inesco Energy AG &copy; 2025 - inesco energy AG
</Typography> </Typography>
</Box> </Box>
<Typography <Typography
@ -33,7 +33,7 @@ function Footer() {
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
Inesco Energy AG inesco energy AG
</Link> </Link>
</Typography> </Typography>
</Box> </Box>

View File

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

View File

@ -42,7 +42,8 @@ function Login() {
setAccessToSalidomo, setAccessToSalidomo,
setAccessToSodiohome, setAccessToSodiohome,
setAccessToSodistore, setAccessToSodistore,
setAccessToSodistoreGrid setAccessToSodistoreGrid,
setAccessToSodistorePro
} = useContext(ProductIdContext); } = useContext(ProductIdContext);
const navigate = useNavigate(); const navigate = useNavigate();
@ -86,16 +87,19 @@ function Login() {
setAccessToSodiohome(response.data.accessToSodioHome); setAccessToSodiohome(response.data.accessToSodioHome);
setAccessToSodistore(response.data.accessToSodistoreMax); setAccessToSodistore(response.data.accessToSodistoreMax);
setAccessToSodistoreGrid(response.data.accessToSodistoreGrid); setAccessToSodistoreGrid(response.data.accessToSodistoreGrid);
if (response.data.accessToSalimax) { setAccessToSodistorePro(response.data.accessToSodistorePro);
navigate(routes.installations); if (response.data.accessToSodioHome) {
} else if (response.data.accessToSalidomo) { navigate(routes.sodiohome_installations);
navigate(routes.salidomo_installations); } else if (response.data.accessToSodistorePro) {
} else if (response.data.accessToSodistoreMax) { navigate(routes.sodistorepro_installations);
navigate(routes.sodistore_installations);
} else if (response.data.accessToSodistoreGrid) { } else if (response.data.accessToSodistoreGrid) {
navigate(routes.sodistoregrid_installations); navigate(routes.sodistoregrid_installations);
} else if (response.data.accessToSodistoreMax) {
navigate(routes.sodistore_installations);
} else if (response.data.accessToSalimax) {
navigate(routes.installations);
} else { } else {
navigate(routes.sodiohome_installations); navigate(routes.salidomo_installations);
} }
} }
}) })

View File

@ -240,12 +240,13 @@ function BatteryViewSodioHome(props: BatteryViewSodioHomeProps) {
align="center" align="center"
sx={{ fontWeight: 'bold' }} sx={{ fontWeight: 'bold' }}
> >
<Link {/* Detailed battery view commented out */}
{/*<Link
style={{ color: 'black' }} style={{ color: 'black' }}
to={routes.detailed_view + BatteryId} to={routes.detailed_view + BatteryId}
> >*/}
{'Battery Cluster ' + BatteryId} {'Battery Cluster ' + BatteryId}
</Link> {/*</Link>*/}
</TableCell> </TableCell>
<TableCell <TableCell
sx={{ sx={{

View File

@ -151,22 +151,25 @@ function MainStatsSodioHome(props: MainStatsSodioHomeProps) {
pathsToSearch.push('Node' + i); pathsToSearch.push('Node' + i);
} }
const total = pathsToSearch.length;
let i = 0; let i = 0;
pathsToSearch.forEach((devicePath) => { pathsToSearch.forEach((devicePath) => {
if ( if (
Object.hasOwnProperty.call(chartData[category].data, devicePath) && Object.hasOwnProperty.call(chartData[category].data, devicePath) &&
chartData[category].data[devicePath].data.length != 0 chartData[category].data[devicePath].data.length != 0
) { ) {
// Spread color indices evenly across the palette for better contrast
const colorIndex = total <= 1 ? 0 : Math.round(i * 9 / (total - 1));
series.push({ series.push({
...chartData[category].data[devicePath], ...chartData[category].data[devicePath],
color: color:
color === 'blue' color === 'blue'
? blueColors[i] ? blueColors[colorIndex]
: color === 'red' : color === 'red'
? redColors[i] ? redColors[colorIndex]
: color === 'green' : color === 'green'
? greenColors[i] ? greenColors[colorIndex]
: orangeColors[i] : orangeColors[colorIndex]
}); });
} }
i++; i++;

View File

@ -57,7 +57,7 @@ function Information(props: InformationProps) {
const canEdit = currentUser.userType == UserType.admin; const canEdit = currentUser.userType == UserType.admin;
const isPartner = currentUser.userType == UserType.partner; const isPartner = currentUser.userType == UserType.partner;
const isSodistore = formValues.product === 3 || formValues.product === 4; const isSodistore = formValues.product === 3 || formValues.product === 4 || formValues.product === 5;
const [networkProviders, setNetworkProviders] = useState<string[]>([]); const [networkProviders, setNetworkProviders] = useState<string[]>([]);
const [loadingProviders, setLoadingProviders] = useState(false); const [loadingProviders, setLoadingProviders] = useState(false);
@ -426,12 +426,18 @@ function Information(props: InformationProps) {
label="S3 Bucket Name" label="S3 Bucket Name"
name="s3bucketname" name="s3bucketname"
value={ value={
formValues.product === 0 || formValues.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.product === 4
? formValues.s3BucketId + ? formValues.s3BucketId +
'-5109c126-e141-43ab-8658-f3c44c838ae8' '-5109c126-e141-43ab-8658-f3c44c838ae8'
: formValues.product === 5
? formValues.s3BucketId +
'-325c9373-9025-4a8d-bf5a-f9eedf1f155c'
: formValues.product === 1
? formValues.s3BucketId +
'-c0436b6a-d276-4cd8-9c44-1eae86cf5d0e'
: formValues.s3BucketId + : formValues.s3BucketId +
'-e7b9a240-3c5d-4d2e-a019-6d8b1f7b73fa' '-e7b9a240-3c5d-4d2e-a019-6d8b1f7b73fa'
} }

View File

@ -0,0 +1,128 @@
export const SODIOHOME_DEVICE_TYPES = [
{ id: 3, name: 'Growatt' },
{ id: 4, name: 'inesco 12K - WR Hybrid' }
] as const;
export const getDeviceTypeName = (deviceId: number): string =>
SODIOHOME_DEVICE_TYPES.find(d => d.id === deviceId)?.name ?? '';
// [inverter][cluster] = batteryCount
export type PresetConfig = number[][];
// 3D array: [inverter][cluster][batteryIndex] = serialNumber
export type BatterySnTree = string[][][];
export const INSTALLATION_PRESETS: Record<string, PresetConfig> = {
'sodistore home 9': [[1, 1]],
'sodistore home 18': [[2, 2]],
'sodistore home 27': [[2, 2], [1, 1]],
'sodistore home 36': [[2, 2], [2, 2]],
};
export const buildSodistoreProPreset = (inverterCount: number): PresetConfig =>
Array.from({ length: inverterCount }, () => [2, 2]);
export const parseSodistoreProInverterCount = (model: string): number => {
const n = parseInt(model, 10);
return isNaN(n) || n < 1 ? 1 : n;
};
export const buildEmptyTree = (preset: PresetConfig): BatterySnTree => {
return preset.map((inv) =>
inv.map((batteryCount) => Array.from({ length: batteryCount }, () => ''))
);
};
export const parseBatterySnTree = (
raw: string,
preset: PresetConfig
): BatterySnTree => {
if (!raw || raw.trim() === '') {
return buildEmptyTree(preset);
}
const isStructured = raw.includes('/') || raw.includes('|');
if (isStructured) {
const inverters = raw.split('/');
return preset.map((invPreset, invIdx) => {
const clusterStr = inverters[invIdx] || '';
const clusters = clusterStr ? clusterStr.split('|') : [];
return invPreset.map((batteryCount, clIdx) => {
const batteries = clusters[clIdx]
? clusters[clIdx].split(',').map((s) => s.trim())
: [];
return Array.from({ length: batteryCount }, (_, i) => batteries[i] || '');
});
});
}
// Legacy flat format: distribute by preset layout
const allSns = raw
.split(',')
.map((s) => s.trim())
.filter((s) => s !== '');
let idx = 0;
return preset.map((inv) =>
inv.map((batteryCount) =>
Array.from({ length: batteryCount }, () => allSns[idx++] || '')
)
);
};
export const serializeBatterySnTree = (tree: BatterySnTree): string => {
return tree
.map((inv) => inv.map((cluster) => cluster.join(',')).join('|'))
.join('/');
};
export const remapTree = (
oldTree: BatterySnTree,
newPreset: PresetConfig
): BatterySnTree => {
return newPreset.map((inv, invIdx) =>
inv.map((batteryCount, clIdx) =>
Array.from(
{ length: batteryCount },
(_, batIdx) => oldTree[invIdx]?.[clIdx]?.[batIdx] || ''
)
)
);
};
export const computeFlatValues = (
preset: PresetConfig,
tree: BatterySnTree
) => {
const totalBatteries = preset.flat().reduce((a, b) => a + b, 0);
const totalClusters = preset.reduce((sum, inv) => sum + inv.length, 0);
return {
batteryNumber: totalBatteries,
batteryClusterNumber: totalClusters,
batterySerialNumbers: serializeBatterySnTree(tree),
};
};
export const wouldLoseData = (
oldTree: BatterySnTree,
newPreset: PresetConfig
): boolean => {
for (let invIdx = 0; invIdx < oldTree.length; invIdx++) {
for (let clIdx = 0; clIdx < (oldTree[invIdx] || []).length; clIdx++) {
for (
let batIdx = 0;
batIdx < (oldTree[invIdx][clIdx] || []).length;
batIdx++
) {
const sn = oldTree[invIdx][clIdx][batIdx];
if (sn && sn.trim() !== '') {
if (invIdx >= newPreset.length) return true;
if (clIdx >= (newPreset[invIdx] || []).length) return true;
const newBatCount = newPreset[invIdx]?.[clIdx] ?? 0;
if (batIdx >= newBatCount) return true;
}
}
}
}
return false;
};

View File

@ -131,7 +131,7 @@ export const fetchAggregatedDataJson = (
} else if (r.status === 200) { } else if (r.status === 200) {
const jsontext = await r.text(); const jsontext = await r.text();
if (product === 2) { if (product === 2 || product === 5) {
return parseSinexcelAggregatedData(jsontext); return parseSinexcelAggregatedData(jsontext);
} }

View File

@ -458,6 +458,10 @@ function InstallationTabs(props: InstallationTabsProps) {
<Navigate <Navigate
to={routes.sodistoregrid_installations + routes.list} to={routes.sodistoregrid_installations + routes.list}
/> />
) : props.product === 5 ? (
<Navigate
to={routes.sodistorepro_installations + routes.list}
/>
) : ( ) : (
<Navigate <Navigate
to={routes.sodistore_installations + routes.list} to={routes.sodistore_installations + routes.list}

View File

@ -248,9 +248,9 @@ function Log(props: LogProps) {
if (source === 'KnowledgeBase') if (source === 'KnowledgeBase')
return <Chip label="Knowledge Base" size="small" sx={{ bgcolor: '#1976d2', color: '#fff', fontWeight: 'bold' }} />; return <Chip label="Knowledge Base" size="small" sx={{ bgcolor: '#1976d2', color: '#fff', fontWeight: 'bold' }} />;
if (source === 'MistralAI') if (source === 'MistralAI')
return <Chip label="Mistral AI" size="small" sx={{ bgcolor: '#7b1fa2', color: '#fff', fontWeight: 'bold' }} />; return <Chip label="AI" size="small" sx={{ bgcolor: '#7b1fa2', color: '#fff', fontWeight: 'bold' }} />;
if (source === 'MistralFailed') if (source === 'MistralFailed')
return <Chip label="Mistral failed" size="small" color="error" />; return <Chip label="AI failed" size="small" color="error" />;
return <Chip label="Not available" size="small" color="default" />; return <Chip label="Not available" size="small" color="default" />;
}; };
@ -288,7 +288,7 @@ function Log(props: LogProps) {
onChange={e => { setDemoAlarm(e.target.value); setDemoResult(null); }} onChange={e => { setDemoAlarm(e.target.value); setDemoResult(null); }}
sx={{ minWidth: 260 }} sx={{ minWidth: 260 }}
> >
<ListSubheader>Sinexcel</ListSubheader> <ListSubheader>inesco 12K - WR Hybrid</ListSubheader>
{DEMO_ALARMS.sinexcel.map(a => ( {DEMO_ALARMS.sinexcel.map(a => (
<MenuItem key={a} value={a}>{a}</MenuItem> <MenuItem key={a} value={a}>{a}</MenuItem>
))} ))}

View File

@ -107,13 +107,15 @@ function UserAccess(props: UserAccessProps) {
const fetchAvailableInstallations = useCallback(async () => { const fetchAvailableInstallations = useCallback(async () => {
try { try {
const [res0, res1, res2, res3] = await Promise.all([ const [res0, res1, res2, res3, res4, res5] = await Promise.all([
axiosConfig.get(`/GetAllInstallationsFromProduct?product=0`), axiosConfig.get(`/GetAllInstallationsFromProduct?product=0`),
axiosConfig.get(`/GetAllInstallationsFromProduct?product=1`), axiosConfig.get(`/GetAllInstallationsFromProduct?product=1`),
axiosConfig.get(`/GetAllInstallationsFromProduct?product=2`), axiosConfig.get(`/GetAllInstallationsFromProduct?product=2`),
axiosConfig.get(`/GetAllInstallationsFromProduct?product=3`) axiosConfig.get(`/GetAllInstallationsFromProduct?product=3`),
axiosConfig.get(`/GetAllInstallationsFromProduct?product=4`),
axiosConfig.get(`/GetAllInstallationsFromProduct?product=5`)
]); ]);
setAvailableInstallations([...res0.data, ...res1.data, ...res2.data, ...res3.data]); setAvailableInstallations([...res0.data, ...res1.data, ...res2.data, ...res3.data, ...res4.data, ...res5.data]);
} catch (err) { } catch (err) {
if (err.response && err.response.status === 401) removeToken(); if (err.response && err.response.status === 401) removeToken();
} }

View File

@ -34,6 +34,7 @@ interface OverviewProps {
s3Credentials: I_S3Credentials; s3Credentials: I_S3Credentials;
id: number; id: number;
device?: number; device?: number;
product?: number;
connected?: boolean; connected?: boolean;
loading?: boolean; loading?: boolean;
} }
@ -565,7 +566,7 @@ function Overview(props: OverviewProps) {
> >
<FormattedMessage id="24_hours" defaultMessage="24-hours" /> <FormattedMessage id="24_hours" defaultMessage="24-hours" />
</Button> </Button>
{props.device !== 3 && ( {props.device !== 3 && props.product !== 2 && (
<Button <Button
variant="contained" variant="contained"
onClick={handleWeekData} onClick={handleWeekData}
@ -817,7 +818,7 @@ function Overview(props: OverviewProps) {
type: 'bar', type: 'bar',
color: '#ff9900' color: '#ff9900'
}, },
...(product !== 2 ? [{ ...((product !== 2 && product !== 5) ? [{
name: 'Net Energy', name: 'Net Energy',
color: '#e65100', color: '#e65100',
type: 'line', type: 'line',
@ -840,7 +841,7 @@ function Overview(props: OverviewProps) {
alignItems="stretch" alignItems="stretch"
spacing={3} spacing={3}
> >
{!(aggregatedData && product === 2) && ( {!(aggregatedData && (product === 2 || product === 5)) && (
<Grid item md={6} xs={12}> <Grid item md={6} xs={12}>
<Card <Card
sx={{ sx={{
@ -933,7 +934,7 @@ function Overview(props: OverviewProps) {
</Card> </Card>
</Grid> </Grid>
)} )}
<Grid item md={(aggregatedData && product === 2) ? 12 : 6} xs={12}> <Grid item md={(aggregatedData && (product === 2 || product === 5)) ? 12 : 6} xs={12}>
<Card <Card
sx={{ sx={{
overflow: 'visible', overflow: 'visible',
@ -1001,14 +1002,14 @@ function Overview(props: OverviewProps) {
<ReactApexChart <ReactApexChart
options={{ options={{
...getChartOptions( ...getChartOptions(
product === 2 (product === 2 || product === 5)
? aggregatedDataArray[aggregatedChartState] ? aggregatedDataArray[aggregatedChartState]
.chartOverview.dcPowerWithoutHeating .chartOverview.dcPowerWithoutHeating
: aggregatedDataArray[aggregatedChartState] : aggregatedDataArray[aggregatedChartState]
.chartOverview.dcPower, .chartOverview.dcPower,
'weekly', 'weekly',
aggregatedDataArray[aggregatedChartState].datelist, aggregatedDataArray[aggregatedChartState].datelist,
product === 2 (product === 2 || product === 5)
) )
}} }}
series={[ series={[
@ -1017,7 +1018,7 @@ function Overview(props: OverviewProps) {
.chartData.dcChargingPower, .chartData.dcChargingPower,
color: '#008FFB' color: '#008FFB'
}, },
...(product !== 2 ? [{ ...((product !== 2 && product !== 5) ? [{
...aggregatedDataArray[aggregatedChartState] ...aggregatedDataArray[aggregatedChartState]
.chartData.heatingPower, .chartData.heatingPower,
color: '#ff9900' color: '#ff9900'
@ -1073,7 +1074,7 @@ function Overview(props: OverviewProps) {
alignItems="stretch" alignItems="stretch"
spacing={3} spacing={3}
> >
{product !== 2 && ( {(product !== 2 && product !== 5) && (
<Grid item md={6} xs={12}> <Grid item md={6} xs={12}>
<Card <Card
sx={{ sx={{
@ -1136,7 +1137,7 @@ function Overview(props: OverviewProps) {
</Card> </Card>
</Grid> </Grid>
)} )}
{product !== 2 && ( {(product !== 2 && product !== 5) && (
<Grid item md={6} xs={12}> <Grid item md={6} xs={12}>
<Card <Card
sx={{ sx={{
@ -1392,7 +1393,7 @@ function Overview(props: OverviewProps) {
</Grid> </Grid>
</Grid> </Grid>
{aggregatedData && product === 2 && ( {aggregatedData && (product === 2 || product === 5) && (
<Grid <Grid
container container
direction="row" direction="row"
@ -1457,7 +1458,7 @@ function Overview(props: OverviewProps) {
alignItems="stretch" alignItems="stretch"
spacing={3} spacing={3}
> >
<Grid item md={product === 2 ? 12 : 6} xs={12}> <Grid item md={(product === 2 || product === 5) ? 12 : 6} xs={12}>
<Card <Card
sx={{ sx={{
overflow: 'visible', overflow: 'visible',
@ -1518,7 +1519,7 @@ function Overview(props: OverviewProps) {
/> />
</Card> </Card>
</Grid> </Grid>
{product !== 2 && ( {(product !== 2 && product !== 5) && (
<Grid item md={6} xs={12}> <Grid item md={6} xs={12}>
<Card <Card
sx={{ sx={{

View File

@ -19,15 +19,18 @@ import { useLocation, useNavigate } from 'react-router-dom';
import routes from '../../../Resources/routes.json'; import routes from '../../../Resources/routes.json';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
import BuildIcon from '@mui/icons-material/Build'; import BuildIcon from '@mui/icons-material/Build';
import { getDeviceTypeName } from '../Information/installationSetupUtils';
interface FlatInstallationViewProps { interface FlatInstallationViewProps {
installations: I_Installation[]; installations: I_Installation[];
product?: number;
} }
const FlatInstallationView = (props: FlatInstallationViewProps) => { const FlatInstallationView = (props: FlatInstallationViewProps) => {
const navigate = useNavigate(); const navigate = useNavigate();
const [selectedInstallation, setSelectedInstallation] = useState<number>(-1); const [selectedInstallation, setSelectedInstallation] = useState<number>(-1);
const currentLocation = useLocation(); const currentLocation = useLocation();
const baseRoute = props.product === 5 ? routes.sodistorepro_installations : routes.sodiohome_installations;
// //
const sortedInstallations = [...props.installations].sort((a, b) => { const sortedInstallations = [...props.installations].sort((a, b) => {
// Compare the status field of each installation and sort them based on the status. // Compare the status field of each installation and sort them based on the status.
@ -50,7 +53,7 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
setSelectedInstallation(-1); setSelectedInstallation(-1);
navigate( navigate(
routes.sodiohome_installations + baseRoute +
routes.list + routes.list +
routes.installation + routes.installation +
`${installationID}` + `${installationID}` +
@ -81,9 +84,9 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
sx={{ sx={{
display: display:
currentLocation.pathname === currentLocation.pathname ===
routes.sodiohome_installations + 'list' || baseRoute + 'list' ||
currentLocation.pathname === currentLocation.pathname ===
routes.sodiohome_installations + routes.list baseRoute + routes.list
? 'block' ? 'block'
: 'none' : 'none'
}} }}
@ -96,14 +99,14 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
<TableCell> <TableCell>
<FormattedMessage id="name" defaultMessage="Name" /> <FormattedMessage id="name" defaultMessage="Name" />
</TableCell> </TableCell>
<TableCell>
<FormattedMessage id="location" defaultMessage="Location" />
</TableCell>
<TableCell> <TableCell>
<FormattedMessage id="installationSN" defaultMessage="Installation SN" /> <FormattedMessage id="installationSN" defaultMessage="Installation SN" />
</TableCell> </TableCell>
<TableCell> <TableCell>
<FormattedMessage id="region" defaultMessage="Region" /> <FormattedMessage id="DeviceType" defaultMessage="Device Type" />
</TableCell>
<TableCell>
<FormattedMessage id="canton" defaultMessage="Canton" />
</TableCell> </TableCell>
<TableCell> <TableCell>
<FormattedMessage id="country" defaultMessage="Country" /> <FormattedMessage id="country" defaultMessage="Country" />
@ -146,19 +149,6 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
</Typography> </Typography>
</TableCell> </TableCell>
<TableCell>
<Typography
variant="body2"
fontWeight="bold"
color="text.primary"
gutterBottom
noWrap
sx={{ marginTop: '10px', fontSize: 'small' }}
>
{installation.location}
</Typography>
</TableCell>
<TableCell> <TableCell>
<Typography <Typography
variant="body2" variant="body2"
@ -181,7 +171,20 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
noWrap noWrap
sx={{ marginTop: '10px', fontSize: 'small' }} sx={{ marginTop: '10px', fontSize: 'small' }}
> >
{installation.region} {getDeviceTypeName(installation.device)}
</Typography>
</TableCell>
<TableCell>
<Typography
variant="body2"
fontWeight="bold"
color="text.primary"
gutterBottom
noWrap
sx={{ marginTop: '10px', fontSize: 'small' }}
>
{installation.canton || ''}
</Typography> </Typography>
</TableCell> </TableCell>

View File

@ -50,7 +50,9 @@ function SodioHomeInstallation(props: singleInstallationProps) {
const s3Bucket = const s3Bucket =
props.current_installation.s3BucketId.toString() + props.current_installation.s3BucketId.toString() +
'-' + '-' +
'e7b9a240-3c5d-4d2e-a019-6d8b1f7b73fa'; (props.current_installation.product === 5
? '325c9373-9025-4a8d-bf5a-f9eedf1f155c'
: 'e7b9a240-3c5d-4d2e-a019-6d8b1f7b73fa');
const context = useContext(UserContext); const context = useContext(UserContext);
const { currentUser } = context; const { currentUser } = context;
@ -603,6 +605,7 @@ function SodioHomeInstallation(props: singleInstallationProps) {
s3Credentials={s3Credentials} s3Credentials={s3Credentials}
id={props.current_installation.id} id={props.current_installation.id}
device={props.current_installation.device} device={props.current_installation.device}
product={props.current_installation.product}
connected={connected} connected={connected}
loading={loading} loading={loading}
/> />

View File

@ -10,12 +10,14 @@ import SodioHomeInstallation from './Installation';
interface installationSearchProps { interface installationSearchProps {
installations: I_Installation[]; installations: I_Installation[];
product?: number;
} }
function InstallationSearch(props: installationSearchProps) { function InstallationSearch(props: installationSearchProps) {
const intl = useIntl(); const intl = useIntl();
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const currentLocation = useLocation(); const currentLocation = useLocation();
const baseRoute = props.product === 5 ? routes.sodistorepro_installations : routes.sodiohome_installations;
// const [filteredData, setFilteredData] = useState(props.installations); // const [filteredData, setFilteredData] = useState(props.installations);
const indexedData = useMemo(() => { const indexedData = useMemo(() => {
@ -46,9 +48,9 @@ function InstallationSearch(props: installationSearchProps) {
sx={{ sx={{
display: display:
currentLocation.pathname === currentLocation.pathname ===
routes.sodiohome_installations + 'list' || baseRoute + 'list' ||
currentLocation.pathname === currentLocation.pathname ===
routes.sodiohome_installations + routes.list baseRoute + routes.list
? 'block' ? 'block'
: 'none' : 'none'
}} }}
@ -79,7 +81,7 @@ function InstallationSearch(props: installationSearchProps) {
</Grid> </Grid>
</Grid> </Grid>
<FlatInstallationView installations={filteredData} /> <FlatInstallationView installations={filteredData} product={props.product} />
<Routes> <Routes>
{filteredData.map((installation) => { {filteredData.map((installation) => {
return ( return (

View File

@ -17,29 +17,32 @@ import { Close as CloseIcon } from '@mui/icons-material';
import { I_Installation } from 'src/interfaces/InstallationTypes'; import { I_Installation } from 'src/interfaces/InstallationTypes';
import { InstallationsContext } from 'src/contexts/InstallationsContextProvider'; import { InstallationsContext } from 'src/contexts/InstallationsContextProvider';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { INSTALLATION_PRESETS, SODIOHOME_DEVICE_TYPES } from '../Information/installationSetupUtils';
interface SodistorehomeInstallationFormPros { interface SodistorehomeInstallationFormPros {
cancel: () => void; cancel: () => void;
submit: () => void; submit: () => void;
parentid: number; parentid: number;
product?: number;
} }
function SodistorehomeInstallationForm(props: SodistorehomeInstallationFormPros) { function SodistorehomeInstallationForm(props: SodistorehomeInstallationFormPros) {
const theme = useTheme(); const theme = useTheme();
const [open, setOpen] = useState(true); const [open, setOpen] = useState(true);
const isSodistorePro = props.product === 5;
const [formValues, setFormValues] = useState<Partial<I_Installation>>({ const [formValues, setFormValues] = useState<Partial<I_Installation>>({
name: '', name: '',
region: '',
location: '',
country: '',
vpnIp: '', vpnIp: '',
installationModel: '',
externalEms: 'No',
...(isSodistorePro ? { device: 4 } : {}),
}); });
const requiredFields = ['name', 'location', 'country', 'vpnIp']; const [inverterCount, setInverterCount] = useState('');
const requiredFields = ['name', 'vpnIp', ...(isSodistorePro ? [] : ['installationModel'])];
const DeviceTypes = [ const DeviceTypes = isSodistorePro
{ id: 3, name: 'Growatt' }, ? [{ id: 4, name: 'inesco 12K - WR Hybrid' }]
{ id: 4, name: 'Sinexcel' } : SODIOHOME_DEVICE_TYPES;
];
const installationContext = useContext(InstallationsContext); const installationContext = useContext(InstallationsContext);
const { createInstallation, loading, setLoading, error, setError } = const { createInstallation, loading, setLoading, error, setError } =
installationContext; installationContext;
@ -55,7 +58,10 @@ function SodistorehomeInstallationForm(props: SodistorehomeInstallationFormPros)
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
setLoading(true); setLoading(true);
formValues.parentId = props.parentid; formValues.parentId = props.parentid;
formValues.product = 2; formValues.product = props.product ?? 2;
if (isSodistorePro) {
formValues.installationModel = inverterCount;
}
const responseData = await createInstallation(formValues); const responseData = await createInstallation(formValues);
props.submit(); props.submit();
}; };
@ -69,6 +75,9 @@ function SodistorehomeInstallationForm(props: SodistorehomeInstallationFormPros)
return false; return false;
} }
} }
if (isSodistorePro && (!inverterCount || parseInt(inverterCount, 10) < 1)) {
return false;
}
return true; return true;
}; };
@ -124,42 +133,6 @@ function SodistorehomeInstallationForm(props: SodistorehomeInstallationFormPros)
error={formValues.name === ''} error={formValues.name === ''}
/> />
</div> </div>
<div>
<TextField
label={<FormattedMessage id="region" defaultMessage="Region" />}
name="region"
value={formValues.region}
onChange={handleChange}
required
error={formValues.region === ''}
/>
</div>
<div>
<TextField
label={
<FormattedMessage id="location" defaultMessage="Location" />
}
name="location"
value={formValues.location}
onChange={handleChange}
required
error={formValues.location === ''}
/>
</div>
<div>
<TextField
label={
<FormattedMessage id="country" defaultMessage="Country" />
}
name="country"
value={formValues.country}
onChange={handleChange}
required
error={formValues.country === ''}
/>
</div>
<div> <div>
<TextField <TextField
label={<FormattedMessage id="VpnIp" defaultMessage="VpnIp" />} label={<FormattedMessage id="VpnIp" defaultMessage="VpnIp" />}
@ -171,6 +144,67 @@ function SodistorehomeInstallationForm(props: SodistorehomeInstallationFormPros)
/> />
</div> </div>
{isSodistorePro ? (
<div>
<TextField
label={
<FormattedMessage
id="numberOfInverters"
defaultMessage="Number of Inverters"
/>
}
name="inverterCount"
type="text"
value={inverterCount}
onChange={(e) => {
const val = e.target.value;
if (val === '' || (/^\d+$/.test(val) && parseInt(val, 10) <= 20)) {
setInverterCount(val);
}
}}
required
error={!inverterCount || parseInt(inverterCount, 10) < 1}
/>
</div>
) : (
<div>
<FormControl
fullWidth
required
error={formValues.installationModel === ''}
sx={{
marginTop: 1,
marginBottom: 1,
width: 390
}}
>
<InputLabel
sx={{
fontSize: 14,
backgroundColor: 'white'
}}
>
<FormattedMessage
id="installationModel"
defaultMessage="Installation Model"
/>
</InputLabel>
<Select
name="installationModel"
value={formValues.installationModel || ''}
onChange={handleChange}
>
{Object.keys(INSTALLATION_PRESETS).map((name) => (
<MenuItem key={name} value={name}>
{name}
</MenuItem>
))}
</Select>
</FormControl>
</div>
)}
{!isSodistorePro && (
<div> <div>
<FormControl <FormControl
fullWidth fullWidth
@ -204,6 +238,7 @@ function SodistorehomeInstallationForm(props: SodistorehomeInstallationFormPros)
</Select> </Select>
</FormControl> </FormControl>
</div> </div>
)}
</Box> </Box>
<div <div

View File

@ -13,6 +13,8 @@ import TreeView from '../Tree/treeView';
import { ProductIdContext } from '../../../contexts/ProductIdContextProvider'; import { ProductIdContext } from '../../../contexts/ProductIdContextProvider';
import { UserType } from '../../../interfaces/UserTypes'; import { UserType } from '../../../interfaces/UserTypes';
import SodioHomeInstallation from './Installation'; import SodioHomeInstallation from './Installation';
import AcknowledgementDialog, { CURRENT_TERMS_VERSION } from '../../../components/AcknowledgementDialog';
import axiosConfig from '../../../Resources/axiosConfig';
interface SodioHomeInstallationTabsProps { interface SodioHomeInstallationTabsProps {
product: number; product: number;
@ -21,7 +23,25 @@ interface SodioHomeInstallationTabsProps {
function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) { function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
const location = useLocation(); const location = useLocation();
const context = useContext(UserContext); const context = useContext(UserContext);
const { currentUser } = context; const { currentUser, setUser } = context;
const showTermsDialog =
currentUser?.acknowledgedTermsVersion == null ||
currentUser.acknowledgedTermsVersion < CURRENT_TERMS_VERSION;
const handleAcknowledgeTerms = () => {
axiosConfig
.put('/AcknowledgeTerms', undefined, {
params: { version: CURRENT_TERMS_VERSION }
})
.then(() => {
const updatedUser = {
...currentUser,
acknowledgedTermsVersion: CURRENT_TERMS_VERSION
};
setUser(updatedUser);
});
};
const tabList = [ const tabList = [
'live', 'live',
'overview', 'overview',
@ -40,12 +60,15 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
useState<boolean>(false); useState<boolean>(false);
const { const {
sodiohomeInstallations, sodiohomeInstallations,
sodistoreProInstallations,
fetchAllInstallations, fetchAllInstallations,
socket, socket,
openSocket, openSocket,
closeSocket closeSocket
} = useContext(InstallationsContext); } = useContext(InstallationsContext);
const { product, setProduct } = useContext(ProductIdContext); const { product, setProduct } = useContext(ProductIdContext);
const baseRoute = props.product === 5 ? routes.sodistorepro_installations : routes.sodiohome_installations;
const installations = props.product === 5 ? sodistoreProInstallations : sodiohomeInstallations;
useEffect(() => { useEffect(() => {
let path = location.pathname.split('/'); let path = location.pathname.split('/');
@ -237,11 +260,11 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
!location.pathname.includes('folder'); !location.pathname.includes('folder');
// Determine if current installation is Growatt (device=3) to hide report tab // Determine if current installation is Growatt (device=3) to hide report tab
const currentInstallation = sodiohomeInstallations.find((i) => const currentInstallation = installations.find((i) =>
location.pathname.includes(`/${i.id}/`) location.pathname.includes(`/${i.id}/`)
); );
const isGrowatt = currentInstallation?.device === 3 const isGrowatt = currentInstallation?.device === 3
|| (sodiohomeInstallations.length === 1 && sodiohomeInstallations[0].device === 3); || (installations.length === 1 && installations[0].device === 3);
const tabs = inInstallationView && currentUser.userType == UserType.admin const tabs = inInstallationView && currentUser.userType == UserType.admin
? [ ? [
@ -413,8 +436,12 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
} }
]; ];
return sodiohomeInstallations.length > 1 ? ( return installations.length > 1 ? (
<> <>
<AcknowledgementDialog
open={showTermsDialog}
onAcknowledge={handleAcknowledgeTerms}
/>
<Container maxWidth="xl" sx={{ marginTop: '20px' }} className="mainframe"> <Container maxWidth="xl" sx={{ marginTop: '20px' }} className="mainframe">
<TabsContainerWrapper> <TabsContainerWrapper>
<Tabs <Tabs
@ -459,7 +486,8 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
<Grid item xs={12}> <Grid item xs={12}>
<Box p={4}> <Box p={4}>
<InstallationSearch <InstallationSearch
installations={sodiohomeInstallations} installations={installations}
product={props.product}
/> />
</Box> </Box>
</Grid> </Grid>
@ -472,7 +500,7 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
path={'*'} path={'*'}
element={ element={
<Navigate <Navigate
to={routes.sodiohome_installations + routes.list} to={baseRoute + routes.list}
></Navigate> ></Navigate>
} }
></Route> ></Route>
@ -481,9 +509,12 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
</Card> </Card>
</Container> </Container>
</> </>
) : sodiohomeInstallations.length === 1 ? ( ) : installations.length === 1 ? (
<> <>
{' '} <AcknowledgementDialog
open={showTermsDialog}
onAcknowledge={handleAcknowledgeTerms}
/>
<Container maxWidth="xl" sx={{ marginTop: '20px' }} className="mainframe"> <Container maxWidth="xl" sx={{ marginTop: '20px' }} className="mainframe">
<TabsContainerWrapper> <TabsContainerWrapper>
<Tabs <Tabs
@ -523,7 +554,7 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
<Grid item xs={12}> <Grid item xs={12}>
<Box p={4}> <Box p={4}>
<SodioHomeInstallation <SodioHomeInstallation
current_installation={sodiohomeInstallations[0]} current_installation={installations[0]}
type="installation" type="installation"
></SodioHomeInstallation> ></SodioHomeInstallation>
</Box> </Box>

View File

@ -37,7 +37,8 @@ const productOptions = [
{ value: 1, label: 'Salidomo' }, { value: 1, label: 'Salidomo' },
{ value: 2, label: 'Sodistore Home' }, { value: 2, label: 'Sodistore Home' },
{ value: 3, label: 'Sodistore Max' }, { value: 3, label: 'Sodistore Max' },
{ value: 4, label: 'Sodistore Grid' } { value: 4, label: 'Sodistore Grid' },
{ value: 5, label: 'Sodistore Pro' }
]; ];
const deviceOptionsByProduct: Record<number, { value: number; label: string }[]> = { const deviceOptionsByProduct: Record<number, { value: number; label: string }[]> = {
@ -47,7 +48,7 @@ const deviceOptionsByProduct: Record<number, { value: number; label: string }[]>
], ],
2: [ 2: [
{ value: 3, label: 'Growatt' }, { value: 3, label: 'Growatt' },
{ value: 4, label: 'Sinexcel' } { value: 4, label: 'inesco 12K - WR Hybrid' }
] ]
}; };

View File

@ -742,7 +742,8 @@ function TicketDetailPage() {
1: routes.salidomo_installations, 1: routes.salidomo_installations,
2: routes.sodiohome_installations, 2: routes.sodiohome_installations,
3: routes.sodistore_installations, 3: routes.sodistore_installations,
4: routes.sodistoregrid_installations 4: routes.sodistoregrid_installations,
5: routes.sodistorepro_installations
}; };
const prefix = productRoutes[detail.installationProduct] ?? routes.installations; const prefix = productRoutes[detail.installationProduct] ?? routes.installations;
navigate( navigate(

View File

@ -60,6 +60,10 @@ function CustomTreeItem(props: CustomTreeItemProps) {
? routes.salidomo_installations ? routes.salidomo_installations
: installation.product == 2 : installation.product == 2
? routes.sodiohome_installations ? routes.sodiohome_installations
: installation.product == 4
? routes.sodistoregrid_installations
: installation.product == 5
? routes.sodistorepro_installations
: routes.sodistore_installations; : routes.sodistore_installations;
let folder_path = let folder_path =
@ -69,6 +73,10 @@ function CustomTreeItem(props: CustomTreeItemProps) {
? routes.salidomo_installations ? routes.salidomo_installations
: product == 2 : product == 2
? routes.sodiohome_installations ? routes.sodiohome_installations
: product == 4
? routes.sodistoregrid_installations
: product == 5
? routes.sodistorepro_installations
: routes.sodistore_installations; : routes.sodistore_installations;
if (installation.type != 'Folder') { if (installation.type != 'Folder') {
@ -209,6 +217,10 @@ function CustomTreeItem(props: CustomTreeItemProps) {
currentLocation.pathname === routes.installations + routes.tree || currentLocation.pathname === routes.installations + routes.tree ||
currentLocation.pathname === currentLocation.pathname ===
routes.sodiohome_installations + routes.tree || routes.sodiohome_installations + routes.tree ||
currentLocation.pathname ===
routes.sodistoregrid_installations + routes.tree ||
currentLocation.pathname ===
routes.sodistorepro_installations + routes.tree ||
currentLocation.pathname.includes('folder') currentLocation.pathname.includes('folder')
? 'block' ? 'block'
: 'none', : 'none',

View File

@ -59,17 +59,18 @@ function TreeInformation(props: TreeInformationProps) {
fetchAllInstallations fetchAllInstallations
} = installationContext; } = installationContext;
const [product, setProduct] = useState('Salimax'); const [product, setProduct] = useState('SodistoreHome');
const handleChangeInstallationChoice = (e) => { const handleChangeInstallationChoice = (e) => {
setProduct(e.target.value); // Directly update the product state setProduct(e.target.value); // Directly update the product state
}; };
const ProductTypes = ['Salimax', 'Salidomo', 'SodistoreHome', 'SodistoreMax', 'SodistoreGrid']; const ProductTypes = ['SodistoreHome', 'SodistorePro', 'SodistoreGrid', 'SodistoreMax', 'Salimax', 'Salidomo'];
const ProductDisplayNames: Record<string, string> = { const ProductDisplayNames: Record<string, string> = {
'SodistoreHome': 'Sodistore Home', 'SodistoreHome': 'Sodistore Home',
'SodistoreMax': 'Sodistore Max', 'SodistoreMax': 'Sodistore Max',
'SodistoreGrid': 'Sodistore Grid' 'SodistoreGrid': 'Sodistore Grid',
'SodistorePro': 'Sodistore Pro'
}; };
const isMobile = window.innerWidth <= 1490; const isMobile = window.innerWidth <= 1490;
@ -345,11 +346,12 @@ function TreeInformation(props: TreeInformationProps) {
/> />
)} )}
{openModalInstallation && product == 'SodistoreHome' && ( {openModalInstallation && (product == 'SodistoreHome' || product == 'SodistorePro') && (
<SodiostorehomeInstallationForm <SodiostorehomeInstallationForm
cancel={handleFormCancel} cancel={handleFormCancel}
submit={handleInstallationFormSubmit} submit={handleInstallationFormSubmit}
parentid={props.folder.id} parentid={props.folder.id}
product={product == 'SodistorePro' ? 5 : undefined}
/> />
)} )}

View File

@ -72,22 +72,24 @@ function userForm(props: userFormProps) {
setLoading(true); setLoading(true);
try { try {
// fetch product 0 const [res0, res1, res2, res3, res4, res5] = await Promise.all([
const res0 = await axiosConfig.get( axiosConfig.get(`/GetAllInstallationsFromProduct?product=0`),
`/GetAllInstallationsFromProduct?product=0` axiosConfig.get(`/GetAllInstallationsFromProduct?product=1`),
); axiosConfig.get(`/GetAllInstallationsFromProduct?product=2`),
const installations0 = res0.data; axiosConfig.get(`/GetAllInstallationsFromProduct?product=3`),
axiosConfig.get(`/GetAllInstallationsFromProduct?product=4`),
axiosConfig.get(`/GetAllInstallationsFromProduct?product=5`)
]);
// fetch product 1 const combined = [
const res1 = await axiosConfig.get( ...res0.data,
`/GetAllInstallationsFromProduct?product=3` ...res1.data,
); ...res2.data,
const installations1 = res1.data; ...res3.data,
...res4.data,
...res5.data
];
// aggregate
const combined = [...installations0, ...installations1];
// update
setInstallations(combined); setInstallations(combined);
} catch (err) { } catch (err) {
if (err.response && err.response.status === 401) { if (err.response && err.response.status === 401) {

View File

@ -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 }}>
inesco Energy{' '} inesco energy{' '}
</TypographyPrimary> </TypographyPrimary>
<TypographySecondary <TypographySecondary
variant="h4" variant="h4"

View File

@ -34,6 +34,9 @@ const InstallationsContextProvider = ({
const [sodistoreGridInstallations, setSodistoreGridInstallations] = useState< const [sodistoreGridInstallations, setSodistoreGridInstallations] = useState<
I_Installation[] I_Installation[]
>([]); >([]);
const [sodistoreProInstallations, setSodistoreProInstallations] = 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);
@ -105,10 +108,24 @@ const InstallationsContextProvider = ({
} }
); );
const updatedSodistorePro = sodistoreProInstallations.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); setSodistoreGridInstallations(updatedSodistoreGrid);
setSodistoreProInstallations(updatedSodistorePro);
// Clear the pending updates after applying // Clear the pending updates after applying
pendingUpdates.current = {}; pendingUpdates.current = {};
@ -116,7 +133,8 @@ const InstallationsContextProvider = ({
salidomoInstallations, salidomoInstallations,
salimax_or_sodistore_Installations, salimax_or_sodistore_Installations,
sodiohomeInstallations, sodiohomeInstallations,
sodistoreGridInstallations sodistoreGridInstallations,
sodistoreProInstallations
]); ]);
useEffect(() => { useEffect(() => {
@ -193,6 +211,8 @@ const InstallationsContextProvider = ({
if (product === 2) { if (product === 2) {
setSodiohomeInstallations(res.data); setSodiohomeInstallations(res.data);
} else if (product === 5) {
setSodistoreProInstallations(res.data);
} else if (product === 1) { } else if (product === 1) {
setSalidomoInstallations(res.data); setSalidomoInstallations(res.data);
} else if (product === 0 || product === 3) { } else if (product === 0 || product === 3) {
@ -418,6 +438,7 @@ const InstallationsContextProvider = ({
salidomoInstallations, salidomoInstallations,
sodiohomeInstallations, sodiohomeInstallations,
sodistoreGridInstallations, sodistoreGridInstallations,
sodistoreProInstallations,
foldersAndInstallations, foldersAndInstallations,
fetchAllInstallations, fetchAllInstallations,
fetchAllFoldersAndInstallations, fetchAllFoldersAndInstallations,
@ -445,6 +466,7 @@ const InstallationsContextProvider = ({
salidomoInstallations, salidomoInstallations,
sodiohomeInstallations, sodiohomeInstallations,
sodistoreGridInstallations, sodistoreGridInstallations,
sodistoreProInstallations,
foldersAndInstallations, foldersAndInstallations,
loading, loading,
error, error,

View File

@ -10,11 +10,13 @@ interface ProductIdContextType {
accessToSodiohome: boolean; accessToSodiohome: boolean;
accessToSodistore: boolean; accessToSodistore: boolean;
accessToSodistoreGrid: boolean; accessToSodistoreGrid: boolean;
accessToSodistorePro: 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; setAccessToSodistoreGrid: (access: boolean) => void;
setAccessToSodistorePro: (access: boolean) => void;
} }
// Create the context. // Create the context.
@ -49,6 +51,10 @@ export const ProductIdContextProvider = ({
const storedValue = localStorage.getItem('accessToSodistoreGrid'); const storedValue = localStorage.getItem('accessToSodistoreGrid');
return storedValue === 'true'; return storedValue === 'true';
}); });
const [accessToSodistorePro, setAccessToSodistorePro] = useState(() => {
const storedValue = localStorage.getItem('accessToSodistorePro');
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
@ -56,6 +62,8 @@ export const ProductIdContextProvider = ({
const [product, setProduct] = useState<number>(() => { const [product, setProduct] = useState<number>(() => {
if (location.includes('salidomo')) { if (location.includes('salidomo')) {
return 1; return 1;
} else if (location.includes('sodistorepro')) {
return 5;
} else if (location.includes('sodiohome')) { } else if (location.includes('sodiohome')) {
return 2; return 2;
} else if (location.includes('sodistoregrid')) { } else if (location.includes('sodistoregrid')) {
@ -92,6 +100,10 @@ export const ProductIdContextProvider = ({
setAccessToSodistoreGrid(access); setAccessToSodistoreGrid(access);
localStorage.setItem('accessToSodistoreGrid', JSON.stringify(access)); localStorage.setItem('accessToSodistoreGrid', JSON.stringify(access));
}; };
const changeAccessSodistorePro = (access: boolean) => {
setAccessToSodistorePro(access);
localStorage.setItem('accessToSodistorePro', JSON.stringify(access));
};
return ( return (
<ProductIdContext.Provider <ProductIdContext.Provider
@ -103,11 +115,13 @@ export const ProductIdContextProvider = ({
accessToSodiohome, accessToSodiohome,
accessToSodistore, accessToSodistore,
accessToSodistoreGrid, accessToSodistoreGrid,
accessToSodistorePro,
setAccessToSalimax: changeAccessSalimax, setAccessToSalimax: changeAccessSalimax,
setAccessToSalidomo: changeAccessSalidomo, setAccessToSalidomo: changeAccessSalidomo,
setAccessToSodiohome: changeAccessSodiohome, setAccessToSodiohome: changeAccessSodiohome,
setAccessToSodistore: changeAccessSodistore, setAccessToSodistore: changeAccessSodistore,
setAccessToSodistoreGrid: changeAccessSodistoreGrid setAccessToSodistoreGrid: changeAccessSodistoreGrid,
setAccessToSodistorePro: changeAccessSodistorePro
}} }}
> >
{children} {children}

View File

@ -86,7 +86,7 @@ export const transformInputToBatteryViewDataJson = async (
}> => { }> => {
const prefixes = ['', 'k', 'M', 'G', 'T']; const prefixes = ['', 'k', 'M', 'G', 'T'];
const MAX_NUMBER = 9999999; const MAX_NUMBER = 9999999;
const isSodioHome = product === 2; const isSodioHome = product === 2 || product === 5;
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'];
@ -169,7 +169,7 @@ export const transformInputToBatteryViewDataJson = async (
); );
const adjustedTimestamp = const adjustedTimestamp =
product == 0 || product == 2 || product == 3 || product == 4 product == 0 || product == 2 || product == 3 || product == 4 || product == 5
? 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
@ -226,26 +226,52 @@ export const transformInputToBatteryViewDataJson = async (
categories.forEach((category) => { categories.forEach((category) => {
pathsToSave.forEach((path) => { pathsToSave.forEach((path) => {
if (pathsToSave.indexOf(path) >= old_length) { if (pathsToSave.indexOf(path) >= old_length) {
chartData[category].data[path] = { name: path, data: [] }; const displayIndex = pathsToSave.indexOf(path);
chartData[category].data[path] = { name: 'Battery Cluster ' + (displayIndex + 1), data: [] };
} }
}); });
}); });
} }
// Map category names to InverterRecord field suffixes const hasDevices = !!inv?.Devices;
const categoryFieldMap = {
// Sinexcel field suffixes differ from Growatt for Voltage/Current
const categoryFieldMapGrowatt = {
Soc: 'Soc', Soc: 'Soc',
Power: 'Power', Power: 'Power',
Voltage: 'Voltage', Voltage: 'Voltage',
Current: 'Current', Current: 'Current',
Soh: 'Soh' Soh: 'Soh'
}; };
const categoryFieldMapSinexcel = {
Soc: 'Soc',
Power: 'Power',
Voltage: 'PackTotalVoltage',
Current: 'PackTotalCurrent',
Soh: 'Soh'
};
for (let j = 0; j < pathsToSave.length; j++) { for (let j = 0; j < pathsToSave.length; j++) {
const batteryIndex = j + 1; // Battery1, Battery2, ...
categories.forEach((category) => { categories.forEach((category) => {
const fieldName = `Battery${batteryIndex}${categoryFieldMap[category]}`; let value: number | undefined;
const value = inv[fieldName];
if (hasDevices) {
// Sinexcel: nested under Devices — 0→D1/B1, 1→D1/B2, 2→D2/B1, ...
const deviceId = String(Math.floor(j / 2) + 1);
const bi = (j % 2) + 1;
const device = inv.Devices[deviceId];
const fieldName = `Battery${bi}${categoryFieldMapSinexcel[category]}`;
value = device?.[fieldName];
// Fallback for Soc
if ((value === undefined || value === null) && category === 'Soc') {
value = device?.[`Battery${bi}SocSecondvalue`];
}
} else {
// Growatt: flat Battery1Soc, Battery2Voltage, ...
const batteryIndex = j + 1;
const fieldName = `Battery${batteryIndex}${categoryFieldMapGrowatt[category]}`;
value = inv[fieldName];
}
if (value !== undefined && value !== null) { if (value !== undefined && value !== null) {
if (value < chartOverview[category].min) { if (value < chartOverview[category].min) {
@ -393,7 +419,7 @@ export const transformInputToDailyDataJson = async (
// custom fallback logic to handle differences between Growatt and Sinexcel. // custom fallback logic to handle differences between Growatt and Sinexcel.
// Growatt has: Battery1AmbientTemperature, GridPower, PvPower // Growatt has: Battery1AmbientTemperature, GridPower, PvPower
// Sinexcel has: Battery1Temperature, TotalGridPower (meter may be offline), PvPower1-4 // Sinexcel has: Battery1Temperature, TotalGridPower (meter may be offline), PvPower1-4
const pathsToSearch = product == 2 const pathsToSearch = (product == 2 || product == 5)
? [ ? [
'SODIOHOME_SOC', 'SODIOHOME_SOC',
'SODIOHOME_TEMPERATURE', 'SODIOHOME_TEMPERATURE',
@ -516,8 +542,8 @@ export const transformInputToDailyDataJson = async (
let value: number | undefined = undefined; let value: number | undefined = undefined;
if (product === 2) { if (product === 2 || product === 5) {
// SodioHome: use top-level aggregated values (Sinexcel multi-inverter) // SodioHome/SodistorePro: use top-level aggregated values (Sinexcel multi-inverter)
const inv = result?.InverterRecord; const inv = result?.InverterRecord;
if (inv) { if (inv) {
switch (category_index) { switch (category_index) {
@ -735,7 +761,7 @@ export const transformInputToAggregatedDataJson = async (
const timestampPromises = []; const timestampPromises = [];
while (currentDay.isBefore(end_date)) { while (currentDay.isBefore(end_date)) {
const dateFormat = product === 2 const dateFormat = (product === 2 || product === 5)
? currentDay.format('DDMMYYYY') ? currentDay.format('DDMMYYYY')
: currentDay.format('YYYY-MM-DD'); : currentDay.format('YYYY-MM-DD');
timestampPromises.push( timestampPromises.push(

View File

@ -7,6 +7,13 @@ export interface I_Installation extends I_S3Credentials {
location: string; location: string;
region: string; region: string;
country: string; country: string;
street?: string;
postCode?: string;
city?: string;
canton?: string;
distributionPartner?: string;
inverterFirmwareVersion?: string;
batteryFirmwareVersion?: string;
installationName: string; installationName: string;
vpnIp: string; vpnIp: string;
orderNumbers: string[] | string; orderNumbers: string[] | string;
@ -18,6 +25,11 @@ export interface I_Installation extends I_S3Credentials {
batteryClusterNumber: number; batteryClusterNumber: number;
batteryNumber: number; batteryNumber: number;
batterySerialNumbers: string; batterySerialNumbers: string;
pvStringsPerInverter: string;
installationModel: string;
externalEms: string;
couplingType: string;
parentId: number; parentId: number;
s3WriteKey: string; s3WriteKey: string;
s3WriteSecret: string; s3WriteSecret: string;

View File

@ -10,6 +10,7 @@ export type InnovEnergyUser = {
type: string; type: string;
folderIds?: number[]; folderIds?: number[];
mustResetPassword: boolean; mustResetPassword: boolean;
acknowledgedTermsVersion?: number;
}; };
export interface I_UserWithInheritedAccess { export interface I_UserWithInheritedAccess {

View File

@ -6,6 +6,13 @@
"alarms": "Alarme", "alarms": "Alarme",
"applyChanges": "Änderungen speichern", "applyChanges": "Änderungen speichern",
"country": "Land", "country": "Land",
"street": "Strasse",
"postCode": "PLZ",
"city": "Ort",
"canton": "Kanton",
"distributionPartner": "Vertriebspartner",
"inverterFirmwareVersion": "Wechselrichter-Firmware-Version",
"batteryFirmwareVersion": "Batterie-Firmware-Version",
"networkProvider": "Netzbetreiber", "networkProvider": "Netzbetreiber",
"createNewFolder": "Neuer Ordner", "createNewFolder": "Neuer Ordner",
"createNewUser": "Neuer Benutzer", "createNewUser": "Neuer Benutzer",
@ -73,6 +80,27 @@
"live": "Live Daten", "live": "Live Daten",
"deleteInstallation": "Installation löschen", "deleteInstallation": "Installation löschen",
"confirmDeleteInstallation": "Möchten Sie diese Installation löschen?", "confirmDeleteInstallation": "Möchten Sie diese Installation löschen?",
"installationModel": "Installationsmodell",
"externalEms": "Externes EMS",
"externalEmsOther": "Externes EMS (angeben)",
"emsNo": "Nein",
"emsOther": "Andere",
"generalInfo": "Allgemeine Informationen",
"installationSetup": "Installationseinrichtung",
"couplingType": "AC/DC-Kopplung",
"couplingAC": "AC-gekoppelt",
"couplingDC": "DC-gekoppelt",
"selectModel": "Modell auswählen...",
"inverterN": "Wechselrichter {n}",
"clusterN": "Cluster {n}",
"clustersBatteriesSummary": "{filledClusters}/{totalClusters} Cluster, {filledBat}/{totalBat} Batterien",
"batteriesSummary": "{filled}/{total} Batterien",
"inverterNSerialNumber": "Wechselrichter {n} Seriennummer",
"dataloggerNSerialNumber": "Datenlogger {n} Seriennummer",
"pvStringsOnInverterN": "Anzahl PV-Strings an Wechselrichter {n}",
"batteryNSerialNumber": "Batterie {n} Seriennummer",
"adminSection": "Admin",
"confirmPresetSwitch": "Der Wechsel zu einer kleineren Konfiguration entfernt einige Batterie-Seriennummern. Fortfahren?",
"bucketLabel": "Bucket", "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.", "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",
@ -85,6 +113,7 @@
"noUsersWithDirectAccessToThis": "Keine Benutzer mit direktem Zugriff", "noUsersWithDirectAccessToThis": "Keine Benutzer mit direktem Zugriff",
"selectUsers": "Benutzer auswählen", "selectUsers": "Benutzer auswählen",
"cancel": "Abbrechen", "cancel": "Abbrechen",
"continue": "Fortfahren",
"addNewFolder": "Neuen Ordner hinzufügen", "addNewFolder": "Neuen Ordner hinzufügen",
"addNewInstallation": "Neue Installation hinzufügen", "addNewInstallation": "Neue Installation hinzufügen",
"deleteFolder": "Ordner löschen", "deleteFolder": "Ordner löschen",
@ -185,7 +214,7 @@
"demo_test_button": "KI-Diagnose", "demo_test_button": "KI-Diagnose",
"demo_hide_button": "KI-Diagnose ausblenden", "demo_hide_button": "KI-Diagnose ausblenden",
"demo_panel_title": "KI-Diagnose", "demo_panel_title": "KI-Diagnose",
"demo_custom_group": "Benutzerdefiniert (kann Mistral KI verwenden)", "demo_custom_group": "Benutzerdefiniert (kann KI verwenden)",
"demo_custom_option": "Benutzerdefinierten Alarm eingeben…", "demo_custom_option": "Benutzerdefinierten Alarm eingeben…",
"demo_custom_placeholder": "z.B. UnknownBatteryFault", "demo_custom_placeholder": "z.B. UnknownBatteryFault",
"demo_diagnose_button": "Diagnostizieren", "demo_diagnose_button": "Diagnostizieren",
@ -614,5 +643,26 @@
"timelineAiDiagnosisCompletedDesc": "KI-Diagnose abgeschlossen.", "timelineAiDiagnosisCompletedDesc": "KI-Diagnose abgeschlossen.",
"timelineAiDiagnosisFailedDesc": "KI-Diagnose fehlgeschlagen.", "timelineAiDiagnosisFailedDesc": "KI-Diagnose fehlgeschlagen.",
"timelineEscalatedDesc": "Ticket eskaliert.", "timelineEscalatedDesc": "Ticket eskaliert.",
"timelineResolutionAddedDesc": "Lösung hinzugefügt von {name}." "timelineResolutionAddedDesc": "Lösung hinzugefügt von {name}.",
"terms_dialog_title": "Willkommen bei inesco energy",
"terms_data_heading": "Ihre Daten",
"terms_data_body": "Ihre Installationsdaten werden sicher in der Schweiz gespeichert. Wir geben Ihre Daten nicht an Dritte weiter.",
"terms_ai_heading": "KI-gestützte Einblicke",
"terms_ai_body": "Wir nutzen einen in der EU gehosteten KI-Dienst, um Diagnosen und Einblicke für Ihre Installationen bereitzustellen. KI-generierte Ergebnisse sind Empfehlungen und sollten von qualifiziertem Personal überprüft werden.",
"terms_cookies_heading": "Browser-Speicher",
"terms_cookies_body": "Diese Plattform speichert Anmelde- und Einstellungsdaten in Ihrem Browser, um Sie angemeldet zu halten und Ihre Sprachauswahl zu speichern.",
"terms_acknowledge_button": "Ich verstehe",
"privacy_menu_item": "Daten & Datenschutz",
"privacy_dialog_title": "Daten & Datenschutz",
"privacy_data_heading": "Wo werden meine Daten gespeichert?",
"privacy_data_body": "Ihre Installationsdaten werden auf Servern in der Schweiz gespeichert. Nur autorisiertes Personal von inesco energy kann zu Supportzwecken auf Ihre Daten zugreifen.",
"privacy_ai_heading": "Wie wird KI eingesetzt?",
"privacy_ai_body": "Wir nutzen einen in der Europäischen Union gehosteten KI-Dienst, um Ihre Installationsdaten zu analysieren und diagnostische Einblicke zu liefern. Die KI verarbeitet technische Daten wie Batteriemesswerte und Fehlercodes. KI-Empfehlungen sollten stets von qualifiziertem Personal überprüft werden.",
"privacy_browser_heading": "Was speichert mein Browser?",
"privacy_browser_body": "Ihr Browser speichert Ihre Anmeldesitzung, um Sie angemeldet zu halten, sowie Ihre Sprach- und Designeinstellungen. Es werden keine Tracking- oder Werbe-Cookies verwendet.",
"privacy_access_heading": "Wer hat Zugriff auf meine Daten?",
"privacy_access_body": "Ihre Daten werden nicht an Dritte weitergegeben. Sie werden ausschliesslich für den Betrieb der Plattform und zur Bereitstellung von Einblicken in Ihre Installationen verwendet.",
"privacy_close_button": "Schliessen",
"sodistorepro": "Sodistore Pro",
"numberOfInverters": "Anzahl der Wechselrichter"
} }

View File

@ -2,6 +2,13 @@
"allInstallations": "All installations", "allInstallations": "All installations",
"applyChanges": "Apply changes", "applyChanges": "Apply changes",
"country": "Country", "country": "Country",
"street": "Street",
"postCode": "Postcode",
"city": "City",
"canton": "Canton",
"distributionPartner": "Distribution Partner",
"inverterFirmwareVersion": "Inverter Firmware Version",
"batteryFirmwareVersion": "Battery Firmware Version",
"networkProvider": "Network Provider", "networkProvider": "Network Provider",
"customerName": "Customer name", "customerName": "Customer name",
"english": "English", "english": "English",
@ -55,6 +62,27 @@
"live": "Live View", "live": "Live View",
"deleteInstallation": "Delete Installation", "deleteInstallation": "Delete Installation",
"confirmDeleteInstallation": "Do you want to delete this installation?", "confirmDeleteInstallation": "Do you want to delete this installation?",
"installationModel": "Installation Model",
"externalEms": "External EMS",
"externalEmsOther": "External EMS (specify)",
"emsNo": "No",
"emsOther": "Other",
"generalInfo": "General Info",
"installationSetup": "Installation Setup",
"couplingType": "AC/DC Coupling",
"couplingAC": "AC-coupled",
"couplingDC": "DC-coupled",
"selectModel": "Select model...",
"inverterN": "Inverter {n}",
"clusterN": "Cluster {n}",
"clustersBatteriesSummary": "{filledClusters}/{totalClusters} clusters, {filledBat}/{totalBat} batteries",
"batteriesSummary": "{filled}/{total} batteries",
"inverterNSerialNumber": "Inverter {n} Serial Number",
"dataloggerNSerialNumber": "Datalogger {n} Serial Number",
"pvStringsOnInverterN": "Number of PV Strings on Inverter {n}",
"batteryNSerialNumber": "Battery {n} Serial Number",
"adminSection": "Admin",
"confirmPresetSwitch": "Switching to a smaller configuration will remove some battery serial number entries. Continue?",
"bucketLabel": "Bucket", "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.", "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",
@ -67,6 +95,7 @@
"noUsersWithDirectAccessToThis": "No users with direct access to this ", "noUsersWithDirectAccessToThis": "No users with direct access to this ",
"selectUsers": "Select Users", "selectUsers": "Select Users",
"cancel": "Cancel", "cancel": "Cancel",
"continue": "Continue",
"addNewFolder": "Add new Folder", "addNewFolder": "Add new Folder",
"addNewInstallation": "Add new Installation", "addNewInstallation": "Add new Installation",
"deleteFolder": "Delete Folder", "deleteFolder": "Delete Folder",
@ -167,7 +196,7 @@
"demo_test_button": "AI Diagnosis", "demo_test_button": "AI Diagnosis",
"demo_hide_button": "Hide AI Diagnosis", "demo_hide_button": "Hide AI Diagnosis",
"demo_panel_title": "AI Diagnosis", "demo_panel_title": "AI Diagnosis",
"demo_custom_group": "Custom (may use Mistral AI)", "demo_custom_group": "Custom (may use AI)",
"demo_custom_option": "Type custom alarm below…", "demo_custom_option": "Type custom alarm below…",
"demo_custom_placeholder": "e.g. UnknownBatteryFault", "demo_custom_placeholder": "e.g. UnknownBatteryFault",
"demo_diagnose_button": "Diagnose", "demo_diagnose_button": "Diagnose",
@ -362,5 +391,26 @@
"timelineAiDiagnosisCompletedDesc": "AI diagnosis completed.", "timelineAiDiagnosisCompletedDesc": "AI diagnosis completed.",
"timelineAiDiagnosisFailedDesc": "AI diagnosis failed.", "timelineAiDiagnosisFailedDesc": "AI diagnosis failed.",
"timelineEscalatedDesc": "Ticket escalated.", "timelineEscalatedDesc": "Ticket escalated.",
"timelineResolutionAddedDesc": "Resolution added by {name}." "timelineResolutionAddedDesc": "Resolution added by {name}.",
"terms_dialog_title": "Welcome to inesco energy",
"terms_data_heading": "Your Data",
"terms_data_body": "Your installation data is securely stored in Switzerland. We do not share your data with third parties.",
"terms_ai_heading": "AI-Powered Insights",
"terms_ai_body": "We use an AI service hosted in the EU to provide diagnostics and insights for your installations. AI-generated results are recommendations and should be verified by qualified personnel.",
"terms_cookies_heading": "Browser Storage",
"terms_cookies_body": "This platform stores login and preference settings in your browser to keep you signed in and remember your language choice.",
"terms_acknowledge_button": "I understand",
"privacy_menu_item": "Data & Privacy",
"privacy_dialog_title": "Data & Privacy",
"privacy_data_heading": "Where is my data stored?",
"privacy_data_body": "Your installation data is stored on servers in Switzerland. Only authorized inesco energy personnel can access your data for support purposes.",
"privacy_ai_heading": "How is AI used?",
"privacy_ai_body": "We use an AI service hosted in the European Union to analyze your installation data and provide diagnostic insights. The AI processes technical data such as battery readings and error codes. AI recommendations should always be verified by qualified personnel.",
"privacy_browser_heading": "What does my browser store?",
"privacy_browser_body": "Your browser stores your login session to keep you signed in, and your language and theme preferences. No tracking or advertising cookies are used.",
"privacy_access_heading": "Who has access to my data?",
"privacy_access_body": "Your data is not shared with third parties. It is used solely to operate the platform and provide you with insights about your installations.",
"privacy_close_button": "Close",
"sodistorepro": "Sodistore Pro",
"numberOfInverters": "Number of Inverters"
} }

View File

@ -4,6 +4,13 @@
"alarms": "Alarmes", "alarms": "Alarmes",
"applyChanges": "Appliquer", "applyChanges": "Appliquer",
"country": "Pays", "country": "Pays",
"street": "Rue",
"postCode": "Code postal",
"city": "Ville",
"canton": "Canton",
"distributionPartner": "Partenaire de distribution",
"inverterFirmwareVersion": "Version firmware onduleur",
"batteryFirmwareVersion": "Version firmware batterie",
"networkProvider": "Gestionnaire de réseau", "networkProvider": "Gestionnaire de réseau",
"createNewFolder": "Nouveau dossier", "createNewFolder": "Nouveau dossier",
"createNewUser": "Nouvel utilisateur", "createNewUser": "Nouvel utilisateur",
@ -67,6 +74,27 @@
"live": "Diffusion en direct", "live": "Diffusion en direct",
"deleteInstallation": "Supprimer l'installation", "deleteInstallation": "Supprimer l'installation",
"confirmDeleteInstallation": "Voulez-vous supprimer cette installation ?", "confirmDeleteInstallation": "Voulez-vous supprimer cette installation ?",
"installationModel": "Modèle d'installation",
"externalEms": "EMS externe",
"externalEmsOther": "EMS externe (préciser)",
"emsNo": "Non",
"emsOther": "Autre",
"generalInfo": "Informations générales",
"installationSetup": "Configuration de l'installation",
"couplingType": "Couplage AC/DC",
"couplingAC": "Couplage AC",
"couplingDC": "Couplage DC",
"selectModel": "Sélectionner le modèle...",
"inverterN": "Onduleur {n}",
"clusterN": "Cluster {n}",
"clustersBatteriesSummary": "{filledClusters}/{totalClusters} clusters, {filledBat}/{totalBat} batteries",
"batteriesSummary": "{filled}/{total} batteries",
"inverterNSerialNumber": "Numéro de série onduleur {n}",
"dataloggerNSerialNumber": "Numéro de série datalogger {n}",
"pvStringsOnInverterN": "Nombre de chaînes PV sur onduleur {n}",
"batteryNSerialNumber": "Numéro de série batterie {n}",
"adminSection": "Admin",
"confirmPresetSwitch": "Le passage à une configuration plus petite supprimera certains numéros de série de batteries. Continuer ?",
"bucketLabel": "Bucket", "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.", "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",
@ -79,6 +107,7 @@
"noUsersWithDirectAccessToThis": "Aucun utilisateur ayant un accès direct", "noUsersWithDirectAccessToThis": "Aucun utilisateur ayant un accès direct",
"selectUsers": "Sélectionnez les utilisateurs", "selectUsers": "Sélectionnez les utilisateurs",
"cancel": "Annuler", "cancel": "Annuler",
"continue": "Continuer",
"addNewFolder": "Ajouter un nouveau dossier", "addNewFolder": "Ajouter un nouveau dossier",
"addNewInstallation": "Ajouter une nouvelle installation", "addNewInstallation": "Ajouter une nouvelle installation",
"deleteFolder": "Supprimer le dossier", "deleteFolder": "Supprimer le dossier",
@ -179,7 +208,7 @@
"demo_test_button": "Diagnostic IA", "demo_test_button": "Diagnostic IA",
"demo_hide_button": "Masquer le diagnostic IA", "demo_hide_button": "Masquer le diagnostic IA",
"demo_panel_title": "Diagnostic IA", "demo_panel_title": "Diagnostic IA",
"demo_custom_group": "Personnalisé (peut utiliser Mistral IA)", "demo_custom_group": "Personnalisé (peut utiliser IA)",
"demo_custom_option": "Saisir une alarme personnalisée…", "demo_custom_option": "Saisir une alarme personnalisée…",
"demo_custom_placeholder": "ex. UnknownBatteryFault", "demo_custom_placeholder": "ex. UnknownBatteryFault",
"demo_diagnose_button": "Diagnostiquer", "demo_diagnose_button": "Diagnostiquer",
@ -614,5 +643,26 @@
"timelineAiDiagnosisCompletedDesc": "Diagnostic IA terminé.", "timelineAiDiagnosisCompletedDesc": "Diagnostic IA terminé.",
"timelineAiDiagnosisFailedDesc": "Diagnostic IA échoué.", "timelineAiDiagnosisFailedDesc": "Diagnostic IA échoué.",
"timelineEscalatedDesc": "Ticket escaladé.", "timelineEscalatedDesc": "Ticket escaladé.",
"timelineResolutionAddedDesc": "Résolution ajoutée par {name}." "timelineResolutionAddedDesc": "Résolution ajoutée par {name}.",
"terms_dialog_title": "Bienvenue chez inesco energy",
"terms_data_heading": "Vos données",
"terms_data_body": "Les données de votre installation sont stockées en toute sécurité en Suisse. Nous ne partageons pas vos données avec des tiers.",
"terms_ai_heading": "Analyses basées sur l'IA",
"terms_ai_body": "Nous utilisons un service d'IA hébergé dans l'UE pour fournir des diagnostics et des analyses pour vos installations. Les résultats générés par l'IA sont des recommandations et doivent être vérifiés par du personnel qualifié.",
"terms_cookies_heading": "Stockage du navigateur",
"terms_cookies_body": "Cette plateforme enregistre vos paramètres de connexion et de préférences dans votre navigateur pour maintenir votre session et mémoriser votre choix de langue.",
"terms_acknowledge_button": "Je comprends",
"privacy_menu_item": "Données et confidentialité",
"privacy_dialog_title": "Données et confidentialité",
"privacy_data_heading": "Où sont stockées mes données ?",
"privacy_data_body": "Les données de votre installation sont stockées sur des serveurs en Suisse. Seul le personnel autorisé d'inesco energy peut accéder à vos données à des fins d'assistance.",
"privacy_ai_heading": "Comment l'IA est-elle utilisée ?",
"privacy_ai_body": "Nous utilisons un service d'IA hébergé dans l'Union européenne pour analyser les données de votre installation et fournir des informations diagnostiques. L'IA traite des données techniques telles que les relevés de batterie et les codes d'erreur. Les recommandations de l'IA doivent toujours être vérifiées par du personnel qualifié.",
"privacy_browser_heading": "Que stocke mon navigateur ?",
"privacy_browser_body": "Votre navigateur stocke votre session de connexion pour vous maintenir connecté, ainsi que vos préférences de langue et de thème. Aucun cookie de suivi ou publicitaire n'est utilisé.",
"privacy_access_heading": "Qui a accès à mes données ?",
"privacy_access_body": "Vos données ne sont pas partagées avec des tiers. Elles sont utilisées uniquement pour le fonctionnement de la plateforme et pour vous fournir des informations sur vos installations.",
"privacy_close_button": "Fermer",
"sodistorepro": "Sodistore Pro",
"numberOfInverters": "Nombre d'onduleurs"
} }

View File

@ -2,6 +2,13 @@
"allInstallations": "Tutte le installazioni", "allInstallations": "Tutte le installazioni",
"applyChanges": "Applica modifiche", "applyChanges": "Applica modifiche",
"country": "Paese", "country": "Paese",
"street": "Via",
"postCode": "CAP",
"city": "Città",
"canton": "Cantone",
"distributionPartner": "Partner di distribuzione",
"inverterFirmwareVersion": "Versione firmware inverter",
"batteryFirmwareVersion": "Versione firmware batteria",
"networkProvider": "Gestore di rete", "networkProvider": "Gestore di rete",
"customerName": "Nome cliente", "customerName": "Nome cliente",
"english": "Inglese", "english": "Inglese",
@ -55,6 +62,27 @@
"live": "Vista in diretta", "live": "Vista in diretta",
"deleteInstallation": "Elimina installazione", "deleteInstallation": "Elimina installazione",
"confirmDeleteInstallation": "Vuoi eliminare questa installazione?", "confirmDeleteInstallation": "Vuoi eliminare questa installazione?",
"installationModel": "Modello di installazione",
"externalEms": "EMS esterno",
"externalEmsOther": "EMS esterno (specificare)",
"emsNo": "No",
"emsOther": "Altro",
"generalInfo": "Informazioni generali",
"installationSetup": "Configurazione installazione",
"couplingType": "Accoppiamento AC/DC",
"couplingAC": "Accoppiamento AC",
"couplingDC": "Accoppiamento DC",
"selectModel": "Seleziona modello...",
"inverterN": "Inverter {n}",
"clusterN": "Cluster {n}",
"clustersBatteriesSummary": "{filledClusters}/{totalClusters} cluster, {filledBat}/{totalBat} batterie",
"batteriesSummary": "{filled}/{total} batterie",
"inverterNSerialNumber": "Numero di serie inverter {n}",
"dataloggerNSerialNumber": "Numero di serie datalogger {n}",
"pvStringsOnInverterN": "Numero di stringhe PV sull'inverter {n}",
"batteryNSerialNumber": "Numero di serie batteria {n}",
"adminSection": "Admin",
"confirmPresetSwitch": "Il passaggio a una configurazione più piccola rimuoverà alcuni numeri di serie delle batterie. Continuare?",
"bucketLabel": "Bucket", "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.", "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",
@ -67,6 +95,7 @@
"noUsersWithDirectAccessToThis": "Nessun utente con accesso diretto a questo", "noUsersWithDirectAccessToThis": "Nessun utente con accesso diretto a questo",
"selectUsers": "Seleziona utenti", "selectUsers": "Seleziona utenti",
"cancel": "Annulla", "cancel": "Annulla",
"continue": "Continua",
"addNewFolder": "Aggiungi nuova cartella", "addNewFolder": "Aggiungi nuova cartella",
"addNewInstallation": "Aggiungi nuova installazione", "addNewInstallation": "Aggiungi nuova installazione",
"deleteFolder": "Elimina cartella", "deleteFolder": "Elimina cartella",
@ -190,7 +219,7 @@
"demo_test_button": "Diagnosi IA", "demo_test_button": "Diagnosi IA",
"demo_hide_button": "Nascondi diagnosi IA", "demo_hide_button": "Nascondi diagnosi IA",
"demo_panel_title": "Diagnosi IA", "demo_panel_title": "Diagnosi IA",
"demo_custom_group": "Personalizzato (potrebbe usare Mistral IA)", "demo_custom_group": "Personalizzato (potrebbe usare IA)",
"demo_custom_option": "Inserisci allarme personalizzato…", "demo_custom_option": "Inserisci allarme personalizzato…",
"demo_custom_placeholder": "es. UnknownBatteryFault", "demo_custom_placeholder": "es. UnknownBatteryFault",
"demo_diagnose_button": "Diagnostica", "demo_diagnose_button": "Diagnostica",
@ -614,5 +643,26 @@
"timelineAiDiagnosisCompletedDesc": "Diagnosi IA completata.", "timelineAiDiagnosisCompletedDesc": "Diagnosi IA completata.",
"timelineAiDiagnosisFailedDesc": "Diagnosi IA fallita.", "timelineAiDiagnosisFailedDesc": "Diagnosi IA fallita.",
"timelineEscalatedDesc": "Ticket escalato.", "timelineEscalatedDesc": "Ticket escalato.",
"timelineResolutionAddedDesc": "Risoluzione aggiunta da {name}." "timelineResolutionAddedDesc": "Risoluzione aggiunta da {name}.",
"terms_dialog_title": "Benvenuto su inesco energy",
"terms_data_heading": "I tuoi dati",
"terms_data_body": "I dati della tua installazione sono archiviati in modo sicuro in Svizzera. Non condividiamo i tuoi dati con terze parti.",
"terms_ai_heading": "Analisi basate sull'IA",
"terms_ai_body": "Utilizziamo un servizio di IA ospitato nell'UE per fornire diagnostica e analisi per le tue installazioni. I risultati generati dall'IA sono raccomandazioni e devono essere verificati da personale qualificato.",
"terms_cookies_heading": "Archiviazione del browser",
"terms_cookies_body": "Questa piattaforma memorizza le impostazioni di accesso e le preferenze nel browser per mantenerti connesso e ricordare la tua scelta linguistica.",
"terms_acknowledge_button": "Ho capito",
"privacy_menu_item": "Dati e privacy",
"privacy_dialog_title": "Dati e privacy",
"privacy_data_heading": "Dove vengono archiviati i miei dati?",
"privacy_data_body": "I dati della tua installazione sono archiviati su server in Svizzera. Solo il personale autorizzato di inesco energy può accedere ai tuoi dati per scopi di assistenza.",
"privacy_ai_heading": "Come viene utilizzata l'IA?",
"privacy_ai_body": "Utilizziamo un servizio di IA ospitato nell'Unione Europea per analizzare i dati della tua installazione e fornire informazioni diagnostiche. L'IA elabora dati tecnici come le letture delle batterie e i codici di errore. Le raccomandazioni dell'IA devono sempre essere verificate da personale qualificato.",
"privacy_browser_heading": "Cosa memorizza il mio browser?",
"privacy_browser_body": "Il tuo browser memorizza la sessione di accesso per mantenerti connesso e le tue preferenze di lingua e tema. Non vengono utilizzati cookie di tracciamento o pubblicitari.",
"privacy_access_heading": "Chi ha accesso ai miei dati?",
"privacy_access_body": "I tuoi dati non vengono condivisi con terze parti. Vengono utilizzati esclusivamente per il funzionamento della piattaforma e per fornirti informazioni sulle tue installazioni.",
"privacy_close_button": "Chiudi",
"sodistorepro": "Sodistore Pro",
"numberOfInverters": "Numero di inverter"
} }

View File

@ -9,9 +9,11 @@ import {
import React, { useContext, useRef, useState } from 'react'; import React, { useContext, useRef, useState } from 'react';
import { styled } from '@mui/material/styles'; import { styled } from '@mui/material/styles';
import ExpandMoreTwoToneIcon from '@mui/icons-material/ExpandMoreTwoTone'; import ExpandMoreTwoToneIcon from '@mui/icons-material/ExpandMoreTwoTone';
import ShieldOutlinedIcon from '@mui/icons-material/ShieldOutlined';
import { ThemeContext } from '../../../../theme/ThemeProvider'; import { ThemeContext } from '../../../../theme/ThemeProvider';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import '../../../../App.css'; import '../../../../App.css';
import DataPrivacyDialog from '../../../../components/DataPrivacyDialog';
interface HeaderButtonsProps { interface HeaderButtonsProps {
language: string; language: string;
@ -79,6 +81,7 @@ function HeaderMenu(props: HeaderButtonsProps) {
const setThemeName = themeContext; const setThemeName = themeContext;
const [darkState, setDarkState] = useState(false); const [darkState, setDarkState] = useState(false);
const [privacyOpen, setPrivacyOpen] = useState(false);
const handleThemeChange = () => { const handleThemeChange = () => {
setDarkState(!darkState); setDarkState(!darkState);
@ -132,6 +135,20 @@ function HeaderMenu(props: HeaderButtonsProps) {
} }
/> />
</ListItem> </ListItem>
<ListItem
classes={{ root: 'MuiListItem-indicators' }}
onClick={() => setPrivacyOpen(true)}
>
<ListItemText
primaryTypographyProps={{ noWrap: true }}
primary={
<Box display="flex" alignItems="center">
<ShieldOutlinedIcon fontSize="small" sx={{ mr: 0.5 }} />
<FormattedMessage id="privacy_menu_item" defaultMessage="Data & Privacy" />
</Box>
}
/>
</ListItem>
</List> </List>
</ListWrapper> </ListWrapper>
<div <div
@ -152,6 +169,10 @@ function HeaderMenu(props: HeaderButtonsProps) {
</MenuItem> </MenuItem>
</Menu> </Menu>
</div> </div>
<DataPrivacyDialog
open={privacyOpen}
onClose={() => setPrivacyOpen(false)}
/>
</div> </div>
); );
} }

View File

@ -170,7 +170,8 @@ function SidebarMenu() {
accessToSodistore, accessToSodistore,
accessToSalidomo, accessToSalidomo,
accessToSodiohome, accessToSodiohome,
accessToSodistoreGrid accessToSodistoreGrid,
accessToSodistorePro
} = useContext(ProductIdContext); } = useContext(ProductIdContext);
return ( return (
@ -185,37 +186,20 @@ function SidebarMenu() {
} }
> >
<SubMenuWrapper> <SubMenuWrapper>
{accessToSalimax && ( {accessToSodiohome && (
<List component="div"> <List component="div">
<ListItem component="div"> <ListItem component="div">
<Button <Button
disableRipple disableRipple
component={RouterLink} component={RouterLink}
onClick={closeSidebar} onClick={closeSidebar}
to="/installations" to="/sodiohome_installations"
startIcon={<BrightnessLowTwoToneIcon />}
>
<Box sx={{ marginTop: '3px' }}>
<FormattedMessage id="salimax" defaultMessage="Salimax" />
</Box>
</Button>
</ListItem>
</List>
)}
{accessToSodistore && (
<List component="div">
<ListItem component="div">
<Button
disableRipple
component={RouterLink}
onClick={closeSidebar}
to="/sodistore_installations"
startIcon={<BrightnessLowTwoToneIcon />} startIcon={<BrightnessLowTwoToneIcon />}
> >
<Box sx={{ marginTop: '3px' }}> <Box sx={{ marginTop: '3px' }}>
<FormattedMessage <FormattedMessage
id="sodistore" id="sodiohome"
defaultMessage="Sodistore Max" defaultMessage="Sodistore Home"
/> />
</Box> </Box>
</Button> </Button>
@ -223,20 +207,20 @@ function SidebarMenu() {
</List> </List>
)} )}
{accessToSalidomo && ( {accessToSodistorePro && (
<List component="div"> <List component="div">
<ListItem component="div"> <ListItem component="div">
<Button <Button
disableRipple disableRipple
component={RouterLink} component={RouterLink}
onClick={closeSidebar} onClick={closeSidebar}
to="/salidomo_installations" to="/sodistorepro_installations"
startIcon={<BrightnessLowTwoToneIcon />} startIcon={<BrightnessLowTwoToneIcon />}
> >
<Box sx={{ marginTop: '3px' }}> <Box sx={{ marginTop: '3px' }}>
<FormattedMessage <FormattedMessage
id="salidomo" id="sodistorepro"
defaultMessage="Salidomo" defaultMessage="Sodistore Pro"
/> />
</Box> </Box>
</Button> </Button>
@ -265,20 +249,59 @@ function SidebarMenu() {
</List> </List>
)} )}
{accessToSodiohome && ( {accessToSodistore && (
<List component="div"> <List component="div">
<ListItem component="div"> <ListItem component="div">
<Button <Button
disableRipple disableRipple
component={RouterLink} component={RouterLink}
onClick={closeSidebar} onClick={closeSidebar}
to="/sodiohome_installations" to="/sodistore_installations"
startIcon={<BrightnessLowTwoToneIcon />} startIcon={<BrightnessLowTwoToneIcon />}
> >
<Box sx={{ marginTop: '3px' }}> <Box sx={{ marginTop: '3px' }}>
<FormattedMessage <FormattedMessage
id="sodiohome" id="sodistore"
defaultMessage="Sodistore Home" defaultMessage="Sodistore Max"
/>
</Box>
</Button>
</ListItem>
</List>
)}
{accessToSalimax && (
<List component="div">
<ListItem component="div">
<Button
disableRipple
component={RouterLink}
onClick={closeSidebar}
to="/installations"
startIcon={<BrightnessLowTwoToneIcon />}
>
<Box sx={{ marginTop: '3px' }}>
<FormattedMessage id="salimax" defaultMessage="Salimax" />
</Box>
</Button>
</ListItem>
</List>
)}
{accessToSalidomo && (
<List component="div">
<ListItem component="div">
<Button
disableRipple
component={RouterLink}
onClick={closeSidebar}
to="/salidomo_installations"
startIcon={<BrightnessLowTwoToneIcon />}
>
<Box sx={{ marginTop: '3px' }}>
<FormattedMessage
id="salidomo"
defaultMessage="Salidomo"
/> />
</Box> </Box>
</Button> </Button>