using System.Text.Json; using System.Text.Json.Serialization; 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; /// /// Parses NDJSON aggregated data files generated by SodistoreHome devices. /// Each file (DDMMYYYY.json) contains one JSON object per line: /// - Type "Hourly": per-hour kWh values (already computed, no diffing needed) /// - Type "Daily": daily totals /// public static class AggregatedJsonParser { private static readonly JsonSerializerOptions JsonOpts = new() { PropertyNameCaseInsensitive = true, NumberHandling = JsonNumberHandling.AllowReadingFromString, }; public static List ParseDaily(String ndjsonContent) { var dailyByDate = new SortedDictionary(); foreach (var line in ndjsonContent.Split('\n', StringSplitOptions.RemoveEmptyEntries)) { if (!line.Contains("\"Type\":\"Daily\"")) continue; try { var raw = JsonSerializer.Deserialize(line, JsonOpts); if (raw is null) continue; var date = raw.Timestamp.ToString("yyyy-MM-dd"); dailyByDate[date] = new DailyEnergyData { Date = date, PvProduction = Math.Round(raw.DailySelfGeneratedElectricity, 4), GridImport = Math.Round(raw.DailyElectricityPurchased, 4), GridExport = Math.Round(raw.DailyElectricityFed, 4), BatteryCharged = Math.Round(raw.BatteryDailyChargeEnergy, 4), BatteryDischarged = Math.Round(raw.BatteryDailyDischargeEnergy, 4), LoadConsumption = Math.Round(raw.DailyLoadPowerConsumption, 4), }; } catch (Exception ex) { Console.Error.WriteLine($"[AggregatedJsonParser] Skipping daily line: {ex.Message}"); } } Console.WriteLine($"[AggregatedJsonParser] Parsed {dailyByDate.Count} daily record(s)"); return dailyByDate.Values.ToList(); } public static List ParseHourly(String ndjsonContent) { var result = new List(); foreach (var line in ndjsonContent.Split('\n', StringSplitOptions.RemoveEmptyEntries)) { if (!line.Contains("\"Type\":\"Hourly\"")) continue; try { var raw = JsonSerializer.Deserialize(line, JsonOpts); if (raw is null) continue; var dt = new DateTime( raw.Timestamp.Year, raw.Timestamp.Month, raw.Timestamp.Day, raw.Timestamp.Hour, 0, 0); result.Add(new HourlyEnergyData { DateTime = dt, Hour = dt.Hour, DayOfWeek = dt.DayOfWeek.ToString(), IsWeekend = dt.DayOfWeek is System.DayOfWeek.Saturday or System.DayOfWeek.Sunday, PvKwh = Math.Round(raw.SelfGeneratedElectricity, 4), GridImportKwh = Math.Round(raw.ElectricityPurchased, 4), BatteryChargedKwh = Math.Round(raw.BatteryChargeEnergy, 4), BatteryDischargedKwh = Math.Round(raw.BatteryDischargeEnergy, 4), LoadKwh = Math.Round(raw.LoadPowerConsumption, 4), }); } catch (Exception ex) { Console.Error.WriteLine($"[AggregatedJsonParser] Skipping hourly line: {ex.Message}"); } } Console.WriteLine($"[AggregatedJsonParser] Parsed {result.Count} hourly record(s)"); return result; } /// /// Converts ISO date "yyyy-MM-dd" to device filename format "ddMMyyyy". /// public static String ToJsonFileName(String isoDate) { var d = DateOnly.ParseExact(isoDate, "yyyy-MM-dd"); return d.ToString("ddMMyyyy") + ".json"; } public static String ToJsonFileName(DateOnly date) => date.ToString("ddMMyyyy") + ".json"; /// /// Tries to read an aggregated JSON file from the installation's S3 bucket. /// S3 key: DDMMYYYY.json (directly in bucket root). /// Returns file content or null if not found / error. /// public static async Task TryReadFromS3(Installation installation, String isoDate) { try { var fileName = ToJsonFileName(isoDate); var region = new S3Region($"https://{installation.S3Region}.{installation.S3Provider}", ExoCmd.S3Credentials!); var bucket = region.Bucket(installation.BucketName()); var s3Url = bucket.Path(fileName); return await s3Url.GetObjectAsString(); } catch (Exception ex) { Console.Error.WriteLine($"[AggregatedJsonParser] S3 read failed for {isoDate}: {ex.Message}"); return null; } } // --- JSON DTOs --- private sealed class HourlyJsonDto { public String Type { get; set; } = ""; public DateTime Timestamp { get; set; } public Double SelfGeneratedElectricity { get; set; } public Double ElectricityPurchased { get; set; } public Double ElectricityFed { get; set; } public Double BatteryChargeEnergy { get; set; } public Double BatteryDischargeEnergy { get; set; } public Double LoadPowerConsumption { get; set; } } private sealed class DailyJsonDto { public String Type { get; set; } = ""; public DateTime Timestamp { get; set; } public Double DailySelfGeneratedElectricity { get; set; } public Double DailyElectricityPurchased { get; set; } public Double DailyElectricityFed { get; set; } public Double BatteryDailyChargeEnergy { get; set; } public Double BatteryDailyDischargeEnergy { get; set; } public Double DailyLoadPowerConsumption { get; set; } } }