Compare commits

..

No commits in common. "812962ace00d4c3c19698233c1c01db01dbc4a3e" and "897f3137f5db87d7cfe0d557415f9b02d7c08dce" have entirely different histories.

57 changed files with 648 additions and 1869 deletions

View File

@ -202,8 +202,6 @@ public class Controller : ControllerBase
bucketPath = "s3://" + installation.S3BucketId + "-e7b9a240-3c5d-4d2e-a019-6d8b1f7b73fa/" + startTimestamp;
else if (installation.Product == (int)ProductType.SodistoreGrid)
bucketPath = "s3://" + installation.S3BucketId + "-5109c126-e141-43ab-8658-f3c44c838ae8/" + startTimestamp;
else if (installation.Product == (int)ProductType.SodistorePro)
bucketPath = "s3://" + installation.S3BucketId + "-325c9373-9025-4a8d-bf5a-f9eedf1f155c/" + startTimestamp;
else
bucketPath = "s3://" + installation.S3BucketId + "-c0436b6a-d276-4cd8-9c44-1eae86cf5d0e/" + startTimestamp;
Console.WriteLine("Fetching data for "+startTimestamp);
@ -817,10 +815,9 @@ public class Controller : ControllerBase
if (installation is null || !user.HasAccessTo(installation))
return Unauthorized();
// AI diagnostics are scoped to SodistoreHome, SodiStoreMax, and SodistorePro only
// AI diagnostics are scoped to SodistoreHome and SodiStoreMax only
if (installation.Product != (int)ProductType.SodioHome &&
installation.Product != (int)ProductType.SodiStoreMax &&
installation.Product != (int)ProductType.SodistorePro)
installation.Product != (int)ProductType.SodiStoreMax)
return BadRequest("AI diagnostics not available for this product.");
var result = await DiagnosticService.DiagnoseAsync(installationId, errorDescription, user.Language ?? "en");
@ -2137,31 +2134,6 @@ 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.");
}
@ -2337,17 +2309,4 @@ public class Controller : ControllerBase
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,8 +8,7 @@ public enum ProductType
Salidomo = 1,
SodioHome =2,
SodiStoreMax=3,
SodistoreGrid=4,
SodistorePro=5
SodistoreGrid=4
}
public enum StatusType
@ -28,13 +27,6 @@ public class Installation : TreeNode
public String Location { get; set; } = "";
public String Region { 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 InstallationName { get; set; } = "";
@ -57,11 +49,7 @@ public class Installation : TreeNode
public int BatteryClusterNumber { get; set; } = 0;
public int BatteryNumber { get; set; } = 0;
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]
public String OrderNumbers { get; set; }
public String VrmLink { get; set; } = "";

View File

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

View File

