Model heating mode when rooms have net heat loss in cold weather (<10°C). AC units with heat pump capability (canHeat) provide heating capacity, with the same 20% headroom threshold used for cooling. Adds cold risk detection, cold-weather actions, and full frontend support including heating mode timeline colors, room budget heating display, and i18n.
197 lines
4.6 KiB
Go
197 lines
4.6 KiB
Go
package risk
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
func makeHours(temps []float64) []HourlyData {
|
|
hours := make([]HourlyData, len(temps))
|
|
for i, t := range temps {
|
|
hours[i] = HourlyData{
|
|
Hour: i,
|
|
TempC: t,
|
|
ApparentC: t,
|
|
HumidityPct: 50,
|
|
IsDay: i >= 6 && i < 21,
|
|
}
|
|
}
|
|
return hours
|
|
}
|
|
|
|
func TestAnalyzeDay_CoolDay(t *testing.T) {
|
|
temps := make([]float64, 24)
|
|
for i := range temps {
|
|
temps[i] = 20 + float64(i%5) // 20-24C
|
|
}
|
|
result := AnalyzeDay(makeHours(temps), DefaultThresholds())
|
|
if result.Level != Low {
|
|
t.Errorf("Level = %v, want Low", result.Level)
|
|
}
|
|
if len(result.Windows) != 0 {
|
|
t.Errorf("Windows = %d, want 0", len(result.Windows))
|
|
}
|
|
}
|
|
|
|
func TestAnalyzeDay_ModeratelyHot(t *testing.T) {
|
|
temps := make([]float64, 24)
|
|
for i := range temps {
|
|
temps[i] = 18 // base below poor night cooling threshold
|
|
}
|
|
// Hot window 11-15
|
|
for i := 11; i <= 15; i++ {
|
|
temps[i] = 32
|
|
}
|
|
result := AnalyzeDay(makeHours(temps), DefaultThresholds())
|
|
if result.Level != Moderate {
|
|
t.Errorf("Level = %v, want Moderate", result.Level)
|
|
}
|
|
if len(result.Windows) != 1 {
|
|
t.Fatalf("Windows = %d, want 1", len(result.Windows))
|
|
}
|
|
w := result.Windows[0]
|
|
if w.StartHour != 11 || w.EndHour != 15 {
|
|
t.Errorf("Window = %d-%d, want 11-15", w.StartHour, w.EndHour)
|
|
}
|
|
if w.PeakTempC != 32 {
|
|
t.Errorf("PeakTempC = %v, want 32", w.PeakTempC)
|
|
}
|
|
}
|
|
|
|
func TestAnalyzeDay_VeryHot(t *testing.T) {
|
|
temps := make([]float64, 24)
|
|
for i := range temps {
|
|
temps[i] = 18 // below poor night cooling threshold
|
|
}
|
|
for i := 10; i <= 17; i++ {
|
|
temps[i] = 37
|
|
}
|
|
result := AnalyzeDay(makeHours(temps), DefaultThresholds())
|
|
if result.Level != High {
|
|
t.Errorf("Level = %v, want High", result.Level)
|
|
}
|
|
}
|
|
|
|
func TestAnalyzeDay_Extreme(t *testing.T) {
|
|
temps := make([]float64, 24)
|
|
for i := range temps {
|
|
temps[i] = 25
|
|
}
|
|
for i := 12; i <= 16; i++ {
|
|
temps[i] = 41
|
|
}
|
|
result := AnalyzeDay(makeHours(temps), DefaultThresholds())
|
|
if result.Level != Extreme {
|
|
t.Errorf("Level = %v, want Extreme", result.Level)
|
|
}
|
|
}
|
|
|
|
func TestAnalyzeDay_PoorNightCoolingElevates(t *testing.T) {
|
|
temps := make([]float64, 24)
|
|
for i := range temps {
|
|
temps[i] = 22
|
|
}
|
|
// Hot day window
|
|
for i := 11; i <= 15; i++ {
|
|
temps[i] = 32
|
|
}
|
|
// Poor night cooling (hour 0-6 and 21-23 above 20C)
|
|
for i := 0; i <= 6; i++ {
|
|
temps[i] = 22
|
|
}
|
|
for i := 21; i < 24; i++ {
|
|
temps[i] = 22
|
|
}
|
|
result := AnalyzeDay(makeHours(temps), DefaultThresholds())
|
|
if !result.PoorNightCool {
|
|
t.Error("expected PoorNightCool = true")
|
|
}
|
|
// Base Moderate + poor night = High
|
|
if result.Level != High {
|
|
t.Errorf("Level = %v, want High (elevated from Moderate)", result.Level)
|
|
}
|
|
}
|
|
|
|
func TestAnalyzeDay_GoodNightCooling(t *testing.T) {
|
|
temps := make([]float64, 24)
|
|
for i := range temps {
|
|
temps[i] = 18 // cool nights
|
|
}
|
|
for i := 11; i <= 15; i++ {
|
|
temps[i] = 32
|
|
}
|
|
result := AnalyzeDay(makeHours(temps), DefaultThresholds())
|
|
if result.PoorNightCool {
|
|
t.Error("expected PoorNightCool = false")
|
|
}
|
|
if result.Level != Moderate {
|
|
t.Errorf("Level = %v, want Moderate (no elevation)", result.Level)
|
|
}
|
|
}
|
|
|
|
func TestAnalyzeDay_Empty(t *testing.T) {
|
|
result := AnalyzeDay(nil, DefaultThresholds())
|
|
if result.Level != Low {
|
|
t.Errorf("Level = %v, want Low", result.Level)
|
|
}
|
|
}
|
|
|
|
func TestAnalyzeDay_MinNightTemp(t *testing.T) {
|
|
temps := make([]float64, 24)
|
|
for i := range temps {
|
|
temps[i] = 25
|
|
}
|
|
temps[3] = 16.5 // coldest night hour
|
|
result := AnalyzeDay(makeHours(temps), DefaultThresholds())
|
|
if result.MinNightTempC != 16.5 {
|
|
t.Errorf("MinNightTempC = %v, want 16.5", result.MinNightTempC)
|
|
}
|
|
}
|
|
|
|
func TestAnalyzeDay_ColdRiskDetected(t *testing.T) {
|
|
temps := make([]float64, 24)
|
|
for i := range temps {
|
|
temps[i] = 5
|
|
}
|
|
temps[3] = -2 // below ColdDayC (0)
|
|
result := AnalyzeDay(makeHours(temps), DefaultThresholds())
|
|
if result.MinTempC != -2 {
|
|
t.Errorf("MinTempC = %v, want -2", result.MinTempC)
|
|
}
|
|
if !result.ColdRisk {
|
|
t.Error("expected ColdRisk = true")
|
|
}
|
|
}
|
|
|
|
func TestAnalyzeDay_ColdRiskAbsent(t *testing.T) {
|
|
temps := make([]float64, 24)
|
|
for i := range temps {
|
|
temps[i] = 15
|
|
}
|
|
temps[5] = 5 // above ColdDayC (0)
|
|
result := AnalyzeDay(makeHours(temps), DefaultThresholds())
|
|
if result.MinTempC != 5 {
|
|
t.Errorf("MinTempC = %v, want 5", result.MinTempC)
|
|
}
|
|
if result.ColdRisk {
|
|
t.Error("expected ColdRisk = false")
|
|
}
|
|
}
|
|
|
|
func TestRiskLevelString(t *testing.T) {
|
|
tests := []struct {
|
|
level RiskLevel
|
|
want string
|
|
}{
|
|
{Low, "low"},
|
|
{Moderate, "moderate"},
|
|
{High, "high"},
|
|
{Extreme, "extreme"},
|
|
{RiskLevel(99), "unknown"},
|
|
}
|
|
for _, tt := range tests {
|
|
if got := tt.level.String(); got != tt.want {
|
|
t.Errorf("RiskLevel(%d).String() = %s, want %s", tt.level, got, tt.want)
|
|
}
|
|
}
|
|
}
|