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

102 lines
5.0 KiB
C#

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
/// <summary>
/// Derives behavioral facts from hourly data for the current week only.
/// All computation is pure C# — no AI involved.
/// </summary>
public static BehavioralPattern Analyze(List<HourlyEnergyData> 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,
};
}
}