Innovenergy_trunk/csharp/App/Backend/Services/AggregatedJsonParser.cs

163 lines
6.5 KiB
C#

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;
/// <summary>
/// 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
/// </summary>
public static class AggregatedJsonParser
{
private static readonly JsonSerializerOptions JsonOpts = new()
{
PropertyNameCaseInsensitive = true,
NumberHandling = JsonNumberHandling.AllowReadingFromString,
};
public static List<DailyEnergyData> ParseDaily(String ndjsonContent)
{
var dailyByDate = new SortedDictionary<String, DailyEnergyData>();
foreach (var line in ndjsonContent.Split('\n', StringSplitOptions.RemoveEmptyEntries))
{
if (!line.Contains("\"Type\":\"Daily\""))
continue;
try
{
var raw = JsonSerializer.Deserialize<DailyJsonDto>(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<HourlyEnergyData> ParseHourly(String ndjsonContent)
{
var result = new List<HourlyEnergyData>();
foreach (var line in ndjsonContent.Split('\n', StringSplitOptions.RemoveEmptyEntries))
{
if (!line.Contains("\"Type\":\"Hourly\""))
continue;
try
{
var raw = JsonSerializer.Deserialize<HourlyJsonDto>(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;
}
/// <summary>
/// Converts ISO date "yyyy-MM-dd" to device filename format "ddMMyyyy".
/// </summary>
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";
/// <summary>
/// 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.
/// </summary>
public static async Task<String?> 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; }
}
}