@ -11,7 +11,6 @@ public static class InstallationMethods
private static readonly String SalidomoBucketNameSalt = "c0436b6a-d276-4cd8-9c44-1eae86cf5d0e";
private static readonly String SodioHomeBucketNameSalt = "e7b9a240-3c5d-4d2e-a019-6d8b1f7b73fa";
private static readonly String SodistoreGridBucketNameSalt = "5109c126-e141-43ab-8658-f3c44c838ae8";
private static readonly String SodistoreProBucketNameSalt = "325c9373-9025-4a8d-bf5a-f9eedf1f155c";
public static String BucketName(this Installation installation)
{
@ -30,11 +29,6 @@ public static class InstallationMethods
return $"{installation.S3BucketId}-{SodistoreGridBucketNameSalt}";
}
if (installation.Product == (int)ProductType.SodistorePro)
{
return $"{installation.S3BucketId}-{SodistoreProBucketNameSalt}";
}
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 || installation.Product == (int)ProductType.SodistorePro)
if (installation.Product == (int)ProductType.SodiStoreMax || installation.Product == (int)ProductType.SodioHome || installation.Product == (int)ProductType.SodistoreGrid)
{
return user is not null
&& user.UserType != 0
@ -295,7 +295,7 @@ public static class SessionMethods
.Apply(Db.Update);
}
if (installation.Product == (int)ProductType.SodiStoreMax || installation.Product == (int)ProductType.SodistoreGrid || installation.Product == (int)ProductType.SodistorePro)
if (installation.Product == (int)ProductType.SodiStoreMax || installation.Product == (int)ProductType.SodistoreGrid)
{
return user is not null

View File

@ -243,22 +243,22 @@ public static class UserMethods
var (subject, body) = (user.Language ?? "en") switch
{
"de" => (
"Passwort Ihres inesco energy Kontos zurücksetzen",
"Passwort Ihres Inesco Energy Kontos zurücksetzen",
$"Sehr geehrte/r {user.Name}\n" +
$"Um Ihr Passwort zurückzusetzen, öffnen Sie bitte diesen Link: {resetLink}?token={encodedToken}"
),
"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" +
$"Pour réinitialiser votre mot de passe, veuillez ouvrir ce lien : {resetLink}?token={encodedToken}"
),
"it" => (
"Reimposta la password del tuo account inesco energy",
"Reimposta la password del tuo account Inesco Energy",
$"Gentile {user.Name}\n" +
$"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" +
$"To reset your password please open this link: {resetLink}?token={encodedToken}"
)
@ -274,89 +274,28 @@ public static class UserMethods
var (subject, body) = (user.Language ?? "en") switch
{
"de" => (
"Ihr neues inesco energy Konto",
"Ihr neues Inesco Energy Konto",
$"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" => (
"Votre nouveau compte inesco energy",
"Votre nouveau compte Inesco Energy",
$"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" => (
"Il tuo nuovo account inesco energy",
"Il tuo nuovo account Inesco Energy",
$"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" +
$"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);
}
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,7 +11,6 @@ public class User : TreeNode
public Boolean MustResetPassword { get; set; } = false;
public String? Password { get; set; } = null!;
public String Language { get; set; } = "en";
public Int32? AcknowledgedTermsVersion { get; set; }
[Unique]
public override String Name { get; set; } = null!;

View File

@ -83,12 +83,10 @@ public static partial class Db
Connection.Execute("UPDATE User SET Language = 'fr' WHERE Language = 'french'");
Connection.Execute("UPDATE User SET Language = 'it' WHERE Language = 'italian'");
// One-time migration: rebrand to inesco energy
Connection.Execute("UPDATE Folder SET Name = 'inesco energy' WHERE Name = 'InnovEnergy'");
Connection.Execute("UPDATE Folder SET Name = 'inesco energy' WHERE Name = '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 = '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 = 'inesco Energy Master Admin'");
Connection.Execute("UPDATE User SET Name = 'inesco Energy Master Admin' WHERE Name = 'InnovEnergy Master Admin'");
//UpdateKeys();
CleanupSessions().SupressAwaitWarning();
@ -132,11 +130,6 @@ public static partial class Db
fileConnection.CreateTable<TicketAiDiagnosis>();
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 CopyDbToMemory(fileConnection);
}

View File

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

View File

@ -17,7 +17,6 @@ public class Session : Relation<String, Int64>
public Boolean AccessToSodistoreMax { get; set; } = false;
public Boolean AccessToSodioHome { get; set; } = false;
public Boolean AccessToSodistoreGrid { get; set; } = false;
public Boolean AccessToSodistorePro { get; set; } = false;
[Ignore] public Boolean Valid => DateTime.Now - LastSeen <=MaxAge ;
// Private backing field
@ -52,7 +51,6 @@ public class Session : Relation<String, Int64>
AccessToSodistoreMax = user.AccessibleInstallations(product: (int)ProductType.SodiStoreMax).ToList().Count > 0;
AccessToSodioHome = user.AccessibleInstallations(product: (int)ProductType.SodioHome).ToList().Count > 0;
AccessToSodistoreGrid = user.AccessibleInstallations(product: (int)ProductType.SodistoreGrid).ToList().Count > 0;
AccessToSodistorePro = user.AccessibleInstallations(product: (int)ProductType.SodistorePro).ToList().Count > 0;
Console.WriteLine("salimax" +user.AccessibleInstallations(product: (int)ProductType.Salimax).ToList().Count);
Console.WriteLine("AccessToSodistoreMax" +user.AccessibleInstallations(product: (int)ProductType.SodiStoreMax).ToList().Count);

View File

@ -1223,7 +1223,7 @@ textarea:focus{outline:none;border-color:#3498db}
<div id="app"></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 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>
var ALARMS = %%ALARMS_JSON%%;
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>
</td></tr>
<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>
</table></td></tr></table></body></html>
""";
@ -1545,7 +1545,7 @@ render();
<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: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>
""";
await SendEmailAsync(email, subject, html);
@ -1645,7 +1645,7 @@ render();
<table style="border-collapse:collapse;width:100%">
{beforeAfterRows}
</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>
""";

View File

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

View File

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

View File

@ -120,7 +120,7 @@ public static class ReportEmailService
GridIn: "Netz Ein",
GridOut: "Netz Aus",
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"
),
"fr" => new EmailStrings(
@ -151,7 +151,7 @@ public static class ReportEmailService
GridIn: "Réseau Ent.",
GridOut: "Réseau Sor.",
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"
),
"it" => new EmailStrings(
@ -182,7 +182,7 @@ public static class ReportEmailService
GridIn: "Rete Ent.",
GridOut: "Rete Usc.",
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"
),
_ => new EmailStrings(
@ -213,7 +213,7 @@ public static class ReportEmailService
GridIn: "Grid In",
GridOut: "Grid Out",
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"
)
};
@ -340,7 +340,7 @@ public static class ReportEmailService
<!-- Header -->
<tr>
<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: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>
@ -546,56 +546,56 @@ public static class ReportEmailService
"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",
"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"),
("de", "yearly") => new AggregatedEmailStrings(
"Jährlicher Leistungsbericht", "Jährliche Erkenntnisse", "Jährliche Zusammenfassung", "Ihre Ersparnisse dieses Jahr",
"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",
"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"),
("fr", "monthly") => new AggregatedEmailStrings(
"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",
"É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",
"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"),
("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",
"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",
"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"),
("it", "monthly") => new AggregatedEmailStrings(
"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",
"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",
"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"),
("it", "yearly") => new AggregatedEmailStrings(
"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",
"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",
"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"),
(_, "monthly") => new AggregatedEmailStrings(
"Monthly Performance Report", "Monthly Insights", "Monthly Summary", "Your Savings This Month",
"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 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"),
_ => new AggregatedEmailStrings(
"Annual Performance Report", "Annual Insights", "Annual Summary", "Your Savings This Year",
"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 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")
};
@ -637,7 +637,7 @@ public static class ReportEmailService
<!-- Header -->
<tr>
<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:14px;margin-top:6px;opacity:0.9"">{installationName}</div>
<div style=""font-size:13px;margin-top:2px;opacity:0.7"">{record.Date}</div>
@ -723,7 +723,7 @@ public static class ReportEmailService
<!-- Header -->
<tr>
<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:14px;margin-top:6px;opacity:0.9"">{installationName}</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
var installation = Db.GetInstallationById(installationId);
var location = !string.IsNullOrWhiteSpace(installation?.City) ? installation.City : installation?.Location;
var location = installation?.Location;
var country = installation?.Country;
var region = !string.IsNullOrWhiteSpace(installation?.Canton) ? installation.Canton : installation?.Region;
var region = installation?.Region;
Console.WriteLine($"[WeeklyReportService] Installation {installationId}: Location='{location}', Region='{region}', Country='{country}', HourlyRecords={currentHourlyData.Count}");
return await GenerateReportFromDataAsync(

View File

@ -105,11 +105,6 @@ public static class RabbitMqManager
monitorLink =
$"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
{
monitorLink =
@ -136,7 +131,7 @@ public static class RabbitMqManager
Console.WriteLine("Send replace battery email to the support team for installation "+installationId);
string recipient = "support@innov.energy";
string subject = $"Battery Alarm from {installation.Name}: 2 or more strings broken";
string text = $"Dear inesco energy Support Team,\n" +
string text = $"Dear inesco Energy Support Team,\n" +
$"\n"+
$"Installation Name: {installation.Name}\n"+
$"\n"+
@ -148,7 +143,7 @@ public static class RabbitMqManager
$"\n"+
$"Thank you for your great support:)";
//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
Db.HandleError(newError, installationId);

View File

@ -31,8 +31,7 @@ public static class WebsocketManager
(installationConnection.Value.Product == (int)ProductType.Salidomo && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(60)) ||
(installationConnection.Value.Product == (int)ProductType.SodioHome && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(4)) ||
(installationConnection.Value.Product == (int)ProductType.SodiStoreMax && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(2)) ||
(installationConnection.Value.Product == (int)ProductType.SodistoreGrid && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(2)) ||
(installationConnection.Value.Product == (int)ProductType.SodistorePro && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(4))
(installationConnection.Value.Product == (int)ProductType.SodistoreGrid && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(2))
)
{
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"
rel="stylesheet"
/>
<title>inesco energy</title>
<title>Inesco Energy</title>
</head>
<body>

View File

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

View File

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

View File

@ -1,95 +0,0 @@
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

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

View File

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

View File

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

View File

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

View File

@ -151,25 +151,22 @@ function MainStatsSodioHome(props: MainStatsSodioHomeProps) {
pathsToSearch.push('Node' + i);
}
const total = pathsToSearch.length;
let i = 0;
pathsToSearch.forEach((devicePath) => {
if (
Object.hasOwnProperty.call(chartData[category].data, devicePath) &&
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({
...chartData[category].data[devicePath],
color:
color === 'blue'
? blueColors[colorIndex]
? blueColors[i]
: color === 'red'
? redColors[colorIndex]
? redColors[i]
: color === 'green'
? greenColors[colorIndex]
: orangeColors[colorIndex]
? greenColors[i]
: orangeColors[i]
});
}
i++;

View File

@ -57,7 +57,7 @@ function Information(props: InformationProps) {
const canEdit = currentUser.userType == UserType.admin;
const isPartner = currentUser.userType == UserType.partner;
const isSodistore = formValues.product === 3 || formValues.product === 4 || formValues.product === 5;
const isSodistore = formValues.product === 3 || formValues.product === 4;
const [networkProviders, setNetworkProviders] = useState<string[]>([]);
const [loadingProviders, setLoadingProviders] = useState(false);
@ -426,18 +426,12 @@ function Information(props: InformationProps) {
label="S3 Bucket Name"
name="s3bucketname"
value={
formValues.product === 0 || formValues.product === 3
formValues.product === 0 || formValues.product == 3
? formValues.s3BucketId +
'-3e5b3069-214a-43ee-8d85-57d72000c19d'
: formValues.product === 4
: formValues.product == 4
? formValues.s3BucketId +
'-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 +
'-e7b9a240-3c5d-4d2e-a019-6d8b1f7b73fa'
}

View File

@ -1,128 +0,0 @@
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) {
const jsontext = await r.text();
if (product === 2 || product === 5) {
if (product === 2) {
return parseSinexcelAggregatedData(jsontext);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,32 +17,29 @@ import { Close as CloseIcon } from '@mui/icons-material';
import { I_Installation } from 'src/interfaces/InstallationTypes';
import { InstallationsContext } from 'src/contexts/InstallationsContextProvider';
import { FormattedMessage } from 'react-intl';
import { INSTALLATION_PRESETS, SODIOHOME_DEVICE_TYPES } from '../Information/installationSetupUtils';
interface SodistorehomeInstallationFormPros {
cancel: () => void;
submit: () => void;
parentid: number;
product?: number;
}
function SodistorehomeInstallationForm(props: SodistorehomeInstallationFormPros) {
const theme = useTheme();
const [open, setOpen] = useState(true);
const isSodistorePro = props.product === 5;
const [formValues, setFormValues] = useState<Partial<I_Installation>>({
name: '',
region: '',
location: '',
country: '',
vpnIp: '',
installationModel: '',
externalEms: 'No',
...(isSodistorePro ? { device: 4 } : {}),
});
const [inverterCount, setInverterCount] = useState('');
const requiredFields = ['name', 'vpnIp', ...(isSodistorePro ? [] : ['installationModel'])];
const requiredFields = ['name', 'location', 'country', 'vpnIp'];
const DeviceTypes = isSodistorePro
? [{ id: 4, name: 'inesco 12K - WR Hybrid' }]
: SODIOHOME_DEVICE_TYPES;
const DeviceTypes = [
{ id: 3, name: 'Growatt' },
{ id: 4, name: 'Sinexcel' }
];
const installationContext = useContext(InstallationsContext);
const { createInstallation, loading, setLoading, error, setError } =
installationContext;
@ -58,10 +55,7 @@ function SodistorehomeInstallationForm(props: SodistorehomeInstallationFormPros)
const handleSubmit = async (e) => {
setLoading(true);
formValues.parentId = props.parentid;
formValues.product = props.product ?? 2;
if (isSodistorePro) {
formValues.installationModel = inverterCount;
}
formValues.product = 2;
const responseData = await createInstallation(formValues);
props.submit();
};
@ -75,9 +69,6 @@ function SodistorehomeInstallationForm(props: SodistorehomeInstallationFormPros)
return false;
}
}
if (isSodistorePro && (!inverterCount || parseInt(inverterCount, 10) < 1)) {
return false;
}
return true;
};
@ -133,6 +124,42 @@ function SodistorehomeInstallationForm(props: SodistorehomeInstallationFormPros)
error={formValues.name === ''}
/>
</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>
<TextField
label={<FormattedMessage id="VpnIp" defaultMessage="VpnIp" />}
@ -144,67 +171,6 @@ function SodistorehomeInstallationForm(props: SodistorehomeInstallationFormPros)
/>
</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>
<FormControl
fullWidth
@ -238,7 +204,6 @@ function SodistorehomeInstallationForm(props: SodistorehomeInstallationFormPros)
</Select>
</FormControl>
</div>
)}
</Box>
<div

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -86,7 +86,7 @@ export const transformInputToBatteryViewDataJson = async (
}> => {
const prefixes = ['', 'k', 'M', 'G', 'T'];
const MAX_NUMBER = 9999999;
const isSodioHome = product === 2 || product === 5;
const isSodioHome = product === 2;
const categories = isSodioHome
? ['Soc', 'Power', 'Voltage', 'Current', 'Soh']
: ['Soc', 'Temperature', 'Power', 'Voltage', 'Current'];
@ -169,7 +169,7 @@ export const transformInputToBatteryViewDataJson = async (
);
const adjustedTimestamp =
product == 0 || product == 2 || product == 3 || product == 4 || product == 5
product == 0 || product == 2 || product == 3 || product == 4
? new Date(timestampArray[i] * 1000)
: new Date(timestampArray[i] * 100000);
//Timezone offset is negative, so we convert the timestamp to the current zone by subtracting the corresponding offset
@ -226,52 +226,26 @@ export const transformInputToBatteryViewDataJson = async (
categories.forEach((category) => {
pathsToSave.forEach((path) => {
if (pathsToSave.indexOf(path) >= old_length) {
const displayIndex = pathsToSave.indexOf(path);
chartData[category].data[path] = { name: 'Battery Cluster ' + (displayIndex + 1), data: [] };
chartData[category].data[path] = { name: path, data: [] };
}
});
});
}
const hasDevices = !!inv?.Devices;
// Sinexcel field suffixes differ from Growatt for Voltage/Current
const categoryFieldMapGrowatt = {
// Map category names to InverterRecord field suffixes
const categoryFieldMap = {
Soc: 'Soc',
Power: 'Power',
Voltage: 'Voltage',
Current: 'Current',
Soh: 'Soh'
};
const categoryFieldMapSinexcel = {
Soc: 'Soc',
Power: 'Power',
Voltage: 'PackTotalVoltage',
Current: 'PackTotalCurrent',
Soh: 'Soh'
};
for (let j = 0; j < pathsToSave.length; j++) {
const batteryIndex = j + 1; // Battery1, Battery2, ...
categories.forEach((category) => {
let value: number | undefined;
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];
}
const fieldName = `Battery${batteryIndex}${categoryFieldMap[category]}`;
const value = inv[fieldName];
if (value !== undefined && value !== null) {
if (value < chartOverview[category].min) {
@ -419,7 +393,7 @@ export const transformInputToDailyDataJson = async (
// custom fallback logic to handle differences between Growatt and Sinexcel.
// Growatt has: Battery1AmbientTemperature, GridPower, PvPower
// Sinexcel has: Battery1Temperature, TotalGridPower (meter may be offline), PvPower1-4
const pathsToSearch = (product == 2 || product == 5)
const pathsToSearch = product == 2
? [
'SODIOHOME_SOC',
'SODIOHOME_TEMPERATURE',
@ -542,8 +516,8 @@ export const transformInputToDailyDataJson = async (
let value: number | undefined = undefined;
if (product === 2 || product === 5) {
// SodioHome/SodistorePro: use top-level aggregated values (Sinexcel multi-inverter)
if (product === 2) {
// SodioHome: use top-level aggregated values (Sinexcel multi-inverter)
const inv = result?.InverterRecord;
if (inv) {
switch (category_index) {
@ -761,7 +735,7 @@ export const transformInputToAggregatedDataJson = async (
const timestampPromises = [];
while (currentDay.isBefore(end_date)) {
const dateFormat = (product === 2 || product === 5)
const dateFormat = product === 2
? currentDay.format('DDMMYYYY')
: currentDay.format('YYYY-MM-DD');
timestampPromises.push(

View File

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

View File

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

View File

@ -6,13 +6,6 @@
"alarms": "Alarme",
"applyChanges": "Änderungen speichern",
"country": "Land",
"street": "Strasse",
"postCode": "PLZ",
"city": "Ort",
"canton": "Kanton",
"distributionPartner": "Vertriebspartner",
"inverterFirmwareVersion": "Wechselrichter-Firmware-Version",
"batteryFirmwareVersion": "Batterie-Firmware-Version",
"networkProvider": "Netzbetreiber",
"createNewFolder": "Neuer Ordner",
"createNewUser": "Neuer Benutzer",
@ -80,27 +73,6 @@
"live": "Live Daten",
"deleteInstallation": "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",
"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",
@ -113,7 +85,6 @@
"noUsersWithDirectAccessToThis": "Keine Benutzer mit direktem Zugriff",
"selectUsers": "Benutzer auswählen",
"cancel": "Abbrechen",
"continue": "Fortfahren",
"addNewFolder": "Neuen Ordner hinzufügen",
"addNewInstallation": "Neue Installation hinzufügen",
"deleteFolder": "Ordner löschen",
@ -214,7 +185,7 @@
"demo_test_button": "KI-Diagnose",
"demo_hide_button": "KI-Diagnose ausblenden",
"demo_panel_title": "KI-Diagnose",
"demo_custom_group": "Benutzerdefiniert (kann KI verwenden)",
"demo_custom_group": "Benutzerdefiniert (kann Mistral KI verwenden)",
"demo_custom_option": "Benutzerdefinierten Alarm eingeben…",
"demo_custom_placeholder": "z.B. UnknownBatteryFault",
"demo_diagnose_button": "Diagnostizieren",
@ -643,26 +614,5 @@
"timelineAiDiagnosisCompletedDesc": "KI-Diagnose abgeschlossen.",
"timelineAiDiagnosisFailedDesc": "KI-Diagnose fehlgeschlagen.",
"timelineEscalatedDesc": "Ticket eskaliert.",
"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"
"timelineResolutionAddedDesc": "Lösung hinzugefügt von {name}."
}

View File

@ -2,13 +2,6 @@
"allInstallations": "All installations",
"applyChanges": "Apply changes",
"country": "Country",
"street": "Street",
"postCode": "Postcode",
"city": "City",
"canton": "Canton",
"distributionPartner": "Distribution Partner",
"inverterFirmwareVersion": "Inverter Firmware Version",
"batteryFirmwareVersion": "Battery Firmware Version",
"networkProvider": "Network Provider",
"customerName": "Customer name",
"english": "English",
@ -62,27 +55,6 @@
"live": "Live View",
"deleteInstallation": "Delete 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",
"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",
@ -95,7 +67,6 @@
"noUsersWithDirectAccessToThis": "No users with direct access to this ",
"selectUsers": "Select Users",
"cancel": "Cancel",
"continue": "Continue",
"addNewFolder": "Add new Folder",
"addNewInstallation": "Add new Installation",
"deleteFolder": "Delete Folder",
@ -196,7 +167,7 @@
"demo_test_button": "AI Diagnosis",
"demo_hide_button": "Hide AI Diagnosis",
"demo_panel_title": "AI Diagnosis",
"demo_custom_group": "Custom (may use AI)",
"demo_custom_group": "Custom (may use Mistral AI)",
"demo_custom_option": "Type custom alarm below…",
"demo_custom_placeholder": "e.g. UnknownBatteryFault",
"demo_diagnose_button": "Diagnose",
@ -391,26 +362,5 @@
"timelineAiDiagnosisCompletedDesc": "AI diagnosis completed.",
"timelineAiDiagnosisFailedDesc": "AI diagnosis failed.",
"timelineEscalatedDesc": "Ticket escalated.",
"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"
"timelineResolutionAddedDesc": "Resolution added by {name}."
}

View File

@ -4,13 +4,6 @@
"alarms": "Alarmes",
"applyChanges": "Appliquer",
"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",
"createNewFolder": "Nouveau dossier",
"createNewUser": "Nouvel utilisateur",
@ -74,27 +67,6 @@
"live": "Diffusion en direct",
"deleteInstallation": "Supprimer l'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",
"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",
@ -107,7 +79,6 @@
"noUsersWithDirectAccessToThis": "Aucun utilisateur ayant un accès direct",
"selectUsers": "Sélectionnez les utilisateurs",
"cancel": "Annuler",
"continue": "Continuer",
"addNewFolder": "Ajouter un nouveau dossier",
"addNewInstallation": "Ajouter une nouvelle installation",
"deleteFolder": "Supprimer le dossier",
@ -208,7 +179,7 @@
"demo_test_button": "Diagnostic IA",
"demo_hide_button": "Masquer le diagnostic IA",
"demo_panel_title": "Diagnostic IA",
"demo_custom_group": "Personnalisé (peut utiliser IA)",
"demo_custom_group": "Personnalisé (peut utiliser Mistral IA)",
"demo_custom_option": "Saisir une alarme personnalisée…",
"demo_custom_placeholder": "ex. UnknownBatteryFault",
"demo_diagnose_button": "Diagnostiquer",
@ -643,26 +614,5 @@
"timelineAiDiagnosisCompletedDesc": "Diagnostic IA terminé.",
"timelineAiDiagnosisFailedDesc": "Diagnostic IA échoué.",
"timelineEscalatedDesc": "Ticket escaladé.",
"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"
"timelineResolutionAddedDesc": "Résolution ajoutée par {name}."
}

View File

@ -2,13 +2,6 @@
"allInstallations": "Tutte le installazioni",
"applyChanges": "Applica modifiche",
"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",
"customerName": "Nome cliente",
"english": "Inglese",
@ -62,27 +55,6 @@
"live": "Vista in diretta",
"deleteInstallation": "Elimina 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",
"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",
@ -95,7 +67,6 @@
"noUsersWithDirectAccessToThis": "Nessun utente con accesso diretto a questo",
"selectUsers": "Seleziona utenti",
"cancel": "Annulla",
"continue": "Continua",
"addNewFolder": "Aggiungi nuova cartella",
"addNewInstallation": "Aggiungi nuova installazione",
"deleteFolder": "Elimina cartella",
@ -219,7 +190,7 @@
"demo_test_button": "Diagnosi IA",
"demo_hide_button": "Nascondi diagnosi IA",
"demo_panel_title": "Diagnosi IA",
"demo_custom_group": "Personalizzato (potrebbe usare IA)",
"demo_custom_group": "Personalizzato (potrebbe usare Mistral IA)",
"demo_custom_option": "Inserisci allarme personalizzato…",
"demo_custom_placeholder": "es. UnknownBatteryFault",
"demo_diagnose_button": "Diagnostica",
@ -643,26 +614,5 @@
"timelineAiDiagnosisCompletedDesc": "Diagnosi IA completata.",
"timelineAiDiagnosisFailedDesc": "Diagnosi IA fallita.",
"timelineEscalatedDesc": "Ticket escalato.",
"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"
"timelineResolutionAddedDesc": "Risoluzione aggiunta da {name}."
}

View File

@ -9,11 +9,9 @@ import {
import React, { useContext, useRef, useState } from 'react';
import { styled } from '@mui/material/styles';
import ExpandMoreTwoToneIcon from '@mui/icons-material/ExpandMoreTwoTone';
import ShieldOutlinedIcon from '@mui/icons-material/ShieldOutlined';
import { ThemeContext } from '../../../../theme/ThemeProvider';
import { FormattedMessage } from 'react-intl';
import '../../../../App.css';
import DataPrivacyDialog from '../../../../components/DataPrivacyDialog';
interface HeaderButtonsProps {
language: string;
@ -81,7 +79,6 @@ function HeaderMenu(props: HeaderButtonsProps) {
const setThemeName = themeContext;
const [darkState, setDarkState] = useState(false);
const [privacyOpen, setPrivacyOpen] = useState(false);
const handleThemeChange = () => {
setDarkState(!darkState);
@ -135,20 +132,6 @@ function HeaderMenu(props: HeaderButtonsProps) {
}
/>
</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>
</ListWrapper>
<div
@ -169,10 +152,6 @@ function HeaderMenu(props: HeaderButtonsProps) {
</MenuItem>
</Menu>
</div>
<DataPrivacyDialog
open={privacyOpen}
onClose={() => setPrivacyOpen(false)}
/>
</div>
);
}

View File

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