package cli import ( "github.com/cnachtigall/heatwave-autopilot/internal/heat" "github.com/cnachtigall/heatwave-autopilot/internal/store" ) // hourWeather holds weather data for a single hour, used for budget computation. type hourWeather struct { Hour int TempC float64 CloudCoverPct float64 SunshineMin float64 } // roomBudgetResult holds the budget result for a single room in a single hour. type roomBudgetResult struct { RoomName string RoomID int64 Result heat.BudgetResult } // computeRoomBudgets computes heat budgets for all rooms in a profile for a given hour. // It returns per-room results and the worst-case BudgetStatus. func computeRoomBudgets(profileID int64, w hourWeather, toggles map[string]bool, indoorTempC float64) ([]roomBudgetResult, heat.BudgetStatus) { rooms, err := db.ListRooms(profileID) if err != nil || len(rooms) == 0 { return nil, heat.Comfortable } var results []roomBudgetResult worstStatus := heat.Comfortable for _, room := range rooms { budget := computeSingleRoomBudget(room, w, toggles, indoorTempC) results = append(results, roomBudgetResult{ RoomName: room.Name, RoomID: room.ID, Result: budget, }) if budget.Status > worstStatus { worstStatus = budget.Status } } return results, worstStatus } func computeSingleRoomBudget(room store.Room, w hourWeather, toggles map[string]bool, indoorTempC float64) heat.BudgetResult { // Devices devices, _ := db.ListDevices(room.ID) var heatDevices []heat.Device for _, d := range devices { heatDevices = append(heatDevices, heat.Device{ WattsIdle: d.WattsIdle, WattsTypical: d.WattsTypical, WattsPeak: d.WattsPeak, DutyCycle: d.DutyCycle, }) } // Determine device mode from toggles mode := heat.ModeTypical if toggles["gaming"] { mode = heat.ModePeak } // Occupants occupants, _ := db.ListOccupants(room.ID) var heatOccupants []heat.Occupant for _, o := range occupants { heatOccupants = append(heatOccupants, heat.Occupant{ Count: o.Count, Activity: heat.ParseActivityLevel(o.ActivityLevel), }) } // AC capacity acCap, _ := db.GetRoomACCapacity(room.ID) // Solar params cloudFactor := 1.0 - (w.CloudCoverPct / 100.0) sunshineFraction := 0.0 if w.SunshineMin > 0 { sunshineFraction = w.SunshineMin / 60.0 if sunshineFraction > 1.0 { sunshineFraction = 1.0 } } solar := heat.SolarParams{ AreaSqm: room.AreaSqm, WindowFraction: room.WindowFraction, SHGC: room.SHGC, ShadingFactor: room.ShadingFactor, OrientationFactor: heat.OrientationFactor(room.Orientation, w.Hour), CloudFactor: cloudFactor, SunshineFraction: sunshineFraction, PeakIrradiance: 800, // W/m² typical clear-sky peak } // Ventilation params volume := room.AreaSqm * room.CeilingHeightM vent := heat.VentilationParams{ ACH: room.VentilationACH, VolumeCubicM: volume, OutdoorTempC: w.TempC, IndoorTempC: indoorTempC, RhoCp: 1.2, // kJ/(m³·K) — standard air at sea level } return heat.ComputeRoomBudget(heat.BudgetInput{ Devices: heatDevices, DeviceMode: mode, Occupants: heatOccupants, Solar: solar, Ventilation: vent, ACCapacityBTUH: acCap, }) }