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
131 lines
2.8 KiB
Go
131 lines
2.8 KiB
Go
package heat
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
func TestSolarGain(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
p SolarParams
|
|
want float64
|
|
}{
|
|
{
|
|
name: "midday south-facing room, clear sky, no shading",
|
|
p: SolarParams{
|
|
AreaSqm: 20,
|
|
WindowFraction: 0.15,
|
|
SHGC: 0.6,
|
|
ShadingFactor: 1.0,
|
|
OrientationFactor: 1.0,
|
|
CloudFactor: 1.0,
|
|
SunshineFraction: 1.0,
|
|
PeakIrradiance: 800,
|
|
},
|
|
// 800 * 1.0 * (20*0.15) * 0.6 * 1.0 * 1.0 * 1.0 = 800 * 3 * 0.6 = 1440
|
|
want: 1440,
|
|
},
|
|
{
|
|
name: "overcast, shutters closed",
|
|
p: SolarParams{
|
|
AreaSqm: 20,
|
|
WindowFraction: 0.15,
|
|
SHGC: 0.6,
|
|
ShadingFactor: 0.2,
|
|
OrientationFactor: 1.0,
|
|
CloudFactor: 0.3,
|
|
SunshineFraction: 0.2,
|
|
PeakIrradiance: 800,
|
|
},
|
|
// 800 * 1.0 * 3 * 0.6 * 0.2 * 0.3 * 0.2 = 1440 * 0.012 = 17.28
|
|
want: 17.28,
|
|
},
|
|
{
|
|
name: "night time (zero irradiance)",
|
|
p: SolarParams{
|
|
AreaSqm: 20,
|
|
WindowFraction: 0.15,
|
|
SHGC: 0.6,
|
|
ShadingFactor: 1.0,
|
|
OrientationFactor: 1.0,
|
|
CloudFactor: 1.0,
|
|
SunshineFraction: 0.0,
|
|
PeakIrradiance: 0,
|
|
},
|
|
want: 0,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := SolarGain(tt.p)
|
|
if !almostEqual(got, tt.want, tolerance) {
|
|
t.Errorf("SolarGain() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestVentilationGain(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
p VentilationParams
|
|
want float64
|
|
}{
|
|
{
|
|
name: "outdoor hotter than indoor, windows open",
|
|
p: VentilationParams{
|
|
ACH: 2.0,
|
|
VolumeCubicM: 45, // 15sqm * 3m ceiling
|
|
OutdoorTempC: 35,
|
|
IndoorTempC: 25,
|
|
RhoCp: 1.2, // kg/m³ * kJ/(kg·K) → ~1.2 kJ/(m³·K) = 1200 J/(m³·K)
|
|
},
|
|
// ACH * vol * rhoCp * deltaT / 3600
|
|
// 2 * 45 * 1200 * 10 / 3600 = 300
|
|
want: 300,
|
|
},
|
|
{
|
|
name: "outdoor cooler than indoor (negative gain = cooling)",
|
|
p: VentilationParams{
|
|
ACH: 2.0,
|
|
VolumeCubicM: 45,
|
|
OutdoorTempC: 18,
|
|
IndoorTempC: 25,
|
|
RhoCp: 1.2,
|
|
},
|
|
// 2 * 45 * 1200 * (-7) / 3600 = -210
|
|
want: -210,
|
|
},
|
|
{
|
|
name: "equal temperatures",
|
|
p: VentilationParams{
|
|
ACH: 2.0,
|
|
VolumeCubicM: 45,
|
|
OutdoorTempC: 25,
|
|
IndoorTempC: 25,
|
|
RhoCp: 1.2,
|
|
},
|
|
want: 0,
|
|
},
|
|
{
|
|
name: "windows closed (ACH=0)",
|
|
p: VentilationParams{
|
|
ACH: 0,
|
|
VolumeCubicM: 45,
|
|
OutdoorTempC: 35,
|
|
IndoorTempC: 25,
|
|
RhoCp: 1.2,
|
|
},
|
|
want: 0,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := VentilationGain(tt.p)
|
|
if !almostEqual(got, tt.want, tolerance) {
|
|
t.Errorf("VentilationGain() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|