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
97 lines
2.4 KiB
Go
97 lines
2.4 KiB
Go
package heat
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
func TestDeviceHeatGain(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
dev Device
|
|
mode DeviceMode
|
|
want float64
|
|
}{
|
|
{
|
|
name: "idle mode uses idle watts",
|
|
dev: Device{WattsIdle: 65, WattsTypical: 200, WattsPeak: 450, DutyCycle: 1.0},
|
|
mode: ModeIdle,
|
|
want: 65,
|
|
},
|
|
{
|
|
name: "typical mode with full duty cycle",
|
|
dev: Device{WattsIdle: 65, WattsTypical: 200, WattsPeak: 450, DutyCycle: 1.0},
|
|
mode: ModeTypical,
|
|
want: 200,
|
|
},
|
|
{
|
|
name: "typical mode with 50% duty cycle",
|
|
dev: Device{WattsIdle: 65, WattsTypical: 200, WattsPeak: 450, DutyCycle: 0.5},
|
|
mode: ModeTypical,
|
|
want: 100,
|
|
},
|
|
{
|
|
name: "peak mode",
|
|
dev: Device{WattsIdle: 65, WattsTypical: 200, WattsPeak: 450, DutyCycle: 1.0},
|
|
mode: ModePeak,
|
|
want: 450,
|
|
},
|
|
{
|
|
name: "zero duty cycle",
|
|
dev: Device{WattsIdle: 65, WattsTypical: 200, WattsPeak: 450, DutyCycle: 0.0},
|
|
mode: ModeTypical,
|
|
want: 0,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := DeviceHeatGain(tt.dev, tt.mode)
|
|
if !almostEqual(got, tt.want, tolerance) {
|
|
t.Errorf("DeviceHeatGain() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOccupantHeatGain(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
count int
|
|
activity ActivityLevel
|
|
want float64
|
|
}{
|
|
{"one sleeping person", 1, Sleeping, 70},
|
|
{"one sedentary person", 1, Sedentary, 100},
|
|
{"two sedentary people", 2, Sedentary, 200},
|
|
{"one light activity", 1, LightActivity, 130},
|
|
{"one moderate activity", 1, ModerateActivity, 200},
|
|
{"one heavy activity", 1, HeavyActivity, 300},
|
|
{"zero people", 0, Sedentary, 0},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := OccupantHeatGain(tt.count, tt.activity)
|
|
if !almostEqual(got, tt.want, tolerance) {
|
|
t.Errorf("OccupantHeatGain(%d, %v) = %v, want %v", tt.count, tt.activity, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTotalInternalGains(t *testing.T) {
|
|
devices := []Device{
|
|
{WattsIdle: 65, WattsTypical: 200, WattsPeak: 450, DutyCycle: 1.0},
|
|
{WattsIdle: 30, WattsTypical: 80, WattsPeak: 120, DutyCycle: 0.5},
|
|
}
|
|
occupants := []Occupant{
|
|
{Count: 1, Activity: Sedentary},
|
|
{Count: 2, Activity: LightActivity},
|
|
}
|
|
|
|
// 200 + 40 + 100 + 260 = 600
|
|
got := TotalInternalGains(devices, ModeTypical, occupants)
|
|
want := 600.0
|
|
if !almostEqual(got, want, tolerance) {
|
|
t.Errorf("TotalInternalGains() = %v, want %v", got, want)
|
|
}
|
|
}
|