Files
HeatGuard/internal/heat/external_gains_test.go
vikingowl 1c9db02334 feat: add web UI with full CRUD setup page
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
2026-02-09 10:39:00 +01:00

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)
}
})
}
}