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
114 lines
3.4 KiB
Go
114 lines
3.4 KiB
Go
package store
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
type Device struct {
|
|
ID int64
|
|
RoomID int64
|
|
Name string
|
|
DeviceType string
|
|
WattsIdle float64
|
|
WattsTypical float64
|
|
WattsPeak float64
|
|
DutyCycle float64
|
|
Schedule string
|
|
CreatedAt time.Time
|
|
}
|
|
|
|
func (s *Store) CreateDevice(roomID int64, name, deviceType string, wattsIdle, wattsTypical, wattsPeak, dutyCycle float64) (*Device, error) {
|
|
if dutyCycle == 0 {
|
|
dutyCycle = 1.0
|
|
}
|
|
res, err := s.db.Exec(
|
|
`INSERT INTO devices (room_id, name, device_type, watts_idle, watts_typical, watts_peak, duty_cycle) VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
roomID, name, deviceType, wattsIdle, wattsTypical, wattsPeak, dutyCycle,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create device: %w", err)
|
|
}
|
|
id, _ := res.LastInsertId()
|
|
return s.GetDevice(id)
|
|
}
|
|
|
|
func (s *Store) GetDevice(id int64) (*Device, error) {
|
|
d := &Device{}
|
|
var created string
|
|
err := s.db.QueryRow(
|
|
`SELECT id, room_id, name, device_type, watts_idle, watts_typical, watts_peak, duty_cycle, schedule, created_at FROM devices WHERE id = ?`, id,
|
|
).Scan(&d.ID, &d.RoomID, &d.Name, &d.DeviceType, &d.WattsIdle, &d.WattsTypical, &d.WattsPeak, &d.DutyCycle, &d.Schedule, &created)
|
|
if err == sql.ErrNoRows {
|
|
return nil, fmt.Errorf("device not found: %d", id)
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get device: %w", err)
|
|
}
|
|
d.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", created)
|
|
return d, nil
|
|
}
|
|
|
|
func (s *Store) ListDevices(roomID int64) ([]Device, error) {
|
|
rows, err := s.db.Query(
|
|
`SELECT id, room_id, name, device_type, watts_idle, watts_typical, watts_peak, duty_cycle, schedule, created_at FROM devices WHERE room_id = ? ORDER BY name`, roomID,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var devices []Device
|
|
for rows.Next() {
|
|
var d Device
|
|
var created string
|
|
if err := rows.Scan(&d.ID, &d.RoomID, &d.Name, &d.DeviceType, &d.WattsIdle, &d.WattsTypical, &d.WattsPeak, &d.DutyCycle, &d.Schedule, &created); err != nil {
|
|
return nil, err
|
|
}
|
|
d.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", created)
|
|
devices = append(devices, d)
|
|
}
|
|
return devices, rows.Err()
|
|
}
|
|
|
|
func (s *Store) ListAllDevices(profileID int64) ([]Device, error) {
|
|
rows, err := s.db.Query(
|
|
`SELECT d.id, d.room_id, d.name, d.device_type, d.watts_idle, d.watts_typical, d.watts_peak, d.duty_cycle, d.schedule, d.created_at
|
|
FROM devices d JOIN rooms r ON d.room_id = r.id WHERE r.profile_id = ? ORDER BY d.name`, profileID,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var devices []Device
|
|
for rows.Next() {
|
|
var d Device
|
|
var created string
|
|
if err := rows.Scan(&d.ID, &d.RoomID, &d.Name, &d.DeviceType, &d.WattsIdle, &d.WattsTypical, &d.WattsPeak, &d.DutyCycle, &d.Schedule, &created); err != nil {
|
|
return nil, err
|
|
}
|
|
d.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", created)
|
|
devices = append(devices, d)
|
|
}
|
|
return devices, rows.Err()
|
|
}
|
|
|
|
func (s *Store) UpdateDevice(id int64, field, value string) error {
|
|
allowed := map[string]bool{
|
|
"name": true, "device_type": true, "watts_idle": true, "watts_typical": true,
|
|
"watts_peak": true, "duty_cycle": true, "schedule": true,
|
|
}
|
|
if !allowed[field] {
|
|
return fmt.Errorf("invalid field: %s", field)
|
|
}
|
|
_, err := s.db.Exec(
|
|
fmt.Sprintf(`UPDATE devices SET %s = ? WHERE id = ?`, field), value, id,
|
|
)
|
|
return err
|
|
}
|
|
|
|
func (s *Store) DeleteDevice(id int64) error {
|
|
_, err := s.db.Exec(`DELETE FROM devices WHERE id = ?`, id)
|
|
return err
|
|
}
|