diff --git a/csharp/App/Backend/Controller.cs b/csharp/App/Backend/Controller.cs index fc73909dc..04a08e632 100644 --- a/csharp/App/Backend/Controller.cs +++ b/csharp/App/Backend/Controller.cs @@ -3061,6 +3061,45 @@ public class Controller : ControllerBase return Ok(summaries); } + // ── Battery full-charge low-voltage anomaly (temporary diagnostic) ── + + [HttpGet(nameof(GetBatteryVoltageAnomalySummary))] + public ActionResult> GetBatteryVoltageAnomalySummary(Token authToken) + { + var user = Db.GetSession(authToken)?.User; + if (user is null || user.UserType != 2) return Unauthorized(); + + // DetectedAt is "yyyy-MM-dd HH:mm:ss" — fixed-width, so lexicographic == chronological. + // If the format ever changes, this ordering breaks silently. + var summary = Db.BatteryVoltageAnomalies + .ToList() + .GroupBy(a => a.InstallationId) + .Select(g => + { + var latest = g.OrderByDescending(a => a.DetectedAt).First(); + return new + { + installationId = g.Key, + lastDetectedAt = latest.DetectedAt, + lastVoltage = latest.Voltage, + lastBattery = latest.BatteryLabel, + count = g.Count() + }; + }) + .ToList(); + + return Ok(summary); + } + + [HttpGet(nameof(GetBatteryVoltageAnomalyLog))] + public ActionResult> GetBatteryVoltageAnomalyLog(Int64 installationId, Token authToken) + { + var user = Db.GetSession(authToken)?.User; + if (user is null || user.UserType != 2) return Unauthorized(); + + return Ok(Db.GetBatteryVoltageAnomalies(installationId)); + } + // ── On-Site Installer Checklist (sodistore home only) ────────────── [HttpGet(nameof(GetOnSiteChecklistForInstallation))] diff --git a/csharp/App/Backend/DataTypes/BatteryVoltageAnomaly.cs b/csharp/App/Backend/DataTypes/BatteryVoltageAnomaly.cs new file mode 100644 index 000000000..78458261e --- /dev/null +++ b/csharp/App/Backend/DataTypes/BatteryVoltageAnomaly.cs @@ -0,0 +1,30 @@ +using SQLite; + +namespace InnovEnergy.App.Backend.DataTypes; + +// Temporary diagnostic: one row per battery cluster per hour where SOC=100% & pack voltage < 58V +// ("BMS fake-full"). When this feature is no longer needed, drop the table, this type, +// BatteryVoltageCheckService, BatteryVoltageAnomalyDetector, the two endpoints, the Db.cs +// CreateTable entries (main + fileConnection), and the related Create/Read/Delete helpers. +public class BatteryVoltageAnomaly +{ + [PrimaryKey, AutoIncrement] + public Int64 Id { get; set; } + + [Indexed] + public Int64 InstallationId { get; set; } + + public String BatteryLabel { get; set; } = ""; + + public String DetectedAt { get; set; } = ""; + + [Indexed] + public String Date { get; set; } = ""; + + public String DateHour { get; set; } = ""; + + public Double Voltage { get; set; } + public Double Soc { get; set; } + + public String CreatedAt { get; set; } = ""; +} diff --git a/csharp/App/Backend/Database/Create.cs b/csharp/App/Backend/Database/Create.cs index 376fd0069..125c3f4ca 100644 --- a/csharp/App/Backend/Database/Create.cs +++ b/csharp/App/Backend/Database/Create.cs @@ -47,7 +47,12 @@ public static partial class Db { return Insert(session); } - + + public static Boolean Create(BatteryVoltageAnomaly anomaly) + { + return Insert(anomaly); + } + public static Boolean Create(InstallationAccess installationAccess) { return Insert(installationAccess); diff --git a/csharp/App/Backend/Database/Db.cs b/csharp/App/Backend/Database/Db.cs index ba414a01d..7d04a7c60 100644 --- a/csharp/App/Backend/Database/Db.cs +++ b/csharp/App/Backend/Database/Db.cs @@ -50,6 +50,9 @@ public static partial class Db public static TableQuery OnSiteChecklistItems => Connection.Table(); public static TableQuery OnSiteChecklistSignatures => Connection.Table(); + // Battery full-charge low-voltage anomaly log (temporary diagnostic) + public static TableQuery BatteryVoltageAnomalies => Connection.Table(); + public static void Init() { @@ -98,6 +101,9 @@ public static partial class Db // On-site installer checklist (sodistore home) Connection.CreateTable(); Connection.CreateTable(); + + // Battery full-charge low-voltage anomaly log (temporary diagnostic) + Connection.CreateTable(); }); // One-time migration: normalize legacy long-form language values to ISO codes @@ -281,6 +287,9 @@ public static partial class Db fileConnection.CreateTable(); fileConnection.CreateTable(); + // Battery full-charge low-voltage anomaly log (temporary diagnostic) + fileConnection.CreateTable(); + // 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"); diff --git a/csharp/App/Backend/Database/Delete.cs b/csharp/App/Backend/Database/Delete.cs index d80b05dc0..0dd289600 100644 --- a/csharp/App/Backend/Database/Delete.cs +++ b/csharp/App/Backend/Database/Delete.cs @@ -375,4 +375,9 @@ public static partial class Db Backup(); Console.WriteLine($"[Db] Cleanup: {oldDailyIds.Count} daily, {oldHourlyIds.Count} hourly, {oldWeeklyIds.Count} weekly, {oldMonthlyIds.Count} monthly, {oldCacheIds.Count} insight cache records deleted (cutoff {cutoff})."); } + + public static void DeleteBatteryVoltageAnomaliesBefore(String isoDate) + { + Connection.Execute("DELETE FROM BatteryVoltageAnomaly WHERE Date < ?", isoDate); + } } \ No newline at end of file diff --git a/csharp/App/Backend/Database/Read.cs b/csharp/App/Backend/Database/Read.cs index 8fc6c16fe..f61bc86cd 100644 --- a/csharp/App/Backend/Database/Read.cs +++ b/csharp/App/Backend/Database/Read.cs @@ -272,4 +272,19 @@ public static partial class Db public static OnSiteChecklistSignature? GetOnSiteChecklistSignature(Int64 installationId) => OnSiteChecklistSignatures.FirstOrDefault(s => s.InstallationId == installationId); + + // ── BatteryVoltageAnomaly Queries (temporary diagnostic) ──────────── + + public static Boolean BatteryVoltageAnomalyExists(Int64 installationId, String batteryLabel, String dateHour) + => BatteryVoltageAnomalies.Any(a => + a.InstallationId == installationId && + a.BatteryLabel == batteryLabel && + a.DateHour == dateHour); + + public static List GetBatteryVoltageAnomalies(Int64 installationId) + => BatteryVoltageAnomalies + .Where(a => a.InstallationId == installationId) + .ToList() + .OrderByDescending(a => a.DetectedAt) + .ToList(); } \ No newline at end of file diff --git a/csharp/App/Backend/Program.cs b/csharp/App/Backend/Program.cs index c090c0aac..7ff11b996 100644 --- a/csharp/App/Backend/Program.cs +++ b/csharp/App/Backend/Program.cs @@ -36,6 +36,7 @@ public static class Program AlarmReviewService.StartDailyScheduler(); DailyIngestionService.StartScheduler(); ReportAggregationService.StartScheduler(); + BatteryVoltageCheckService.StartScheduler(); var builder = WebApplication.CreateBuilder(args); RabbitMqManager.InitializeEnvironment(); diff --git a/csharp/App/Backend/Services/BatteryVoltageAnomalyDetector.cs b/csharp/App/Backend/Services/BatteryVoltageAnomalyDetector.cs new file mode 100644 index 000000000..60744daa5 --- /dev/null +++ b/csharp/App/Backend/Services/BatteryVoltageAnomalyDetector.cs @@ -0,0 +1,113 @@ +using System.Text; +using System.Text.Json; + +namespace InnovEnergy.App.Backend.Services; + +public readonly record struct VoltageAnomaly(String BatteryLabel, Double Soc, Double Voltage, DateTime? Timestamp); + +public static class BatteryVoltageAnomalyDetector +{ + private const Double FullSoc = 100.0; + private const Double LowVoltageVolt = 58.0; + private const Int32 MaxClusters = 8; + + public static List Detect(String liveJson, Int32 device) + { + var decoded = Decode(liveJson); + using var doc = JsonDocument.Parse(decoded); + var root = Unwrap(doc.RootElement); + var ts = ReadTimestamp(root); + + return device switch + { + 3 => DetectGrowatt(root, ts), + 4 => DetectSinexcel(root, ts), + _ => new List(), + }; + } + + private static List DetectSinexcel(JsonElement root, DateTime? ts) + { + var found = new List(); + if (!root.TryGetProperty("Devices", out var devices) || devices.ValueKind != JsonValueKind.Object) + { + Console.WriteLine("[BattVoltage] Sinexcel JSON has no Devices node — check live-file shape assumption."); + return found; + } + + foreach (var dev in devices.EnumerateObject()) + { + for (var cl = 1; cl <= MaxClusters; cl++) + { + // Battery1 uses "SocSecondvalue" (capital S), Battery2 uses "Socsecondvalue" + // (lowercase s) per the documented live-data schema; try both casings. + var soc = ReadDouble(dev.Value, $"Battery{cl}Soc") + ?? ReadDouble(dev.Value, $"Battery{cl}SocSecondvalue") + ?? ReadDouble(dev.Value, $"Battery{cl}Socsecondvalue"); + var v = ReadDouble(dev.Value, $"Battery{cl}PackTotalVoltage"); + if (soc is null || v is null) continue; + if (IsAnomaly(soc.Value, v.Value)) + found.Add(new VoltageAnomaly($"Inv{dev.Name}/Bat{cl}", soc.Value, v.Value, ts)); + } + } + return found; + } + + private static List DetectGrowatt(JsonElement root, DateTime? ts) + { + var found = new List(); + for (var i = 1; i <= MaxClusters; i++) + { + var soc = ReadDouble(root, $"Battery{i}Soc"); + var v = ReadDouble(root, $"Battery{i}Voltage"); + if (soc is null || v is null) continue; + if (IsAnomaly(soc.Value, v.Value)) + found.Add(new VoltageAnomaly($"Bat{i}", soc.Value, v.Value, ts)); + } + return found; + } + + private static Boolean IsAnomaly(Double soc, Double v) => soc >= FullSoc && v < LowVoltageVolt; + + private static Double? ReadDouble(JsonElement e, String name) + { + if (!e.TryGetProperty(name, out var p)) return null; + if (p.ValueKind == JsonValueKind.Number && p.TryGetDouble(out var d)) return d; + if (p.ValueKind == JsonValueKind.String && + Double.TryParse(p.GetString(), System.Globalization.NumberStyles.Float, + System.Globalization.CultureInfo.InvariantCulture, out var ds)) return ds; + return null; + } + + private static DateTime? ReadTimestamp(JsonElement e) + { + if (!e.TryGetProperty("Timestamp", out var p)) return null; + if (p.ValueKind == JsonValueKind.String && p.TryGetDateTime(out var dt)) + { + // Unspecified-Kind (no offset in source) is assumed UTC; otherwise convert. + // Without this, downstream ToString("yyyy-MM-dd HH") prints local-time for + // some sources and UTC for others, breaking the (..., DateHour) dedup key + // and the cleanup-cutoff comparison. + return dt.Kind == DateTimeKind.Unspecified + ? DateTime.SpecifyKind(dt, DateTimeKind.Utc) + : dt.ToUniversalTime(); + } + if (p.ValueKind == JsonValueKind.Number && p.TryGetInt64(out var unix)) + return DateTimeOffset.FromUnixTimeSeconds(unix).UtcDateTime; + return null; + } + + private static JsonElement Unwrap(JsonElement root) + => root.ValueKind == JsonValueKind.Object && + root.TryGetProperty("InverterRecord", out var inner) + ? inner + : root; + + private static String Decode(String raw) + { + var t = raw.Trim(); + if (t.StartsWith('{') || t.StartsWith('[')) return raw; + try { return Encoding.UTF8.GetString(Convert.FromBase64String(t)); } + catch { return raw; } + } +} diff --git a/csharp/App/Backend/Services/BatteryVoltageCheckService.cs b/csharp/App/Backend/Services/BatteryVoltageCheckService.cs new file mode 100644 index 000000000..fb2bd314e --- /dev/null +++ b/csharp/App/Backend/Services/BatteryVoltageCheckService.cs @@ -0,0 +1,144 @@ +using InnovEnergy.App.Backend.Database; +using InnovEnergy.App.Backend.DataTypes; +using InnovEnergy.App.Backend.DataTypes.Methods; +using InnovEnergy.Lib.S3Utils; +using InnovEnergy.Lib.S3Utils.DataTypes; +using S3Region = InnovEnergy.Lib.S3Utils.DataTypes.S3Region; + +namespace InnovEnergy.App.Backend.Services; + +public static class BatteryVoltageCheckService +{ + private static Timer? _timer; + private const Int32 WindowStartUtc = 8; + private const Int32 WindowEndUtc = 17; + private const Int32 RetentionDays = 90; + private const Int64 MinUnixTs = 1_000_000_000L; + + public static void StartScheduler() + { + _timer = new Timer(_ => + { + try { RunOnce().GetAwaiter().GetResult(); } + catch (Exception ex) { Console.Error.WriteLine($"[BattVoltage] Scheduler error: {ex.Message}"); } + }, null, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(30)); + + Console.WriteLine($"[BattVoltage] Scheduler started (every 30 min, UTC {WindowStartUtc:00}:00–{WindowEndUtc:00}:00)."); + } + + private static async Task RunOnce() + { + // Retention runs regardless of the midday window — otherwise a backend whose + // uptime never overlaps 08–17 UTC (frequent evening restarts) never prunes. + CleanupOld(); + + var hour = DateTime.UtcNow.Hour; + if (hour < WindowStartUtc || hour >= WindowEndUtc) return; + + var installations = Db.Installations + .Where(i => (i.Product == (Int32)ProductType.SodioHome || + i.Product == (Int32)ProductType.SodistorePro) + && i.DataCollectionEnabled) + .ToList(); + + var total = 0; + foreach (var inst in installations) + { + try { total += await CheckInstallation(inst); } + catch (Exception ex) { Console.Error.WriteLine($"[BattVoltage] Inst {inst.Id} failed: {ex.Message}"); } + } + + if (total > 0) Console.WriteLine($"[BattVoltage] Run complete: {total} new anomaly row(s)."); + } + + private static async Task CheckInstallation(Installation inst) + { + var liveJson = await TryReadNewestLiveFile(inst); + if (liveJson is null) return 0; + + List anomalies; + try { anomalies = BatteryVoltageAnomalyDetector.Detect(liveJson, inst.Device); } + catch (Exception ex) + { + Console.Error.WriteLine($"[BattVoltage] Inst {inst.Id} detect failed: {ex.Message}"); + return 0; + } + + var inserted = 0; + foreach (var a in anomalies) + { + var detected = a.Timestamp ?? DateTime.UtcNow; + var dateHour = detected.ToString("yyyy-MM-dd HH"); + if (Db.BatteryVoltageAnomalyExists(inst.Id, a.BatteryLabel, dateHour)) continue; + + Db.Create(new BatteryVoltageAnomaly + { + InstallationId = inst.Id, + BatteryLabel = a.BatteryLabel, + DetectedAt = detected.ToString("yyyy-MM-dd HH:mm:ss"), + Date = detected.ToString("yyyy-MM-dd"), + DateHour = dateHour, + Voltage = Math.Round(a.Voltage, 2), + Soc = a.Soc, + CreatedAt = DateTime.UtcNow.ToString("o"), + }); + inserted++; + Console.WriteLine($"[BattVoltage] Inst {inst.Id} {a.BatteryLabel}: SOC {a.Soc} @ {a.Voltage:F2}V"); + } + return inserted; + } + + private static async Task TryReadNewestLiveFile(Installation inst) + { + try + { + var region = new S3Region($"https://{inst.S3Region}.{inst.S3Provider}", ExoCmd.S3Credentials!); + var bucket = region.Bucket(inst.BucketName()); + + // Prefix-scope to recent files: first 5 digits of current unix-ts covers + // a ~27h window (vs a full-bucket scan of potentially 100k+ objects on + // 10-second cadence). At a digit boundary we may briefly miss until next + // poll — acceptable for a 30-min temporary diagnostic. + var nowTs = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + var prefix = (nowTs / 100_000L).ToString(); + + S3Url? newest = null; + var newestTs = Int64.MinValue; + + await foreach (var url in bucket.ListObjects(prefix)) + { + if (!TryParseUnixTs(url.Path, out var ts)) continue; + if (ts <= newestTs) continue; + newestTs = ts; + newest = url; + } + + if (newest is null) return null; + return await newest.GetObjectAsString(); + } + catch (Exception ex) + { + Console.Error.WriteLine($"[BattVoltage] Inst {inst.Id} S3 read failed: {ex.Message}"); + return null; + } + } + + private static Boolean TryParseUnixTs(String path, out Int64 ts) + { + ts = 0; + var slash = path.LastIndexOf('/'); + var name = slash >= 0 ? path[(slash + 1)..] : path; + var dot = name.LastIndexOf('.'); + var stem = dot > 0 ? name[..dot] : name; + if (!Int64.TryParse(stem, out var v)) return false; + if (v < MinUnixTs) return false; + ts = v; + return true; + } + + private static void CleanupOld() + { + var cutoff = DateTime.UtcNow.AddDays(-RetentionDays).ToString("yyyy-MM-dd"); + Db.DeleteBatteryVoltageAnomaliesBefore(cutoff); + } +} diff --git a/typescript/frontend-marios2/src/content/dashboards/BatteryView/BatteryViewSodioHome.tsx b/typescript/frontend-marios2/src/content/dashboards/BatteryView/BatteryViewSodioHome.tsx index 9d6d74cd9..1e7f4badd 100644 --- a/typescript/frontend-marios2/src/content/dashboards/BatteryView/BatteryViewSodioHome.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/BatteryView/BatteryViewSodioHome.tsx @@ -1,5 +1,6 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useContext, useEffect, useMemo, useState } from 'react'; import { + Card, Container, Grid, Paper, @@ -24,6 +25,10 @@ import { ActiveCluster, getActiveClusters } from '../Information/installationSetupUtils'; +import { UserContext } from 'src/contexts/userContext'; +import { UserType } from 'src/interfaces/UserTypes'; +import axiosConfig from 'src/Resources/axiosConfig'; +import { BatteryAnomalyLogEntry } from 'src/interfaces/BatteryAnomalyTypes'; interface BatteryViewSodioHomeProps { values: JSONRecordData; @@ -47,6 +52,22 @@ function BatteryViewSodioHome(props: BatteryViewSodioHomeProps) { const hasDevices = !!inverter?.Devices; + const { currentUser } = useContext(UserContext); + const showAnomalyLog = currentUser?.userType === UserType.admin; + const [anomalyLog, setAnomalyLog] = useState([]); + + useEffect(() => { + if (!showAnomalyLog || !props.installationId) return; + axiosConfig + .get('/GetBatteryVoltageAnomalyLog', { + params: { installationId: props.installationId } + }) + .then((res) => { + if (Array.isArray(res.data)) setAnomalyLog(res.data); + }) + .catch(() => setAnomalyLog([])); + }, [showAnomalyLog, props.installationId]); + const activeClusters: ActiveCluster[] = useMemo(() => { const parsed = getActiveClusters(batterySerialNumbers || ''); if (parsed.length > 0) return parsed; @@ -376,6 +397,67 @@ function BatteryViewSodioHome(props: BatteryViewSodioHomeProps) { )} + {showAnomalyLog && ( + + + + + + + + + + + + + + + + + + + + + + + + + {anomalyLog.length === 0 ? ( + + + + + + + + ) : ( + anomalyLog.map((entry) => ( + + {entry.batteryLabel} + {entry.detectedAt} + {entry.voltage.toFixed(2)} V + {entry.soc.toFixed(0)} % + + )) + )} + +
+
+
+
+ )} ); } diff --git a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/FlatInstallationView.tsx b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/FlatInstallationView.tsx index 8c1322721..68c51a44f 100644 --- a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/FlatInstallationView.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/FlatInstallationView.tsx @@ -19,6 +19,7 @@ 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 WarningAmberIcon from '@mui/icons-material/WarningAmber'; import { getDeviceTypeName } from '../Information/installationSetupUtils'; import { UserContext } from 'src/contexts/userContext'; import { UserType } from 'src/interfaces/UserTypes'; @@ -27,6 +28,10 @@ import { CHECKLIST_ENABLED_PRODUCTS, ChecklistSummary } from 'src/interfaces/ChecklistTypes'; +import { + BatteryAnomalySummary, + VOLTAGE_ANOMALY_PRODUCTS +} from 'src/interfaces/BatteryAnomalyTypes'; import SetupProgress from '../Checklist/SetupProgress'; interface FlatInstallationViewProps { @@ -44,12 +49,18 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => { const showChecklistColumn = currentUser?.userType === UserType.admin && CHECKLIST_ENABLED_PRODUCTS.has(props.product ?? -1); + const showAnomalyColumn = + currentUser?.userType === UserType.admin && + VOLTAGE_ANOMALY_PRODUCTS.has(props.product ?? -1); const isListViewPath = currentLocation.pathname === baseRoute + 'list' || currentLocation.pathname === baseRoute + routes.list; const [progressMap, setProgressMap] = useState>( new Map() ); + const [anomalyMap, setAnomalyMap] = useState>( + new Map() + ); useEffect(() => { if (!showChecklistColumn || !isListViewPath) return; @@ -66,6 +77,21 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => { .catch(() => setProgressMap(new Map())); }, [showChecklistColumn, isListViewPath]); + useEffect(() => { + if (!showAnomalyColumn || !isListViewPath) return; + axiosConfig + .get('/GetBatteryVoltageAnomalySummary') + .then((res) => { + if (!Array.isArray(res.data)) return; + const map = new Map(); + res.data.forEach((s: BatteryAnomalySummary) => { + map.set(s.installationId, s); + }); + setAnomalyMap(map); + }) + .catch(() => setAnomalyMap(new Map())); + }, [showAnomalyColumn, isListViewPath]); + const sortedInstallations = useMemo(() => { return [...props.installations].sort((a, b) => { // Data-collection-disabled installations sink below everything (even offline). @@ -158,6 +184,14 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => { /> )} + {showAnomalyColumn && ( + + + + )} @@ -335,6 +369,49 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => { })()} )} + {showAnomalyColumn && ( + + {(() => { + const a = anomalyMap.get(installation.id); + if (!a) { + return ( + + — + + ); + } + return ( +
+ + + {a.lastDetectedAt} · {a.lastVoltage.toFixed(2)} V + +
+ ); + })()} +
+ )} ); })} diff --git a/typescript/frontend-marios2/src/interfaces/BatteryAnomalyTypes.ts b/typescript/frontend-marios2/src/interfaces/BatteryAnomalyTypes.ts new file mode 100644 index 000000000..880db5eaa --- /dev/null +++ b/typescript/frontend-marios2/src/interfaces/BatteryAnomalyTypes.ts @@ -0,0 +1,21 @@ +export interface BatteryAnomalySummary { + installationId: number; + lastDetectedAt: string; + lastVoltage: number; + lastBattery: string; + count: number; +} + +export interface BatteryAnomalyLogEntry { + id: number; + installationId: number; + batteryLabel: string; + detectedAt: string; + date: string; + dateHour: string; + voltage: number; + soc: number; + createdAt: string; +} + +export const VOLTAGE_ANOMALY_PRODUCTS = new Set([2, 5]); diff --git a/typescript/frontend-marios2/src/lang/de.json b/typescript/frontend-marios2/src/lang/de.json index 98e68932f..9390e9273 100644 --- a/typescript/frontend-marios2/src/lang/de.json +++ b/typescript/frontend-marios2/src/lang/de.json @@ -801,6 +801,10 @@ "checklistStep10Sub3": "Atef kontaktieren, falls externes EMS vorhanden ist", "checklistNoAttachments": "Noch keine Datei angehängt.", "setupProgress": "Monitor-Onboarding-Fortschritt", + "batteryVoltageAnomaly": "Voll bei Unterspannung", + "voltageAnomalyLogTitle": "Protokoll: voll bei Unterspannung", + "voltageAnomalyTime": "Zeit", + "noVoltageAnomalies": "Keine Anomalien erfasst", "checklistPhaseEmpty": "Nicht gestartet", "checklistPhasePreparation": "Vorbereitung", "checklistPhaseOnSite": "Vor Ort", diff --git a/typescript/frontend-marios2/src/lang/en.json b/typescript/frontend-marios2/src/lang/en.json index 947214b68..d5831aee2 100644 --- a/typescript/frontend-marios2/src/lang/en.json +++ b/typescript/frontend-marios2/src/lang/en.json @@ -549,6 +549,10 @@ "checklistStep10Sub3": "Contact Atef if there is external EMS", "checklistNoAttachments": "No file attached yet.", "setupProgress": "Monitor Onboarding Progress", + "batteryVoltageAnomaly": "Full @ low voltage", + "voltageAnomalyLogTitle": "Full-charge low-voltage log", + "voltageAnomalyTime": "Time", + "noVoltageAnomalies": "No anomalies recorded", "checklistPhaseEmpty": "Not started", "checklistPhasePreparation": "Preparation", "checklistPhaseOnSite": "On-site", diff --git a/typescript/frontend-marios2/src/lang/fr.json b/typescript/frontend-marios2/src/lang/fr.json index 7ac2b185a..d0fefd0c2 100644 --- a/typescript/frontend-marios2/src/lang/fr.json +++ b/typescript/frontend-marios2/src/lang/fr.json @@ -801,6 +801,10 @@ "checklistStep10Sub3": "Contacter Atef en cas d'EMS externe", "checklistNoAttachments": "Aucun fichier joint pour le moment.", "setupProgress": "Progression d'onboarding Monitor", + "batteryVoltageAnomaly": "Plein à basse tension", + "voltageAnomalyLogTitle": "Journal : plein à basse tension", + "voltageAnomalyTime": "Heure", + "noVoltageAnomalies": "Aucune anomalie enregistrée", "checklistPhaseEmpty": "Non commencé", "checklistPhasePreparation": "Préparation", "checklistPhaseOnSite": "Sur site", diff --git a/typescript/frontend-marios2/src/lang/it.json b/typescript/frontend-marios2/src/lang/it.json index 84b4cf727..8f191897b 100644 --- a/typescript/frontend-marios2/src/lang/it.json +++ b/typescript/frontend-marios2/src/lang/it.json @@ -801,6 +801,10 @@ "checklistStep10Sub3": "Contattare Atef se è presente un EMS esterno", "checklistNoAttachments": "Nessun file allegato.", "setupProgress": "Avanzamento onboarding Monitor", + "batteryVoltageAnomaly": "Pieno a bassa tensione", + "voltageAnomalyLogTitle": "Registro: pieno a bassa tensione", + "voltageAnomalyTime": "Ora", + "noVoltageAnomalies": "Nessuna anomalia registrata", "checklistPhaseEmpty": "Non avviato", "checklistPhasePreparation": "Preparazione", "checklistPhaseOnSite": "In sito",