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
118 lines
3.2 KiB
Go
118 lines
3.2 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/cnachtigall/heatwave-autopilot/internal/heat"
|
|
"github.com/cnachtigall/heatwave-autopilot/internal/llm"
|
|
"github.com/cnachtigall/heatwave-autopilot/internal/risk"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var summaryDate string
|
|
|
|
func init() {
|
|
summaryCmd := &cobra.Command{
|
|
Use: "summary",
|
|
Short: "Generate a 3-bullet AI summary of the heat model",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
p, err := getActiveProfile()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
provider := getLLMProvider()
|
|
if provider.Name() == "none" {
|
|
return fmt.Errorf("LLM not configured. Set llm.provider in config or use --llm flag")
|
|
}
|
|
|
|
dateStr := summaryDate
|
|
if dateStr == "" {
|
|
dateStr = time.Now().Format("2006-01-02")
|
|
}
|
|
date, err := time.Parse("2006-01-02", dateStr)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid date: %s", dateStr)
|
|
}
|
|
|
|
loc, _ := time.LoadLocation(p.Timezone)
|
|
from := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, loc)
|
|
to := from.Add(24 * time.Hour)
|
|
|
|
forecasts, err := db.GetForecasts(p.ID, from, to, "")
|
|
if err != nil || len(forecasts) == 0 {
|
|
return fmt.Errorf("no forecast data for %s", dateStr)
|
|
}
|
|
|
|
hourlyData := buildHourlyData(forecasts, loc)
|
|
dayRisk := risk.AnalyzeDay(hourlyData, risk.DefaultThresholds())
|
|
|
|
// Build heat sources from devices
|
|
devices, _ := db.ListAllDevices(p.ID)
|
|
var sources []llm.HeatSource
|
|
for _, d := range devices {
|
|
sources = append(sources, llm.HeatSource{
|
|
Name: d.Name,
|
|
Watts: d.WattsTypical * d.DutyCycle,
|
|
})
|
|
}
|
|
|
|
// AC headroom (simplified — sum all AC units)
|
|
acUnits, _ := db.ListACUnits(p.ID)
|
|
var totalACBTU float64
|
|
for _, ac := range acUnits {
|
|
totalACBTU += ac.CapacityBTU
|
|
}
|
|
var totalGainW float64
|
|
for _, s := range sources {
|
|
totalGainW += s.Watts
|
|
}
|
|
headroom := totalACBTU - heat.WattsToBTUH(totalGainW)
|
|
|
|
// Warnings
|
|
warnings, _ := db.GetActiveWarnings(p.ID, time.Now())
|
|
var warningStrs []string
|
|
for _, w := range warnings {
|
|
warningStrs = append(warningStrs, w.Headline)
|
|
}
|
|
|
|
// Risk windows
|
|
var riskWindows []llm.RiskWindowSummary
|
|
for _, w := range dayRisk.Windows {
|
|
riskWindows = append(riskWindows, llm.RiskWindowSummary{
|
|
StartHour: w.StartHour,
|
|
EndHour: w.EndHour,
|
|
PeakTempC: w.PeakTempC,
|
|
Level: w.Level.String(),
|
|
})
|
|
}
|
|
|
|
input := llm.SummaryInput{
|
|
Date: dateStr,
|
|
PeakTempC: dayRisk.PeakTempC,
|
|
MinNightTempC: dayRisk.MinNightTempC,
|
|
RiskLevel: dayRisk.Level.String(),
|
|
TopHeatSources: sources,
|
|
ACHeadroomBTUH: headroom,
|
|
BudgetStatus: heat.Comfortable.String(),
|
|
ActiveWarnings: warningStrs,
|
|
RiskWindows: riskWindows,
|
|
}
|
|
|
|
result, err := provider.Summarize(context.Background(), input)
|
|
if err != nil {
|
|
fmt.Fprintf(cmd.ErrOrStderr(), "LLM call failed: %v\nFalling back to raw data:\n", err)
|
|
fmt.Printf("Peak: %.1f°C | Night min: %.1f°C | Risk: %s\n", dayRisk.PeakTempC, dayRisk.MinNightTempC, dayRisk.Level)
|
|
return nil
|
|
}
|
|
|
|
fmt.Println(result)
|
|
return nil
|
|
},
|
|
}
|
|
summaryCmd.Flags().StringVar(&summaryDate, "date", "", "date (YYYY-MM-DD)")
|
|
rootCmd.AddCommand(summaryCmd)
|
|
}
|