package cli import ( "context" "fmt" "os" "time" "github.com/cnachtigall/heatwave-autopilot/internal/action" "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 ( heatplanOutput string ) func init() { heatplanCmd := &cobra.Command{ Use: "heatplan", Short: "Generate a 1-page plain-language heat plan", } generateCmd := &cobra.Command{ Use: "generate", Short: "Generate a heat plan document", 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 := time.Now().Format("2006-01-02") date, _ := time.Parse("2006-01-02", 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. Run: heatwave forecast fetch") } hourlyData := buildHourlyData(forecasts, loc) dayRisk := risk.AnalyzeDay(hourlyData, risk.DefaultThresholds()) 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}) } 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 } warnings, _ := db.GetActiveWarnings(p.ID, time.Now()) var warningStrs []string for _, w := range warnings { warningStrs = append(warningStrs, w.Headline) } 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(), }) } summaryInput := llm.SummaryInput{ Date: dateStr, PeakTempC: dayRisk.PeakTempC, MinNightTempC: dayRisk.MinNightTempC, RiskLevel: dayRisk.Level.String(), TopHeatSources: sources, ACHeadroomBTUH: totalACBTU - heat.WattsToBTUH(totalGainW), BudgetStatus: heat.Comfortable.String(), ActiveWarnings: warningStrs, RiskWindows: riskWindows, } // Build timeline actions, _ := action.LoadDefaultActions() toggles, _ := db.GetActiveToggleNames(p.ID) var timelineSlots []llm.TimelineSlotSummary var actionSummaries []llm.ActionSummary for _, h := range hourlyData { ctx := action.HourContext{ Hour: h.Hour, TempC: h.TempC, HumidityPct: h.HumidityPct, IsDay: h.IsDay, RiskLevel: dayRisk.Level, BudgetStatus: heat.Comfortable, ActiveToggles: toggles, } matched := action.SelectActions(actions, ctx) var actionNames []string for _, a := range matched { actionNames = append(actionNames, a.Name) actionSummaries = append(actionSummaries, llm.ActionSummary{ Name: a.Name, Category: string(a.Category), Impact: string(a.Impact), Hour: h.Hour, }) } timelineSlots = append(timelineSlots, llm.TimelineSlotSummary{ Hour: h.Hour, TempC: h.TempC, RiskLevel: dayRisk.Level.String(), BudgetStatus: heat.Comfortable.String(), Actions: actionNames, }) } // Care checklist occupants, _ := db.ListAllOccupants(p.ID) var careList []string for _, o := range occupants { if o.Vulnerable { careList = append(careList, fmt.Sprintf("Check vulnerable occupant (room %d) at 10:00, 14:00, 18:00", o.RoomID)) } } input := llm.HeatPlanInput{ Summary: summaryInput, Timeline: timelineSlots, Actions: actionSummaries, CareChecklist: careList, } result, err := provider.GenerateHeatPlan(context.Background(), input) if err != nil { return fmt.Errorf("LLM call failed: %w", err) } if heatplanOutput != "" { if err := os.WriteFile(heatplanOutput, []byte(result), 0o644); err != nil { return fmt.Errorf("write output: %w", err) } fmt.Printf("Heat plan written to %s\n", heatplanOutput) } else { fmt.Println(result) } return nil }, } generateCmd.Flags().StringVar(&heatplanOutput, "output", "", "output file path") heatplanCmd.AddCommand(generateCmd) rootCmd.AddCommand(heatplanCmd) }