163 lines
6.5 KiB
C#
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; }
|
|
}
|
|
}
|