using System.Text.Json; using InnovEnergy.App.Backend.DataTypes; using InnovEnergy.Lib.Mailer; using MailKit.Net.Smtp; using MailKit.Security; using MimeKit; namespace InnovEnergy.App.Backend.Services; public static class ReportEmailService { /// /// Sends the weekly report as a nicely formatted HTML email in the user's language. /// Uses MailKit directly (same config as existing Mailer library) but with HTML support. /// public static async Task SendReportEmailAsync(WeeklyReportResponse report, string recipientEmail, string language = "en", string customerName = null) { var strings = GetStrings(language); var nameSegment = !string.IsNullOrWhiteSpace(customerName) ? $" — {customerName}" : ""; var subject = $"{strings.Title} — {report.InstallationName}{nameSegment} ({report.PeriodStart} to {report.PeriodEnd})"; var html = BuildHtmlEmail(report, strings, customerName); var config = await ReadMailerConfig(); var from = new MailboxAddress(config.SenderName, config.SenderAddress); var to = new MailboxAddress(recipientEmail, recipientEmail); var msg = new MimeMessage { From = { from }, To = { to }, Subject = subject, Body = new TextPart("html") { Text = html } }; Console.WriteLine($"[ReportEmailService] SMTP: {config.SmtpUsername}@{config.SmtpServerUrl}:{config.SmtpPort}"); using var smtp = new SmtpClient(); await smtp.ConnectAsync(config.SmtpServerUrl, config.SmtpPort, SecureSocketOptions.StartTls); await smtp.AuthenticateAsync(config.SmtpUsername, config.SmtpPassword); await smtp.SendAsync(msg); await smtp.DisconnectAsync(true); Console.WriteLine($"[ReportEmailService] Report sent to {recipientEmail}"); } private static async Task ReadMailerConfig() { await using var fileStream = File.OpenRead(MailerConfig.DefaultFile); var config = await JsonSerializer.DeserializeAsync(fileStream); return config ?? throw new InvalidOperationException("Failed to read MailerConfig.json"); } // ── Translation strings ───────────────────────────────────────────────── private record EmailStrings( string Title, string Insights, string Summary, string SavingsHeader, string DailyBreakdown, string Metric, string ThisWeek, string LastWeek, string Change, string PvProduction, string Consumption, string GridImport, string GridExport, string BatteryInOut, string SolarEnergyUsed, string StayedAtHome, string EstMoneySaved, string AtRate, string SolarCoverage, string FromSolar, string BatteryEff, string OutVsIn, string Day, string Load, string GridIn, string GridOut, string BattInOut, string Footer ); private static EmailStrings GetStrings(string language) => language switch { "de" => new EmailStrings( Title: "Wöchentlicher Leistungsbericht", Insights: "Wöchentliche Erkenntnisse", Summary: "Wöchentliche Zusammenfassung", SavingsHeader: "Ihre Ersparnisse diese Woche", DailyBreakdown: "Tägliche Aufschlüsselung (kWh)", Metric: "Kennzahl", ThisWeek: "Diese Woche", LastWeek: "Letzte Woche", Change: "Änderung", PvProduction: "PV-Produktion", Consumption: "Verbrauch", GridImport: "Netzbezug", GridExport: "Netzeinspeisung", BatteryInOut: "Batterie Laden / Entladen", SolarEnergyUsed: "Energie gespart", StayedAtHome: "Solar + Batterie, nicht vom Netz", EstMoneySaved: "Geschätzte Ersparnis", AtRate: "bei 0.39 CHF/kWh", SolarCoverage: "Energieunabhängigkeit", FromSolar: "aus eigenem Solar + Batterie System", BatteryEff: "Batterie-Eff.", OutVsIn: "Entladung vs. Ladung", Day: "Tag", Load: "Last", GridIn: "Netz Ein", GridOut: "Netz Aus", BattInOut: "Batt. Laden/Entl.", Footer: "Erstellt von inesco Energy Monitor" ), "fr" => new EmailStrings( Title: "Rapport de performance hebdomadaire", Insights: "Aperçus de la semaine", Summary: "Résumé de la semaine", SavingsHeader: "Vos économies cette semaine", DailyBreakdown: "Détail quotidien (kWh)", Metric: "Indicateur", ThisWeek: "Cette semaine", LastWeek: "Semaine dernière", Change: "Variation", PvProduction: "Production PV", Consumption: "Consommation", GridImport: "Import réseau", GridExport: "Export réseau", BatteryInOut: "Batterie Charge / Décharge", SolarEnergyUsed: "Énergie économisée", StayedAtHome: "solaire + batterie, non achetée au réseau", EstMoneySaved: "Économies estimées", AtRate: "à 0.39 CHF/kWh", SolarCoverage: "Indépendance énergétique", FromSolar: "de votre système solaire + batterie", BatteryEff: "Eff. batterie", OutVsIn: "décharge vs charge", Day: "Jour", Load: "Charge", GridIn: "Réseau Ent.", GridOut: "Réseau Sor.", BattInOut: "Batt. Ch./Déch.", Footer: "Généré par inesco Energy Monitor" ), "it" => new EmailStrings( Title: "Rapporto settimanale delle prestazioni", Insights: "Approfondimenti settimanali", Summary: "Riepilogo settimanale", SavingsHeader: "I tuoi risparmi questa settimana", DailyBreakdown: "Dettaglio giornaliero (kWh)", Metric: "Metrica", ThisWeek: "Questa settimana", LastWeek: "La settimana scorsa", Change: "Variazione", PvProduction: "Produzione PV", Consumption: "Consumo", GridImport: "Import dalla rete", GridExport: "Export nella rete", BatteryInOut: "Batteria Carica / Scarica", SolarEnergyUsed: "Energia risparmiata", StayedAtHome: "solare + batteria, non acquistata dalla rete", EstMoneySaved: "Risparmio stimato", AtRate: "a 0.39 CHF/kWh", SolarCoverage: "Indipendenza energetica", FromSolar: "dal proprio impianto solare + batteria", BatteryEff: "Eff. batteria", OutVsIn: "scarica vs carica", Day: "Giorno", Load: "Carico", GridIn: "Rete Ent.", GridOut: "Rete Usc.", BattInOut: "Batt. Car./Sc.", Footer: "Generato da inesco Energy Monitor" ), _ => new EmailStrings( Title: "Weekly Performance Report", Insights: "Weekly Insights", Summary: "Weekly Summary", SavingsHeader: "Your Savings This Week", DailyBreakdown: "Daily Breakdown (kWh)", Metric: "Metric", ThisWeek: "This Week", LastWeek: "Last Week", Change: "Change", PvProduction: "PV Production", Consumption: "Consumption", GridImport: "Grid Import", GridExport: "Grid Export", BatteryInOut: "Battery Charge / Discharge", SolarEnergyUsed: "Energy Saved", StayedAtHome: "solar + battery, not bought from grid", EstMoneySaved: "Est. Money Saved", AtRate: "at 0.39 CHF/kWh", SolarCoverage: "Energy Independence", FromSolar: "from your own solar + battery system", BatteryEff: "Battery Eff.", OutVsIn: "discharge vs charge", Day: "Day", Load: "Load", GridIn: "Grid In", GridOut: "Grid Out", BattInOut: "Batt. Ch./Dis.", Footer: "Generated by inesco Energy Monitor" ) }; // ── HTML email template ───────────────────────────────────────────── public static string BuildHtmlEmail(WeeklyReportResponse r, string language = "en", string customerName = null) => BuildHtmlEmail(r, GetStrings(language), customerName); private static string BuildHtmlEmail(WeeklyReportResponse r, EmailStrings s, string customerName = null) { var cur = r.CurrentWeek; var prev = r.PreviousWeek; // Parse AI insight into
  • bullet points (split on newlines, strip leading "- " or "1. ", strip ** markdown) var insightLines = r.AiInsight .Split('\n', StringSplitOptions.RemoveEmptyEntries) .Select(l => System.Text.RegularExpressions.Regex.Replace(l.Trim(), @"^[\d]+[.)]\s*|^[-*]\s*", "").Replace("**", "")) .Where(l => l.Length > 0) .ToList(); var insightHtml = insightLines.Count > 1 ? "
      " + string.Join("", insightLines.Select(l => $"
    • {FormatInsightLine(l)}
    • ")) + "
    " : $"

    {FormatInsightLine(r.AiInsight)}

    "; // Detect which components are present across all daily data var showPv = r.DailyData.Any(d => d.PvProduction > 0.1); var showGrid = r.DailyData.Any(d => d.GridImport > 0.1); // Daily rows — colorful bar chart (pixel widths, email-safe) // Scale each day's bars so their combined total always fills maxBarPx (right-edge aligned). // This replicates the web page's CSS flexbox flex-shrink:1 behaviour. const int maxBarPx = 400; var dailyRows = ""; foreach (var d in r.DailyData) { var dayName = DateTime.Parse(d.Date).ToString("ddd dd.MM"); var isCurrentWeek = string.Compare(d.Date, r.PeriodStart, StringComparison.Ordinal) >= 0; var opacity = isCurrentWeek ? "1" : "0.55"; var fontWeight = isCurrentWeek ? "bold" : "normal"; var dayTotal = (showPv ? d.PvProduction : 0) + d.LoadConsumption + (showGrid ? d.GridImport : 0); if (dayTotal < 0.1) dayTotal = 0.1; var pvPx = showPv ? (int)(d.PvProduction / dayTotal * maxBarPx) : 0; var ldPx = (int)(d.LoadConsumption / dayTotal * maxBarPx); var giPx = showGrid ? (int)(d.GridImport / dayTotal * maxBarPx) : 0; var pvSpan = showPv ? $@"" : ""; var gridSpan = showGrid ? $@"" : ""; var ldRadius = (!showPv ? "border-radius:2px 0 0 2px;" : "") + (!showGrid ? "border-radius:0 2px 2px 0;" : ""); var valueText = (showPv ? $"PV {d.PvProduction:F1} | " : "") + $"{s.Load} {d.LoadConsumption:F1}" + (showGrid ? $" | {s.GridIn} {d.GridImport:F1}" : "") + " kWh"; dailyRows += $@" {dayName}
    {valueText}
    {pvSpan}{gridSpan}
    "; } // Week-over-week comparison rows var comparisonHtml = prev != null ? $@" {s.PvProduction} {cur.TotalPvProduction:F1} kWh {prev.TotalPvProduction:F1} kWh {FormatChange(r.PvChangePercent)} {s.Consumption} {cur.TotalConsumption:F1} kWh {prev.TotalConsumption:F1} kWh {FormatChange(r.ConsumptionChangePercent)} {s.GridImport} {cur.TotalGridImport:F1} kWh {prev.TotalGridImport:F1} kWh {FormatChange(r.GridImportChangePercent)} {s.GridExport} {cur.TotalGridExport:F1} kWh {prev.TotalGridExport:F1} kWh — {s.BatteryInOut} {cur.TotalBatteryCharged:F1}/{cur.TotalBatteryDischarged:F1} kWh {prev.TotalBatteryCharged:F1}/{prev.TotalBatteryDischarged:F1} kWh — " : $@" {s.PvProduction}{cur.TotalPvProduction:F1} kWh {s.Consumption}{cur.TotalConsumption:F1} kWh {s.GridImport}{cur.TotalGridImport:F1} kWh {s.GridExport}{cur.TotalGridExport:F1} kWh {s.BatteryInOut}{cur.TotalBatteryCharged:F1}/{cur.TotalBatteryDischarged:F1} kWh"; var comparisonHeaders = prev != null ? $@"{s.ThisWeek} {s.LastWeek} {s.Change}" : $@"{s.ThisWeek}"; return $@"
    {s.Title}
    {r.InstallationName}
    {(!string.IsNullOrWhiteSpace(customerName) ? $@"
    {customerName}
    " : "")}
    {r.PeriodStart} — {r.PeriodEnd}
    {s.Insights}
    {insightHtml}
    {s.Summary}
    {comparisonHeaders} {comparisonHtml}
    {s.Metric}
    {s.SavingsHeader}
    {SavingsBox(s.SolarEnergyUsed, $"{r.TotalEnergySaved:F1} kWh", s.StayedAtHome, "#27ae60")} {SavingsBox(s.EstMoneySaved, $"~{r.TotalSavingsCHF:F0} CHF", s.AtRate, "#2980b9")} {SavingsBox(s.SolarCoverage, $"{r.SelfSufficiencyPercent:F0}%", s.FromSolar, "#8e44ad")} {SavingsBox(s.BatteryEff, $"{r.BatteryEfficiencyPercent:F0}%", s.OutVsIn, "#e67e22")}
    {s.DailyBreakdown}
    {dailyRows}
    {(showPv ? @$"PV   " : "")} {s.Load}    {(showGrid ? @$"{s.GridIn}" : "")}
    {s.Footer}
    "; } private static string SavingsBox(string label, string value, string subtitle, string color) => $@"
    {value}
    {label}
    {subtitle}
    "; // Bolds "Title" before first colon, and numbers+units in the rest private static string FormatInsightLine(string line) { var colonIdx = line.IndexOf(':'); string result; if (colonIdx > 0) { var title = line[..colonIdx]; var rest = line[colonIdx..]; // includes the colon result = $"{title}{rest}"; } else { result = line; } // Bold all numbers: time ranges (14:00–18:00), times (09:00), decimals, integers result = System.Text.RegularExpressions.Regex.Replace( result, @"(\d{1,2}:\d{2}(?:[–\-]\d{1,2}:\d{2})?|\d+[.,]\d+|\d+)", "$1"); return result; } private static string FormatChange(double pct) => pct == 0 ? "—" : pct > 0 ? $"+{pct:F1}%" : $"{pct:F1}%"; private static string ChangeColor(double pct) => pct > 0 ? "#27ae60" : pct < 0 ? "#e74c3c" : "#888"; // ── Monthly / Yearly Report Emails ──────────────────────────────────── private static readonly string[] MonthNamesEn = { "", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" }; private static readonly string[] MonthNamesDe = { "", "Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember" }; private static readonly string[] MonthNamesFr = { "", "Janvier", "Février", "Mars", "Avril", "Mai", "Juin", "Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre" }; private static readonly string[] MonthNamesIt = { "", "Gennaio", "Febbraio", "Marzo", "Aprile", "Maggio", "Giugno", "Luglio", "Agosto", "Settembre", "Ottobre", "Novembre", "Dicembre" }; public static async Task SendMonthlyReportEmailAsync( MonthlyReportSummary report, string installationName, string recipientEmail, string language = "en", string customerName = null) { var monthNames = language switch { "de" => MonthNamesDe, "fr" => MonthNamesFr, "it" => MonthNamesIt, _ => MonthNamesEn }; var monthName = report.Month >= 1 && report.Month <= 12 ? monthNames[report.Month] : report.Month.ToString(); var s = GetAggregatedStrings(language, "monthly"); var nameSegment = !string.IsNullOrWhiteSpace(customerName) ? $" — {customerName}" : ""; var subject = $"{s.Title} — {installationName}{nameSegment} ({monthName} {report.Year})"; var html = BuildAggregatedHtmlEmail(report.PeriodStart, report.PeriodEnd, installationName, report.TotalPvProduction, report.TotalConsumption, report.TotalGridImport, report.TotalGridExport, report.TotalBatteryCharged, report.TotalBatteryDischarged, report.TotalEnergySaved, report.TotalSavingsCHF, report.SelfSufficiencyPercent, report.BatteryEfficiencyPercent, report.AiInsight, $"{report.WeekCount} {s.CountLabel}", s, customerName); await SendHtmlEmailAsync(subject, html, recipientEmail); } public static async Task SendYearlyReportEmailAsync( YearlyReportSummary report, string installationName, string recipientEmail, string language = "en", string customerName = null) { var s = GetAggregatedStrings(language, "yearly"); var nameSegment = !string.IsNullOrWhiteSpace(customerName) ? $" — {customerName}" : ""; var subject = $"{s.Title} — {installationName}{nameSegment} ({report.Year})"; var html = BuildAggregatedHtmlEmail(report.PeriodStart, report.PeriodEnd, installationName, report.TotalPvProduction, report.TotalConsumption, report.TotalGridImport, report.TotalGridExport, report.TotalBatteryCharged, report.TotalBatteryDischarged, report.TotalEnergySaved, report.TotalSavingsCHF, report.SelfSufficiencyPercent, report.BatteryEfficiencyPercent, report.AiInsight, $"{report.MonthCount} {s.CountLabel}", s, customerName); await SendHtmlEmailAsync(subject, html, recipientEmail); } private static async Task SendHtmlEmailAsync(string subject, string html, string recipientEmail) { var config = await ReadMailerConfig(); var from = new MailboxAddress(config.SenderName, config.SenderAddress); var to = new MailboxAddress(recipientEmail, recipientEmail); var msg = new MimeMessage { From = { from }, To = { to }, Subject = subject, Body = new TextPart("html") { Text = html } }; using var smtp = new SmtpClient(); await smtp.ConnectAsync(config.SmtpServerUrl, config.SmtpPort, SecureSocketOptions.StartTls); await smtp.AuthenticateAsync(config.SmtpUsername, config.SmtpPassword); await smtp.SendAsync(msg); await smtp.DisconnectAsync(true); Console.WriteLine($"[ReportEmailService] Report sent to {recipientEmail}"); } // ── Aggregated report translation strings ───────────────────────────── private record AggregatedEmailStrings( string Title, string Insights, string Summary, string SavingsHeader, string Metric, string Total, string PvProduction, string Consumption, string GridImport, string GridExport, string BatteryInOut, string SolarEnergyUsed, string StayedAtHome, string EstMoneySaved, string AtRate, string SolarCoverage, string FromSolar, string BatteryEff, string OutVsIn, string CountLabel, string Footer ); private static AggregatedEmailStrings GetAggregatedStrings(string language, string type) => (language, type) switch { ("de", "monthly") => new AggregatedEmailStrings( "Monatlicher Leistungsbericht", "Monatliche Erkenntnisse", "Monatliche Zusammenfassung", "Ihre Ersparnisse diesen Monat", "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 inesco Energy Monitor"), ("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 inesco Energy Monitor"), ("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 inesco Energy Monitor"), ("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 inesco Energy Monitor"), ("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 inesco Energy Monitor"), ("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 inesco Energy Monitor"), (_, "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 inesco Energy Monitor"), _ => 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 inesco Energy Monitor") }; // ── Aggregated HTML email template ──────────────────────────────────── private static string BuildAggregatedHtmlEmail( string periodStart, string periodEnd, string installationName, double pvProduction, double consumption, double gridImport, double gridExport, double batteryCharged, double batteryDischarged, double energySaved, double savingsCHF, double selfSufficiency, double batteryEfficiency, string aiInsight, string countLabel, AggregatedEmailStrings s, string customerName = null) { var insightLines = aiInsight .Split('\n', StringSplitOptions.RemoveEmptyEntries) .Select(l => System.Text.RegularExpressions.Regex.Replace(l.Trim(), @"^[\d]+[.)]\s*|^[-*]\s*", "").Replace("**", "")) .Where(l => l.Length > 0) .ToList(); var insightHtml = insightLines.Count > 1 ? "
      " + string.Join("", insightLines.Select(l => $"
    • {FormatInsightLine(l)}
    • ")) + "
    " : $"

    {FormatInsightLine(aiInsight)}

    "; return $@"
    {s.Title}
    {installationName}
    {(!string.IsNullOrWhiteSpace(customerName) ? $@"
    {customerName}
    " : "")}
    {periodStart} — {periodEnd}
    {countLabel}
    {s.Insights}
    {insightHtml}
    {s.Summary}
    {s.Metric} {s.Total}
    {s.PvProduction}{pvProduction:F1} kWh
    {s.Consumption}{consumption:F1} kWh
    {s.GridImport}{gridImport:F1} kWh
    {s.GridExport}{gridExport:F1} kWh
    {s.BatteryInOut}{batteryCharged:F1} / {batteryDischarged:F1} kWh
    {s.SavingsHeader}
    {SavingsBox(s.SolarEnergyUsed, $"{energySaved:F1} kWh", s.StayedAtHome, "#27ae60")} {SavingsBox(s.EstMoneySaved, $"~{savingsCHF:F0} CHF", s.AtRate, "#2980b9")} {SavingsBox(s.SolarCoverage, $"{selfSufficiency:F0}%", s.FromSolar, "#8e44ad")} {SavingsBox(s.BatteryEff, $"{batteryEfficiency:F0}%", s.OutVsIn, "#e67e22")}
    {s.Footer}
    "; } }