using InnovEnergy.App.Backend.Database; using InnovEnergy.App.Backend.DataTypes; namespace InnovEnergy.App.Backend.Services; /// /// Ingests daily energy totals from xlsx files into the DailyEnergyRecord SQLite table. /// This is the source-of-truth population step for the report pipeline. /// /// Current data source: xlsx files placed in tmp_report/{installationId}.xlsx /// Future data source: S3 raw records (replace ExcelDataParser call with S3DailyExtractor) /// /// Runs automatically at 01:00 UTC daily. Can also be triggered manually via the /// IngestDailyData API endpoint. /// public static class DailyIngestionService { private static readonly String TmpReportDir = Environment.CurrentDirectory + "/tmp_report/"; private static Timer? _dailyTimer; /// /// Starts the daily scheduler. Call once on app startup. /// Ingests xlsx data at 01:00 UTC every day. /// public static void StartScheduler() { var now = DateTime.UtcNow; var next = now.Date.AddDays(1).AddHours(1); // 01:00 UTC tomorrow _dailyTimer = new Timer( _ => { try { IngestAllInstallationsAsync().GetAwaiter().GetResult(); } catch (Exception ex) { Console.Error.WriteLine($"[DailyIngestion] Scheduler error: {ex.Message}"); } }, null, next - now, TimeSpan.FromDays(1)); Console.WriteLine($"[DailyIngestion] Scheduler started. Next run: {next:yyyy-MM-dd HH:mm} UTC"); } /// /// Ingests xlsx data for all SodioHome installations. Safe to call manually. /// public static async Task IngestAllInstallationsAsync() { Console.WriteLine($"[DailyIngestion] Starting ingestion for all SodioHome installations..."); var installations = Db.Installations .Where(i => i.Product == (Int32)ProductType.SodioHome) .ToList(); foreach (var installation in installations) { try { await IngestInstallationAsync(installation.Id); } catch (Exception ex) { Console.Error.WriteLine($"[DailyIngestion] Failed for installation {installation.Id}: {ex.Message}"); } } Console.WriteLine($"[DailyIngestion] Ingestion complete."); } /// /// Parses all xlsx files matching {installationId}*.xlsx in tmp_report/ and stores /// any new days as DailyEnergyRecord rows. Supports multiple time-ranged files per /// installation (e.g. 123_0203_0803.xlsx, 123_0901_1231.xlsx). Idempotent. /// public static async Task IngestInstallationAsync(Int64 installationId) { if (!Directory.Exists(TmpReportDir)) { Console.WriteLine($"[DailyIngestion] tmp_report directory not found, skipping."); return; } var xlsxFiles = Directory.GetFiles(TmpReportDir, $"{installationId}*.xlsx"); if (xlsxFiles.Length == 0) { Console.WriteLine($"[DailyIngestion] No xlsx found for installation {installationId}, skipping."); return; } var newDailyCount = 0; var newHourlyCount = 0; var totalParsed = 0; foreach (var xlsxPath in xlsxFiles.OrderBy(f => f)) { // Ingest daily records List days; try { days = ExcelDataParser.Parse(xlsxPath); } catch (Exception ex) { Console.Error.WriteLine($"[DailyIngestion] Failed to parse daily {Path.GetFileName(xlsxPath)}: {ex.Message}"); continue; } totalParsed += days.Count; foreach (var day in days) { if (Db.DailyRecordExists(installationId, day.Date)) continue; Db.Create(new DailyEnergyRecord { InstallationId = installationId, Date = day.Date, PvProduction = day.PvProduction, LoadConsumption = day.LoadConsumption, GridImport = day.GridImport, GridExport = day.GridExport, BatteryCharged = day.BatteryCharged, BatteryDischarged = day.BatteryDischarged, CreatedAt = DateTime.UtcNow.ToString("o"), }); newDailyCount++; } // Ingest hourly records List hours; try { hours = ExcelDataParser.ParseHourly(xlsxPath); } catch (Exception ex) { Console.Error.WriteLine($"[DailyIngestion] Failed to parse hourly {Path.GetFileName(xlsxPath)}: {ex.Message}"); continue; } foreach (var hour in hours) { var dateHour = $"{hour.DateTime:yyyy-MM-dd HH}"; if (Db.HourlyRecordExists(installationId, dateHour)) continue; Db.Create(new HourlyEnergyRecord { InstallationId = installationId, Date = hour.DateTime.ToString("yyyy-MM-dd"), Hour = hour.Hour, DateHour = dateHour, DayOfWeek = hour.DayOfWeek, IsWeekend = hour.IsWeekend, PvKwh = hour.PvKwh, LoadKwh = hour.LoadKwh, GridImportKwh = hour.GridImportKwh, BatteryChargedKwh = hour.BatteryChargedKwh, BatteryDischargedKwh = hour.BatteryDischargedKwh, BattSoC = hour.BattSoC, CreatedAt = DateTime.UtcNow.ToString("o"), }); newHourlyCount++; } } Console.WriteLine($"[DailyIngestion] Installation {installationId}: {newDailyCount} new day(s), {newHourlyCount} new hour(s) ingested ({totalParsed} days across {xlsxFiles.Length} file(s))."); await Task.CompletedTask; } }