Add server-side rendered setup UI accessible via `heatwave web`. The dashboard is now re-rendered per request and includes a nav bar linking to the new /setup page. Setup provides full CRUD for profiles, rooms, devices, occupants, AC units (with room assignment), scenario toggles, and forecast fetching — all via POST/redirect/GET forms. - Add ShowNav field to DashboardData for conditional nav bar - Extract fetchForecastForProfile() for reuse by web handler - Create setup.html.tmpl with Tailwind-styled entity sections - Create web_handlers.go with 15 route handlers and flash cookies - Switch web.go from pre-rendered to per-request dashboard rendering - Graceful dashboard fallback when no forecast data exists
121 lines
3.2 KiB
Go
121 lines
3.2 KiB
Go
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,
|
|
})
|
|
}
|