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
107 lines
2.9 KiB
Go
107 lines
2.9 KiB
Go
package cli
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
|
|
"github.com/cnachtigall/heatwave-autopilot/internal/store"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var (
|
|
deviceRoom int64
|
|
deviceType string
|
|
deviceIdle float64
|
|
deviceTypical float64
|
|
devicePeak float64
|
|
deviceDuty float64
|
|
)
|
|
|
|
func init() {
|
|
deviceCmd := &cobra.Command{
|
|
Use: "device",
|
|
Short: "Manage heat-producing devices",
|
|
}
|
|
|
|
addCmd := &cobra.Command{
|
|
Use: "add <name>",
|
|
Short: "Add a device",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
d, err := db.CreateDevice(deviceRoom, args[0], deviceType, deviceIdle, deviceTypical, devicePeak, deviceDuty)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("Device added: %s (ID: %d, typical: %.0fW)\n", d.Name, d.ID, d.WattsTypical)
|
|
return nil
|
|
},
|
|
}
|
|
addCmd.Flags().Int64Var(&deviceRoom, "room", 0, "room ID")
|
|
addCmd.Flags().StringVar(&deviceType, "type", "other", "device type (pc, monitor, appliance, other)")
|
|
addCmd.Flags().Float64Var(&deviceIdle, "watts-idle", 0, "idle power draw (W)")
|
|
addCmd.Flags().Float64Var(&deviceTypical, "watts-typical", 0, "typical power draw (W)")
|
|
addCmd.Flags().Float64Var(&devicePeak, "watts-peak", 0, "peak power draw (W)")
|
|
addCmd.Flags().Float64Var(&deviceDuty, "duty-cycle", 1.0, "duty cycle (0.0-1.0)")
|
|
addCmd.MarkFlagRequired("room")
|
|
|
|
listCmd := &cobra.Command{
|
|
Use: "list",
|
|
Short: "List devices",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
p, err := getActiveProfile()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
roomFilter, _ := cmd.Flags().GetInt64("room")
|
|
var devices []store.Device
|
|
if roomFilter > 0 {
|
|
devices, err = db.ListDevices(roomFilter)
|
|
} else {
|
|
devices, err = db.ListAllDevices(p.ID)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(devices) == 0 {
|
|
fmt.Println("No devices found")
|
|
return nil
|
|
}
|
|
for _, d := range devices {
|
|
fmt.Printf(" [%d] %s — type: %s, idle: %.0fW, typical: %.0fW, peak: %.0fW, duty: %.0f%%\n",
|
|
d.ID, d.Name, d.DeviceType, d.WattsIdle, d.WattsTypical, d.WattsPeak, d.DutyCycle*100)
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
listCmd.Flags().Int64("room", 0, "filter by room ID")
|
|
|
|
editCmd := &cobra.Command{
|
|
Use: "edit <id> <field> <value>",
|
|
Short: "Edit a device field",
|
|
Args: cobra.ExactArgs(3),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
id, err := strconv.ParseInt(args[0], 10, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid device ID: %s", args[0])
|
|
}
|
|
return db.UpdateDevice(id, args[1], args[2])
|
|
},
|
|
}
|
|
|
|
removeCmd := &cobra.Command{
|
|
Use: "remove <id>",
|
|
Short: "Remove a device",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
id, err := strconv.ParseInt(args[0], 10, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid device ID: %s", args[0])
|
|
}
|
|
return db.DeleteDevice(id)
|
|
},
|
|
}
|
|
|
|
deviceCmd.AddCommand(addCmd, listCmd, editCmd, removeCmd)
|
|
rootCmd.AddCommand(deviceCmd)
|
|
}
|