using InnovEnergy.App.Backend.DataTypes; namespace InnovEnergy.App.Backend.Services; public static class BehaviorAnalyzer { private const double SolarActiveThresholdKwh = 0.1; // min PV kWh in an hour to count as "solar active" private const double LowSoCThreshold = 20.0; // % below which battery is considered depleted /// /// Derives behavioral facts from hourly data for the current week only. /// All computation is pure C# — no AI involved. /// public static BehavioralPattern Analyze(List hourlyData) { if (hourlyData.Count == 0) return new BehavioralPattern(); // ── Per-hour averages across the week ────────────────────────────── // Group by hour-of-day (0-23), average each metric across all days var byHour = Enumerable.Range(0, 24).Select(h => { var rows = hourlyData.Where(r => r.Hour == h).ToList(); if (rows.Count == 0) return (Hour: h, AvgPv: 0.0, AvgLoad: 0.0, AvgGridImport: 0.0); return ( Hour: h, AvgPv: rows.Average(r => r.PvKwh), AvgLoad: rows.Average(r => r.LoadKwh), AvgGridImport: rows.Average(r => r.GridImportKwh) ); }).ToList(); // ── Peak load hour ───────────────────────────────────────────────── var peakLoadEntry = byHour.OrderByDescending(h => h.AvgLoad).First(); // ── Peak solar hour and end of solar window ──────────────────────── var peakSolarEntry = byHour.OrderByDescending(h => h.AvgPv).First(); // Solar window: last hour in the day where avg PV > threshold var solarActiveHours = byHour.Where(h => h.AvgPv >= SolarActiveThresholdKwh).ToList(); var peakSolarEndHour = solarActiveHours.Count > 0 ? solarActiveHours.Max(h => h.Hour) : peakSolarEntry.Hour; // ── Highest grid-import hour ──────────────────────────────────────── var worstGridEntry = byHour.OrderByDescending(h => h.AvgGridImport).First(); // ── Avoidable grid imports: grid drawn during hours when solar was active ── // For each actual hourly record: if solar > threshold AND grid import > 0 → avoidable var avoidableGridKwh = Math.Round( hourlyData .Where(r => r.PvKwh >= SolarActiveThresholdKwh && r.GridImportKwh > 0) .Sum(r => r.GridImportKwh), 1); // ── Weekday vs weekend average daily load ────────────────────────── var weekdayDays = hourlyData .Where(r => !r.IsWeekend) .GroupBy(r => r.DateTime.Date) .Select(g => g.Sum(r => r.LoadKwh)) .ToList(); var weekendDays = hourlyData .Where(r => r.IsWeekend) .GroupBy(r => r.DateTime.Date) .Select(g => g.Sum(r => r.LoadKwh)) .ToList(); var weekdayAvg = weekdayDays.Count > 0 ? Math.Round(weekdayDays.Average(), 1) : 0; var weekendAvg = weekendDays.Count > 0 ? Math.Round(weekendDays.Average(), 1) : 0; // ── Battery depletion hour ───────────────────────────────────────── // For each day, find the first evening hour (after 18:00) where SoC < threshold // Average that hour across days where it occurs var depletionHours = hourlyData .Where(r => r.Hour >= 18 && r.BattSoC > 0 && r.BattSoC < LowSoCThreshold) .GroupBy(r => r.DateTime.Date) .Select(g => g.OrderBy(r => r.Hour).First().Hour) .ToList(); var avgDepletedHour = depletionHours.Count > 0 ? (int)Math.Round(depletionHours.Average()) : -1; var batteryDepletsNight = depletionHours.Count >= 3; // happens on 3+ nights = consistent pattern return new BehavioralPattern { PeakLoadHour = peakLoadEntry.Hour, AvgPeakLoadKwh = Math.Round(peakLoadEntry.AvgLoad, 2), PeakSolarHour = peakSolarEntry.Hour, PeakSolarEndHour = peakSolarEndHour, AvgPeakSolarKwh = Math.Round(peakSolarEntry.AvgPv, 2), HighestGridImportHour = worstGridEntry.Hour, AvgGridImportAtPeakHour = Math.Round(worstGridEntry.AvgGridImport, 2), AvoidableGridKwh = avoidableGridKwh, WeekdayAvgDailyLoad = weekdayAvg, WeekendAvgDailyLoad = weekendAvg, AvgBatteryDepletedHour = avgDepletedHour, BatteryDepletesOvernight = batteryDepletsNight, }; } }