package cli import ( "fmt" "time" "github.com/cnachtigall/heatwave-autopilot/internal/action" "github.com/cnachtigall/heatwave-autopilot/internal/risk" "github.com/spf13/cobra" ) var planDate string func init() { planCmd := &cobra.Command{ Use: "plan", Short: "Generate action plans", } todayCmd := &cobra.Command{ Use: "today", Short: "Generate today's action plan", RunE: func(cmd *cobra.Command, args []string) error { return runPlan(time.Now().Format("2006-01-02")) }, } generateCmd := &cobra.Command{ Use: "generate", Short: "Generate plan for a specific date", RunE: func(cmd *cobra.Command, args []string) error { if planDate == "" { planDate = time.Now().Format("2006-01-02") } return runPlan(planDate) }, } generateCmd.Flags().StringVar(&planDate, "date", "", "date (YYYY-MM-DD)") planCmd.AddCommand(todayCmd, generateCmd) rootCmd.AddCommand(planCmd) } func runPlan(dateStr string) error { p, err := getActiveProfile() if err != nil { return err } date, err := time.Parse("2006-01-02", dateStr) if err != nil { return fmt.Errorf("invalid date: %s", dateStr) } loc, err := time.LoadLocation(p.Timezone) if err != nil { loc = time.UTC } 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 { return err } if len(forecasts) == 0 { return fmt.Errorf("no forecast data for %s. Run: heatwave forecast fetch", dateStr) } // Build hourly risk data hourlyData := make([]risk.HourlyData, 0, 24) for _, f := range forecasts { tempC := 0.0 if f.TemperatureC != nil { tempC = *f.TemperatureC } apparentC := tempC if f.ApparentTempC != nil { apparentC = *f.ApparentTempC } humPct := 50.0 if f.HumidityPct != nil { humPct = *f.HumidityPct } h := f.Timestamp.In(loc).Hour() hourlyData = append(hourlyData, risk.HourlyData{ Hour: h, TempC: tempC, ApparentC: apparentC, HumidityPct: humPct, IsDay: h >= 6 && h < 21, }) } // Analyze risks th := risk.DefaultThresholds() dayRisk := risk.AnalyzeDay(hourlyData, th) fmt.Printf("=== Heat Plan for %s ===\n", dateStr) fmt.Printf("Risk Level: %s | Peak: %.1f°C | Night Min: %.1f°C\n", dayRisk.Level, dayRisk.PeakTempC, dayRisk.MinNightTempC) if dayRisk.PoorNightCool { fmt.Println("Warning: Poor nighttime cooling expected") } fmt.Println() if len(dayRisk.Windows) > 0 { fmt.Println("Risk Windows:") for _, w := range dayRisk.Windows { fmt.Printf(" %02d:00–%02d:00 | Peak %.1f°C | %s — %s\n", w.StartHour, w.EndHour, w.PeakTempC, w.Level, w.Reason) } fmt.Println() } // Load actions and build timeline actions, err := action.LoadDefaultActions() if err != nil { return fmt.Errorf("load actions: %w", err) } // Get toggles toggles, _ := db.GetActiveToggleNames(p.ID) // Build hour contexts with real budget computation contexts := make([]action.HourContext, 0, len(hourlyData)) for i, h := range hourlyData { cloudPct := 50.0 sunMin := 0.0 if i < len(forecasts) { if forecasts[i].CloudCoverPct != nil { cloudPct = *forecasts[i].CloudCoverPct } if forecasts[i].SunshineMin != nil { sunMin = *forecasts[i].SunshineMin } } w := hourWeather{Hour: h.Hour, TempC: h.TempC, CloudCoverPct: cloudPct, SunshineMin: sunMin} _, worstStatus := computeRoomBudgets(p.ID, w, toggles, 25.0) contexts = append(contexts, action.HourContext{ Hour: h.Hour, TempC: h.TempC, HumidityPct: h.HumidityPct, IsDay: h.IsDay, RiskLevel: dayRisk.Level, BudgetStatus: worstStatus, ActiveToggles: toggles, }) } timeline := action.BuildTimeline(contexts, actions) fmt.Println("Hour-by-Hour Plan:") for _, slot := range timeline { if len(slot.Actions) == 0 { continue } fmt.Printf(" %02d:00 (%.1f°C):\n", slot.Hour, slot.TempC) for _, a := range slot.Actions { fmt.Printf(" - %s [%s, effort: %s]\n", a.Name, a.Impact, a.Effort) } } return nil }